Upstream tarball 20080721
[amule.git] / src / utils / scripts / sanity
blobd82831aa0f8749f34410c6cd9a9e7497aeb54b96
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 sainity 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 Sainity Checkers
72 # This class represents the basic sainity-check, which returns all
73 # files as positive results, regardless of the contents.
74 class SainityCheck
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 < SainityCheck
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 < SainityCheck
148 def initialize
149 super
151 @name = "EmptyStringAssignment"
152 @title = "Assignment To 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 < SainityCheck
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 purpuse 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 < SainityCheck
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 unnescesarry 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 < SainityCheck
218 def initialize
219 super
221 @name = "Assert"
222 @type = "Consistancy"
223 @desc = "wxASSERT()s should be used rather than normal assert()s "
224 @desc += "for the sake of consistancy."
227 def parse_file(file, lines)
228 results = lines.select do |line|
229 line.text =~ /assert\s*\(/
232 add_results( file, results )
238 class PassByValue < SainityCheck
239 def initialize
240 super
242 @name = "PassByValue"
243 @title = "Pass By Value"
244 @type = "Good Practice"
245 @desc = "Passing objects by value means an extra overhead for large datatypes. "
246 @desc += "Therefore should these always be passed by const reference when possible."
247 @desc += "Non-const references should only be used for functions where the function is "
248 @desc += "supposed to change the actual value of the argument and return another or no value."
251 def parse_file(file, lines)
252 results = Array.new
254 # Only handle header files
255 if file =~ /\.h$/
256 # Items that should be passed by const-ref
257 items = [ "wxString", "wxRect", "wxPoint", "CMD4Hash" ]
259 lines.each do |line|
260 # Try to identify function definitions
261 if line.text =~ /^\s*(virtual|static|inline|)\s*\w+\s+\w+\s*\(.*\)/
262 # Split by arguments
263 line.text.match(/\(.*\)/)[0].split(",").each do |str|
264 items.each do |item|
265 if str =~ /#{item}\s*[^\s\*]/ and not str =~ /const/
266 results.push( line )
274 add_results( file, results )
280 class CStr < SainityCheck
281 def initialize
282 super
284 @name = "CStr"
285 @title = "C_Str or GetData"
286 @type = "Unicoding"
287 @desc = "Checks for usage of c_str() or GetData(). Using c_str will often result in "
288 @desc += "problems on Unicoded builds and should therefore be avoided. "
289 @desc += "Please note that the GetData check isn't that precise, because many other "
290 @desc += "classes have GetData members, so it does some crude filtering."
293 def parse_file(file, lines)
294 results = lines.select do |line|
295 if line.text =~ /c_str\(\)/
296 true
297 else
298 line.text =~ /GetData\(\)/ and line.text =~ /(wxT\(|wxString|_\()/
302 add_results( file, results )
308 class IfNotDefined < SainityCheck
309 def initialize
310 super
312 @name = "IfDefined"
313 @title = "#if (!)defined"
314 @type = "Consistancy"
315 @desc = "Use #ifndef or #ifdef instead for reasons of simplicity."
318 def parse_file(file, lines)
319 results = lines.select do |line|
320 if line.text =~ /^#if.*[\!]?defined\(/
321 not line.text =~ /(\&\&|\|\|)/
325 add_results( file, results )
331 class GPLLicense < SainityCheck
332 def initialize
333 super
335 @name = "MissingGPL"
336 @title = "Missing GPL License"
337 @type = "License"
338 @desc = "All header files should contain the proper GPL blorb."
341 def parse_file(file, lines)
342 if file =~ /\.h$/
343 if lines.find { |x| x.text =~ /This (program|library) is free software;/ } == nil
344 add_results( file )
352 class Copyright < SainityCheck
353 def initialize
354 super
356 @name = "MissingCopyright"
357 @title = "Missing Copyright Notice"
358 @type = "License"
359 @desc = "All files should contain the proper Copyright notice."
362 def parse_file(file, lines)
363 if file =~ /\.h$/
364 found = lines.select do |line|
365 line.text =~ /Copyright\s*\([cC]\)\s*[-\d,]+ aMule (Project|Team)/
368 if found.empty? then
369 add_results( file )
377 class PartOfAmule < SainityCheck
378 def initialize
379 super
381 @name = "aMuleNotice"
382 @title = "Missing aMule notice"
383 @type = "License"
384 @desc = "All files should contain a notice that they are part of the aMule project."
387 def parse_file(file, lines)
388 if file =~ /\.h$/
389 found = lines.select do |line|
390 line.text =~ /This file is part of the aMule Project/ or
391 line.text =~ /This file is part of aMule/
394 if found.empty? then
395 add_results( file )
403 class MissingBody < SainityCheck
404 def initialize
405 super
407 @name = "MissingBody"
408 @title = "Missing Body in Loop"
409 @type = "Garbage"
410 @desc = "This checks looks for loops without any body. For example \"while(true);\" "
411 @desc += "In most cases this is a sign of either useless code or bugs. Only in a few "
412 @desc += "cases is it valid code, and in those it can often be represented clearer "
413 @desc += "in other ways."
416 def parse_file(file, lines)
417 results = lines.select do |line|
418 if line.text =~ /^[^}]*while\s*\(.*\)\s*;/ or
419 line.text =~ /^\s*for\s*\(.*\)\s*;[^\)]*$/
420 # Avoid returning "for" spanning multiple lines
421 # TODO A better way to count instances
422 line.text.split("(").size == line.text.split(")").size
423 else
424 false
428 add_results( file, results )
434 class Translation < SainityCheck
435 def initialize
436 super
438 @name = "Translation"
439 @type = "Consistancy"
440 @desc = "Calls to AddLogLineM should translate the message, whereas "
441 @desc += "calls to AddDebugLogLine shouldn't. This is because the user "
442 @desc += "is meant to see normal log lines, whereas the the debug-lines "
443 @desc += "are only meant for the developers and I dont know about you, but "
444 @desc += "I dont plan on learning every language we choose to translate "
445 @desc += "aMule to. :P"
448 def parse_file(file, lines)
449 results = lines.select do |line|
450 if line.text =~ /\"/
451 line.text =~ /AddLogLine(M|)\(.*wxT\(/ or
452 line.text =~ /AddDebugLogLine(M|)\(.*_\(/
453 else
454 false
458 add_results( file, results )
464 class IfZero < SainityCheck
465 def initialize
466 super
468 @name = "PreIfConstant"
469 @title = "#if 0-9"
470 @type = "Garbage"
471 @desc = "Disabled code should be removed as soon as possible. If you wish to disable code "
472 @desc += "for only a short period, then please add a comment before the #if. Code with #if [1-9] "
473 @desc += "should be left, but the #ifs removed unless there is a pressing need for them."
476 def parse_file(file, lines)
477 results = lines.select do |line|
478 line.text =~ /#if\s+[0-9]/
481 add_results( file, results )
487 class InlinedIfTrue < SainityCheck
488 def initialize
489 super
491 @name = "InlinedIf"
492 @name = "Inlined If true/false"
493 @type = "Garbage"
494 @desc = "Using variations of (x ? true : false) or (x ? false : true) is just plain stupid."
497 def parse_file(file, lines)
498 results = lines.select do |line|
499 line.text =~ /\?\s*(true|false)\s*:\s*(true|false)/i
502 add_results( file, results )
508 class LoopOnConstant < SainityCheck
509 def initialize
510 super
512 @name = "LoopingOnConstant"
513 @title = "Looping On Constant"
514 @type = "Garbage"
515 @desc = "This checks detects loops that evaluate constant values "
516 @desc += "(true,false,0..) or \"for\" loops with no conditionals. all "
517 @desc += "are often a sign of poor code and in most cases can be avoided "
518 @desc += "or replaced with more elegant code."
521 def parse_file(file, lines)
522 results = lines.select do |line|
523 line.text =~ /while\s*\(\s*([0-9]+|true|false)\s*\)/i or
524 line.text =~ /for\s*\([^;]*;\s*;[^;]*\)/
527 add_results( file, results )
533 class MissingImplementation < SainityCheck
534 def initialize
535 super
537 @name = "MissingImplementation"
538 @title = "Missing Function Implementation"
539 @type = "Garbage"
540 @desc = "Forgetting to remove a function-definition after having removed it "
541 @desc += "from the .cpp file only leads to cluttered header files. The only "
542 @desc += "case where non-implemented functions should be used is when it is "
543 @desc += "necesarry to prevent usage of for instance assignment between "
544 @desc += "instances of a class."
546 @declarations = Array.new
547 @definitions = Array.new
550 def results
551 @definitions.each do |definition|
552 @declarations.delete_if do |pair|
553 pair.first == definition
558 return @declarations.map do |pair|
559 Result.new( self, pair.last.first, pair.last.last )
563 def parse_file(file, lines)
564 if file =~ /\.h$/ then
565 level = 0
566 tree = Array.new
568 lines.each do |line|
569 level += line.text.count( "{" ) - line.text.count( "}" )
571 tree.delete_if do |struct|
572 struct.first > level
576 if line.text !~ /^\s*\/\// and line.text !~ /^\s*#/ and line.text !~ /typedef/ then
577 if line.text =~ /^\s*(class|struct)\s+/ and line.text.count( ";" ) == 0 then
578 cur_level = level;
580 if line.text.count( "{" ) == 0 then
581 cur_level += 1
584 name = line.text.scan( /^\s*(class|struct)\s+([^\:{;]+)/ )
585 if name != [] then
586 name = name.first.last.strip
587 else
588 name = "Unknown at line " + line.number.to_s + " in " + file
591 tree << [ cur_level, name ]
592 elsif line.text =~ /;\s*$/ and line.text.count( "{" ) == 0 then
593 # No pure virtual functions and no return(blah) calls (which otherwise can fit the requirements)
594 if line.text !~ /=\s*0\s*;\s*$/ and line.text !~ /return/ then
595 re = /^\s*(virtual\s+|static\s+|inline\s+|)\w+(\s+[\*\&]?|[\*\&]\s+)(\w+)\(/.match( line.text )
597 if re and level > 0 and tree.last then
598 @declarations << [ tree.last.last + "::" + re[ re.length - 1 ], [ file, line ] ]
604 else
605 lines.each do |line|
606 if line.text =~ /\b\w+::\w+\s*\([^;]+$/
607 @definitions << line.text.scan( /\b(\w+::\w+)\s*\([^;]+$/ ).first.first
609 end
620 # List of enabled filters
621 filterList = Array.new
622 filterList.push CompareAgainstEmptyString.new
623 filterList.push AssignmentToEmptyString.new
624 filterList.push NoIfNDef.new
625 filterList.push ThisDeference.new
626 filterList.push Assert.new
627 filterList.push PassByValue.new
628 filterList.push CStr.new
629 filterList.push IfNotDefined.new
630 filterList.push GPLLicense.new
631 filterList.push Copyright.new
632 filterList.push PartOfAmule.new
633 filterList.push MissingBody.new
634 filterList.push Translation.new
635 filterList.push IfZero.new
636 filterList.push InlinedIfTrue.new
637 filterList.push LoopOnConstant.new
638 filterList.push MissingImplementation.new
641 # Sort enabled filters by type and name. The reason why this is done here is
642 # because it's much easier than manually resorting every time I add a filter
643 # or change the name or type of an existing filter.
644 filterList.sort! do |x,y|
645 cmp = x.type <=> y.type
647 if cmp == 0 then
648 x.title <=> y.title
649 else
656 def parse_files( path, filters )
657 filters = filters.dup
659 require "find"
661 Find.find( path ) do |filename|
662 if filename =~ /\.(cpp|h)$/ and not IsFiltered(filename) then
663 File.open(filename, "r") do |aFile|
664 # Read lines and add line-numbers
665 lines = Array.new
666 aFile.each_line do |line|
667 lines.push( Line.new( aFile.lineno, line ) )
670 lines.freeze
672 # Check the file against each filter
673 filters.each do |filter|
674 # Process the file with this filter
675 filter.parse_file( filename, lines )
681 results = Array.new
682 filters.each do |filter|
683 results += filter.results
686 results
692 # Helper-function
693 def get_val( key, list )
694 if not list.last or list.last.first != key then
695 list << [ key, Array.new ]
698 list.last.last
703 def create_result_tree( path, filters )
704 # Gather the results
705 results = parse_files( path, filters )
707 # Sort the results by the following sequence of variables: Path -> File -> Filter -> Line
708 results.sort! do |a, b|
709 if (a.file_path <=> b.file_path) == 0 then
710 if (a.file_name <=> b.file_name) == 0 then
711 if (a.type.title <=> b.type.title) == 0 then
712 a.line.number <=> b.line.number
713 else
714 a.type.title <=> b.type.title
716 else
717 a.file_name <=> b.file_name
719 else
720 a.file_path <=> b.file_path
725 # Create a tree of results: [ Path, [ File, [ Filter, [ Line ] ] ] ]
726 result_tree = Array.new
727 results.each do |result|
728 get_val( result.type, get_val( result.file_name, get_val( result.file_path, result_tree ) ) ) << result
732 result_tree
737 def create_filter_tree( filters )
738 # Change the filterList to a tree: [ Type, [ Filter ] ]
739 filter_tree = Array.new
741 filters.each do |filter|
742 get_val( filter.type, filter_tree ) << filter
745 filter_tree
750 # Converts a number to a string and pads with zeros so that length becomes at least 5
751 def PadNum( number )
752 num = number.to_s
754 if ( num.size < 5 )
755 ( "0" * ( 5 - num.size ) ) + num
756 else
763 # Helper-function that escapes some chars to HTML codes
764 def HTMLEsc( str )
765 str.gsub!( /\&/, "&amp;" )
766 str.gsub!( /\"/, "&quot;" )
767 str.gsub!( /</, "&lt;" )
768 str.gsub!( />/, "&gt;" )
769 str.gsub!( /\n/, "<br>" )
770 str.gsub( /\t/, "&nbsp;" )
775 # Fugly output code goes here
776 # ... Abandon hope, yee who read past here
777 # TODO Enable use of templates.
778 # TODO Abandon hope.
779 def OutputHTML( filters, results )
780 text =
781 "<html>
782 <head>
783 <STYLE TYPE=\"text/css\">
784 <!--
785 .dir {
786 background-color: \#A0A0A0;
787 padding-left: 10pt;
788 padding-right: 10pt;
790 .file {
791 background-color: \#838383;
792 padding-left: 10pt;
793 padding-right: 10pt;
795 .filter {
796 background-color: #757575;
797 padding-left: 10pt;
798 padding-right: 10pt;
799 padding-top: 5pt;
802 </STYLE>
803 </head>
804 <body bgcolor=\"\#BDBDBD\">
806 <h1>Filters</h1>
807 <dl>
809 # List the filters
810 filters.each do |filterType|
811 text +=
812 " <dt><b>#{filterType.first}</b></dt>
813 <dd>
814 <dl>
816 filterType.last.each do |filter|
817 text +=
818 " <dt id=\"#{filter.name}\"><i>#{filter.title}</i></dt>
819 <dd>
820 #{HTMLEsc(filter.desc)}<p>
821 </dd>
825 text +=
826 " </dl>
827 </dd>
831 text +=
832 " </dl>
836 <h1>Directories</h1>
837 <ul>
840 # List the directories
841 results.each do |dir|
842 text +=
843 " <li>
844 <a href=\"\##{dir.first}\">#{dir.first}</a>
845 </li>
849 text +=
850 " </ul>
854 <h1>Results</h1>
857 results.each do |dir|
858 text +=
859 " <div class=\"dir\">
860 <h2 id=\"#{dir.first}\">#{dir.first}</h2>
863 dir.last.each do |file|
864 text +=
865 " <div class=\"file\">
866 <h3>#{file.first}</h3>
868 <ul>
871 file.last.each do |filter|
872 text +=
873 " <li>
874 <div class=\"filter\">
875 <b><a href=\"\##{filter.first.name}\">#{filter.first.title}</a></b>
877 <ul>
880 filter.last.each do |result|
881 if result.line then
882 text +=
883 " <li><b>#{PadNum(result.line.number)}:</b> #{HTMLEsc(result.line.text.strip)}</li>
888 text +=
889 " </ul>
890 </div>
891 </li>
894 text +=
895 " </ul>
896 </div>
900 text +=
901 " </div>
907 text +=
908 " </body>
909 </html>"
911 return text;
916 # Columnizing, using the http://www.rubygarden.org/ruby?UsingTestUnit example because I'm lazy
917 # TODO Rewrite it to better support newlines and stuff
918 def Columnize( text, width, indent )
919 return indent + text.scan(/(.{1,#{width}})(?: |$)/).join("\n#{indent}")
924 # Fugly output code also goes here, this is a bit more sparse than the HTML stuff
925 def OutputTEXT( filters, results )
927 # List the filters
928 text = "Filters\n"
929 filters.each do |filterType|
930 text += "\t* #{filterType.first}\n"
932 filterType.last.each do |filter|
933 text += "\t\t- #{filter.title}\n"
935 text += Columnize( filter.desc, 80, "\t\t\t" ) + "\n\n"
939 # List the directories
940 text += "\n\nDirectories\n"
941 results.each do |dir|
942 text += "\t#{dir.first}\n"
945 text += "\n\nResults\n"
947 # To avoid bad readability, I only use fullpaths here instead of sections per dir
948 results.each do |dir|
949 dir.last.each do |file|
950 text += "\t#{dir.first}#{file.first}\n"
952 file.last.each do |filter|
953 text += "\t\t* #{filter.first.title}\n"
955 filter.last.each do |result|
956 if result.line then
957 text += "\t\t\t#{PadNum(result.line.number)}: #{result.line.text.strip}\n"
962 text += "\n"
966 return text;
971 #TODO Improved parameter-handling, add =<file> for the outputing to a file
972 ARGV.each do |param|
973 case param
974 when "--text" then
975 puts OutputTEXT( create_filter_tree( filterList ), create_result_tree( ".", filterList ) )
976 when "--html" then
977 puts OutputHTML( create_filter_tree( filterList ), create_result_tree( ".", filterList ) )