reek 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/History.txt +11 -1
  2. data/README.txt +1 -0
  3. data/lib/reek.rb +8 -9
  4. data/lib/reek/checker.rb +10 -2
  5. data/lib/reek/class_checker.rb +4 -7
  6. data/lib/reek/file_checker.rb +0 -6
  7. data/lib/reek/method_checker.rb +56 -30
  8. data/lib/reek/object_refs.rb +5 -2
  9. data/lib/reek/printer.rb +45 -7
  10. data/lib/reek/rake_task.rb +5 -3
  11. data/lib/reek/smells/control_couple.rb +53 -0
  12. data/lib/reek/smells/duplication.rb +54 -0
  13. data/lib/reek/smells/feature_envy.rb +65 -0
  14. data/lib/reek/smells/large_class.rb +35 -0
  15. data/lib/reek/smells/long_method.rb +35 -0
  16. data/lib/reek/smells/long_parameter_list.rb +36 -0
  17. data/lib/reek/smells/long_yield_list.rb +20 -0
  18. data/lib/reek/smells/nested_iterators.rb +24 -0
  19. data/lib/reek/smells/smell.rb +56 -0
  20. data/lib/reek/smells/smells.rb +24 -0
  21. data/lib/reek/smells/uncommunicative_name.rb +72 -0
  22. data/lib/reek/smells/utility_function.rb +34 -0
  23. data/lib/reek/version.rb +1 -1
  24. data/spec/integration_spec.rb +6 -6
  25. data/spec/reek/printer_spec.rb +21 -21
  26. data/spec/reek/report_spec.rb +5 -5
  27. data/spec/reek/{control_couple_spec.rb → smells/control_couple_spec.rb} +1 -1
  28. data/spec/reek/smells/duplication_spec.rb +60 -0
  29. data/spec/reek/smells/feature_envy_spec.rb +91 -0
  30. data/spec/reek/{large_class_spec.rb → smells/large_class_spec.rb} +8 -8
  31. data/spec/reek/{long_method_spec.rb → smells/long_method_spec.rb} +1 -1
  32. data/spec/reek/{long_parameter_list_spec.rb → smells/long_parameter_list_spec.rb} +1 -1
  33. data/spec/reek/smells/nested_iterators_spec.rb +43 -0
  34. data/spec/reek/{smell_spec.rb → smells/smell_spec.rb} +2 -2
  35. data/spec/reek/smells/uncommunicative_name_spec.rb +83 -0
  36. data/spec/reek/{utility_function_spec.rb → smells/utility_function_spec.rb} +1 -1
  37. data/spec/samples/inline.reek +13 -5
  38. data/spec/samples/optparse.reek +32 -10
  39. data/spec/samples/redcloth.reek +24 -6
  40. data/spec/script_spec.rb +1 -1
  41. data/tasks/reek.rake +9 -0
  42. data/website/index.html +3 -2
  43. data/website/index.txt +3 -1
  44. metadata +24 -12
  45. data/lib/reek/smells.rb +0 -192
  46. data/spec/reek/feature_envy_spec.rb +0 -222
  47. data/spec/reek/nested_iterators_spec.rb +0 -42
  48. data/spec/reek/uncommunicative_name_spec.rb +0 -106
data/History.txt CHANGED
@@ -1,4 +1,14 @@
1
- == 0.2.x 2008-??-??
1
+ == 0.3.1 2008-11-17
2
+
3
+ * Minor enhancements:
4
+ * Uncommunicative Name now checks instance variables more thoroughly
5
+ * Uncommunicative Name now warns about names of the form 'x2'
6
+ * Added check for duplicated calls within a method
7
+ * Reduced scope of Feature Envy warnings to cover only overuse of lvars
8
+ * Added rdoc comments explaining what each smell is about
9
+ * Chained iterators are no longer mis-reported as nested
10
+
11
+ == 0.3.0 2008-11-02
2
12
 
3
13
  * Minor enhancements:
4
14
  * New smell: first naive checks for Control Couple
data/README.txt CHANGED
@@ -20,6 +20,7 @@ Reek currently includes very naive checks for the following smells:
20
20
  * Utility Function
