reek 0.3.0 → 0.3.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 (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