reek 0.2.2 → 0.2.3

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.
data/History.txt CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.2.3 2008-09-22
2
+
3
+ * Minor enhancements:
4
+ * Only reports Feature Envy when the method isn't a Utility Function
5
+ * General improvements to assessing Feature Envy
6
+ * Tweaks:
7
+ * Fixed: coping with parameterless yield call
8
+ * Fixed: copes with :self as an expression
9
+ * Fixed: displaying the receiver of many more kinds of Feature Envy
10
+ * Fixed: Large Class calculation for Object
11
+
1
12
  == 0.2.2 2008-09-15
2
13
 
3
14
  * Tweaks:
data/README.txt CHANGED
@@ -9,7 +9,7 @@
9
9
  Reek is a tool that examines Ruby classes, modules and methods and
10
10
  reports any code smells it finds.
11
11
 
12
- === SUPPORTED SMELLS:
12
+ === SUPPORTED CODE SMELLS:
13
13
 
14
14
  * Long Method
15
15
  * Large Class
@@ -28,8 +28,9 @@ reports any code smells it finds.
28
28
 
29
29
  == SYNOPSIS:
30
30
 
31
- $ cd my_project/lib
32
- $ reek
31
+ $ reek [options] [source_files]
32
+
33
+ (See `reek --help` for details.)
33
34
 
34
35
  == REQUIREMENTS:
35
36
 
@@ -37,13 +38,23 @@ reports any code smells it finds.
37
38
 
38
39
  == INSTALL:
39
40
 
40
- * sudo gem install reek
41
+ Get the latest version of the gem:
42
+
43
+ $ gem install reek
44
+
45
+ Or get the latest unpackaged source code:
46
+
47
+ $ git clone git://github.com/kevinrutherford/reek.git
48
+
49
+ or
50
+
51
+ $ git clone git://rubyforge.org/reek.git
41
52
 
42
53
  == LICENSE:
43
54
 
44
55
  (The MIT License)
45
56
 
46
- Copyright (c) 2008 Kevin Rutherford, Rutherford Software
57
+ Copyright (c) 2008 Kevin Rutherford, Rutherford Software Ltd
47
58
 
48
59
  Permission is hereby granted, free of charge, to any person obtaining
