Fix some of the errors reported by the sanity checker
[amule.git] / src / utils / scripts / sanity
blob23417d8de9e90d37a6b38a19b4cbab37dcb40b9c
1 #!/usr/bin/ruby
3 # This file is part of the aMule project.
5 # Copyright (c) 2003-2008 aMule Team ( admin@amule.org / http://www.amule.org )
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either
10 # version 2 of the License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
23 # This function returns true if a file is filtered
24 # and shound't be included in the sanity testing.
25 def IsFiltered(filename)
26 (["./config.h", "./configWIN32.h"].index(filename) != nil) or
27 (filename =~ /^.\/intl\//) or
28 (filename =~ /CryptoPP/)
29 end
33 # This class represents lines of code, with line-number and text
34 # It is used to store the source-files once they have been read
35 # and afterwards to store the lines returned by the filters.
36 class Line
37 def initialize( number, text )
38 @number = number
39 @text = text
40 end
42 attr_reader :number
43 attr_reader :text
44 end
48 class Result
49 def initialize( type, file, line = nil )
50 @type = type
51 @file = file
52 @line = line
53 end
55 def file_name
56 @file.slice( /[^\/]+$/ )
57 end
59 def file_path
60 @file.slice( /^.*\// )
61 end
63 attr_reader :type
64 attr_reader :file
65 attr_reader :line
66 end
70 # Base class for Sanity Checkers
72 # This class represents the basic sanity-check, which returns all
73 # files as positive results, regardless of the contents.
74 class SanityCheck
75 def initialize
76 @name = "None"
77 @title = nil
78 @type = "None"
79 @desc = "None"
80 @results = Array.new
81 end
83 attr_reader :name
84 attr_reader :type
85 attr_reader :desc
87 def title
88 if @title then
89 @title
90 else
91 @name
92 end
93 end
96 def results
97 @results
98 end
100 # This function will be called for each file, with the argument "file" as the
101 # name of the file and the argument "lines" being an array of Line objects for
102 # each line of the file.
104 def parse_file(file, lines)
105 raise "Missing parse_file() implementation for Filter: #{@name}"
109 private
111 def add_results( file, lines = [nil] )
112 lines.each do |line|
113 @results << Result.new( self, file, line )
122 class CompareAgainstEmptyString < SanityCheck
123 def initialize
124 super
126 @name = "CmpEmptyString"
127 @title = "Comparing With Empty String"
128 @type = "Good Practice"
129 @desc = "Comparisons with empty strings, such as wxT(\"\"), wxEmptyString and "
130 @desc += "_(\"\") should be avoided since they force the creation of a temporary "
131 @desc += "string object. The proper method is to use the IsEmpty() member-function "
132 @desc += "of wxString."
135 def parse_file(file, lines)
136 results = lines.select do |line|
137 line.text =~ /[!=]=\s*(wxEmptyString|wxT\(""\)|_\(""\))/ or
138 line.text =~ /(wxEmptyString|wxT\(""\)|_\(""\))\s*[!=]=/
141 add_results( file, results )
147 class AssignmentToEmptyString < SanityCheck
148 def initialize
149 super
151 @name = "EmptyStringAssignment"
152 @title = "Assigning The Empty String"
153 @type = "Good Practice"
154 @desc = "Assigning an empty string such as wxT(\"\"), wxEmptyString and _(\"\") "
155 @desc += "to a wxString should be avoided, since it forces the creation of a "
156 @desc += "temporary object which is assigned to the string. The proper way to "
157 @desc += "clear a string is to use the Clear() member-function of wxString."
160 def parse_file(file, lines)
161 if file =~ /\.cpp$/
162 results = lines.select do |line|
163 line.text =~ /[^=!]=\s*(wxEmptyString|wxT\(""\)|_\(""\))/
166 add_results( file, results )
173 class NoIfNDef < SanityCheck
174 def initialize
175 super
177 @name = "NoIfNDef"
178 @title = "No #ifndef in headerfile"
179 @type = "Good Practice"
180 @desc = "All header files should contain a #ifndef __<FILENAME>__. The purpose is to ensure "
181 @desc += "that the header can't be included twice, which would introduce a number of problems."
184 def parse_file(file, lines)
185 if file =~ /\.h$/ then
186 if not lines.find { |x| x.text =~ /^#ifndef.*_H/ } then
187 add_results( file )
195 class ThisDeference < SanityCheck
196 def initialize
197 super
199 @name = "ThisDeference"
200 @title = "Dereference of \"this\""
201 @type = "Good Practice"
202 @desc = "In all but the case of templates, using \"this->\" is unnecessary and "
203 @desc += "only decreases the readability of the code."
206 def parse_file(file, lines)
207 results = lines.select do |line|
208 line.text =~ /\bthis->/
211 add_results( file, results )
217 class Assert < SanityCheck
218 def initialize
219 super
221 @name = "Assert"
222 @type = "Consistency"
223 @desc = "wxASSERT()s should be used rather than normal assert()s "
224 @desc += "for the sake of consistency."
227 def parse_file(file, lines)
228 results = lines.select do |line|
229 line.text =~ /assert\s*\(/
232 add_results( file, results )
238 class WxFailUsage < SanityCheck
239 def initialize
240 super
242 @name = "WxFailUsagesy"
243 @title = "Always failing wxASSERT()"
244 @type = "Good Practice"
245 @desc = "Use wxFAIL instead of an always failing wxASSERT()."
248 def parse_file(file, lines)
249 results = lines.select do |line|
250 line.text =~ /\bwxASSERT\s*\(\s*(0|false)\s*\)/ or
251 line.text =~ /\bwxASSERT_MSG\s*\(\s*(0|false)\s*,/
254 add_results( file, results )
260 class PassByValue < SanityCheck
261 def initialize
262 super
264 @name = "PassByValue"
265 @title = "Pass By Value"
266 @type = "Good Practice"
267 @desc = "Passing objects by value means an extra overhead for large datatypes. "
268 @desc += "Therefore these should always be passed by const reference when possible. "
269 @desc += "Non-const references should only be used for functions where the function is "
270 @desc += "supposed to change the actual value of the argument and return another or no value."
273 def parse_file(file, lines)
274 results = Array.new
276 # Only handle header files
277 if file =~ /\.h$/
278 # Items that should be passed by const-ref
279 items = [ "wxString", "wxRect", "wxPoint", "CMD4Hash", "CPath" ]
281 lines.each do |line|
282 # Try to identify function definitions
283 if line.text =~ /^\s*(virtual|static|inline|)\s*\w+\s+\w+\s*\(.*\)/
284 # Split by arguments
285 line.text.match(/\(.*\)/)[0].split(",").each do |str|
286 items.each do |item|
287 if str =~ /#{item}\s*[^\s\*]/ and not str =~ /const/
288 results.push( line )
296 add_results( file, results )
302 class CStr < SanityCheck
303 def initialize
304 super
306 @name = "CStr"
307 @title = "C_Str or GetData"
308 @type = "Unicoding"
309 @desc = "Checks for usage of c_str() or GetData(). Using c_str will often result in "
310 @desc += "problems on Unicoded builds and should therefore be avoided. "
311 @desc += "Please note that the GetData check isn't that precise, because many other "
312 @desc += "classes have GetData members, so it does some crude filtering."
315 def parse_file(file, lines)
316 results = lines.select do |line|
317 if line.text =~ /c_str\(\)/ and line.text !~ /char2unicode\(/
318 true
319 else
320 line.text =~ /GetData\(\)/ and line.text =~ /(wxT\(|wxString|_\()/
324 add_results( file, results )
330 class IfNotDefined < SanityCheck
331 def initialize
332 super
334 @name = "IfDefined"
335 @title = "#if (!)defined"
336 @type = "Consistency"
337 @desc = "Use #ifndef or #ifdef instead for reasons of simplicity."
340 def parse_file(file, lines)
341 results = lines.select do |line|
342 if line.text =~ /^#if.*[\!]?defined\(/
343 not line.text =~ /(\&\&|\|\|)/
347 add_results( file, results )
353 class GPLLicense < SanityCheck
354 def initialize
355 super
357 @name = "MissingGPL"
358 @title = "Missing GPL License"
359 @type = "License"
360 @desc = "All header files should contain the proper GPL blorb."
363 def parse_file(file, lines)
364 if file =~ /\.h$/
365 if lines.find { |x| x.text =~ /This (program|library) is free software;/ } == nil
366 add_results( file )
374 class Copyright < SanityCheck
375 def initialize
376 super
378 @name = "MissingCopyright"
379 @title = "Missing Copyright Notice"
380 @type = "License"
381 @desc = "All files should contain the proper Copyright notice."
384 def parse_file(file, lines)
385 if file =~ /\.h$/
386 found = lines.select do |line|
387 line.text =~ /Copyright\s*\([cC]\)\s*[-\d,]+ aMule (Project|Team)/
390 if found.empty? then
391 add_results( file )
399 class PartOfAmule < SanityCheck
400 def initialize
401 super
403 @name = "aMuleNotice"
404 @title = "Missing aMule notice"
405 @type = "License"
406 @desc = "All files should contain a notice that they are part of the aMule project."
409 def parse_file(file, lines)
410 if file =~ /\.h$/
411 found = lines.select do |line|
412 line.text =~ /This file is part of the aMule Project/ or
413 line.text =~ /This file is part of aMule/
416 if found.empty? then
417 add_results( file )
425 class MissingBody < SanityCheck
426 def initialize
427 super
429 @name = "MissingBody"
430 @title = "Missing Body in Loop"
431 @type = "Garbage"
432 @desc = "This check looks for loops without any body. For example \"while(true);\" "
433 @desc += "In most cases this is a sign of either useless code or bugs. Only in a few "
434 @desc += "cases is it valid code, and in those it can often be represented clearer "
435 @desc += "in other ways."
438 def parse_file(file, lines)
439 results = lines.select do |line|
440 if line.text =~ /^[^}]*while\s*\(.*\)\s*;/ or
441 line.text =~ /^\s*for\s*\(.*\)\s*;[^\)]*$/
442 # Avoid returning "for" spanning multiple lines
443 # TODO A better way to count instances
444 line.text.split("(").size == line.text.split(")").size
445 else
446 false
450 add_results( file, results )
456 class Translation < SanityCheck
457 def initialize
458 super
460 @name = "Translation"
461 @type = "Consistency"
462 @desc = "Calls to AddLogLine should translate the message, whereas "
463 @desc += "calls to AddDebugLogLine shouldn't. This is because the user "
464 @desc += "is meant to see normal log lines, whereas the the debug-lines "
465 @desc += "are only meant for the developers and I don't know about you, but "
466 @desc += "I don't plan on learning every language we choose to translate "
467 @desc += "aMule to. :P"
470 def parse_file(file, lines)
471 results = lines.select do |line|
472 if line.text =~ /\"/
473 line.text =~ /AddLogLine.?.?\(.*wxT\(/ or
474 line.text =~ /AddDebugLogLine.?\(.*_\(/
475 else
476 false
480 add_results( file, results )
486 class IfZero < SanityCheck
487 def initialize
488 super
490 @name = "PreIfConstant"
491 @title = "#if 0-9"
492 @type = "Garbage"
493 @desc = "Disabled code should be removed as soon as possible. If you wish to disable code "
494 @desc += "for only a short period, then please add a comment before the #if. Code with #if [1-9] "
495 @desc += "should be left, but the #ifs removed unless there is a pressing need for them."
498 def parse_file(file, lines)
499 results = lines.select do |line|
500 line.text =~ /#if\s+[0-9]/
503 add_results( file, results )
509 class InlinedIfTrue < SanityCheck
510 def initialize
511 super
513 @name = "InlinedIf"
514 @title = "Inlined If true/false"
515 @type = "Garbage"
516 @desc = "Using variations of (x ? true : false) or (x ? false : true) is just plain stupid."
519 def parse_file(file, lines)
520 results = lines.select do |line|
521 line.text =~ /\?\s*(true|false)\s*:\s*(true|false)/i
524 add_results( file, results )
530 class LoopOnConstant < SanityCheck
531 def initialize
532 super
534 @name = "LoopingOnConstant"
535 @title = "Looping On Constant"
536 @type = "Garbage"
537 @desc = "This checks detects loops that evaluate constant values "
538 @desc += "(true,false,0..) or \"for\" loops with no conditionals. all "
539 @desc += "are often a sign of poor code and in most cases can be avoided "
540 @desc += "or replaced with more elegant code."
543 def parse_file(file, lines)
544 results = lines.select do |line|
545 line.text =~ /while\s*\(\s*([0-9]+|true|false)\s*\)/i or
546 line.text =~ /for\s*\([^;]*;\s*;[^;]*\)/
549 add_results( file, results )
555 class MissingImplementation < SanityCheck
556 def initialize
557 super
559 @name = "MissingImplementation"
560 @title = "Missing Function Implementation"
561 @type = "Garbage"
562 @desc = "Forgetting to remove a function-definition after having removed it "
563 @desc += "from the .cpp file only leads to cluttered header files. The only "
564 @desc += "case where non-implemented functions should be used is when it is "
565 @desc += "necessary to prevent usage of for instance assignment between "
566 @desc += "instances of a class."
568 @declarations = Array.new
569 @definitions = Array.new
572 def results
573 @definitions.each do |definition|
574 @declarations.delete_if do |pair|
575 pair.first == definition
580 return @declarations.map do |pair|
581 Result.new( self, pair.last.first, pair.last.last )
585 def parse_file(file, lines)
586 if file =~ /\.h$/ then
587 level = 0
588 tree = Array.new
590 lines.each do |line|
591 level += line.text.count( "{" ) - line.text.count( "}" )
593 tree.delete_if do |struct|
594 struct.first > level
598 if line.text !~ /^\s*\/\// and line.text !~ /^\s*#/ and line.text !~ /typedef/ then
599 if line.text =~ /^\s*(class|struct)\s+/ and line.text.count( ";" ) == 0 then
600 cur_level = level;
602 if line.text.count( "{" ) == 0 then
603 cur_level += 1
606 name = line.text.scan( /^\s*(class|struct)\s+([^\:{;]+)/ )
607 if name != [] then
608 name = name.first.last.strip
609 else
610 name = "Unknown at line " + line.number.to_s + " in " + file
613 tree << [ cur_level, name ]
614 elsif line.text =~ /;\s*$/ and line.text.count( "{" ) == 0 then
615 # No pure virtual functions and no return(blah) calls (which otherwise can fit the requirements)
616 if line.text !~ /=\s*0\s*;\s*$/ and line.text !~ /return/ then
617 re = /^\s*(virtual\s+|static\s+|inline\s+|)\w+(\s+[\*\&]?|[\*\&]\s+)(\w+)\(/.match( line.text )
619 if re and level > 0 and tree.last then
620 @declarations << [ tree.last.last + "::" + re[ re.length - 1 ], [ file, line ] ]
626 else
627 lines.each do |line|
628 if line.text =~ /\b\w+::\w+\s*\([^;]+$/
629 @definitions << line.text.scan( /\b(\w+::\w+)\s*\([^;]+$/ ).first.first
642 # List of enabled filters
643 filterList = Array.new
644 filterList.push CompareAgainstEmptyString.new
645 filterList.push AssignmentToEmptyString.new
646 filterList.push NoIfNDef.new
647 filterList.push ThisDeference.new
648 filterList.push Assert.new
649 filterList.push PassByValue.new
650 filterList.push CStr.new
651 filterList.push IfNotDefined.new
652 filterList.push GPLLicense.new
653 filterList.push Copyright.new
654 filterList.push PartOfAmule.new
655 filterList.push MissingBody.new
656 filterList.push Translation.new
657 filterList.push IfZero.new
658 filterList.push InlinedIfTrue.new
659 filterList.push LoopOnConstant.new
660 filterList.push MissingImplementation.new
661 filterList.push WxFailUsage.new
664 # Sort enabled filters by type and name. The reason why this is done here is
665 # because it's much easier than manually resorting every time I add a filter
666 # or change the name or type of an existing filter.
667 filterList.sort! do |x,y|
668 cmp = x.type <=> y.type
670 if cmp == 0 then
671 x.title <=> y.title
672 else
679 def parse_files( path, filters )
680 filters = filters.dup
682 require "find"
684 Find.find( path ) do |filename|
685 if filename =~ /\.(cpp|h)$/ and not IsFiltered(filename) then
686 File.open(filename, "r") do |aFile|
687 # Read lines and add line-numbers
688 lines = Array.new
689 aFile.each_line do |line|
690 lines.push( Line.new( aFile.lineno, line ) )
693 lines.freeze
695 # Check the file against each filter
696 filters.each do |filter|
697 # Process the file with this filter
698 filter.parse_file( filename, lines )
704 results = Array.new
705 filters.each do |filter|
706 results += filter.results
709 results
715 # Helper-function
716 def get_val( key, list )
717 if not list.last or list.last.first != key then
718 list << [ key, Array.new ]
721 list.last.last
726 def create_result_tree( path, filters )
727 # Gather the results
728 results = parse_files( path, filters )
730 # Sort the results by the following sequence of variables: Path -> File -> Filter -> Line
731 results.sort! do |a, b|
732 if (a.file_path <=> b.file_path) == 0 then
733 if (a.file_name <=> b.file_name) == 0 then
734 if (a.type.title <=> b.type.title) == 0 then
735 a.line.number <=> b.line.number
736 else
737 a.type.title <=> b.type.title
739 else
740 a.file_name <=> b.file_name
742 else
743 a.file_path <=> b.file_path
748 # Create a tree of results: [ Path, [ File, [ Filter, [ Line ] ] ] ]
749 result_tree = Array.new
750 results.each do |result|
751 get_val( result.type, get_val( result.file_name, get_val( result.file_path, result_tree ) ) ) << result
755 result_tree
760 def create_filter_tree( filters )
761 # Change the filterList to a tree: [ Type, [ Filter ] ]
762 filter_tree = Array.new
764 filters.each do |filter|
765 get_val( filter.type, filter_tree ) << filter
768 filter_tree
773 # Converts a number to a string and pads with zeros so that length becomes at least 5
774 def PadNum( number )
775 num = number.to_s
777 if ( num.size < 5 )
778 ( "0" * ( 5 - num.size ) ) + num
779 else
786 # Helper-function that escapes some chars to HTML codes
787 def HTMLEsc( str )
788 str.gsub!( /\&/, "&amp;" )
789 str.gsub!( /\"/, "&quot;" )
790 str.gsub!( /</, "&lt;" )
791 str.gsub!( />/, "&gt;" )
792 str.gsub!( /\n/, "<br>" )
793 str.gsub( /\t/, "&nbsp;" )
798 # Fugly output code goes here
799 # ... Abandon hope, yee who read past here
800 # TODO Enable use of templates.
801 # TODO Abandon hope.
802 def OutputHTML( filters, results )
803 text =
804 "<html>
805 <head>
806 <STYLE TYPE=\"text/css\">
807 <!--
808 .dir {
809 background-color: \#A0A0A0;
810 padding-left: 10pt;
811 padding-right: 10pt;
813 .file {
814 background-color: \#838383;
815 padding-left: 10pt;
816 padding-right: 10pt;
818 .filter {
819 background-color: #757575;
820 padding-left: 10pt;
821 padding-right: 10pt;
822 padding-top: 5pt;
825 </STYLE>
826 </head>
827 <body bgcolor=\"\#BDBDBD\">
829 <h1>Filters</h1>
830 <dl>
832 # List the filters
833 filters.each do |filterType|
834 text +=
835 " <dt><b>#{filterType.first}</b></dt>
836 <dd>
837 <dl>
839 filterType.last.each do |filter|
840 text +=
841 " <dt id=\"#{filter.name}\"><i>#{filter.title}</i></dt>
842 <dd>
843 #{HTMLEsc(filter.desc)}<p>
844 </dd>
848 text +=
849 " </dl>
850 </dd>
854 text +=
855 " </dl>
859 <h1>Directories</h1>
860 <ul>
863 # List the directories
864 results.each do |dir|
865 text +=
866 " <li>
867 <a href=\"\##{dir.first}\">#{dir.first}</a>
868 </li>
872 text +=
873 " </ul>
877 <h1>Results</h1>
880 results.each do |dir|
881 text +=
882 " <div class=\"dir\">
883 <h2 id=\"#{dir.first}\">#{dir.first}</h2>
886 dir.last.each do |file|
887 text +=
888 " <div class=\"file\">
889 <h3>#{file.first}</h3>
891 <ul>
894 file.last.each do |filter|
895 text +=
896 " <li>
897 <div class=\"filter\">
898 <b><a href=\"\##{filter.first.name}\">#{filter.first.title}</a></b>
900 <ul>
903 filter.last.each do |result|
904 if result.line then
905 text +=
906 " <li><b>#{PadNum(result.line.number)}:</b> #{HTMLEsc(result.line.text.strip)}</li>
911 text +=
912 " </ul>
913 </div>
914 </li>
917 text +=
918 " </ul>
919 </div>
923 text +=
924 " </div>
930 text +=
931 " </body>
932 </html>"
934 return text;
939 # Columnizing, using the http://www.rubygarden.org/ruby?UsingTestUnit example because I'm lazy
940 # TODO Rewrite it to better support newlines and stuff
941 def Columnize( text, width, indent )
942 return indent + text.scan(/(.{1,#{width}})(?: |$)/).join("\n#{indent}")
947 # Fugly output code also goes here, this is a bit more sparse than the HTML stuff
948 def OutputTEXT( filters, results )
950 # List the filters
951 text = "Filters\n"
952 filters.each do |filterType|
953 text += "\t* #{filterType.first}\n"
955 filterType.last.each do |filter|
956 text += "\t\t- #{filter.title}\n"
958 text += Columnize( filter.desc, 80, "\t\t\t" ) + "\n\n"
962 # List the directories
963 text += "\n\nDirectories\n"
964 results.each do |dir|
965 text += "\t#{dir.first}\n"
968 text += "\n\nResults\n"
970 # To avoid bad readability, I only use fullpaths here instead of sections per dir
971 results.each do |dir|
972 dir.last.each do |file|
973 text += "\t#{dir.first}#{file.first}\n"
975 file.last.each do |filter|
976 text += "\t\t* #{filter.first.title}\n"
978 filter.last.each do |result|
979 if result.line then
980 text += "\t\t\t#{PadNum(result.line.number)}: #{result.line.text.strip}\n"
985 text += "\n"
989 return text;
994 #TODO Improved parameter-handling, add =<file> for the outputing to a file
995 ARGV.each do |param|
996 case param
997 when "--text" then
998 puts OutputTEXT( create_filter_tree( filterList ), create_result_tree( ".", filterList ) )
999 when "--html" then
1000 puts OutputHTML( create_filter_tree( filterList ), create_result_tree( ".", filterList ) )