reek 1.3.6 → 1.3.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +6 -0
- data/README.md +11 -1
- data/config/defaults.reek +1 -0
- data/features/command_line_interface/options.feature +1 -0
- data/features/rake_task/rake_task.feature +3 -0
- data/features/ruby_api/api.feature +1 -3
- data/features/samples.feature +27 -20
- data/features/support/env.rb +2 -2
- data/lib/reek/cli/application.rb +0 -4
- data/lib/reek/cli/command_line.rb +10 -12
- data/lib/reek/cli/reek_command.rb +1 -1
- data/lib/reek/cli/report.rb +36 -8
- data/lib/reek/config_file_exception.rb +3 -0
- data/lib/reek/core/code_context.rb +18 -8
- data/lib/reek/core/code_parser.rb +65 -61
- data/lib/reek/core/method_context.rb +4 -0
- data/lib/reek/core/module_context.rb +2 -2
- data/lib/reek/core/smell_repository.rb +3 -0
- data/lib/reek/core/sniffer.rb +0 -1
- data/lib/reek/core/stop_context.rb +1 -1
- data/lib/reek/smells/attribute.rb +1 -1
- data/lib/reek/smells/control_parameter.rb +79 -45
- data/lib/reek/smells/data_clump.rb +1 -1
- data/lib/reek/smells/duplicate_method_call.rb +1 -1
- data/lib/reek/smells/long_parameter_list.rb +1 -1
- data/lib/reek/smells/long_yield_list.rb +1 -1
- data/lib/reek/smells/nested_iterators.rb +1 -1
- data/lib/reek/smells/nil_check.rb +10 -5
- data/lib/reek/smells/repeated_conditional.rb +1 -1
- data/lib/reek/smells/smell_detector.rb +2 -3
- data/lib/reek/smells/too_many_instance_variables.rb +1 -1
- data/lib/reek/smells/too_many_methods.rb +1 -1
- data/lib/reek/smells/too_many_statements.rb +1 -1
- data/lib/reek/smells/uncommunicative_method_name.rb +4 -4
- data/lib/reek/smells/uncommunicative_module_name.rb +4 -4
- data/lib/reek/smells/uncommunicative_parameter_name.rb +9 -9
- data/lib/reek/smells/uncommunicative_variable_name.rb +1 -1
- data/lib/reek/smells/unused_parameters.rb +2 -6
- data/lib/reek/smells/utility_function.rb +1 -1
- data/lib/reek/source/code_comment.rb +1 -1
- data/lib/reek/source/config_file.rb +9 -8
- data/lib/reek/source/sexp_extensions.rb +2 -2
- data/lib/reek/source/sexp_node.rb +8 -5
- data/lib/reek/source/source_repository.rb +5 -0
- data/lib/reek/version.rb +1 -1
- data/reek.gemspec +3 -2
- data/spec/reek/cli/report_spec.rb +38 -8
- data/spec/reek/core/code_context_spec.rb +35 -3
- data/spec/reek/core/module_context_spec.rb +1 -1
- data/spec/reek/smells/repeated_conditional_spec.rb +1 -1
- data/spec/reek/smells/smell_detector_shared.rb +1 -2
- data/spec/reek/smells/too_many_statements_spec.rb +39 -25
- data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +44 -30
- data/spec/reek/smells/unused_parameters_spec.rb +15 -11
- data/spec/reek/source/sexp_extensions_spec.rb +2 -2
- data/spec/reek/source/sexp_node_spec.rb +0 -1
- data/spec/samples/ruby20_syntax.rb +1 -5
- metadata +172 -162
- data/lib/reek/cli/yaml_command.rb +0 -32
- data/lib/reek/core/hash_extensions.rb +0 -29
- data/spec/reek/cli/yaml_command_spec.rb +0 -47
- data/spec/reek/core/config_spec.rb +0 -38
@@ -10,12 +10,11 @@ module Reek
|
|
10
10
|
#
|
11
11
|
class CodeContext
|
12
12
|
|
13
|
-
attr_reader :exp
|
13
|
+
attr_reader :exp
|
14
14
|
|
15
15
|
def initialize(outer, exp)
|
16
16
|
@outer = outer
|
17
17
|
@exp = exp
|
18
|
-
@config = local_config
|
19
18
|
end
|
20
19
|
|
21
20
|
def name
|
@@ -56,12 +55,23 @@ module Reek
|
|
56
55
|
exp.full_name(outer)
|
57
56
|
end
|
58
57
|
|
59
|
-
def
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
58
|
+
def config_for(detector_class)
|
59
|
+
outer_config_for(detector_class).merge(
|
60
|
+
config[detector_class.smell_class_name] || {})
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def config
|
66
|
+
@config ||= if @exp
|
67
|
+
Source::CodeComment.new(@exp.comments || '').config
|
68
|
+
else
|
69
|
+
{}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def outer_config_for(detector_class)
|
74
|
+
@outer ? @outer.config_for(detector_class) : {}
|
65
75
|
end
|
66
76
|
end
|
67
77
|
end
|
@@ -29,124 +29,128 @@ module Reek
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def process_module(exp)
|
32
|
-
|
33
|
-
|
34
|
-
push(scope) do
|
35
|
-
process_default(exp) unless exp.superclass == [:const, :Struct]
|
36
|
-
check_smells(exp[0])
|
32
|
+
inside_new_context(ModuleContext, exp) do
|
33
|
+
process_default(exp)
|
37
34
|
end
|
38
|
-
scope
|
39
35
|
end
|
40
36
|
|
41
|
-
|
42
|
-
process_module(exp)
|
43
|
-
end
|
37
|
+
alias process_class process_module
|
44
38
|
|
45
39
|
def process_defn(exp)
|
46
|
-
|
40
|
+
inside_new_context(MethodContext, exp) do
|
41
|
+
count_statement_list(exp.body)
|
42
|
+
process_default(exp)
|
43
|
+
end
|
47
44
|
end
|
48
45
|
|
49
46
|
def process_defs(exp)
|
50
|
-
|
47
|
+
inside_new_context(SingletonMethodContext, exp) do
|
48
|
+
count_statement_list(exp.body)
|
49
|
+
process_default(exp)
|
50
|
+
end
|
51
51
|
end
|
52
52
|
|
53
|
-
def process_args(
|
53
|
+
def process_args(_) end
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
55
|
+
#
|
56
|
+
# Recording of calls to methods and self
|
57
|
+
#
|
58
58
|
|
59
|
-
def
|
60
|
-
@element.
|
59
|
+
def process_call(exp)
|
60
|
+
@element.record_call_to(exp)
|
61
61
|
process_default(exp)
|
62
62
|
end
|
63
63
|
|
64
|
-
|
65
|
-
|
64
|
+
alias process_attrasgn process_call
|
65
|
+
alias process_op_asgn1 process_call
|
66
|
+
|
67
|
+
def process_ivar(exp)
|
68
|
+
@element.record_use_of_self
|
66
69
|
process_default(exp)
|
67
70
|
end
|
68
71
|
|
69
|
-
|
70
|
-
|
72
|
+
alias process_iasgn process_ivar
|
73
|
+
|
74
|
+
def process_self(_)
|
75
|
+
@element.record_use_of_self
|
76
|
+
end
|
77
|
+
|
78
|
+
alias process_zsuper process_self
|
79
|
+
|
80
|
+
#
|
81
|
+
# Statement counting
|
82
|
+
#
|
83
|
+
|
84
|
+
def process_iter(exp)
|
85
|
+
count_clause(exp[3])
|
86
|
+
process_default(exp)
|
71
87
|
end
|
72
88
|
|
73
|
-
def
|
74
|
-
|
89
|
+
def process_block(exp)
|
90
|
+
count_statement_list(exp[1..-1])
|
91
|
+
@element.count_statements(-1)
|
92
|
+
process_default(exp)
|
75
93
|
end
|
76
94
|
|
77
95
|
def process_if(exp)
|
78
96
|
count_clause(exp[2])
|
79
97
|
count_clause(exp[3])
|
80
|
-
process_default(exp)
|
81
98
|
@element.count_statements(-1)
|
99
|
+
process_default(exp)
|
82
100
|
end
|
83
101
|
|
84
102
|
def process_while(exp)
|
85
|
-
process_until(exp)
|
86
|
-
end
|
87
|
-
|
88
|
-
def process_until(exp)
|
89
103
|
count_clause(exp[2])
|
90
|
-
|
104
|
+
@element.count_statements(-1)
|
105
|
+
process_default(exp)
|
91
106
|
end
|
92
107
|
|
108
|
+
alias process_until process_while
|
109
|
+
|
93
110
|
def process_for(exp)
|
94
111
|
count_clause(exp[3])
|
95
|
-
|
112
|
+
@element.count_statements(-1)
|
113
|
+
process_default(exp)
|
96
114
|
end
|
97
115
|
|
98
116
|
def process_rescue(exp)
|
99
117
|
count_clause(exp[1])
|
100
|
-
|
118
|
+
@element.count_statements(-1)
|
119
|
+
process_default(exp)
|
101
120
|
end
|
102
121
|
|
103
122
|
def process_resbody(exp)
|
104
|
-
|
123
|
+
count_statement_list(exp[2..-1].compact)
|
124
|
+
process_default(exp)
|
105
125
|
end
|
106
126
|
|
107
127
|
def process_case(exp)
|
108
|
-
|
128
|
+
count_statement_list(exp[2..-1].compact)
|
109
129
|
@element.count_statements(-1)
|
110
|
-
end
|
111
|
-
|
112
|
-
def process_when(exp)
|
113
|
-
@element.count_statements(CodeParser.count_statements(exp[2..-1].compact))
|
114
130
|
process_default(exp)
|
115
131
|
end
|
116
132
|
|
117
|
-
def
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
def process_iasgn(exp)
|
122
|
-
@element.record_use_of_self
|
133
|
+
def process_when(exp)
|
134
|
+
count_statement_list(exp[2..-1].compact)
|
135
|
+
@element.count_statements(-1)
|
123
136
|
process_default(exp)
|
124
137
|
end
|
125
138
|
|
126
|
-
|
127
|
-
@element.record_use_of_self
|
128
|
-
end
|
139
|
+
private
|
129
140
|
|
130
141
|
def count_clause(sexp)
|
131
|
-
if sexp
|
132
|
-
@element.count_statements(1)
|
133
|
-
end
|
142
|
+
@element.count_statements(1) if sexp
|
134
143
|
end
|
135
144
|
|
136
|
-
def
|
137
|
-
|
138
|
-
ignore += 1 if stmts[1] == s(:nil)
|
139
|
-
stmts.length - ignore
|
145
|
+
def count_statement_list(statement_list)
|
146
|
+
@element.count_statements statement_list.length
|
140
147
|
end
|
141
148
|
|
142
|
-
|
143
|
-
|
144
|
-
def handle_context(klass, type, exp)
|
149
|
+
def inside_new_context(klass, exp)
|
145
150
|
scope = klass.new(@element, exp)
|
146
151
|
push(scope) do
|
147
|
-
|
148
|
-
|
149
|
-
check_smells(type)
|
152
|
+
yield
|
153
|
+
check_smells(exp[0])
|
150
154
|
end
|
151
155
|
scope
|
152
156
|
end
|
@@ -155,9 +159,9 @@ module Reek
|
|
155
159
|
@sniffer.examine(@element, type)
|
156
160
|
end
|
157
161
|
|
158
|
-
def push(
|
162
|
+
def push(scope)
|
159
163
|
orig = @element
|
160
|
-
@element =
|
164
|
+
@element = scope
|
161
165
|
yield
|
162
166
|
@element = orig
|
163
167
|
end
|
data/lib/reek/core/sniffer.rb
CHANGED
@@ -8,10 +8,10 @@ module Reek
|
|
8
8
|
# Control Coupling occurs when a method or block checks the value of
|
9
9
|
# a parameter in order to decide which execution path to take. The
|
10
10
|
# offending parameter is often called a Control Couple.
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# A simple example would be the <tt>quoted</tt> parameter
|
13
13
|
# in the following method:
|
14
|
-
#
|
14
|
+
#
|
15
15
|
# def write(quoted)
|
16
16
|
# if quoted
|
17
17
|
# write_quoted(@value)
|
@@ -19,15 +19,15 @@ module Reek
|
|
19
19
|
# puts @value
|
20
20
|
# end
|
21
21
|
# end
|
22
|
-
#
|
22
|
+
#
|
23
23
|
# Control Coupling is a kind of duplication, because the calling method
|
24
24
|
# already knows which path should be taken.
|
25
|
-
#
|
25
|
+
#
|
26
26
|
# Control Coupling reduces the code's flexibility by creating a
|
27
27
|
# dependency between the caller and callee:
|
28
28
|
# any change to the possible values of the controlling parameter must
|
29
29
|
# be reflected on both sides of the call.
|
30
|
-
#
|
30
|
+
#
|
31
31
|
# A Control Couple also reveals a loss of simplicity: the called
|
32
32
|
# method probably has more than one responsibility,
|
33
33
|
# because it includes at least two different code paths.
|
@@ -55,60 +55,94 @@ module Reek
|
|
55
55
|
# @return [Array<SmellWarning>]
|
56
56
|
#
|
57
57
|
def examine_context(ctx)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
@source, SMELL_SUBCLASS,
|
64
|
-
{PARAMETER_KEY => param})
|
65
|
-
smell
|
58
|
+
ControlParameterCollector.new(ctx).control_parameters.map do |control_parameter|
|
59
|
+
SmellWarning.new(SMELL_CLASS, ctx.full_name, control_parameter.lines,
|
60
|
+
control_parameter.smell_message,
|
61
|
+
@source, SMELL_SUBCLASS,
|
62
|
+
{PARAMETER_KEY => control_parameter.name})
|
66
63
|
end
|
67
64
|
end
|
68
65
|
|
69
|
-
|
66
|
+
#
|
67
|
+
# Collects information about a single control parameter.
|
68
|
+
#
|
69
|
+
class FoundControlParameter
|
70
|
+
def initialize(param)
|
71
|
+
@param = param
|
72
|
+
@occurences = []
|
73
|
+
end
|
70
74
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
75
|
+
def record(occurences)
|
76
|
+
@occurences.concat occurences
|
77
|
+
end
|
78
|
+
|
79
|
+
def smell_message
|
80
|
+
"is controlled by argument #{name}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def lines
|
84
|
+
@occurences.map(&:line)
|
76
85
|
end
|
77
|
-
result
|
78
|
-
end
|
79
86
|
|
80
|
-
|
81
|
-
|
82
|
-
method_ctx.exp.each_node(:lvar, [:if, :case, :and, :or, :args]) do |node|
|
83
|
-
return true if node.value == param
|
87
|
+
def name
|
88
|
+
@param.to_s
|
84
89
|
end
|
85
|
-
false
|
86
90
|
end
|
87
91
|
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
92
|
+
#
|
93
|
+
# Collects all control parameters in a given context.
|
94
|
+
#
|
95
|
+
class ControlParameterCollector
|
96
|
+
def initialize(context)
|
97
|
+
@context = context
|
98
|
+
end
|
99
|
+
|
100
|
+
def control_parameters
|
101
|
+
result = Hash.new {|hash, key| hash[key] = FoundControlParameter.new(key)}
|
102
|
+
potential_parameters.each do |param|
|
103
|
+
matches = find_matches(param)
|
104
|
+
result[param].record(matches) if matches.any?
|
95
105
|
end
|
106
|
+
result.values
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Returns parameters that aren't used outside of a conditional statements and that
|
112
|
+
# could be good candidates for being a control parameter.
|
113
|
+
def potential_parameters
|
114
|
+
@context.exp.parameter_names.select {|param| !used_outside_conditional?(param)}
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns wether the parameter is used outside of the conditional statement.
|
118
|
+
def used_outside_conditional?(param)
|
119
|
+
nodes = @context.exp.each_node(:lvar, [:if, :case, :and, :or, :args])
|
120
|
+
nodes.any? {|node| node.value == param}
|
121
|
+
end
|
122
|
+
|
123
|
+
# Find the use of the param that match the definition of a control parameter.
|
124
|
+
def find_matches(param)
|
125
|
+
matches = []
|
126
|
+
[:if, :case, :and, :or].each do |keyword|
|
127
|
+
@context.local_nodes(keyword).each do |node|
|
128
|
+
return [] if used_besides_in_condition?(node, param)
|
129
|
+
node.each_node(:lvar, []) {|inner| matches.push(inner) if inner.value == param}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
matches
|
96
133
|
end
|
97
|
-
matchs
|
98
|
-
end
|
99
134
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
times_in_conditional += 1 if inner.class == Sexp && inner[VALUE_POSITION] == param
|
135
|
+
# Returns wether the parameter is used somewhere besides in the condition of the
|
136
|
+
# conditional statement.
|
137
|
+
def used_besides_in_condition?(node, param)
|
138
|
+
times_in_conditional, times_total = 0, 0
|
139
|
+
node.each_node(:lvar, [:if, :case]) {|lvar| times_total +=1 if lvar.value == param}
|
140
|
+
if node.condition
|
141
|
+
times_in_conditional += 1 if node.condition[VALUE_POSITION] == param
|
142
|
+
times_in_conditional += node.condition.count {|inner| inner.class == Sexp && inner[VALUE_POSITION] == param}
|
109
143
|
end
|
144
|
+
return times_total > times_in_conditional
|
110
145
|
end
|
111
|
-
return times_total > times_in_conditional
|
112
146
|
end
|
113
147
|
end
|
114
148
|
end
|