simplabs-excellent 1.0.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. data/History.txt +19 -0
  2. data/README.rdoc +34 -0
  3. data/VERSION.yml +1 -1
  4. data/bin/excellent +21 -6
  5. data/lib/simplabs/excellent.rb +7 -5
  6. data/lib/simplabs/excellent/checks.rb +18 -1
  7. data/lib/simplabs/excellent/checks/abc_metric_method_check.rb +19 -56
  8. data/lib/simplabs/excellent/checks/assignment_in_conditional_check.rb +16 -16
  9. data/lib/simplabs/excellent/checks/base.rb +30 -21
  10. data/lib/simplabs/excellent/checks/case_missing_else_check.rb +13 -5
  11. data/lib/simplabs/excellent/checks/class_line_count_check.rb +10 -8
  12. data/lib/simplabs/excellent/checks/class_name_check.rb +11 -10
  13. data/lib/simplabs/excellent/checks/control_coupling_check.rb +13 -10
  14. data/lib/simplabs/excellent/checks/cyclomatic_complexity_block_check.rb +25 -9
  15. data/lib/simplabs/excellent/checks/cyclomatic_complexity_check.rb +4 -20
  16. data/lib/simplabs/excellent/checks/cyclomatic_complexity_method_check.rb +25 -10
  17. data/lib/simplabs/excellent/checks/duplication_check.rb +50 -0
  18. data/lib/simplabs/excellent/checks/empty_rescue_body_check.rb +10 -18
  19. data/lib/simplabs/excellent/checks/flog_block_check.rb +40 -0
  20. data/lib/simplabs/excellent/checks/flog_check.rb +27 -0
  21. data/lib/simplabs/excellent/checks/flog_class_check.rb +40 -0
  22. data/lib/simplabs/excellent/checks/flog_method_check.rb +40 -0
  23. data/lib/simplabs/excellent/checks/for_loop_check.rb +20 -4
  24. data/lib/simplabs/excellent/checks/line_count_check.rb +3 -21
  25. data/lib/simplabs/excellent/checks/method_line_count_check.rb +9 -7
  26. data/lib/simplabs/excellent/checks/method_name_check.rb +13 -9
  27. data/lib/simplabs/excellent/checks/module_line_count_check.rb +9 -7
  28. data/lib/simplabs/excellent/checks/module_name_check.rb +11 -7
  29. data/lib/simplabs/excellent/checks/name_check.rb +3 -8
  30. data/lib/simplabs/excellent/checks/nested_iterators_check.rb +33 -0
  31. data/lib/simplabs/excellent/checks/parameter_number_check.rb +13 -12
  32. data/lib/simplabs/excellent/checks/rails.rb +17 -0
  33. data/lib/simplabs/excellent/checks/rails/attr_accessible_check.rb +38 -0
  34. data/lib/simplabs/excellent/checks/rails/attr_protected_check.rb +39 -0
  35. data/lib/simplabs/excellent/checks/singleton_variable_check.rb +32 -0
  36. data/lib/simplabs/excellent/extensions/sexp.rb +21 -0
  37. data/lib/simplabs/excellent/extensions/string.rb +23 -0
  38. data/lib/simplabs/excellent/parsing.rb +12 -0
  39. data/lib/simplabs/excellent/parsing/abc_measure.rb +52 -0
  40. data/lib/simplabs/excellent/parsing/block_context.rb +43 -0
  41. data/lib/simplabs/excellent/parsing/call_context.rb +36 -0
  42. data/lib/simplabs/excellent/parsing/case_context.rb +31 -0
  43. data/lib/simplabs/excellent/parsing/class_context.rb +68 -0
  44. data/lib/simplabs/excellent/parsing/code_processor.rb +154 -0
  45. data/lib/simplabs/excellent/parsing/conditional_context.rb +25 -0
  46. data/lib/simplabs/excellent/parsing/cvar_context.rb +28 -0
  47. data/lib/simplabs/excellent/parsing/cyclomatic_complexity_measure.rb +73 -0
  48. data/lib/simplabs/excellent/parsing/flog_measure.rb +192 -0
  49. data/lib/simplabs/excellent/parsing/for_loop_context.rb +15 -0
  50. data/lib/simplabs/excellent/parsing/if_context.rb +38 -0
  51. data/lib/simplabs/excellent/parsing/method_context.rb +50 -0
  52. data/lib/simplabs/excellent/parsing/module_context.rb +29 -0
  53. data/lib/simplabs/excellent/{core → parsing}/parser.rb +4 -2
  54. data/lib/simplabs/excellent/parsing/resbody_context.rb +39 -0
  55. data/lib/simplabs/excellent/parsing/scopeable.rb +34 -0
  56. data/lib/simplabs/excellent/parsing/sexp_context.rb +125 -0
  57. data/lib/simplabs/excellent/parsing/singleton_method_context.rb +55 -0
  58. data/lib/simplabs/excellent/parsing/until_context.rb +24 -0
  59. data/lib/simplabs/excellent/parsing/while_context.rb +24 -0
  60. data/lib/simplabs/excellent/runner.rb +105 -0
  61. data/lib/simplabs/excellent/warning.rb +53 -0
  62. data/spec/checks/abc_metric_method_check_spec.rb +36 -8
  63. data/spec/checks/assignment_in_conditional_check_spec.rb +31 -14
  64. data/spec/checks/case_missing_else_check_spec.rb +8 -8
  65. data/spec/checks/class_line_count_check_spec.rb +24 -11
  66. data/spec/checks/class_name_check_spec.rb +9 -9
  67. data/spec/checks/control_coupling_check_spec.rb +84 -13
  68. data/spec/checks/cyclomatic_complexity_block_check_spec.rb +13 -17
  69. data/spec/checks/cyclomatic_complexity_method_check_spec.rb +32 -6
  70. data/spec/checks/duplication_check_spec.rb +139 -0
  71. data/spec/checks/empty_rescue_body_check_spec.rb +54 -16
  72. data/spec/checks/flog_block_check_spec.rb +28 -0
  73. data/spec/checks/flog_class_check_spec.rb +28 -0
  74. data/spec/checks/flog_method_check_spec.rb +46 -0
  75. data/spec/checks/for_loop_check_spec.rb +11 -11
  76. data/spec/checks/method_line_count_check_spec.rb +11 -12
  77. data/spec/checks/method_name_check_spec.rb +34 -13
  78. data/spec/checks/module_line_count_check_spec.rb +11 -12
  79. data/spec/checks/module_name_check_spec.rb +31 -7
  80. data/spec/checks/nested_iterators_check_spec.rb +44 -0
  81. data/spec/checks/parameter_number_check_spec.rb +48 -12
  82. data/spec/checks/rails/attr_accessible_check_spec.rb +79 -0
  83. data/spec/checks/rails/attr_protected_check_spec.rb +77 -0
  84. data/spec/checks/singleton_variable_check_spec.rb +66 -0
  85. data/spec/{core/extensions/underscore_spec.rb → extensions/string_spec.rb} +1 -1
  86. metadata +58 -15
  87. data/README.markdown +0 -30
  88. data/lib/simplabs/excellent/checks/class_variable_check.rb +0 -25
  89. data/lib/simplabs/excellent/core.rb +0 -2
  90. data/lib/simplabs/excellent/core/checking_visitor.rb +0 -34
  91. data/lib/simplabs/excellent/core/error.rb +0 -31
  92. data/lib/simplabs/excellent/core/extensions/underscore.rb +0 -27
  93. data/lib/simplabs/excellent/core/iterator_visitor.rb +0 -29
  94. data/lib/simplabs/excellent/core/parse_tree_runner.rb +0 -88
  95. data/lib/simplabs/excellent/core/visitable_sexp.rb +0 -31
  96. data/spec/checks/class_variable_check_spec.rb +0 -26