49
60
  a copy of this software and associated documentation files (the
data/lib/reek/checker.rb CHANGED
@@ -7,16 +7,26 @@ require 'sexp_processor'
7
7
  module Reek
8
8
 
9
9
  class Checker < SexpProcessor
10
+
11
+ def self.parse_tree_for(code) # :nodoc:
12
+ ParseTree.new.parse_tree_for_string(code)
13
+ end
14
+
10
15
  attr_accessor :description
11
16
 
12
17
  # Creates a new Ruby code checker. Any smells discovered by
13
18
  # +check_source+ or +check_object+ will be stored in +report+.
14
19
  def initialize(report)
15
20
  super()
16
- @require_empty = false
17
21
  @smells = report
18
- @description = ''
19
22
  @unsupported -= [:cfunc]
23
+ @default_method = :process_default
24
+ @require_empty = @warn_on_default = false
25
+ end
26
+
27
+ def process_default(exp)
28
+ exp[1..-1].each { |e| process(e) if Array === e}
29
+ s(exp)
20
30
  end
21
31
 
22
32
  def report(smell) # :nodoc:
@@ -27,7 +37,7 @@ module Reek
27
37
  # Any smells found are saved in the +Report+ object that
28
38
  # was passed to this object's constructor.
29
39
  def check_source(code)
30
- check_parse_tree ParseTree.new.parse_tree_for_string(code)
40
+ check_parse_tree(Checker.parse_tree_for(code))
31
41
  end
32
42
 
33
43
  # Analyses the given Ruby object +obj+ looking for smells.
@@ -2,6 +2,7 @@ $:.unshift File.dirname(__FILE__)
2
2
 
3
3
  require 'reek/checker'
4
4
  require 'reek/smells'
5
+ require 'reek/object_refs'
5
6
  require 'set'
6
7
 
7
8
  module Reek
@@ -11,9 +12,10 @@ module Reek
11
12
  def initialize(smells, klass_name)
12
13
  super(smells)
13
14
  @class_name = @description = klass_name
14
- @calls = Hash.new(0)
15
+ @refs = ObjectRefs.new
15
16
  @lvars = Set.new
16
17
  @num_statements = 0
18
+ @depends_on_self = false
17
19
  end
18
20
 
19
21
  def process_defn(exp)
@@ -31,7 +33,17 @@ module Reek
31
33
  end
32
34
 
33
35
  def process_attrset(exp)
34
- @calls[:self] += 1 if /^@/ === exp[1].to_s
36
+ @depends_on_self = true if /^@/ === exp[1].to_s
37
+ s(exp)
38
+ end
39
+
40
+ def process_lit(exp)
41
+ val = exp[1]
42
+ @depends_on_self = true if val == :self
43
+ s(exp)
44
+ end
45
+
46
+ def process_lvar(exp)
35
47
  s(exp)
36
48
  end
37
49
 
@@ -50,53 +62,66 @@ module Reek
50
62
  end
51
63
 
52
64
  def process_yield(exp)
53
- LongYieldList.check(exp[1], self)
54
- process(exp[1])
65
+ args = exp[1]
66
+ if args
67
+ LongYieldList.check(args, self)
68
+ process(args)
69
+ end
55
70
  s(exp)
56
71
  end
57
72
 
58
73
  def process_call(exp)
59
- record_receiver(exp[1])
60
- process_actual_parameters(exp[3])
61
- process(exp[3]) if exp.length > 3
74
+ receiver, meth, args = exp[1..3]
75
+ @refs.record_ref(receiver)
76
+ process(receiver)
77
+ process(args) if args
62
78
  s(exp)
63
79
  end
64
80
 
65
81
  def process_fcall(exp)
66
- @calls[:self] += 1
67
- process(exp[2]) if exp.length > 2
82
+ @depends_on_self = true
83
+ @refs.record_reference_to_self
84
+ process(exp[2]) if exp.length >= 3
68
85
  s(exp)
69
86
  end
70
87
 
71
88
  def process_cfunc(exp)
72
- @calls[:self] += 1
89
+ @depends_on_self = true
73
90
  s(exp)
74
91
  end
75
92
 
76
93
  def process_vcall(exp)
77
- @calls[:self] += 1
94
+ @depends_on_self = true
78
95
  s(exp)
79
96
  end
80
97
 
81
98
  def process_ivar(exp)
82
99
  UncommunicativeName.check(exp[1], self, 'field')
83
- @calls[:self] += 1
100
+ @depends_on_self = true
101
+ s(exp)
102
+ end
103
+
104
+ def process_gvar(exp)
84
105
  s(exp)
85
106
  end
86
107
 
87
108
  def process_lasgn(exp)
88
109
  @lvars << exp[1]
89
- @calls[s(:lvar, exp[1])] += 1
90
110
  process(exp[2])
91
111
  s(exp)
92
112
  end
93
113
 
94
114
  def process_iasgn(exp)
95
- @calls[:self] += 1
115
+ @depends_on_self = true
96
116
  process(exp[2])
97
117
  s(exp)
98
118
  end
99
119
 
120
+ def process_self(exp)
121
+ @depends_on_self = true
122
+ s(exp)
123
+ end
124
+
100
125
  private
101
126
 
102
127
  def self.count_statements(exp)
@@ -109,47 +134,29 @@ module Reek
109
134
  Array === exp and exp[0] == :gvar
110
135
  end
111
136
 
112
- def record_receiver(exp)
113
- receiver = MethodChecker.unpack_array(process(exp))
114
- @calls[receiver] += 1 unless MethodChecker.is_global_variable?(receiver)
115
- end
116
-
117
- def self.unpack_array(receiver)
118
- receiver = receiver[0] if Array === receiver and Array === receiver[0] and receiver.length == 1
119
- receiver = :self if receiver == s(:self)
120
- receiver
121
- end
122
-
123
- def is_override?
137
+ def self.is_override?(class_name, method_name)
124
138
  begin
125
- klass = Object.const_get(@class_name)
139
+ klass = Object.const_get(class_name)
126
140
  rescue
127
141
  return false
128
142
  end
129
- klass.superclass.instance_methods.include?(@description.to_s.split('#')[1])
143
+ return false unless klass.superclass
144
+ klass.superclass.instance_methods.include?(method_name)
145
+ end
146
+
147
+ def method_name
148
+ @description.to_s.split('#')[1]
149
+ end
150
+
151
+ def is_override?
152
+ MethodChecker.is_override?(@class_name, method_name)
130
153
  end
131
154
 
132
155
  def check_method_properties
133
156
  @lvars.each {|lvar| UncommunicativeName.check(lvar, self, 'local variable') }
134
- @calls[:self] += 1 if is_override?
135
- UtilityFunction.check(@calls, self)
136
- FeatureEnvy.check(@calls, self)
137
- LongMethod.check(@num_statements, self)
138
- end
139
-
140
- def process_actual_parameters(exp)
141
- return unless Array === exp and exp[0] == :array
142
- exp[1..-1].each do |param|
143
- if Array === param
144
- if param.length == 1
145
- @calls[:self] += 1 if param[0] == :self
146
- else
147
- @calls[param] += 1
148
- end
149
- else
150
- @calls[:self] += 1 if param == :self
151
- end
152
- end
157
+ @depends_on_self = true if is_override?
158
+ FeatureEnvy.check(@refs, self) unless UtilityFunction.check(@depends_on_self, self)
159
+ LongMethod.check(@num_statements, self) unless method_name == 'initialize'
153
160
  end
154
161
  end
155
162
  end
@@ -0,0 +1,53 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'sexp'
5
+ require 'reek/printer'
6
+
7
+ module Reek
8
+
9
+ class ObjectRefs
10
+ SELF_REF = Sexp.from_array([:lit, :self])
11
+
12
+ def initialize
13
+ @refs = Hash.new(0)
14
+ record_reference_to_self
15
+ end
16
+
17
+ def record_reference_to_self
18
+ record_ref(SELF_REF)
19
+ end
20
+
21
+ def record_ref(exp)
22
+ type = exp[0]
23
+ case type
24
+ when :gvar
25
+ return
26
+ when :self
27
+ record_reference_to_self
28
+ else
29
+ @refs[exp] += 1
30
+ end
31
+ end
32
+
33
+ def refs_to_self
34
+ @refs[SELF_REF]
35
+ end
36
+
37
+ def max_refs
38
+ @refs.values.max or 0
39
+ end
40
+
41
+ # TODO
42
+ # Should be moved to Hash; but Hash has 58 methods, and there's currently
43
+ # no way to turn off that report; which would therefore make the tests fail
44
+ def max_keys
45
+ max = max_refs
46
+ @refs.reject {|k,v| v != max}.keys
47
+ end
48
+
49
+ def self_is_max?
50
+ max_keys.length == 0 || @refs[SELF_REF] == max_refs
51
+ end
52
+ end
53
+ end
data/lib/reek/printer.rb CHANGED
@@ -13,38 +13,90 @@ module Reek
13
13
 
14
14
  def initialize
15
15
  super
16
- @require_empty = false
16
+ @default_method = :process_default
17
+ @require_empty = @warn_on_default = false
17
18
  @report = ''
18
19
  end
19
20
 
20
21
  def print(sexp)
21
- @report = sexp.inspect
22
+ @report = sexp.to_s
22
23
  return @report unless Array === sexp
23
- begin
24
- process(sexp)
25
- rescue
26
- raise "Error in print, parsing:\n #{sexp.inspect}"
27
- end
24
+ process(sexp)
28
25
  @report
29
26
  end
30
27
 
28
+ def process_default(exp)
29
+ @report = exp.inspect
30
+ s(exp)
31
+ end
32
+
33
+ def process_array(exp)
34
+ @report = Printer.print(exp[1])
35
+ s(exp)
36
+ end
37
+
31
38
  def process_lvar(exp)
39
+ @report = exp[1].to_s
40
+ s(exp)
41
+ end
42
+
43
+ def process_lit(exp)
44
+ @report = exp[1].to_s
45
+ s(exp)
46
+ end
47
+
48
+ def process_str(exp)
32
49
  @report = exp[1].inspect
33
50
  s(exp)
34
51
  end
35
52
 
53
+ def process_xstr(exp)
54
+ @report = "`#{exp[1]}`"
55
+ s(exp)
56
+ end
57
+
36
58
  def process_dvar(exp)
37
- @report = exp[1].inspect
59
+ @report = Printer.print(exp[1])
38
60
  s(exp)
39
61
  end
40
62
 
41
63
  def process_gvar(exp)
42
- @report = exp[1].inspect
64
+ @report = exp[1].to_s
65
+ s(exp)
66
+ end
67
+
68
+ def process_ivar(exp)
69
+ @report = exp[1].to_s
70
+ s(exp)
71
+ end
72
+
73
+ def process_vcall(exp)
74
+ meth, args = exp[1..2]
75
+ @report = meth.to_s
76
+ @report += "(#{Printer.print(args)})" if args
77
+ s(exp)
78
+ end
79
+
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)
85
+ end
86
+
87
+ def process_cvar(exp)
88
+ @report = Printer.print(exp[1])
43
89
  s(exp)
