reek 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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