21
21
  * Nested Iterators
22
22
  * Control Couple
23
+ * Duplication
23
24
 
24
25
  == FEATURES/PROBLEMS:
25
26
 
data/lib/reek.rb CHANGED
@@ -6,21 +6,20 @@ require 'reek/report'
6
6
  module Reek # :doc:
7
7
 
8
8
  #
9
- # Analyse the given source files, looking for code smells.
9
+ # Analyse the given source, looking for code smells.
10
+ # The source can be a filename or a String containing Ruby code.
10
11
  # Returns a +Report+ listing the smells found.
11
12
  #
12
- def self.analyse(*files) # :doc:
13
+ def self.analyse(src) # :doc:
13
14
  report = Report.new
14
- files.each do |file|
15
- source = Reek.read(file)
16
- FileChecker.new(report).check_source(source)
17
- end
15
+ source = Reek.get_source(src)
16
+ FileChecker.new(report).check_source(source)
18
17
  report
19
18
  end
20
19
 
21
20
  private
22
21
 
23
- def self.read(file)
24
- File.exists?(file) ? IO.readlines(file).join : file
22
+ def self.get_source(src)
23
+ File.exists?(src) ? IO.readlines(src).join : src
25
24
  end
26
- end
25
+ end
data/lib/reek/checker.rb CHANGED
@@ -12,8 +12,6 @@ module Reek
12
12
  ParseTree.new.parse_tree_for_string(code)
13
13
  end
14
14
 
15
- attr_accessor :description
16
-
17
15
  # Creates a new Ruby code checker. Any smells discovered by
18
16
  # +check_source+ or +check_object+ will be stored in +report+.
19
17
  def initialize(report)
@@ -29,6 +27,11 @@ module Reek
29
27
  s(exp)
30
28
  end
31
29
 
30
+ def process_defn(exp) # :nodoc:
31
+ Reek::MethodChecker.new(@smells, @description).process(exp)
32
+ s(exp)
33
+ end
34
+
32
35
  def report(smell) # :nodoc:
33
36
  @smells << smell
34
37
  end
@@ -56,3 +59,8 @@ module Reek
56
59
  end
57
60
  end
58
61
  end
62
+
63
+ # SMELL:
64
+ # This is here to resolve a circular dependency -- MethodChecker inherits
65
+ # Checker, which calls MethodChecker. Yuk!
66
+ require 'reek/method_checker'
@@ -1,11 +1,13 @@
1
1
  $:.unshift File.dirname(__FILE__)
2
2
 
3
3
  require 'reek/checker'
4
- require 'reek/method_checker'
4
+ require 'reek/smells/large_class'
5
5
 
6
6
  module Reek
7
7
 
8
8
  class ClassChecker < Checker
9
+
10
+ attr_accessor :description
9
11
 
10
12
  def initialize(report)
11
13
  super(report)
@@ -15,14 +17,9 @@ module Reek
15
17
  def process_class(exp) # :nodoc:
16
18
  @description = exp[1].to_s
17
19
  superclass = exp[2]
18
- LargeClass.check(@description, self)
20
+ Smells::LargeClass.check(@description, self)
19
21
  exp[3..-1].each { |defn| process(defn) } unless superclass == [:const, :Struct]
20
22
  s(exp)
21
23
  end
22
-
23
- def process_defn(exp) # :nodoc:
24
- Reek::MethodChecker.new(@smells, @description).process(exp)
25
- s(exp)
26
- end
27
24
  end
28
25
  end
@@ -2,7 +2,6 @@ $:.unshift File.dirname(__FILE__)
2
2
 
3
3
  require 'reek/checker'
4
4
  require 'reek/class_checker'
5
- require 'reek/method_checker'
6
5
 
7
6
  module Reek
8
7
 
@@ -17,10 +16,5 @@ module Reek
17
16
  Reek::ClassChecker.new(@smells).process(exp)
18
17
  s(exp)
19
18
  end
20
-
21
- def process_defn(exp) # :nodoc:
22
- Reek::MethodChecker.new(@smells, @description).process(exp)
23
- s(exp)
24
- end
25
19
  end