44
90
  end
45
91
 
46
92
  def process_const(exp)
47
- @report = exp[1].inspect
93
+ @report = Printer.print(exp[1])
94
+ s(exp)
95
+ end
96
+
97
+ def process_colon2(exp)
98
+ mod, member = exp[1..2]
99
+ @report = "#{Printer.print(mod)}::#{Printer.print(member)}"
48
100
  s(exp)
49
101
  end
50
102
 
@@ -54,8 +106,9 @@ module Reek
54
106
  end
55
107
 
56
108
  def process_call(exp)
57
- @report = "#{exp[1]}.#{exp[2]}"
58
- @report += "(#{exp[3]})" if exp.length > 3
109
+ receiver, meth, args = exp[1..3]
110
+ @report = "#{Printer.print(receiver)}.#{meth}"
111
+ @report += "(#{Printer.print(args)})" if args
59
112
  s(exp)
60
113
  end
61
114
  end
data/lib/reek/smells.rb CHANGED
@@ -18,12 +18,9 @@ module Reek
18
18
 
19
19
  def self.check(exp, context, arg=nil)
20
20
  smell = new(context, arg)
21
- if smell.recognise?(exp)
22
- context.report(smell)
23
- true
24
- else
25
- false
26
- end
21
+ return false unless smell.recognise?(exp)
22
+ context.report(smell)
23
+ true
27
24
  end