@@ -0,0 +1,34 @@
1
+ module Simplabs
2
+
3
+ module Excellent
4
+
5
+ module Parsing
6
+
7
+ module Scopeable #:nodoc:
8
+
9
+ private
10
+
11
+ def get_names
12
+ if @exp[1].is_a?(Sexp)
13
+ name = @exp[1].pop.to_s.strip
14
+ [name, "#{extract_prefixes}#{name}"]
15
+ else
16
+ [@exp[1].to_s, nil]
17
+ end
18
+ end
19
+
20
+ def extract_prefixes(exp = @exp[1].deep_clone, prefix = '')
21
+ prefix = "#{exp.pop}::#{prefix}" if exp.last.is_a?(Symbol)
22
+ if exp.last.is_a?(Sexp)
23
+ prefix = extract_prefixes(exp.last, prefix)
24
+ end
25
+ prefix
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,125 @@
1
+ module Simplabs
2
+
3
+ module Excellent
4
+
5
+ module Parsing
6
+
7
+ # For most nodes the Excellent processor processes, it will create the corresponding context that contains meta information of the processed
8
+ # node. This is the base class for all these contexts.
9
+ #
10
+ # === Example
11
+ #
12
+ # For a method like the following:
13
+ #
14
+ # module Shop
15
+ # class Basket
16
+ # def buy_product(product)
17
+ # other_method
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ # four context will be generated:
23
+ #
24
+ # ModuleContext
25
+ # name: 'Shop'
26
+ # full_name: 'Shop'
27
+ # parent: nil
28
+ # ClassContext
29
+ # name: 'Basket'
30
+ # full_name: 'Shop::Basket'
31
+ # parent: ModuleContext
32
+ # MethodContext
33
+ # name: 'buy_product'
34
+ # full_name: 'Shop::Basket#buy_product'
35
+ # parent: ClassContext
36
+ # parameters: [:product]
37
+ # CallContext (other_method)
38
+ # name: nil
39
+ # full_name: nil
40
+ # parent: MethodContext
41
+ # method: :other_method
42
+ #
43
+ # === Custom Processors
44
+ #
45
+ # The Excelent processor will also invoke custom processor methods on the contexts if they are defined. To process call nodes in the context for
46
+ # example, you could simply define a +process_call+ method in the context that will be invoked with each call Sexp (S-expression, see
47
+ # http://en.wikipedia.org/wiki/S_expression) that is processed by the Excellent processor.
48
+ #
49
+ # def process_call(exp)
50
+ # super
51
+ # do_something()
52
+ # end
53
+ #
54
+ # Custom <b>processor methods must always call +super+</b> since there might be several processor methods defined in several modules that are in the
55
+ # included in the context and all of these have to be invoked. Also <b>processor methods must not modify the passed Sexp</b> since other processor
56
+ # methods also need the complete Sexp. If you have to modify the Sexp in a processor method, deep clone it:
57
+ #
58
+ # exp = exp.deep_clone
59
+ #
60
+ class SexpContext
61
+
62
+ # The parent context
63
+ attr_reader :parent
64
+
65
+ # The name of the code fragment the context is bound to (e.g. 'User' for a +class+)
66
+ attr_reader :name
67
+
68
+ # The file the code fragment was read from
69
+ attr_reader :file
70
+
71
+ # The line the code fragment is located at
72
+ attr_reader :line
73
+
74
+ # Initializes a SexpContext.
75
+ #
76
+ # Always call +super+ in inherited custom contexts!
77
+ #
78
+ # === Parameters
79
+ #
80
+ # * <tt>exp</tt> - The Sexp (S-expression, see http://en.wikipedia.org/wiki/S_expression) the context is created for.
81
+ # * <tt>parent</tt> - The parent context.
82
+ def initialize(exp, parent = nil)
83
+ @exp = exp
84
+ @parent = parent
85
+ @file = exp.file
86
+ @line = exp.line
87
+ @full_name = nil
88
+ end
89
+
90
+ # Gets the full name of the code fragment the context is bound to. For a method +name+ might be '+add_product+' while +full_name+ might be
91
+ # 'Basket#add_product'.
92
+ def full_name
93
+ return @full_name if @full_name
94
+ return @name if @parent.blank?
95
+ "#{@parent.full_name}::#{@name}"
96
+ end
97
+
98
+ def method_missing(method, *args) #:nodoc:
99
+ return if method.to_s =~ /^process_[a-zA-Z0-9_]+$/
100
+ super
101
+ end
102
+
103
+ private
104
+
105
+ def count_lines(node = @exp, line_numbers = [])
106
+ count = 0
107
+ line_numbers << node.line
108
+ node.children.each { |child| count += count_lines(child, line_numbers) }
109
+ line_numbers.uniq.length
110
+ end
111
+
112
+ def has_assignment?(exp = @exp[1])
113
+ found_assignment = false
114
+ found_assignment = found_assignment || exp.node_type == :lasgn
115
+ exp.children.each { |child| found_assignment = found_assignment || has_assignment?(child) }
116
+ found_assignment
117
+ end
118
+
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+
125
+ end
@@ -0,0 +1,55 @@
1
+ require 'simplabs/excellent/parsing/cyclomatic_complexity_measure'
2
+ require 'simplabs/excellent/parsing/abc_measure'
3
+
4
+ module Simplabs
5
+
6
+ module Excellent
7
+
8
+ module Parsing
9
+
10
+ class SingletonMethodContext < MethodContext #:nodoc:
11
+
12
+ include CyclomaticComplexityMeasure
13
+ include AbcMeasure
14
+
15
+ attr_reader :parameters
16
+ attr_reader :calls
17
+
18
+ def initialize(exp, parent)
19
+ super
20
+ @name = exp[2].to_s
21
+ @full_name = get_full_name
22
+ @calls = Hash.new(0)
23
+ end
24
+
25
+ def full_name
26
+ return @full_name if @full_name
27
+ parent = @parent.is_a?(BlockContext) ? @parent.parent : @parent
28
+ return @name if parent.blank?
29
+ "#{parent.full_name}.#{@name}"
30
+ end
31
+
32
+ def record_call_to(exp)
33
+ @calls[exp] += 1
34
+ end
35
+
36
+ private
37
+
38
+ def get_full_name
39
+ if @exp[1].is_a?(Sexp)
40
+ if @exp[1].node_type == :call
41
+ return "#{@exp[1][2]}.#{@name}"
42
+ elsif @exp[1].node_type == :const
43
+ return "#{@exp[1][1]}.#{@name}"
44
+ end
45
+ end
46
+ nil
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,24 @@
1
+ module Simplabs
2
+
3
+ module Excellent
4
+
5
+ module Parsing
6
+
7
+ class UntilContext < SexpContext #:nodoc:
8
+
9
+ def initialize(exp, parent)
10
+ super
11
+ @contains_assignment = has_assignment?
12
+ end
13
+
14
+ def tests_assignment?
15
+ @contains_assignment
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,24 @@
1
+ module Simplabs
2
+
3
+ module Excellent
4
+
5
+ module Parsing
6
+
7
+ class WhileContext < SexpContext #:nodoc:
8
+
9
+ def initialize(exp, parent)
10
+ super
11
+ @contains_assignment = has_assignment?
12
+ end
13
+
14
+ def tests_assignment?
15
+ @contains_assignment
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,105 @@
1
+ require 'pp'
2
+ require 'yaml'
3
+ require 'simplabs/excellent/parsing/parser'
4
+ require 'simplabs/excellent/parsing/code_processor'
5
+
6
+ module Simplabs
7
+
8
+ module Excellent
9
+
10
+ # The Runner is the interface to invoke parsing and processing of source code. You can pass either a String containing the code to process or the
11
+ # name of a file to read the code to process from.
12
+ class Runner
13
+
14
+ DEFAULT_CONFIG = {
15
+ :AssignmentInConditionalCheck => { },
16
+ :CaseMissingElseCheck => { },
17
+ :ClassLineCountCheck => { :threshold => 300 },
18
+ :ClassNameCheck => { :pattern => /^[A-Z][a-zA-Z0-9]*$/ },
19
+ :SingletonVariableCheck => { },
20
+ :CyclomaticComplexityBlockCheck => { :complexity => 4 },
21
+ :CyclomaticComplexityMethodCheck => { :complexity => 8 },
22
+ :EmptyRescueBodyCheck => { },
23
+ :ForLoopCheck => { },
24
+ :MethodLineCountCheck => { :line_count => 20 },
25
+ :MethodNameCheck => { :pattern => /^[_a-z<>=\[|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/ },
26
+ :ModuleLineCountCheck => { :line_count => 300 },
27
+ :ModuleNameCheck => { :pattern => /^[A-Z][a-zA-Z0-9]*$/ },
28
+ :ParameterNumberCheck => { :parameter_count => 3 },
29
+ :FlogMethodCheck => { },
30
+ :FlogBlockCheck => { },
31
+ :FlogClassCheck => { },
32
+ :'Rails::AttrProtectedCheck' => { },
33
+ :'Rails::AttrAccessibleCheck' => { }
34
+ }
35
+
36
+ attr_accessor :config #:nodoc:
37
+
38
+ # Initializes a Runner
39
+ #
40
+ # ==== Parameters
41
+ #
42
+ # * <tt>checks</tt> - The checks to apply - passed instances of the various check classes. If no checks are specified, all checks will be applied.
43
+ def initialize(*checks)
44
+ @config = DEFAULT_CONFIG
45
+ @checks = checks unless checks.empty?
46
+ @parser = Parsing::Parser.new
47
+ end
48
+
49
+ # Processes the +code+ and sets the file name of the warning to +filename+
50
+ #
51
+ # ==== Parameters
52
+ #
53
+ # * <tt>filename</tt> - The name of the file the code was read from.
54
+ # * <tt>code</tt> - The code to process (String).
55
+ def check(filename, code)
56
+ @checks ||= load_checks
57
+ @processor ||= Parsing::CodeProcessor.new(@checks)
58
+ node = parse(filename, code)
59
+ @processor.process(node)
60
+ end
61
+
62
+ # Processes the +code+, setting the file name of the warnings to '+dummy-file.rb+'
63
+ #
64
+ # ==== Parameters
65
+ #
66
+ # * <tt>code</tt> - The code to process (String).
67
+ def check_code(code)
68
+ check('dummy-file.rb', code)
69
+ end
70
+
71
+ # Processes the file +filename+. The code will be read from the file.
72
+ #
73
+ # ==== Parameters
74
+ #
75
+ # * <tt>filename</tt> - The name of the file to read the code from.
76
+ def check_file(filename)
77
+ check(filename, File.read(filename))
78
+ end
79
+
80
+ # Gets the warnings that were produced by the checks.
81
+ def warnings
82
+ @checks ||= []
83
+ @checks.collect { |check| check.warnings }.flatten
84
+ end
85
+
86
+ private
87
+
88
+ def parse(filename, code)
89
+ @parser.parse(code, filename)
90
+ end
91
+
92
+ def load_checks
93
+ check_objects = []
94
+ DEFAULT_CONFIG.each_pair do |key, value|
95
+ klass = eval("Simplabs::Excellent::Checks::#{key.to_s}")
96
+ check_objects << (value.empty? ? klass.new : klass.new(value))
97
+ end
98
+ check_objects
99
+ end
100
+
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,53 @@
1
+ require 'simplabs/excellent/extensions/string'
2
+
3
+ module Simplabs
4
+
5
+ module Excellent
6
+
7
+ # The warnings returned by Excellent provide information about the +file+ the *possibly* problematic code was found in as well as the +line+ of the
8
+ # occurence, a warning message and a Hash with specific information about the warning.
9
+ #
10
+ # Warnings also provide the message template that was used to generate the message. The message template contains tokens like <tt>{{token}}</tt> that
11
+ # correspond to the valeus in the +info+ hash, e.g.:
12
+ #
13
+ # '{{method}} has abc score of {{score}}.'
14
+ # { :method => 'User#full_name', :score => 10 }
15
+ class Warning
16
+
17
+ # The check that produced the warning
18
+ attr_reader :check
19
+
20
+ # The name of the file the check found the problematic code in
21
+ attr_reader :filename
22
+
23
+ # The file number where the check found the problematic code
24
+ attr_reader :line_number
25
+
26
+ # The warning message
27
+ attr_reader :message
28
+
29
+ # Additional info for the warning (see above)
30
+ attr_reader :info
31
+
32
+ # The template used to produce the warning (see above)
33
+ attr_reader :message_template
34
+
35
+ def initialize(check, message, filename, line_number, info) #:nodoc:
36
+ @check = check.to_s.underscore.to_sym
37
+ @info = info
38
+ @filename = filename
39
+ @line_number = line_number.to_i
40
+
41
+ @message = ''
42
+ if !message.nil?
43
+ @message_template = message
44
+ @info.each { |key, value| message.gsub!(/\{\{#{key}\}\}/, value.to_s) }
45
+ @message = message
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -3,14 +3,24 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
3
3
  describe Simplabs::Excellent::Checks::AbcMetricMethodCheck do
4
4
 
5
5
  before do
6
- @excellent = Simplabs::Excellent::Core::ParseTreeRunner.new(Simplabs::Excellent::Checks::AbcMetricMethodCheck.new({ :threshold => 0 }))
6
+ @excellent = Simplabs::Excellent::Runner.new(Simplabs::Excellent::Checks::AbcMetricMethodCheck.new({ :threshold => 0 }))
7
7
  end
8
8
 
9
9
  describe '#evaluate' do
10
10
 
11
11
  describe 'when processing assignments' do
12
12
 
13
- ['=', '*=', '/=', '%=', '+=', '<<=', '>>=', '&=', '|=', '^=', '-=', '**='].each do |assignment|
13
+ it "should find =" do
14
+ content = <<-END
15
+ def method_name
16
+ foo = 1
17
+ end
18
+ END
19
+
20
+ verify_content_score(content, 1, 0, 0)
21
+ end
22
+
23
+ ['*=', '/=', '%=', '+=', '<<=', '>>=', '&=', '|=', '^=', '-=', '**='].each do |assignment|
14
24
 
15
25
  it "should find #{assignment}" do
16
26
  content = <<-END
@@ -19,7 +29,8 @@ describe Simplabs::Excellent::Checks::AbcMetricMethodCheck do
19
29
  end
20
30
  END
21
31
 
22
- verify_content_score(content, 1, 0, 0)
32
+ # these special assignments have score 2 since before the value is assigned, a method is called on the old value
33
+ verify_content_score(content, 1, 0, 1)
23
34
  end
24
35
 
25
36
  end
@@ -78,17 +89,34 @@ describe Simplabs::Excellent::Checks::AbcMetricMethodCheck do
78
89
 
79
90
  end
80
91
 
92
+ it 'should also work on singleton methods' do
93
+ content = <<-END
94
+ class Class
95
+ def self.method_name
96
+ foo = 1
97
+ end
98
+ end
99
+ END
100
+ @excellent.check_content(content)
101
+ warnings = @excellent.warnings
102
+
103
+ warnings.should_not be_empty
104
+ warnings[0].info.should == { :method => 'Class.method_name', :score => 1.0 }
105
+ warnings[0].line_number.should == 2
106
+ warnings[0].message.should == "Class.method_name has abc score of 1.0."
107
+ end
108
+
81
109
  end
82
110
 
83
111
  def verify_content_score(content, a, b, c)
84
112
  score = Math.sqrt(a*a + b*b + c*c)
85
113
  @excellent.check_content(content)
86
- errors = @excellent.errors
114
+ warnings = @excellent.warnings
87
115
 
88
- errors.should_not be_empty
89
- errors[0].info[:method].should == :method_name
90
- errors[0].info[:score].should == score
91
- errors[0].message.should == "Method method_name has abc score of #{score}."
116
+ warnings.should_not be_empty
117
+ warnings[0].info.should == { :method => 'method_name', :score => score }
118
+ warnings[0].line_number.should == 1
119
+ warnings[0].message.should == "method_name has abc score of #{score}."
92
120
  end
93
121
 
94
122
  end