reek 0.2.0 → 0.2.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.
- data/History.txt +6 -0
- data/bin/reek +2 -25
- data/lib/reek.rb +6 -5
- data/lib/reek/class_checker.rb +1 -2
- data/lib/reek/file_checker.rb +26 -0
- data/lib/reek/method_checker.rb +55 -17
- data/lib/reek/options.rb +11 -3
- data/lib/reek/printer.rb +1 -0
- data/lib/reek/report.rb +6 -6
- data/lib/reek/smells.rb +39 -19
- data/lib/reek/version.rb +1 -1
- data/spec/integration_spec.rb +38 -0
- data/spec/reek/feature_envy_spec.rb +3 -4
- data/spec/reek/long_parameter_list_spec.rb +138 -0
- data/spec/reek/method_checker_spec.rb +8 -138
- data/spec/reek/report_spec.rb +2 -2
- data/spec/reek/utility_function_spec.rb +16 -0
- data/spec/samples/optparse.rb +1788 -0
- data/spec/samples/optparse.reek +56 -0
- data/spec/samples/optparse/date.rb +17 -0
- data/spec/samples/optparse/shellwords.rb +6 -0
- data/spec/samples/optparse/time.rb +10 -0
- data/spec/samples/optparse/uri.rb +6 -0
- data/spec/samples/optparse/version.rb +70 -0
- data/spec/samples/redcloth.rb +1130 -0
- data/spec/samples/redcloth.reek +54 -0
- data/tasks/rspec.rake +2 -2
- data/tasks/samples.rake +15 -0
- data/website/index.html +2 -2
- metadata +15 -3
- data/spec/reek_spec.rb +0 -27
data/History.txt
CHANGED
data/bin/reek
CHANGED
@@ -3,31 +3,8 @@
|
|
3
3
|
# Created on 2008-2-17.
|
4
4
|
# Copyright (c) 2008 Kevin Rutherford, Rutherford Software Ltd. All rights reserved.
|
5
5
|
|
6
|
-
begin
|
7
|
-
require 'rubygems'
|
8
|
-
rescue LoadError
|
9
|
-
# no rubygems to load, so we fail silently
|
10
|
-
end
|
11
|
-
|
12
6
|
require 'reek'
|
13
7
|
require 'reek/options'
|
14
|
-
require 'reek/version'
|
15
|
-
|
16
|
-
def classes_currently_loaded
|
17
|
-
result = []
|
18
|
-
ObjectSpace.each_object(Module) { |klass| result << klass }
|
19
|
-
result
|
20
|
-
end
|
21
|
-
|
22
|
-
Reek::Options.parse(ARGV)
|
23
8
|
|
24
|
-
|
25
|
-
files
|
26
|
-
files = Dir['**/*.rb'] if files.empty?
|
27
|
-
files.each { |name| require name }
|
28
|
-
new_classes = classes_currently_loaded - old_classes
|
29
|
-
if new_classes.empty?
|
30
|
-
puts 'Nothing to analyse!'
|
31
|
-
else
|
32
|
-
puts Reek.analyse(*new_classes).to_s
|
33
|
-
end
|
9
|
+
files = Reek::Options.parse(ARGV)
|
10
|
+
puts Reek.analyse(*files).to_s
|
data/lib/reek.rb
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
$:.unshift File.dirname(__FILE__)
|
2
2
|
|
3
|
-
require 'reek/
|
3
|
+
require 'reek/file_checker'
|
4
4
|
require 'reek/report'
|
5
5
|
|
6
6
|
module Reek # :doc:
|
7
7
|
|
8
8
|
#
|
9
|
-
# Analyse the given
|
9
|
+
# Analyse the given source files, looking for code smells.
|
10
10
|
# Returns a +Report+ listing the smells found.
|
11
11
|
#
|
12
|
-
def self.analyse(*
|
12
|
+
def self.analyse(*files) # :doc:
|
13
13
|
report = Report.new
|
14
|
-
|
15
|
-
|
14
|
+
files.each do |file|
|
15
|
+
source = IO.readlines(file).join
|
16
|
+
FileChecker.new(report).check_source(source)
|
16
17
|
end
|
17
18
|
report
|
18
19
|
end
|
data/lib/reek/class_checker.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/checker'
|
4
|
+
require 'reek/class_checker'
|
5
|
+
require 'reek/method_checker'
|
6
|
+
|
7
|
+
module Reek
|
8
|
+
|
9
|
+
class FileChecker < Checker
|
10
|
+
|
11
|
+
def initialize(report)
|
12
|
+
super(report)
|
13
|
+
@description = ''
|
14
|
+
end
|
15
|
+
|
16
|
+
def process_class(exp) # :nodoc:
|
17
|
+
Reek::ClassChecker.new(@smells).process(exp)
|
18
|
+
s(exp)
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_defn(exp) # :nodoc:
|
22
|
+
Reek::MethodChecker.new(@smells, @description).process(exp)
|
23
|
+
s(exp)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/reek/method_checker.rb
CHANGED
@@ -10,22 +10,17 @@ module Reek
|
|
10
10
|
|
11
11
|
def initialize(smells, klass_name)
|
12
12
|
super(smells)
|
13
|
-
@class_name = klass_name
|
14
|
-
@description = klass_name
|
13
|
+
@class_name = @description = klass_name
|
15
14
|
@calls = Hash.new(0)
|
16
15
|
@lvars = Set.new
|
17
|
-
@
|
16
|
+
@num_statements = 0
|
18
17
|
end
|
19
18
|
|
20
19
|
def process_defn(exp)
|
21
|
-
@num_statements = 0
|
22
20
|
@description = "#{@class_name}##{exp[1]}"
|
23
21
|
UncommunicativeName.check(exp[1], self, 'method')
|
24
22
|
process(exp[2])
|
25
|
-
|
26
|
-
UtilityFunction.check(@calls, self)
|
27
|
-
FeatureEnvy.check(@calls, self)
|
28
|
-
LongMethod.check(@num_statements, self)
|
23
|
+
check_method_properties
|
29
24
|
s(exp)
|
30
25
|
end
|
31
26
|
|
@@ -49,7 +44,7 @@ module Reek
|
|
49
44
|
end
|
50
45
|
|
51
46
|
def process_block(exp)
|
52
|
-
@num_statements += count_statements(exp)
|
47
|
+
@num_statements += MethodChecker.count_statements(exp)
|
53
48
|
exp[1..-1].each { |s| process(s) }
|
54
49
|
s(exp)
|
55
50
|
end
|
@@ -61,12 +56,8 @@ module Reek
|
|
61
56
|
end
|
62
57
|
|
63
58
|
def process_call(exp)
|
64
|
-
|
65
|
-
|
66
|
-
if receiver[0] != :gvar
|
67
|
-
receiver = :self if receiver == s(:self)
|
68
|
-
@calls[receiver] += 1
|
69
|
-
end
|
59
|
+
record_receiver(exp[1])
|
60
|
+
process_actual_parameters(exp[3])
|
70
61
|
process(exp[3]) if exp.length > 3
|
71
62
|
s(exp)
|
72
63
|
end
|
@@ -107,11 +98,58 @@ module Reek
|
|
107
98
|
end
|
108
99
|
|
109
100
|
private
|
110
|
-
|
111
|
-
def count_statements(exp)
|
101
|
+
|
102
|
+
def self.count_statements(exp)
|
112
103
|
result = exp.length - 1
|
113
104
|
result -= 1 if Array === exp[1] and exp[1][0] == :args
|
114
105
|
result
|
115
106
|
end
|
107
|
+
|
108
|
+
def self.is_global_variable?(exp)
|
109
|
+
Array === exp and exp[0] == :gvar
|
110
|
+
end
|
111
|
+
|
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?
|
124
|
+
begin
|
125
|
+
klass = Object.const_get(@class_name)
|
126
|
+
rescue
|
127
|
+
return false
|
128
|
+
end
|
129
|
+
klass.superclass.instance_methods.include?(@description.to_s.split('#')[1])
|
130
|
+
end
|
131
|
+
|
132
|
+
def check_method_properties
|
133
|
+
@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
|
153
|
+
end
|
116
154
|
end
|
117
155
|
end
|
data/lib/reek/options.rb
CHANGED
@@ -48,13 +48,21 @@ module Reek
|
|
48
48
|
result
|
49
49
|
end
|
50
50
|
|
51
|
+
def self.fatal_error(e) # :nodoc:
|
52
|
+
puts "Error: #{e}"
|
53
|
+
puts "Use '-h' for help."
|
54
|
+
exit(1)
|
55
|
+
end
|
56
|
+
|
51
57
|
def self.parse(args)
|
52
58
|
begin
|
53
59
|
@@opts = parse_args(args)
|
60
|
+
files = ARGV
|
61
|
+
files = Dir['**/*.rb'] if files.empty?
|
62
|
+
fatal_error('no source files specified') if files.empty?
|
63
|
+
files
|
54
64
|
rescue OptionParser::ParseError => e
|
55
|
-
|
56
|
-
puts "Use '-h' for help."
|
57
|
-
exit(1)
|
65
|
+
fatal_error(e)
|
58
66
|
end
|
59
67
|
end
|
60
68
|
end
|
data/lib/reek/printer.rb
CHANGED
data/lib/reek/report.rb
CHANGED
@@ -5,13 +5,13 @@ require 'set'
|
|
5
5
|
module Reek
|
6
6
|
|
7
7
|
class SortByContext
|
8
|
-
def compare(smell1, smell2)
|
8
|
+
def self.compare(smell1, smell2)
|
9
9
|
smell1.detailed_report <=> smell2.detailed_report
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
13
|
class SortBySmell
|
14
|
-
def compare(smell1, smell2)
|
14
|
+
def self.compare(smell1, smell2)
|
15
15
|
smell1.report <=> smell2.report
|
16
16
|
end
|
17
17
|
end
|
@@ -19,8 +19,8 @@ module Reek
|
|
19
19
|
class Report
|
20
20
|
|
21
21
|
SORT_ORDERS = {
|
22
|
-
:context => SortByContext
|
23
|
-
:smell => SortBySmell
|
22
|
+
:context => SortByContext,
|
23
|
+
:smell => SortBySmell
|
24
24
|
}
|
25
25
|
|
26
26
|
def initialize # :nodoc:
|
@@ -39,8 +39,8 @@ module Reek
|
|
39
39
|
@smells.length
|
40
40
|
end
|
41
41
|
|
42
|
-
def [](
|
43
|
-
@smells.to_a[
|
42
|
+
def [](index) # :nodoc:
|
43
|
+
@smells.to_a[index]
|
44
44
|
end
|
45
45
|
|
46
46
|
# Creates a formatted report of all the smells recorded in
|
data/lib/reek/smells.rb
CHANGED
@@ -18,7 +18,16 @@ module Reek
|
|
18
18
|
|
19
19
|
def self.check(exp, context, arg=nil)
|
20
20
|
smell = new(context, arg)
|
21
|
-
|
21
|
+
if smell.recognise?(exp)
|
22
|
+
context.report(smell)
|
23
|
+
true
|
24
|
+
else
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def recognise?(stuff)
|
30
|
+
@context != nil
|
22
31
|
end
|
23
32
|
|
24
33
|
def hash # :nodoc:
|
@@ -39,6 +48,8 @@ module Reek
|
|
39
48
|
"[#{name}] #{detailed_report}"
|
40
49
|
end
|
41
50
|
|
51
|
+
alias inspect report
|
52
|
+
|
42
53
|
def to_s
|
43
54
|
report
|
44
55
|
end
|
@@ -47,28 +58,30 @@ module Reek
|
|
47
58
|
class LongParameterList < Smell
|
48
59
|
MAX_ALLOWED = 3
|
49
60
|
|
50
|
-
def count_parameters(exp)
|
61
|
+
def self.count_parameters(exp)
|
51
62
|
result = exp.length - 1
|
52
63
|
result -= 1 if Array === exp[-1] and exp[-1][0] == :block
|
53
64
|
result
|
54
65
|
end
|
55
66
|
|
56
67
|
def recognise?(args)
|
57
|
-
count_parameters(args)
|
68
|
+
@num_params = LongParameterList.count_parameters(args)
|
69
|
+
@num_params > MAX_ALLOWED
|
58
70
|
end
|
59
71
|
|
60
72
|
def detailed_report
|
61
|
-
"#{@context.to_s} has
|
73
|
+
"#{@context.to_s} has #{@num_params} parameters"
|
62
74
|
end
|
63
75
|
end
|
64
76
|
|
65
77
|
class LongYieldList < LongParameterList
|
66
78
|
def recognise?(args)
|
67
|
-
|
79
|
+
@num_params = args.length
|
80
|
+
Array === args and @num_params > MAX_ALLOWED
|
68
81
|
end
|
69
82
|
|
70
83
|
def detailed_report
|
71
|
-
"#{@context} yields
|
84
|
+
"#{@context} yields #{@num_params} parameters"
|
72
85
|
end
|
73
86
|
end
|
74
87
|
|
@@ -86,21 +99,28 @@ module Reek
|
|
86
99
|
end
|
87
100
|
|
88
101
|
class FeatureEnvy < Smell
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
92
114
|
end
|
93
115
|
|
94
116
|
def recognise?(calls)
|
95
|
-
|
96
|
-
return
|
97
|
-
receivers = calls.keys.select { |key| calls[key] == max }
|
98
|
-
@receiver = receivers.map {|r| Printer.print(r)}.sort.join(' and ')
|
99
|
-
return true
|
117
|
+
@receivers = FeatureEnvy.max_keys(calls)
|
118
|
+
return !(@receivers.include?(:self))
|
100
119
|
end
|
101
120
|
|
102
121
|
def detailed_report
|
103
|
-
|
122
|
+
receiver = @receivers.map {|r| Printer.print(r)}.sort.join(' and ')
|
123
|
+
"#{@context} uses #{receiver} more than self"
|
104
124
|
end
|
105
125
|
end
|
106
126
|
|
@@ -118,13 +138,13 @@ module Reek
|
|
118
138
|
MAX_ALLOWED = 25
|
119
139
|
|
120
140
|
def recognise?(name)
|
121
|
-
|
122
|
-
num_methods =
|
123
|
-
num_methods > MAX_ALLOWED
|
141
|
+
klass = Object.const_get(name) rescue return
|
142
|
+
@num_methods = klass.instance_methods.length - klass.superclass.instance_methods.length
|
143
|
+
@num_methods > MAX_ALLOWED
|
124
144
|
end
|
125
145
|
|
126
146
|
def detailed_report
|
127
|
-
"#{@context} has
|
147
|
+
"#{@context} has #{@num_methods} methods"
|
128
148
|
end
|
129
149
|
end
|
130
150
|
|
data/lib/reek/version.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe 'Integration test:' do
|
4
|
+
Dir['spec/samples/*.rb'].each do |source|
|
5
|
+
describe source do
|
6
|
+
before :each do
|
7
|
+
@expected = IO.readlines(source.sub(/\.rb/, '.reek'))
|
8
|
+
@expected.each {|line| line.chomp!}
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should report the correct smells' do
|
12
|
+
actual = `ruby -Ilib bin/reek #{source} 2>/dev/null`.split(/\n/)
|
13
|
+
@expected.zip(actual).each do |p|
|
14
|
+
actual = p[1] ? p[1].chomp : p[1]
|
15
|
+
actual.should == p[0]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should report the correct smells in smell order' do
|
20
|
+
actual = `ruby -Ilib bin/reek --sort smell #{source} 2>/dev/null`.split(/\n/)
|
21
|
+
@expected.sort.zip(actual).each do |p|
|
22
|
+
actual = p[1] ? p[1].chomp : p[1]
|
23
|
+
actual.should == p[0]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'Reek source code:' do
|
31
|
+
Dir['lib/**/*.rb'].each do |source|
|
32
|
+
describe source do
|
33
|
+
it 'should report no smells' do
|
34
|
+
`ruby -Ilib bin/reek #{source}`.should == "\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|