26
20
  end
@@ -1,7 +1,13 @@
1
1
  $:.unshift File.dirname(__FILE__)
2
2
 
3
3
  require 'reek/checker'
4
- require 'reek/smells'
4
+ require 'reek/smells/control_couple'
5
+ require 'reek/smells/feature_envy'
6
+ require 'reek/smells/long_parameter_list'
7
+ require 'reek/smells/long_yield_list'
8
+ require 'reek/smells/nested_iterators'
9
+ require 'reek/smells/utility_function'
10
+ require 'reek/smells/smells'
5
11
  require 'reek/object_refs'
6
12
  require 'set'
7
13
 
@@ -9,27 +15,37 @@ module Reek
9
15
 
10
16
  class MethodChecker < Checker
11
17
 
18
+ attr_reader :local_variables, :name, :parameters, :num_statements
19
+ attr_reader :instance_variables # TODO: should be on the class
20
+ attr_reader :calls, :depends_on_self, :refs
21
+
12
22
  def initialize(smells, klass_name)
13
23
  super(smells)
14
- @class_name = @description = klass_name
24
+ @class_name = klass_name
15
25
  @refs = ObjectRefs.new
16
- @lvars = Set.new
26
+ @local_variables = Set.new
27
+ @instance_variables = Set.new
28
+ @parameters = []
29
+ @calls = Hash.new(0)
17
30
  @num_statements = 0
18
31
  @depends_on_self = false
19
32
  end
33
+
34
+ def description
35
+ "#{@class_name}##{@name}"
36
+ end
20
37
 
21
38
  def process_defn(exp)
22
- @description = "#{@class_name}##{exp[1]}"
23
- UncommunicativeName.check(exp[1], self, 'method')
24
- process(exp[2])
39
+ name, args = exp[1..2]
40
+ @name = name.to_s
41
+ process(args)
25
42
  check_method_properties
26
43
  s(exp)
27
44
  end
28
45
 
29
46
  def process_args(exp)
30
- LongParameterList.check(exp, self)
31
- exp.each { |arg| UncommunicativeName.check(arg, self, 'parameter') }
32
- @args = exp[1..-1]
47
+ Smells::LongParameterList.check(exp, self)
48
+ @parameters = exp[1..-1]
33
49
  s(exp)
34
50
  end
35
51
 
@@ -49,10 +65,8 @@ module Reek
49
65
  end
50
66
 
51
67
  def process_iter(exp)
52
- NestedIterators.check(@inside_an_iter, self)
53
- @inside_an_iter = true
54
- exp[1..-1].each { |s| process(s) }
55
- @inside_an_iter = false
68
+ Smells::NestedIterators.check(@inside_an_iter, self)
69
+ cascade_iter(exp)
56
70
  s(exp)
57
71
  end
58
72
 
@@ -65,16 +79,16 @@ module Reek
65
79
  def process_yield(exp)
66
80
  args = exp[1]
67
81
  if args
68
- LongYieldList.check(args, self)
82
+ Smells::LongYieldList.check(args, self)
69
83
  process(args)
70
84
  end
71
85
  s(exp)
72
86
  end
73
87
 
74
88
  def process_call(exp)
89
+ @calls[exp] += 1
75
90
  receiver, meth, args = exp[1..3]
76
- @refs.record_ref(receiver)
77
- process(receiver)
91
+ deal_with_receiver(receiver, meth)
78
92
  process(args) if args
79
93
  s(exp)
80
94
  end
@@ -97,15 +111,15 @@ module Reek
97
111
  end
98
112
 
99
113
  def process_if(exp)
100
- process(exp[1])
101
- process(exp[2])
102
- process(exp[3]) if exp[3]
103
- ControlCouple.check(exp[1], self, @args)
114
+ cond, then_part, else_part = exp[1..3]
115
+ deal_with_conditional(cond, then_part)
116
+ process(else_part) if else_part
117
+ Smells::ControlCouple.check(cond, self, @parameters)
104
118
  s(exp)
105
119
  end
106
120
 
107
121
  def process_ivar(exp)
