reek 1.3.4 → 1.3.5
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.
- 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
|