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/)
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.
37 def initialize( number
, text
)
49 def initialize( type
, file
, line
= nil )
56 @file.slice( /[^\/]+$/ )
60 @file.slice( /^.*\// )
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.
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}"
111 def add_results( file
, lines
= [nil] )
113 @results << Result
.new( self, file
, line
)
122 class CompareAgainstEmptyString
< SanityCheck
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
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
)
162 results
= lines
.select
do |line
|
163 line
.text
=~
/[^=!]=\s*(wxEmptyString|wxT\(""\)|_\(""\))/
166 add_results( file
, results
)
173 class NoIfNDef
< SanityCheck
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
195 class ThisDeference
< SanityCheck
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
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
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
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
)
276 # Only handle header files
278 # Items that should be passed by const-ref
279 items
= [ "wxString", "wxRect", "wxPoint", "CMD4Hash", "CPath" ]
282 # Try to identify function definitions
283 if line
.text
=~
/^\s*(virtual|static|inline|)\s*\w+\s+\w+\s*\(.*\)/
285 line
.text
.match(/\(.*\)/)[0].split(",").each
do |str
|
287 if str
=~
/#{item}\s*[^\s\*]/ and not str
=~
/const/
296 add_results( file
, results
)
302 class CStr
< SanityCheck
307 @title = "C_Str or GetData"
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\(/
320 line
.text
=~
/GetData\(\)/ and line
.text
=~
/(wxT\(|wxString|_\()/
324 add_results( file
, results
)
330 class IfNotDefined
< SanityCheck
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
358 @title = "Missing GPL License"
360 @desc = "All header files should contain the proper GPL blorb."
363 def parse_file(file
, lines
)
365 if lines
.find
{ |x
| x
.text
=~
/This (program|library) is free software;/ } == nil
374 class Copyright
< SanityCheck
378 @name = "MissingCopyright"
379 @title = "Missing Copyright Notice"
381 @desc = "All files should contain the proper Copyright notice."
384 def parse_file(file
, lines
)
386 found
= lines
.select
do |line
|
387 line
.text
=~
/Copyright\s*\([cC]\)\s*[-\d,]+ aMule (Project|Team)/
399 class PartOfAmule
< SanityCheck
403 @name = "aMuleNotice"
404 @title = "Missing aMule notice"
406 @desc = "All files should contain a notice that they are part of the aMule project."
409 def parse_file(file
, lines
)
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/
425 class MissingBody
< SanityCheck
429 @name = "MissingBody"
430 @title = "Missing Body in Loop"
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
450 add_results( file
, results
)
456 class Translation
< SanityCheck
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
|
473 line
.text
=~
/AddLogLine.?.?\(.*wxT\(/ or
474 line
.text
=~
/AddDebugLogLine.?\(.*_\(/
480 add_results( file
, results
)
486 class IfZero
< SanityCheck
490 @name = "PreIfConstant"
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
514 @title = "Inlined If true/false"
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
534 @name = "LoopingOnConstant"
535 @title = "Looping On Constant"
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
559 @name = "MissingImplementation"
560 @title = "Missing Function Implementation"
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
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
591 level
+= line
.text
.count( "{" ) - line
.text
.count( "}" )
593 tree
.delete_if
do |struct
|
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
602 if line
.text
.count( "{" ) == 0 then
606 name
= line
.text
.scan( /^\s*(class|struct)\s+([^\:{;]+)/ )
608 name
= name
.first
.last
.strip
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
] ]
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
679 def parse_files( path
, filters
)
680 filters
= filters
.dup
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
689 aFile
.each_line
do |line
|
690 lines
.push( Line
.new( aFile
.lineno
, line
) )
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
)
705 filters
.each
do |filter
|
706 results
+= filter
.results
716 def get_val( key
, list
)
717 if not list
.last
or list
.last
.first
!= key
then
718 list
<< [ key
, Array
.new
]
726 def create_result_tree( path
, filters
)
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
737 a
.type
.title
<=> b
.type
.title
740 a
.file_name
<=> b
.file_name
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
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
773 # Converts a number to a string and pads with zeros so that length becomes at least 5
778 ( "0" * ( 5 - num
.size
) ) + num
786 # Helper-function that escapes some chars to HTML codes
788 str
.gsub
!( /\&/, "&" )
789 str
.gsub
!( /\"/, """ )
790 str
.gsub
!( /</, "<" )
791 str
.gsub
!( />/, ">" )
792 str
.gsub
!( /\n/, "<br>" )
793 str
.gsub( /\t/, " " )
798 # Fugly output code goes here
799 # ... Abandon hope, yee who read past here
800 # TODO Enable use of templates.
802 def OutputHTML( filters
, results
)
806 <STYLE TYPE=\"text/css\">
809 background-color: \#A0A0A0;
814 background-color: \#838383;
819 background-color: #757575;
827 <body bgcolor=\"\#BDBDBD\">
833 filters
.each
do |filterType
|
835 " <dt><b>#{filterType.first}</b></dt>
839 filterType
.last
.each
do |filter
|
841 " <dt id=\"#{filter.name}\"><i>#{filter.title}</i></dt>
843 #{HTMLEsc(filter.desc)}<p>
863 # List the directories
864 results
.each
do |dir
|
867 <a href=\"\##{dir.first}\">#{dir.first}</a>
880 results
.each
do |dir
|
882 " <div class=\"dir\">
883 <h2 id=\"#{dir.first}\">#{dir.first}</h2>
886 dir
.last
.each
do |file
|
888 " <div class=\"file\">
889 <h3>#{file.first}</h3>
894 file
.last
.each
do |filter
|
897 <div class=\"filter\">
898 <b><a href=\"\##{filter.first.name}\">#{filter.first.title}</a></b>
903 filter
.last
.each
do |result
|
906 " <li><b>#{PadNum(result.line.number)}:</b> #{HTMLEsc(result.line.text.strip)}</li>
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
)
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
|
980 text
+= "\t\t\t#{PadNum(result.line.number)}: #{result.line.text.strip}\n"
994 #TODO Improved parameter-handling, add =<file> for the outputing to a file
998 puts
OutputTEXT( create_filter_tree( filterList
), create_result_tree( ".", filterList
) )
1000 puts
OutputHTML( create_filter_tree( filterList
), create_result_tree( ".", filterList
) )