reek 0.3.0 → 0.3.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 +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
|