28
25
 
29
26
  def recognise?(stuff)
@@ -99,34 +96,21 @@ module Reek
99
96
  end
100
97
 
101
98
  class FeatureEnvy < Smell
102
-
103
- # TODO
104
- # Should be moved to Hash; but Hash has 58 methods, and there's currently
105
- # no way to turn off that report; which would therefore make the tests fail
106
- def self.max_keys(calls)
107
- max = calls.values.max or return [:self]
108
- calls.keys.select { |key| calls[key] == max }
109
- end
110
-
111
- def initialize(context, *receivers)
112
- super(context)
113
- @receivers = receivers
114
- end
115
99
 
116
- def recognise?(calls)
117
- @receivers = FeatureEnvy.max_keys(calls)
118
- return !(@receivers.include?(:self))
100
+ def recognise?(refs)
101
+ @refs = refs
102
+ !refs.self_is_max?
119
103
  end
120
104
 
121
105
  def detailed_report
122
- receiver = @receivers.map {|r| Printer.print(r)}.sort.join(' and ')
106
+ receiver = @refs.max_keys.map {|r| Printer.print(r)}.sort.join(' and ')
123
107
  "#{@context} uses #{receiver} more than self"
124
108
  end
125
109
  end
126
110
 
127
111
  class UtilityFunction < Smell
128
- def recognise?(calls)
129
- calls[:self] == 0
112
+ def recognise?(depends_on_self)
113
+ !depends_on_self
130
114
  end
131
115
 
132
116
  def detailed_report
@@ -137,9 +121,14 @@ module Reek
137
121
  class LargeClass < Smell
138
122
  MAX_ALLOWED = 25
139
123
 
124
+ def self.non_inherited_methods(klass)
125
+ return klass.instance_methods if klass.superclass.nil?
126
+ klass.instance_methods - klass.superclass.instance_methods
127
+ end
128
+
140
129
  def recognise?(name)
141
130
  klass = Object.const_get(name) rescue return
142
- @num_methods = klass.instance_methods.length - klass.superclass.instance_methods.length
131
+ @num_methods = LargeClass.non_inherited_methods(klass).length
143
132
  @num_methods > MAX_ALLOWED
144
133
  end
145
134
 
@@ -154,11 +143,15 @@ module Reek
154
143
  @symbol_type = symbol_type
155
144
  end
156
145
 
146
+ def self.effective_length(name)
147
+ return 500 if name == '*'
148
+ name = name[1..-1] while /^@/ === name
149
+ name.length
150
+ end
151
+
157
152
  def recognise?(symbol)
158
153
  @symbol = symbol.to_s
159
- return false if @symbol == '*'
160
- min_len = (/^@/ === @symbol) ? 3 : 2;
161
- @symbol.length < min_len
154
+ UncommunicativeName.effective_length(@symbol) < 2
162
155
  end
163
156
 
164
157
  def detailed_report