reek 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +11 -1
- data/README.txt +1 -0
- data/lib/reek.rb +8 -9
- data/lib/reek/checker.rb +10 -2
- data/lib/reek/class_checker.rb +4 -7
- data/lib/reek/file_checker.rb +0 -6
- data/lib/reek/method_checker.rb +56 -30
- data/lib/reek/object_refs.rb +5 -2
- data/lib/reek/printer.rb +45 -7
- data/lib/reek/rake_task.rb +5 -3
- data/lib/reek/smells/control_couple.rb +53 -0
- data/lib/reek/smells/duplication.rb +54 -0
- data/lib/reek/smells/feature_envy.rb +65 -0
- data/lib/reek/smells/large_class.rb +35 -0
- data/lib/reek/smells/long_method.rb +35 -0
- data/lib/reek/smells/long_parameter_list.rb +36 -0
- data/lib/reek/smells/long_yield_list.rb +20 -0
- data/lib/reek/smells/nested_iterators.rb +24 -0
- data/lib/reek/smells/smell.rb +56 -0
- data/lib/reek/smells/smells.rb +24 -0
- data/lib/reek/smells/uncommunicative_name.rb +72 -0
- data/lib/reek/smells/utility_function.rb +34 -0
- data/lib/reek/version.rb +1 -1
- data/spec/integration_spec.rb +6 -6
- data/spec/reek/printer_spec.rb +21 -21
- data/spec/reek/report_spec.rb +5 -5
- data/spec/reek/{control_couple_spec.rb → smells/control_couple_spec.rb} +1 -1
- data/spec/reek/smells/duplication_spec.rb +60 -0
- data/spec/reek/smells/feature_envy_spec.rb +91 -0
- data/spec/reek/{large_class_spec.rb → smells/large_class_spec.rb} +8 -8
- data/spec/reek/{long_method_spec.rb → smells/long_method_spec.rb} +1 -1
- data/spec/reek/{long_parameter_list_spec.rb → smells/long_parameter_list_spec.rb} +1 -1
- data/spec/reek/smells/nested_iterators_spec.rb +43 -0
- data/spec/reek/{smell_spec.rb → smells/smell_spec.rb} +2 -2
- data/spec/reek/smells/uncommunicative_name_spec.rb +83 -0
- data/spec/reek/{utility_function_spec.rb → smells/utility_function_spec.rb} +1 -1
- data/spec/samples/inline.reek +13 -5
- data/spec/samples/optparse.reek +32 -10
- data/spec/samples/redcloth.reek +24 -6
- data/spec/script_spec.rb +1 -1
- data/tasks/reek.rake +9 -0
- data/website/index.html +3 -2
- data/website/index.txt +3 -1
- metadata +24 -12
- data/lib/reek/smells.rb +0 -192
- data/spec/reek/feature_envy_spec.rb +0 -222
- data/spec/reek/nested_iterators_spec.rb +0 -42
- data/spec/reek/uncommunicative_name_spec.rb +0 -106
@@ -0,0 +1,54 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
require 'reek/printer'
|
5
|
+
|
6
|
+
module Reek
|
7
|
+
module Smells
|
8
|
+
|
9
|
+
#
|
10
|
+
# Duplication occurs when two fragments of code look nearly identical,
|
11
|
+
# or when two fragments of code have nearly identical effects
|
12
|
+
# at some conceptual level.
|
13
|
+
#
|
14
|
+
# Currently +Duplication+ checks for repeated identical method calls
|
15
|
+
# within any one method definition. For example, the following method
|
16
|
+
# will report a warning:
|
17
|
+
#
|
18
|
+
# def double_thing()
|
19
|
+
# @other.thing + @other.thing
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
class Duplication < Smell
|
23
|
+
|
24
|
+
#
|
25
|
+
# Checks the given +method+ for duplication.
|
26
|
+
# Any smells found are added to the +report+; returns true in that case,
|
27
|
+
# and false otherwise.
|
28
|
+
#
|
29
|
+
def self.examine(method, report)
|
30
|
+
look_for_duplicate_calls(method, report)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.look_for_duplicate_calls(method, report) # :nodoc:
|
34
|
+
smell_reported = false
|
35
|
+
method.calls.select {|key,val| val > 1}.each do |call_exp|
|
36
|
+
call = call_exp[0]
|
37
|
+
report << new(method, call_exp[0]) unless call[2] == :new
|
38
|
+
smell_reported = true
|
39
|
+
end
|
40
|
+
return smell_reported
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(context, call)
|
44
|
+
super(context)
|
45
|
+
@call = call
|
46
|
+
end
|
47
|
+
|
48
|
+
def detailed_report
|
49
|
+
"#{@context} calls #{Printer.print(@call)} more than once"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# Feature Envy occurs when a code fragment references another object
|
10
|
+
# more often than it references itself, or when several clients do
|
11
|
+
# the same series of manipulations on a particular type of object.
|
12
|
+
#
|
13
|
+
# A simple example would be the following method, which "belongs"
|
14
|
+
# on the Item class and not on the Cart class:
|
15
|
+
#
|
16
|
+
# class Cart
|
17
|
+
# def price
|
18
|
+
# @item.price + @item.tax
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Feature Envy reduces the code's ability to communicate intent:
|
23
|
+
# code that "belongs" on one class but which is located in another
|
24
|
+
# can be hard to find, and may upset the "System of Names"
|
25
|
+
# in the host class.
|
26
|
+
#
|
27
|
+
# Feature Envy also affects the design's flexibility: A code fragment
|
28
|
+
# that is in the wrong class creates couplings that may not be natural
|
29
|
+
# within the application's domain, and creates a loss of cohesion
|
30
|
+
# in the unwilling host class.
|
31
|
+
#
|
32
|
+
# Currently +FeatureEnvy+ reports any method that refers to self less
|
33
|
+
# often than it refers to (ie. send messages to) some other object.
|
34
|
+
#
|
35
|
+
class FeatureEnvy < Smell
|
36
|
+
|
37
|
+
#
|
38
|
+
# Checks whether the given +method+ includes any code fragment that
|
39
|
+
# might "belong" on another class.
|
40
|
+
# Any smells found are added to the +report+; returns true in that case,
|
41
|
+
# and false otherwise.
|
42
|
+
#
|
43
|
+
def self.examine(method, report)
|
44
|
+
return false if method.name == 'initialize'
|
45
|
+
return false if method.refs.self_is_max?
|
46
|
+
smell_found = false
|
47
|
+
method.refs.max_keys.each do |r|
|
48
|
+
report << new(method, Printer.print(r))
|
49
|
+
smell_found = true
|
50
|
+
end
|
51
|
+
smell_found
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(context, receiver)
|
55
|
+
super(context)
|
56
|
+
@receiver = receiver
|
57
|
+
end
|
58
|
+
|
59
|
+
def detailed_report
|
60
|
+
"#{@context} refers to #{@receiver} more than self"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# A Large Class is a class or module that has a large number of
|
10
|
+
# instance variables, methods or lines of code.
|
11
|
+
#
|
12
|
+
# Currently +LargeClass+ only reports classes having more than
|
13
|
+
# +MAX_ALLOWED+ public methods.
|
14
|
+
#
|
15
|
+
class LargeClass < Smell
|
16
|
+
MAX_ALLOWED = 25
|
17
|
+
|
18
|
+
def self.non_inherited_methods(klass)
|
19
|
+
return klass.instance_methods if klass.superclass.nil?
|
20
|
+
klass.instance_methods - klass.superclass.instance_methods
|
21
|
+
end
|
22
|
+
|
23
|
+
def recognise?(name)
|
24
|
+
klass = Object.const_get(name) rescue return
|
25
|
+
@num_methods = LargeClass.non_inherited_methods(klass).length
|
26
|
+
@num_methods > MAX_ALLOWED
|
27
|
+
end
|
28
|
+
|
29
|
+
def detailed_report
|
30
|
+
"#{@context} has #{@num_methods} methods"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# A Long Method is any method that has a large number of lines.
|
10
|
+
#
|
11
|
+
# Currently +LongMethod+ reports any method with more than
|
12
|
+
# +MAX_ALLOWED+ statements.
|
13
|
+
#
|
14
|
+
class LongMethod < Smell
|
15
|
+
|
16
|
+
MAX_ALLOWED = 5
|
17
|
+
|
18
|
+
def self.examine(method, report)
|
19
|
+
return if method.name == 'initialize'
|
20
|
+
num = method.num_statements
|
21
|
+
report << new(method, num) if num > MAX_ALLOWED
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(context, num)
|
25
|
+
super(context)
|
26
|
+
@num_stmts = num
|
27
|
+
end
|
28
|
+
|
29
|
+
def detailed_report
|
30
|
+
"#{@context} has approx #{@num_stmts} statements"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# A Long Parameter List occurs when a method has more than one
|
10
|
+
# or two parameters, or when a method yields more than one or
|
11
|
+
# two objects to an associated block.
|
12
|
+
#
|
13
|
+
# Currently +LongParameterList+ reports any method with more
|
14
|
+
# than +MAX_ALLOWED+ parameters.
|
15
|
+
#
|
16
|
+
class LongParameterList < Smell
|
17
|
+
MAX_ALLOWED = 3
|
18
|
+
|
19
|
+
def self.count_parameters(exp)
|
20
|
+
result = exp.length - 1
|
21
|
+
result -= 1 if Array === exp[-1] and exp[-1][0] == :block
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
def recognise?(args)
|
26
|
+
@num_params = LongParameterList.count_parameters(args)
|
27
|
+
@num_params > MAX_ALLOWED
|
28
|
+
end
|
29
|
+
|
30
|
+
def detailed_report
|
31
|
+
"#{@context.to_s} has #{@num_params} parameters"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
class LongYieldList < LongParameterList
|
9
|
+
def recognise?(args)
|
10
|
+
@num_params = args.length
|
11
|
+
Array === args and @num_params > MAX_ALLOWED
|
12
|
+
end
|
13
|
+
|
14
|
+
def detailed_report
|
15
|
+
"#{@context} yields #{@num_params} parameters"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# A Nested Iterator occurs when a block contains another block.
|
10
|
+
#
|
11
|
+
# +NestedIterators+ reports failing methods only once.
|
12
|
+
#
|
13
|
+
class NestedIterators < Smell
|
14
|
+
def recognise?(already_in_iter)
|
15
|
+
already_in_iter && @context
|
16
|
+
end
|
17
|
+
|
18
|
+
def detailed_report
|
19
|
+
"#{@context} has nested iterators"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/options'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
class Smell
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
def self.convert_camel_case(class_name)
|
12
|
+
class_name.gsub(/([a-z])([A-Z])/) { |s| "#{$1} #{$2}"}
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(context, arg=nil)
|
16
|
+
@context = context
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.check(exp, context, arg=nil)
|
20
|
+
smell = new(context, arg)
|
21
|
+
return false unless smell.recognise?(exp)
|
22
|
+
context.report(smell)
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def recognise?(stuff)
|
27
|
+
@context != nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def hash # :nodoc:
|
31
|
+
report.hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def <=>(other) # :nodoc:
|
35
|
+
Options[:sort_order].compare(self, other)
|
36
|
+
end
|
37
|
+
|
38
|
+
alias eql? <=>
|
39
|
+
|
40
|
+
def name
|
41
|
+
Smell.convert_camel_case(self.class.name.split(/::/)[2])
|
42
|
+
end
|
43
|
+
|
44
|
+
def report
|
45
|
+
"[#{name}] #{detailed_report}"
|
46
|
+
end
|
47
|
+
|
48
|
+
alias inspect report
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
report
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/control_couple'
|
4
|
+
require 'reek/smells/duplication'
|
5
|
+
require 'reek/smells/feature_envy'
|
6
|
+
require 'reek/smells/long_method'
|
7
|
+
require 'reek/smells/long_parameter_list'
|
8
|
+
require 'reek/smells/long_yield_list'
|
9
|
+
require 'reek/smells/nested_iterators'
|
10
|
+
require 'reek/smells/uncommunicative_name'
|
11
|
+
require 'reek/smells/utility_function'
|
12
|
+
|
13
|
+
module Reek
|
14
|
+
|
15
|
+
SMELLS = {
|
16
|
+
:defn => [
|
17
|
+
Smells::UncommunicativeName,
|
18
|
+
Smells::LongMethod,
|
19
|
+
Smells::Duplication,
|
20
|
+
Smells::UtilityFunction,
|
21
|
+
Smells::FeatureEnvy
|
22
|
+
]
|
23
|
+
}
|
24
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# An Uncommunicative Name is a name that doesn't communicate its intent
|
10
|
+
# well enough.
|
11
|
+
#
|
12
|
+
# Poor names make it hard for the reader to build a mental picture
|
13
|
+
# of what's going on in the code. They can also be mis-interpreted;
|
14
|
+
# and they hurt the flow of reading, because the reader must slow
|
15
|
+
# down to interpret the names.
|
16
|
+
#
|
17
|
+
# Currently +UncommunicativeName+ checks for
|
18
|
+
# * 1-character names
|
19
|
+
# * names consisting of a single character followed by a number
|
20
|
+
#
|
21
|
+
class UncommunicativeName < Smell
|
22
|
+
|
23
|
+
#
|
24
|
+
# Checks the given +method+ for uncommunicative method name,
|
25
|
+
# parameter names, local variable names and instance variable names.
|
26
|
+
# Any smells found are added to the +report+; returns true in that case,
|
27
|
+
# and false otherwise.
|
28
|
+
#
|
29
|
+
def self.examine(method, report)
|
30
|
+
smell_reported = consider(method.name, method, report, 'method')
|
31
|
+
method.parameters.each do |param|
|
32
|
+
smell_reported = consider(param, method, report, 'parameter') || smell_reported
|
33
|
+
end
|
34
|
+
method.local_variables.each do |lvar|
|
35
|
+
smell_reported = consider(lvar, method, report, 'local variable') || smell_reported
|
36
|
+
end
|
37
|
+
method.instance_variables.each do |ivar|
|
38
|
+
smell_reported = consider(ivar, method, report, 'field') || smell_reported
|
39
|
+
end
|
40
|
+
smell_reported
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.consider(sym, method, report, type) # :nodoc:
|
44
|
+
name = sym.to_s
|
45
|
+
if is_bad_name?(name)
|
46
|
+
report << new(name, method, type)
|
47
|
+
return true
|
48
|
+
end
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.is_bad_name?(name)
|
53
|
+
return false if name == '*'
|
54
|
+
name = name[1..-1] while /^@/ === name
|
55
|
+
return true if name.length < 2
|
56
|
+
return true if /^.[0-9]$/ === name
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(name, context, symbol_type)
|
61
|
+
super(context, symbol_type)
|
62
|
+
@bad_name = name
|
63
|
+
@symbol_type = symbol_type
|
64
|
+
end
|
65
|
+
|
66
|
+
def detailed_report
|
67
|
+
"#{@context} uses the #{@symbol_type} name '#{@bad_name}'"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'reek/smells/smell'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# A Utility Function is any instance method that has no
|
10
|
+
# dependency on the state of the instance.
|
11
|
+
#
|
12
|
+
class UtilityFunction < Smell
|
13
|
+
|
14
|
+
#
|
15
|
+
# Checks whether the given +method+ is a utility function.
|
16
|
+
# Any smells found are added to the +report+; returns true in that case,
|
17
|
+
# and false otherwise.
|
18
|
+
#
|
19
|
+
def self.examine(method, report)
|
20
|
+
return false if method.name == 'initialize'
|
21
|
+
if method.num_statements > 0 and !method.depends_on_self
|
22
|
+
report << new(method)
|
23
|
+
true
|
24
|
+
end
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def detailed_report
|
29
|
+
"#{@context} doesn't depend on instance state"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|