108
- UncommunicativeName.check(exp[1], self, 'field')
122
+ @instance_variables << exp[1]
109
123
  @depends_on_self = true
110
124
  s(exp)
111
125
  end
@@ -115,12 +129,13 @@ module Reek
115
129
  end
116
130
 
117
131
  def process_lasgn(exp)
118
- @lvars << exp[1]
132
+ @local_variables << exp[1]
119
133
  process(exp[2])
120
134
  s(exp)
121
135
  end
122
136
 
123
137
  def process_iasgn(exp)
138
+ @instance_variables << exp[1]
124
139
  @depends_on_self = true
125
140
  process(exp[2])
126
141
  s(exp)
@@ -153,20 +168,31 @@ module Reek
153
168
  return false unless klass.superclass
154
169
  klass.superclass.instance_methods.include?(method_name)
155
170
  end
156
-
157
- def method_name
158
- @description.to_s.split('#')[1]
159
- end
160
171
 
161
172
  def is_override?
162
- MethodChecker.is_override?(@class_name, method_name)
173
+ MethodChecker.is_override?(@class_name, @name)
163
174
  end
164
175
 
165
176
  def check_method_properties
166
- @lvars.each {|lvar| UncommunicativeName.check(lvar, self, 'local variable') }
167
177
  @depends_on_self = true if is_override?
168
- FeatureEnvy.check(@refs, self) unless UtilityFunction.check(@depends_on_self, self, @num_statements)
169
- LongMethod.check(@num_statements, self) unless method_name == 'initialize'
178
+ SMELLS[:defn].each {|smell| smell.examine(self, @smells) }
179
+ end
180
+
181
+ def cascade_iter(exp)
182
+ process(exp[1])
183
+ @inside_an_iter = true
184
+ exp[2..-1].each { |s| process(s) }
185
+ @inside_an_iter = false
186
+ end
187
+
188
+ def deal_with_conditional(cond, then_part)
189
+ process(cond)
190
+ process(then_part)
191
+ end
192
+
193
+ def deal_with_receiver(receiver, meth)
194
+ @refs.record_ref(receiver) if (receiver[0] == :lvar and meth != :new)
195
+ process(receiver)
170
196
  end
171
197
  end
172
198
  end
@@ -7,8 +7,6 @@ require 'reek/printer'
7
7
  module Reek
8
8
 
9
9
  class ObjectRefs
10
- SELF_REF = Sexp.from_array([:lit, :self])
11
-
12
10
  def initialize
13
11
  @refs = Hash.new(0)
14
12
  record_reference_to_self
@@ -49,5 +47,10 @@ module Reek
49
47
  def self_is_max?
50
48
  max_keys.length == 0 || @refs[SELF_REF] == max_refs
51
49
  end
50
+
51
+ private
52
+
53
+ SELF_REF = Sexp.from_array([:lit, :self])
54
+
52
55
  end
53
56
  end
data/lib/reek/printer.rb CHANGED
@@ -31,7 +31,7 @@ module Reek
31
31
  end
32
32
 
33
33
  def process_array(exp)
34
- @report = Printer.print(exp[1])
34
+ @report = exp[1..-1].map {|arg| Printer.print(arg)}.join(', ')
35
35
  s(exp)
36
36
  end
37
37
 
@@ -78,10 +78,7 @@ module Reek
78
78
  end
79
79
 
80
80
  def process_fcall(exp)
81
- meth, args = exp[1..2]
82
- @report = meth.to_s
83
- @report += "(#{Printer.print(args)})" if args
84
- s(exp)
81
+ process_vcall(exp)
85
82
  end
86
83
 
87
84
  def process_cvar(exp)
@@ -105,12 +102,53 @@ module Reek
105
102
  s(exp)
106
103
  end
107
104
 
