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