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 +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
|