reek 1.3.4 → 1.3.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +5 -0
- data/README.md +27 -2
- data/features/command_line_interface/options.feature +5 -2
- data/features/command_line_interface/smells_count.feature +43 -45
- data/features/command_line_interface/stdin.feature +9 -15
- data/features/configuration_files/masking_smells.feature +9 -17
- data/features/rake_task/rake_task.feature +4 -4
- data/features/reports/reports.feature +80 -21
- data/features/samples.feature +8 -18
- data/features/step_definitions/reek_steps.rb +4 -0
- data/lib/reek/cli/application.rb +3 -6
- data/lib/reek/cli/command_line.rb +16 -6
- data/lib/reek/cli/reek_command.rb +4 -12
- data/lib/reek/cli/report.rb +61 -19
- data/lib/reek/config_file_exception.rb +5 -0
- data/lib/reek/smells/control_parameter.rb +45 -14
- data/lib/reek/smells/data_clump.rb +15 -39
- data/lib/reek/smells/duplicate_method_call.rb +76 -26
- data/lib/reek/source/config_file.rb +30 -19
- data/lib/reek/source/sexp_extensions.rb +139 -0
- data/lib/reek/source/sexp_node.rb +64 -0
- data/lib/reek/source/source_code.rb +1 -1
- data/lib/reek/source/tree_dresser.rb +30 -175
- data/lib/reek/spec/should_reek.rb +2 -5
- data/lib/reek/version.rb +1 -1
- data/reek.gemspec +1 -1
- data/spec/matchers/smell_of_matcher.rb +12 -15
- data/spec/reek/cli/report_spec.rb +10 -6
- data/spec/reek/core/code_parser_spec.rb +0 -6
- data/spec/reek/smells/control_parameter_spec.rb +195 -8
- data/spec/reek/smells/data_clump_spec.rb +28 -3
- data/spec/reek/smells/uncommunicative_method_name_spec.rb +7 -7
- data/spec/reek/source/sexp_extensions_spec.rb +290 -0
- data/spec/reek/source/sexp_node_spec.rb +28 -0
- data/spec/reek/source/source_code_spec.rb +59 -19
- data/spec/reek/source/tree_dresser_spec.rb +7 -314
- data/spec/reek/spec/should_reek_spec.rb +51 -64
- data/spec/samples/all_but_one_masked/dirty.rb +2 -2
- data/spec/samples/corrupt_config_file/dirty.rb +1 -0
- data/spec/samples/masked/dirty.rb +1 -1
- data/spec/samples/masked_by_dotfile/dirty.rb +2 -2
- data/spec/samples/no_config_file/dirty.rb +8 -0
- data/spec/samples/not_quite_masked/dirty.rb +0 -3
- data/spec/samples/three_smelly_files/dirty_one.rb +3 -0
- data/spec/samples/three_smelly_files/dirty_three.rb +5 -0
- data/spec/samples/three_smelly_files/dirty_two.rb +4 -0
- data/spec/spec_helper.rb +5 -0
- metadata +145 -137
- data/spec/reek/cli/reek_command_spec.rb +0 -46
data/lib/reek/cli/application.rb
CHANGED
@@ -23,7 +23,7 @@ module Reek
|
|
23
23
|
begin
|
24
24
|
cmd = @options.parse
|
25
25
|
cmd.execute(self)
|
26
|
-
rescue
|
26
|
+
rescue OptionParser::InvalidOption, ConfigFileException => error
|
27
27
|
$stderr.puts "Error: #{error}"
|
28
28
|
@status = STATUS_ERROR
|
29
29
|
end
|
@@ -42,11 +42,8 @@ module Reek
|
|
42
42
|
@status = STATUS_SMELLS
|
43
43
|
end
|
44
44
|
|
45
|
-
def
|
46
|
-
|
47
|
-
total_smells_message += 's' unless total_smells_count == 1
|
48
|
-
total_smells_message += "\n"
|
49
|
-
output total_smells_message
|
45
|
+
def update_status(reporter)
|
46
|
+
reporter.has_smells? ? report_smells : report_success
|
50
47
|
end
|
51
48
|
end
|
52
49
|
end
|
@@ -17,10 +17,11 @@ module Reek
|
|
17
17
|
def initialize(argv)
|
18
18
|
@argv = argv
|
19
19
|
@parser = OptionParser.new
|
20
|
-
@report_class =
|
20
|
+
@report_class = QuietReport
|
21
21
|
@warning_formatter = WarningFormatterWithLineNumbers
|
22
22
|
@command_class = ReekCommand
|
23
23
|
@config_files = []
|
24
|
+
@sort_by_issue_count = false
|
24
25
|
set_options
|
25
26
|
end
|
26
27
|
|
@@ -72,15 +73,24 @@ EOB
|
|
72
73
|
end
|
73
74
|
|
74
75
|
@parser.separator "\nReport formatting:"
|
75
|
-
@parser.on("-q", "--
|
76
|
-
@report_class =
|
76
|
+
@parser.on("-q", "--quiet", "Suppress headings for smell-free source files (this is the default)") do |opt|
|
77
|
+
@report_class = QuietReport
|
77
78
|
end
|
78
|
-
@parser.on("-
|
79
|
+
@parser.on("-V", "--no-quiet", "--verbose", "Show headings for smell-free source files") do |opt|
|
80
|
+
@report_class = VerboseReport
|
81
|
+
end
|
82
|
+
@parser.on("-n", "--no-line-numbers", "Suppress line numbers from the output") do
|
79
83
|
@warning_formatter = SimpleWarningFormatter
|
80
84
|
end
|
85
|
+
@parser.on("--line-numbers", "Show line numbers in the output (this is the default)") do
|
86
|
+
@warning_formatter = WarningFormatterWithLineNumbers
|
87
|
+
end
|
81
88
|
@parser.on("-s", "--single-line", "Show IDE-compatible single-line-per-warning") do
|
82
89
|
@warning_formatter = SingleLineWarningFormatter
|
83
90
|
end
|
91
|
+
@parser.on("-S", "--sort-by-issue-count", 'Sort by "issue-count", listing the "smelliest" files first') do
|
92
|
+
@sort_by_issue_count = true
|
93
|
+
end
|
84
94
|
@parser.on("-y", "--yaml", "Report smells in YAML format") do
|
85
95
|
@command_class = YamlCommand
|
86
96
|
# SMELL: the args passed to the command should be tested, because it may
|
@@ -100,8 +110,8 @@ EOB
|
|
100
110
|
if @command_class == YamlCommand
|
101
111
|
YamlCommand.create(sources, @config_files)
|
102
112
|
else
|
103
|
-
|
104
|
-
ReekCommand.create(sources,
|
113
|
+
reporter = @report_class.new(@warning_formatter, ReportFormatter, @sort_by_issue_count)
|
114
|
+
ReekCommand.create(sources, reporter, @config_files)
|
105
115
|
end
|
106
116
|
end
|
107
117
|
end
|
@@ -18,20 +18,12 @@ module Reek
|
|
18
18
|
@config_files = config_files
|
19
19
|
end
|
20
20
|
|
21
|
-
def execute(
|
22
|
-
total_smells_count = 0
|
21
|
+
def execute(app)
|
23
22
|
@sources.each do |source|
|
24
|
-
|
25
|
-
total_smells_count += examiner.smells_count
|
26
|
-
view.output @reporter.report(examiner)
|
23
|
+
@reporter.add_examiner(Examiner.new(source, @config_files))
|
27
24
|
end
|
28
|
-
|
29
|
-
|
30
|
-
else
|
31
|
-
view.report_success
|
32
|
-
end
|
33
|
-
|
34
|
-
view.output_smells_total(total_smells_count) if @sources.count > 1
|
25
|
+
app.update_status(@reporter)
|
26
|
+
@reporter.show
|
35
27
|
end
|
36
28
|
end
|
37
29
|
end
|
data/lib/reek/cli/report.rb
CHANGED
@@ -31,36 +31,78 @@ module Reek
|
|
31
31
|
def self.format(warning)
|
32
32
|
"#{warning.source}:#{warning.lines.first}: #{SimpleWarningFormatter.format(warning)}"
|
33
33
|
end
|
34
|
-
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Report
|
37
|
+
def initialize(warning_formatter = SimpleWarningFormatter, report_formatter = ReportFormatter, sort_by_issue_count = false)
|
38
|
+
@warning_formatter = warning_formatter
|
39
|
+
@report_formatter = report_formatter
|
40
|
+
@examiners = []
|
41
|
+
@total_smell_count = 0
|
42
|
+
@sort_by_issue_count = sort_by_issue_count
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_examiner(examiner)
|
46
|
+
@total_smell_count += examiner.smells_count
|
47
|
+
@examiners << examiner
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def show
|
52
|
+
sort_examiners
|
53
|
+
display_summary
|
54
|
+
display_total_smell_count
|
55
|
+
end
|
56
|
+
|
57
|
+
def has_smells?
|
58
|
+
@total_smell_count > 0
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def sort_examiners
|
64
|
+
@examiners.sort! {|a, b| b.smells_count <=> a.smells_count } if @sort_by_issue_count
|
65
|
+
end
|
35
66
|
|
36
|
-
|
37
|
-
|
38
|
-
#
|
39
|
-
class VerboseReport
|
40
|
-
def initialize(warning_formatter = SimpleWarningFormatter, report_formatter = ReportFormatter)
|
41
|
-
@warning_formatter = warning_formatter
|
42
|
-
@report_formatter = report_formatter
|
67
|
+
def display_summary
|
68
|
+
print gather_results.reject(&:empty?).join("\n")
|
43
69
|
end
|
44
70
|
|
45
|
-
def
|
71
|
+
def display_total_smell_count
|
72
|
+
if @examiners.size > 1
|
73
|
+
print "\n"
|
74
|
+
print total_smell_count_message
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def total_smell_count_message
|
79
|
+
"#{@total_smell_count} total warning#{'s' unless @total_smell_count == 1 }\n"
|
80
|
+
end
|
81
|
+
|
82
|
+
def summarize_single_examiner(examiner)
|
46
83
|
result = @report_formatter.header examiner
|
47
84
|
if examiner.smelly?
|
48
85
|
formatted_list = @report_formatter.format_list examiner.smells, @warning_formatter
|
49
86
|
result += ":\n#{formatted_list}"
|
50
87
|
end
|
51
|
-
result
|
88
|
+
result
|
52
89
|
end
|
53
90
|
end
|
54
91
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
92
|
+
class VerboseReport < Report
|
93
|
+
def gather_results
|
94
|
+
@examiners.each_with_object([]) do |examiner, result|
|
95
|
+
result << summarize_single_examiner(examiner)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class QuietReport < Report
|
101
|
+
def gather_results
|
102
|
+
@examiners.each_with_object([]) do |examiner, result|
|
103
|
+
if examiner.smelly?
|
104
|
+
result << summarize_single_examiner(examiner)
|
105
|
+
end
|
64
106
|
end
|
65
107
|
end
|
66
108
|
end
|
@@ -46,6 +46,7 @@ module Reek
|
|
46
46
|
SMELL_CLASS = 'ControlCouple'
|
47
47
|
SMELL_SUBCLASS = self.name.split(/::/)[-1]
|
48
48
|
PARAMETER_KEY = 'parameter'
|
49
|
+
VALUE_POSITION = 1
|
49
50
|
|
50
51
|
#
|
51
52
|
# Checks whether the given method chooses its execution path
|
@@ -54,31 +55,61 @@ module Reek
|
|
54
55
|
# @return [Array<SmellWarning>]
|
55
56
|
#
|
56
57
|
def examine_context(ctx)
|
57
|
-
control_parameters(ctx).map do |
|
58
|
-
param =
|
58
|
+
control_parameters(ctx).map do |lvars, occurs|
|
59
|
+
param = lvars.format_ruby
|
59
60
|
lines = occurs.map {|exp| exp.line}
|
60
61
|
smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, lines,
|
61
|
-
|
62
|
-
|
63
|
-
|
62
|
+
"is controlled by argument #{param}",
|
63
|
+
@source, SMELL_SUBCLASS,
|
64
|
+
{PARAMETER_KEY => param})
|
64
65
|
smell
|
65
66
|
end
|
66
67
|
end
|
67
68
|
|
68
|
-
|
69
|
+
private
|
69
70
|
|
70
71
|
def control_parameters(method_ctx)
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
cond = if_node[1]
|
76
|
-
if cond[0] == :lvar and params.include?(cond[1])
|
77
|
-
result[cond].push(cond)
|
78
|
-
end
|
72
|
+
result = Hash.new {|hash, key| hash[key] = []}
|
73
|
+
method_ctx.exp.parameter_names.each do |param|
|
74
|
+
next if used_outside_conditional?(method_ctx, param)
|
75
|
+
find_matchs(method_ctx, param).each {|match| result[match].push(match)}
|
79
76
|
end
|
80
77
|
result
|
81
78
|
end
|
79
|
+
|
80
|
+
# Returns wether the parameter is used outside of the conditional statement.
|
81
|
+
def used_outside_conditional?(method_ctx, param)
|
82
|
+
method_ctx.exp.each_node(:lvar, [:if, :case, :and, :or, :args]) do |node|
|
83
|
+
return true if node.value == param
|
84
|
+
end
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
# Find the use of the param that match the definition of a control parameter.
|
89
|
+
def find_matchs(method_ctx, param)
|
90
|
+
matchs = []
|
91
|
+
[:if, :case, :and, :or].each do |keyword|
|
92
|
+
method_ctx.local_nodes(keyword).each do |node|
|
93
|
+
return [] if used_besides_in_condition?(node, param)
|
94
|
+
node.each_node(:lvar, []) {|inner| matchs.push(inner) if inner.value == param}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
matchs
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns wether the parameter is used somewhere besides in the condition of the
|
101
|
+
# conditional statement.
|
102
|
+
def used_besides_in_condition?(node, param)
|
103
|
+
times_in_conditional, times_total = 0, 0
|
104
|
+
node.each_node(:lvar, [:if, :case]) {|inner| times_total +=1 if inner[VALUE_POSITION] == param}
|
105
|
+
if node.condition
|
106
|
+
times_in_conditional += 1 if node.condition[VALUE_POSITION] == param
|
107
|
+
node.condition.each do |inner|
|
108
|
+
times_in_conditional += 1 if inner.class == Sexp && inner[VALUE_POSITION] == param
|
109
|
+
end
|
110
|
+
end
|
111
|
+
return times_total > times_in_conditional
|
112
|
+
end
|
82
113
|
end
|
83
114
|
end
|
84
115
|
end
|
@@ -89,49 +89,29 @@ module Reek
|
|
89
89
|
def initialize(ctx, min_clump_size, max_copies)
|
90
90
|
@min_clump_size = min_clump_size
|
91
91
|
@max_copies = max_copies
|
92
|
-
@candidate_methods = ctx.local_nodes(:defn).
|
93
|
-
|
94
|
-
end.map {|defn_node| CandidateMethod.new(defn_node)}
|
95
|
-
delete_infrequent_parameters
|
96
|
-
delete_small_methods
|
92
|
+
@candidate_methods = ctx.local_nodes(:defn).map {|defn_node|
|
93
|
+
CandidateMethod.new(defn_node)}
|
97
94
|
end
|
98
95
|
|
99
|
-
def
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
end
|
106
|
-
end
|
96
|
+
def candidate_clumps
|
97
|
+
@candidate_methods.each_cons(@max_copies + 1).map do |methods|
|
98
|
+
common_argument_names_for(methods)
|
99
|
+
end.select do |clump|
|
100
|
+
clump.length >= @min_clump_size
|
101
|
+
end.uniq
|
107
102
|
end
|
108
103
|
|
109
|
-
def
|
110
|
-
|
111
|
-
tail = methods[1..-1]
|
112
|
-
clumps_containing(methods[0], tail, results)
|
113
|
-
collect_clumps_in(tail, results)
|
114
|
-
end
|
115
|
-
|
116
|
-
def clumps
|
117
|
-
results = Hash.new([])
|
118
|
-
collect_clumps_in(@candidate_methods, results)
|
119
|
-
results.each_key { |key| results[key].uniq! }
|
120
|
-
results
|
104
|
+
def common_argument_names_for(methods)
|
105
|
+
methods.collect(&:arg_names).inject(:&)
|
121
106
|
end
|
122
107
|
|
123
|
-
def
|
124
|
-
@candidate_methods
|
125
|
-
meth.arg_names.length >= @min_clump_size
|
126
|
-
end
|
108
|
+
def methods_containing_clump(clump)
|
109
|
+
@candidate_methods.select { |method| clump & method.arg_names == clump }
|
127
110
|
end
|
128
111
|
|
129
|
-
def
|
130
|
-
|
131
|
-
|
132
|
-
occurs = @candidate_methods.inject(0) {|sum, cm| cm.arg_names.include?(param) ? sum+1 : sum}
|
133
|
-
meth.delete(param) if occurs <= @max_copies
|
134
|
-
end
|
112
|
+
def clumps
|
113
|
+
candidate_clumps.map do |clump|
|
114
|
+
[clump, methods_containing_clump(clump)]
|
135
115
|
end
|
136
116
|
end
|
137
117
|
end
|
@@ -148,10 +128,6 @@ module Reek
|
|
148
128
|
@params
|
149
129
|
end
|
150
130
|
|
151
|
-
def delete(param)
|
152
|
-
@params.delete(param)
|
153
|
-
end
|
154
|
-
|
155
131
|
def line
|
156
132
|
@defn.line
|
157
133
|
end
|
@@ -18,7 +18,6 @@ module Reek
|
|
18
18
|
# end
|
19
19
|
#
|
20
20
|
class DuplicateMethodCall < SmellDetector
|
21
|
-
|
22
21
|
SMELL_CLASS = 'Duplication'
|
23
22
|
SMELL_SUBCLASS = self.name.split(/::/)[-1]
|
24
23
|
|
@@ -51,39 +50,90 @@ module Reek
|
|
51
50
|
# @return [Array<SmellWarning>]
|
52
51
|
#
|
53
52
|
def examine_context(ctx)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, copies.map {|exp| exp.line},
|
63
|
-
"calls #{call} #{multiple}",
|
64
|
-
@source, SMELL_SUBCLASS,
|
65
|
-
{CALL_KEY => call, OCCURRENCES_KEY => occurs})
|
66
|
-
smell
|
53
|
+
max_allowed_calls = value(MAX_ALLOWED_CALLS_KEY, ctx, DEFAULT_MAX_CALLS)
|
54
|
+
allow_calls = value(ALLOW_CALLS_KEY, ctx, DEFAULT_ALLOW_CALLS)
|
55
|
+
|
56
|
+
CallCollector.new(ctx, max_allowed_calls, allow_calls).smelly_calls.map do |found_call|
|
57
|
+
SmellWarning.new(SMELL_CLASS, ctx.full_name, found_call.lines,
|
58
|
+
found_call.smell_message,
|
59
|
+
@source, SMELL_SUBCLASS,
|
60
|
+
{CALL_KEY => found_call.call, OCCURRENCES_KEY => found_call.occurs})
|
67
61
|
end
|
68
62
|
end
|
69
63
|
|
70
|
-
|
64
|
+
# Collects information about a single found call
|
65
|
+
class FoundCall
|
66
|
+
def initialize(call_node)
|
67
|
+
@call_node = call_node
|
68
|
+
@occurences = []
|
69
|
+
end
|
70
|
+
|
71
|
+
def record(occurence)
|
72
|
+
@occurences.push occurence
|
73
|
+
end
|
74
|
+
|
75
|
+
def smell_message
|
76
|
+
multiple = occurs == 2 ? 'twice' : "#{occurs} times"
|
77
|
+
"calls #{call} #{multiple}"
|
78
|
+
end
|
71
79
|
|
72
|
-
|
73
|
-
|
74
|
-
method_ctx.local_nodes(:call) do |call_node|
|
75
|
-
next if call_node.method_name == :new
|
76
|
-
next if call_node.receiver.nil? && call_node.args.empty?
|
77
|
-
result[call_node].push(call_node)
|
80
|
+
def call
|
81
|
+
@call ||= @call_node.format_ruby
|
78
82
|
end
|
79
|
-
|
80
|
-
|
83
|
+
|
84
|
+
def occurs
|
85
|
+
@occurences.length
|
86
|
+
end
|
87
|
+
|
88
|
+
def lines
|
89
|
+
@occurences.map {|exp| exp.line}
|
81
90
|
end
|
82
|
-
result.to_a.sort_by {|call_exp, _| call_exp.format_ruby}
|
83
91
|
end
|
84
92
|
|
85
|
-
|
86
|
-
|
93
|
+
# Collects all calls in a given context
|
94
|
+
class CallCollector
|
95
|
+
attr_reader :context
|
96
|
+
|
97
|
+
def initialize(context, max_allowed_calls, allow_calls)
|
98
|
+
@context = context
|
99
|
+
@max_allowed_calls = max_allowed_calls
|
100
|
+
@allow_calls = allow_calls
|
101
|
+
end
|
102
|
+
|
103
|
+
def calls
|
104
|
+
result = Hash.new {|hash,key| hash[key] = FoundCall.new(key)}
|
105
|
+
collect_calls(result)
|
106
|
+
collect_assignments(result)
|
107
|
+
result.values.sort_by {|found_call| found_call.call}
|
108
|
+
end
|
109
|
+
|
110
|
+
def smelly_calls
|
111
|
+
calls.select {|found_call| smelly_call? found_call }
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def collect_assignments(result)
|
117
|
+
context.local_nodes(:attrasgn) do |asgn_node|
|
118
|
+
result[asgn_node].record(asgn_node) if asgn_node.args
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def collect_calls(result)
|
123
|
+
context.local_nodes(:call) do |call_node|
|
124
|
+
next if call_node.method_name == :new
|
125
|
+
next if !call_node.receiver && call_node.args.empty?
|
126
|
+
result[call_node].record(call_node)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def smelly_call?(found_call)
|
131
|
+
found_call.occurs > @max_allowed_calls and not allow_calls?(found_call.call)
|
132
|
+
end
|
133
|
+
|
134
|
+
def allow_calls?(method)
|
135
|
+
@allow_calls.any? { |allow| /#{allow}/ === method }
|
136
|
+
end
|
87
137
|
end
|
88
138
|
end
|
89
139
|
end
|