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 +11 -0
- data/README.txt +16 -5
- data/lib/reek/checker.rb +13 -3
- data/lib/reek/method_checker.rb +54 -47
- data/lib/reek/object_refs.rb +53 -0
- data/lib/reek/printer.rb +65 -12
- data/lib/reek/smells.rb +22 -29
- data/lib/reek/version.rb +1 -1
- data/spec/integration_spec.rb +3 -11
- data/spec/reek/feature_envy_spec.rb +163 -24
- data/spec/reek/long_method_spec.rb +11 -11
- data/spec/reek/method_checker_spec.rb +34 -0
- data/spec/reek/nested_iterators_spec.rb +4 -4
- data/spec/reek/object_refs_spec.rb +129 -0
- data/spec/reek/printer_spec.rb +30 -0
- data/spec/reek/report_spec.rb +3 -3
- data/spec/reek/smell_spec.rb +1 -1
- data/spec/reek/utility_function_spec.rb +27 -0
- data/spec/reek_source_spec.rb +11 -0
- data/spec/samples/inline.rb +704 -0
- data/spec/samples/inline.reek +17 -0
- data/spec/samples/optparse.reek +10 -16
- data/spec/samples/redcloth.reek +6 -19
- data/tasks/samples.rake +2 -0
- data/website/index.html +26 -63
- data/website/index.txt +4 -12
- metadata +8 -2
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
|
-
$
|
32
|
-
|
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
|
-
|
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
|
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.
|
data/lib/reek/method_checker.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
@
|
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
|
-
|
54
|
-
|
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
|
-
|
60
|
-
|
61
|
-
process(
|
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
|
-
@
|
67
|
-
|
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
|
-
@
|
89
|
+
@depends_on_self = true
|
73
90
|
s(exp)
|
74
91
|
end
|
75
92
|
|
76
93
|
def process_vcall(exp)
|
77
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
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(
|
139
|
+
klass = Object.const_get(class_name)
|
126
140
|
rescue
|
127
141
|
return false
|
128
142
|
end
|
129
|
-
klass.superclass
|
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
|
-
@
|
135
|
-
UtilityFunction.check(@
|
136
|
-
|
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
|
-
@
|
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.
|
22
|
+
@report = sexp.to_s
|
22
23
|
return @report unless Array === sexp
|
23
|
-
|
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]
|
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].
|
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]
|
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
|
-
|
58
|
-
@report
|
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
|
-
|
22
|
-
|
23
|
-
|
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?(
|
117
|
-
@
|
118
|
-
|
100
|
+
def recognise?(refs)
|
101
|
+
@refs = refs
|
102
|
+
!refs.self_is_max?
|
119
103
|
end
|
120
104
|
|
121
105
|
def detailed_report
|
122
|
-
receiver = @
|
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?(
|
129
|
-
|
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 =
|
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
|
-
|
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
|