105
+ def process_nth_ref(exp)
106
+ @report = "$#{exp[1]}"
107
+ s(exp)
108
+ end
109
+
110
+ def process_lasgn(exp)
111
+ @report = "#{exp[1]}=#{Printer.print(exp[2])}"
112
+ s(exp)
113
+ end
114
+
115
+ def process_str(exp)
116
+ @report = exp[1]
117
+ s(exp)
118
+ end
119
+
120
+ def process_dstr(exp)
121
+ @report = '"' + exp[1..-1].map {|sub| Printer.print(sub)}.join + '"'
122
+ s(exp)
123
+ end
124
+
125
+ def process_evstr(exp)
126
+ @report = "\#\{#{Printer.print(exp[1])}\}"
127
+ s(exp)
128
+ end
129
+
130
+ def process_true(exp)
131
+ @report = 'true'
132
+ s(exp)
133
+ end
134
+
108
135
  def process_call(exp)
109
136
  receiver, meth, args = exp[1..3]
110
- @report = "#{Printer.print(receiver)}.#{meth}"
111
- @report += "(#{Printer.print(args)})" if args
137
+ @report = "#{Printer.print(receiver)}"
138
+ if meth.to_s == '[]'
139
+ @report += Printer.format_array_args(args)
140
+ else
141
+ @report += ".#{meth}" + (args ? "(#{Printer.print(args)})" : '')
142
+ end
112
143
  s(exp)
113
144
  end
145
+
146
+ private
147
+
148
+ def Printer.format_array_args(args)
149
+ args_str = args ? Printer.print(args) : ''
150
+ "[#{args_str}]"
151
+ end
114
152
  end
115
153
 
116
154
  end
@@ -54,7 +54,7 @@ module Reek
54
54
  # Array of commandline options to pass to ruby. Defaults to [].
55
55
  attr_accessor :ruby_opts
56
56
 
57
- # Whether or not to fail Rake when an error occurs (typically when specs fail).
57
+ # Whether or not to fail Rake when an error occurs (typically when smells are found).
58
58
  # Defaults to true.
59
59
  attr_accessor :fail_on_error
60
60
 
@@ -76,6 +76,8 @@ module Reek
76
76
  define
77
77
  end
78
78
 
79
+ private
80
+
79
81
  def define # :nodoc:
80
82
  desc "Check for code smells" unless ::Rake.application.last_comment
81
83
  task(name) { run_task }
@@ -98,9 +100,9 @@ module Reek
98
100
  end
99
101
 
100
102
  def cmd_words
101
- [self.class.ruby_exe] +
103
+ [RakeTask.ruby_exe] +
102
104
  ruby_options +
103
- [ %Q|"#{self.class.reek_script}"| ] +
105
+ [ %Q|"#{RakeTask.reek_script}"| ] +
104
106
  [sort_option] +
105
107
  source_file_list.collect { |fn| %["#{fn}"] }
106
108
  end
@@ -0,0 +1,53 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ #
9
+ # Control Coupling occurs when a method or block checks the value of
10
+ # a parameter in order to decide which execution path to take. The
11
+ # offending parameter is often called a Control Couple.
12
+ #
13
+ # A simple example would be the <tt>quoted</tt> parameter
14
+ # in the following method:
15
+ #
16
+ # def write(quoted)
17
+ # if quoted
18
+ # write_quoted(@value)
19
+ # else
20
+ # puts @value
21
+ # end
22
+ # end
23
+ #
24
+ # Control Coupling is a kind of duplication, because the calling method
25
+ # already knows which path should be taken.
26
+ #
27
+ # Control Coupling reduces the code's flexibility by creating a
28
+ # dependency between the caller and callee:
29
+ # any change to the possible values of the controlling parameter must
30
+ # be reflected on both sides of the call.
31
+ #
32
+ # A Control Couple also reveals a loss of simplicity: the called
33
+ # method probably has more than one responsibility,
34
+ # because it includes at least two different code paths.
35
+ #
36
+ class ControlCouple < Smell
37
+ def initialize(context, args)
38
+ super
39
+ @args = args
40
+ end
41
+
42
+ def recognise?(cond)
43
+ @couple = cond
44
+ cond[0] == :lvar and @args.include?(@couple[1])
45
+ end
46
+
47
+ def detailed_report
48
+ "#{@context} is controlled by argument #{Printer.print(@couple)}"
49
+ end
50
+ end
51
+
52
+ end
53
+ end