reek 3.1 → 3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/{CHANGELOG → CHANGELOG.md} +150 -123
- data/README.md +61 -21
- data/Rakefile +2 -1
- data/bin/reek +1 -0
- data/config/defaults.reek +2 -2
- data/docs/Attribute.md +9 -13
- data/docs/Basic-Smell-Options.md +2 -2
- data/docs/Command-Line-Options.md +2 -2
- data/docs/Too-Many-Instance-Variables.md +1 -1
- data/features/samples.feature +22 -31
- data/features/step_definitions/sample_file_steps.rb +2 -2
- data/features/support/env.rb +1 -0
- data/lib/reek.rb +1 -0
- data/lib/reek/ast/ast_node_class_map.rb +5 -1
- data/lib/reek/ast/node.rb +4 -2
- data/lib/reek/ast/object_refs.rb +9 -5
- data/lib/reek/ast/reference_collector.rb +4 -2
- data/lib/reek/cli/application.rb +12 -9
- data/lib/reek/cli/command.rb +4 -0
- data/lib/reek/cli/input.rb +4 -4
- data/lib/reek/cli/option_interpreter.rb +11 -7
- data/lib/reek/cli/options.rb +42 -40
- data/lib/reek/cli/reek_command.rb +3 -3
- data/lib/reek/cli/warning_collector.rb +7 -3
- data/lib/reek/code_comment.rb +5 -1
- data/lib/reek/configuration/app_configuration.rb +4 -4
- data/lib/reek/context/code_context.rb +19 -17
- data/lib/reek/examiner.rb +8 -6
- data/lib/reek/rake/task.rb +13 -22
- data/lib/reek/report/formatter.rb +5 -1
- data/lib/reek/report/report.rb +46 -44
- data/lib/reek/smells/attribute.rb +42 -24
- data/lib/reek/smells/control_parameter.rb +21 -13
- data/lib/reek/smells/data_clump.rb +17 -9
- data/lib/reek/smells/duplicate_method_call.rb +12 -6
- data/lib/reek/smells/long_parameter_list.rb +2 -2
- data/lib/reek/smells/long_yield_list.rb +4 -4
- data/lib/reek/smells/nested_iterators.rb +4 -2
- data/lib/reek/smells/nil_check.rb +6 -2
- data/lib/reek/smells/repeated_conditional.rb +2 -2
- data/lib/reek/smells/smell_configuration.rb +15 -7
- data/lib/reek/smells/smell_detector.rb +23 -10
- data/lib/reek/smells/smell_repository.rb +9 -16
- data/lib/reek/smells/smell_warning.rb +6 -6
- data/lib/reek/smells/too_many_instance_variables.rb +4 -4
- data/lib/reek/smells/too_many_methods.rb +2 -2
- data/lib/reek/smells/too_many_statements.rb +4 -4
- data/lib/reek/smells/uncommunicative_method_name.rb +5 -5
- data/lib/reek/smells/uncommunicative_module_name.rb +5 -5
- data/lib/reek/smells/uncommunicative_parameter_name.rb +8 -4
- data/lib/reek/smells/uncommunicative_variable_name.rb +8 -4
- data/lib/reek/source/source_code.rb +6 -2
- data/lib/reek/source/source_locator.rb +4 -4
- data/lib/reek/spec/should_reek.rb +9 -4
- data/lib/reek/spec/should_reek_of.rb +8 -5
- data/lib/reek/spec/should_reek_only_of.rb +12 -8
- data/lib/reek/tree_dresser.rb +6 -2
- data/lib/reek/tree_walker.rb +28 -22
- data/lib/reek/version.rb +1 -1
- data/reek.gemspec +6 -5
- data/spec/gem/yard_spec.rb +6 -9
- data/spec/reek/code_comment_spec.rb +1 -1
- data/spec/reek/report/xml_report_spec.rb +11 -21
- data/spec/reek/smells/attribute_spec.rb +73 -57
- data/spec/reek/smells/too_many_instance_variables_spec.rb +26 -12
- data/spec/reek/source/source_locator_spec.rb +2 -2
- data/spec/samples/checkstyle.xml +12 -1
- data/spec/spec_helper.rb +1 -0
- metadata +20 -7
- data/spec/samples/unusual_syntax.rb +0 -21
data/lib/reek/rake/task.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require 'private_attr/everywhere'
|
3
4
|
require 'rake'
|
4
5
|
require 'rake/tasklib'
|
5
6
|
require 'pathname'
|
@@ -38,17 +39,17 @@ module Reek
|
|
38
39
|
|
39
40
|
# Path to reek's config file.
|
40
41
|
# Setting the REEK_CFG environment variable overrides this.
|
41
|
-
|
42
|
+
attr_accessor :config_file
|
42
43
|
|
43
44
|
# Glob pattern to match source files.
|
44
45
|
# Setting the REEK_SRC environment variable overrides this.
|
45
46
|
# Defaults to 'lib/**/*.rb'.
|
46
|
-
|
47
|
+
attr_accessor :source_files
|
47
48
|
|
48
49
|
# String containing commandline options to be passed to Reek.
|
49
50
|
# Setting the REEK_OPTS environment variable overrides this value.
|
50
51
|
# Defaults to ''.
|
51
|
-
|
52
|
+
attr_accessor :reek_opts
|
52
53
|
|
53
54
|
# Whether or not to fail Rake when an error occurs (typically when smells are found).
|
54
55
|
# Defaults to true.
|
@@ -59,10 +60,11 @@ module Reek
|
|
59
60
|
attr_writer :verbose
|
60
61
|
|
61
62
|
def initialize(name = :reek)
|
63
|
+
@config_file = ENV['REEK_CFG']
|
62
64
|
@name = name
|
63
|
-
@reek_opts = ''
|
65
|
+
@reek_opts = ENV['REEK_OPTS'] || ''
|
64
66
|
@fail_on_error = true
|
65
|
-
@source_files = 'lib/**/*.rb'
|
67
|
+
@source_files = ENV['REEK_SRC'] || 'lib/**/*.rb'
|
66
68
|
@verbose = false
|
67
69
|
|
68
70
|
yield self if block_given?
|
@@ -71,15 +73,17 @@ module Reek
|
|
71
73
|
|
72
74
|
private
|
73
75
|
|
76
|
+
private_attr_reader :fail_on_error, :name, :verbose
|
77
|
+
|
74
78
|
def define_task
|
75
79
|
desc 'Check for code smells'
|
76
|
-
task(
|
80
|
+
task(name) { run_task }
|
77
81
|
end
|
78
82
|
|
79
83
|
def run_task
|
80
|
-
puts "\n\n!!! Running 'reek' rake command: #{command}\n\n" if
|
84
|
+
puts "\n\n!!! Running 'reek' rake command: #{command}\n\n" if verbose
|
81
85
|
system(*command)
|
82
|
-
abort("\n\n!!! `reek` has found smells - exiting!") if sys_call_failed? &&
|
86
|
+
abort("\n\n!!! `reek` has found smells - exiting!") if sys_call_failed? && fail_on_error
|
83
87
|
end
|
84
88
|
|
85
89
|
def command
|
@@ -88,25 +92,12 @@ module Reek
|
|
88
92
|
reject(&:empty?)
|
89
93
|
end
|
90
94
|
|
91
|
-
def source_files
|
92
|
-
FileList[ENV['REEK_SRC'] || @source_files]
|
93
|
-
end
|
94
|
-
|
95
|
-
def reek_opts
|
96
|
-
ENV['REEK_OPTS'] || @reek_opts
|
97
|
-
end
|
98
|
-
|
99
|
-
def config_file
|
100
|
-
ENV['REEK_CFG'] || @config_file
|
101
|
-
end
|
102
|
-
|
103
95
|
def sys_call_failed?
|
104
96
|
!$CHILD_STATUS.success?
|
105
97
|
end
|
106
98
|
|
107
99
|
def config_file_as_argument
|
108
|
-
|
109
|
-
['-c', @config_file]
|
100
|
+
config_file ? ['-c', config_file] : []
|
110
101
|
end
|
111
102
|
|
112
103
|
def reek_opts_as_arguments
|
@@ -35,7 +35,7 @@ module Reek
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def format(warning)
|
38
|
-
"#{
|
38
|
+
"#{location_formatter.format(warning)}#{base_format(warning)}"
|
39
39
|
end
|
40
40
|
|
41
41
|
private
|
@@ -43,6 +43,10 @@ module Reek
|
|
43
43
|
def base_format(warning)
|
44
44
|
"#{warning.context} #{warning.message} (#{warning.smell_type})"
|
45
45
|
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
private_attr_reader :location_formatter
|
46
50
|
end
|
47
51
|
|
48
52
|
#
|
data/lib/reek/report/report.rb
CHANGED
@@ -30,8 +30,8 @@ module Reek
|
|
30
30
|
#
|
31
31
|
# @param [Reek::Examiner] examiner object to report on
|
32
32
|
def add_examiner(examiner)
|
33
|
-
|
34
|
-
|
33
|
+
self.total_smell_count += examiner.smells_count
|
34
|
+
examiners << examiner
|
35
35
|
self
|
36
36
|
end
|
37
37
|
|
@@ -42,13 +42,22 @@ module Reek
|
|
42
42
|
|
43
43
|
# @api private
|
44
44
|
def smells?
|
45
|
-
|
45
|
+
total_smell_count > 0
|
46
46
|
end
|
47
47
|
|
48
48
|
# @api private
|
49
49
|
def smells
|
50
|
-
|
50
|
+
examiners.map(&:smells).flatten
|
51
51
|
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
attr_accessor :total_smell_count
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
private_attr_reader :examiners, :options, :report_formatter,
|
60
|
+
:sort_by_issue_count, :warning_formatter
|
52
61
|
end
|
53
62
|
|
54
63
|
#
|
@@ -64,7 +73,7 @@ module Reek
|
|
64
73
|
private
|
65
74
|
|
66
75
|
def smell_summaries
|
67
|
-
|
76
|
+
examiners.map { |ex| summarize_single_examiner(ex) }.reject(&:empty?)
|
68
77
|
end
|
69
78
|
|
70
79
|
def display_summary
|
@@ -72,33 +81,33 @@ module Reek
|
|
72
81
|
end
|
73
82
|
|
74
83
|
def display_total_smell_count
|
75
|
-
return unless
|
84
|
+
return unless examiners.size > 1
|
76
85
|
print total_smell_count_message
|
77
86
|
end
|
78
87
|
|
79
88
|
def summarize_single_examiner(examiner)
|
80
89
|
result = heading_formatter.header(examiner)
|
81
90
|
if examiner.smelly?
|
82
|
-
formatted_list =
|
83
|
-
|
91
|
+
formatted_list = report_formatter.format_list(examiner.smells,
|
92
|
+
warning_formatter)
|
84
93
|
result += ":\n#{formatted_list}"
|
85
94
|
end
|
86
95
|
result
|
87
96
|
end
|
88
97
|
|
89
98
|
def sort_examiners
|
90
|
-
|
99
|
+
examiners.sort_by!(&:smells_count).reverse! if sort_by_issue_count
|
91
100
|
end
|
92
101
|
|
93
102
|
def total_smell_count_message
|
94
103
|
colour = smells? ? WARNINGS_COLOR : NO_WARNINGS_COLOR
|
95
|
-
s =
|
96
|
-
Rainbow("#{
|
104
|
+
s = total_smell_count == 1 ? '' : 's'
|
105
|
+
Rainbow("#{total_smell_count} total warning#{s}\n").color(colour)
|
97
106
|
end
|
98
107
|
|
99
108
|
def heading_formatter
|
100
109
|
@heading_formatter ||=
|
101
|
-
|
110
|
+
options.fetch(:heading_formatter, HeadingFormatter::Quiet).new(report_formatter)
|
102
111
|
end
|
103
112
|
end
|
104
113
|
|
@@ -118,7 +127,7 @@ module Reek
|
|
118
127
|
def show
|
119
128
|
print ::JSON.generate(
|
120
129
|
smells.map do |smell|
|
121
|
-
smell.yaml_hash(
|
130
|
+
smell.yaml_hash(warning_formatter)
|
122
131
|
end
|
123
132
|
)
|
124
133
|
end
|
@@ -144,53 +153,46 @@ module Reek
|
|
144
153
|
require 'rexml/document'
|
145
154
|
|
146
155
|
def show
|
147
|
-
|
148
|
-
|
149
|
-
smells.group_by(&:source).each do |file, file_smells|
|
150
|
-
file_to_xml(file, file_smells, checkstyle)
|
151
|
-
end
|
152
|
-
|
153
|
-
print_xml(checkstyle.parent)
|
156
|
+
document.write output: $stdout, indent: 2
|
157
|
+
$stdout.puts
|
154
158
|
end
|
155
159
|
|
156
160
|
private
|
157
161
|
|
158
162
|
def document
|
159
|
-
REXML::Document.new.tap do |
|
160
|
-
|
163
|
+
REXML::Document.new.tap do |document|
|
164
|
+
document << REXML::XMLDecl.new << checkstyle
|
161
165
|
end
|
162
166
|
end
|
163
167
|
|
164
|
-
def
|
165
|
-
REXML::Element.new('
|
166
|
-
|
167
|
-
|
168
|
+
def checkstyle
|
169
|
+
REXML::Element.new('checkstyle').tap do |checkstyle|
|
170
|
+
smells.group_by(&:source).each do |source, source_smells|
|
171
|
+
checkstyle << file(source, source_smells)
|
172
|
+
end
|
168
173
|
end
|
169
174
|
end
|
170
175
|
|
171
|
-
def
|
172
|
-
|
173
|
-
|
176
|
+
def file(name, smells)
|
177
|
+
REXML::Element.new('file').tap do |file|
|
178
|
+
file.add_attribute 'name', File.realpath(name)
|
179
|
+
smells.each do |smell|
|
180
|
+
smell.lines.each do |line|
|
181
|
+
file << error(smell, line)
|
182
|
+
end
|
183
|
+
end
|
174
184
|
end
|
175
185
|
end
|
176
186
|
|
177
|
-
def
|
178
|
-
REXML::Element.new('error'
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
['source', smell.smell_type]
|
185
|
-
]
|
186
|
-
element.add_attributes(attributes)
|
187
|
+
def error(smell, line)
|
188
|
+
REXML::Element.new('error').tap do |error|
|
189
|
+
error.add_attributes 'column' => 0,
|
190
|
+
'line' => line,
|
191
|
+
'message' => smell.message,
|
192
|
+
'severity' => 'warning',
|
193
|
+
'source' => smell.smell_type
|
187
194
|
end
|
188
195
|
end
|
189
|
-
|
190
|
-
def print_xml(document)
|
191
|
-
formatter = REXML::Formatters::Default.new
|
192
|
-
puts formatter.write(document, '')
|
193
|
-
end
|
194
196
|
end
|
195
197
|
end
|
196
198
|
end
|
@@ -9,22 +9,18 @@ module Reek
|
|
9
9
|
# invites client classes to become too intimate with its inner workings,
|
10
10
|
# and in particular with its representation of state.
|
11
11
|
#
|
12
|
-
#
|
13
|
-
# +
|
14
|
-
# that are private.
|
12
|
+
# This detector raises a warning for every public +attr_writer+,
|
13
|
+
# +attr_accessor+, and +attr+ with the writable flag set to +true+.
|
15
14
|
#
|
16
15
|
# See {file:docs/Attribute.md} for details.
|
17
16
|
# @api private
|
18
17
|
#
|
19
18
|
# TODO: Catch attributes declared "by hand"
|
20
19
|
class Attribute < SmellDetector
|
21
|
-
ATTR_DEFN_METHODS = [:
|
20
|
+
ATTR_DEFN_METHODS = [:attr_writer, :attr_accessor]
|
22
21
|
VISIBILITY_MODIFIERS = [:private, :public, :protected]
|
23
22
|
|
24
23
|
def initialize(*args)
|
25
|
-
@visiblity_tracker = {}
|
26
|
-
@visiblity_mode = :public
|
27
|
-
@result = Set.new
|
28
24
|
super
|
29
25
|
end
|
30
26
|
|
@@ -32,39 +28,61 @@ module Reek
|
|
32
28
|
[:class, :module]
|
33
29
|
end
|
34
30
|
|
35
|
-
def self.default_config
|
36
|
-
super.merge(SmellConfiguration::ENABLED_KEY => false)
|
37
|
-
end
|
38
|
-
|
39
31
|
#
|
40
32
|
# Checks whether the given class declares any attributes.
|
41
33
|
#
|
42
34
|
# @return [Array<SmellWarning>]
|
43
35
|
#
|
44
36
|
def examine_context(ctx)
|
37
|
+
self.visiblity_tracker = {}
|
38
|
+
self.visiblity_mode = :public
|
45
39
|
attributes_in(ctx).map do |attribute, line|
|
46
40
|
SmellWarning.new self,
|
47
41
|
context: ctx.full_name,
|
48
42
|
lines: [line],
|
49
|
-
message: "declares the attribute #{attribute}",
|
43
|
+
message: "declares the writable attribute #{attribute}",
|
50
44
|
parameters: { name: attribute.to_s }
|
51
45
|
end
|
52
46
|
end
|
53
47
|
|
54
48
|
private
|
55
49
|
|
50
|
+
private_attr_accessor :visiblity_mode, :visiblity_tracker
|
51
|
+
private_attr_reader :result
|
52
|
+
|
56
53
|
def attributes_in(module_ctx)
|
54
|
+
attributes = Set.new
|
57
55
|
module_ctx.local_nodes(:send) do |call_node|
|
58
|
-
|
59
|
-
track_visibility(call_node)
|
60
|
-
elsif ATTR_DEFN_METHODS.include?(call_node.method_name)
|
61
|
-
call_node.arg_names.each do |arg|
|
62
|
-
@visiblity_tracker[arg] = @visiblity_mode
|
63
|
-
@result << [arg, call_node.line]
|
64
|
-
end
|
65
|
-
end
|
56
|
+
attributes += track_attributes(call_node)
|
66
57
|
end
|
67
|
-
|
58
|
+
attributes.select { |name, _line| recorded_public_methods.include?(name) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def track_attributes(call_node)
|
62
|
+
if attribute_writer? call_node
|
63
|
+
return track_arguments call_node.args, call_node.line
|
64
|
+
end
|
65
|
+
track_visibility call_node if visibility_modifier? call_node
|
66
|
+
[]
|
67
|
+
end
|
68
|
+
|
69
|
+
def attribute_writer?(call_node)
|
70
|
+
ATTR_DEFN_METHODS.include?(call_node.method_name) ||
|
71
|
+
attr_with_writable_flag?(call_node)
|
72
|
+
end
|
73
|
+
|
74
|
+
def attr_with_writable_flag?(call_node)
|
75
|
+
call_node.method_name == :attr && call_node.args.last.type == :true
|
76
|
+
end
|
77
|
+
|
78
|
+
def track_arguments(args, line)
|
79
|
+
args.select { |arg| arg.type == :sym }.map { |arg| track_argument(arg, line) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def track_argument(arg, line)
|
83
|
+
arg_name = arg.children.first
|
84
|
+
visiblity_tracker[arg_name] = visiblity_mode
|
85
|
+
[arg_name, line]
|
68
86
|
end
|
69
87
|
|
70
88
|
def visibility_modifier?(call_node)
|
@@ -73,14 +91,14 @@ module Reek
|
|
73
91
|
|
74
92
|
def track_visibility(call_node)
|
75
93
|
if call_node.arg_names.any?
|
76
|
-
call_node.arg_names.each { |arg|
|
94
|
+
call_node.arg_names.each { |arg| visiblity_tracker[arg] = call_node.method_name }
|
77
95
|
else
|
78
|
-
|
96
|
+
self.visiblity_mode = call_node.method_name
|
79
97
|
end
|
80
98
|
end
|
81
99
|
|
82
100
|
def recorded_public_methods
|
83
|
-
|
101
|
+
visiblity_tracker.select { |_, visbility| visbility == :public }
|
84
102
|
end
|
85
103
|
end
|
86
104
|
end
|
@@ -73,16 +73,20 @@ module Reek
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def smells?
|
76
|
-
|
76
|
+
occurences.any?
|
77
77
|
end
|
78
78
|
|
79
79
|
def lines
|
80
|
-
|
80
|
+
occurences.map(&:line)
|
81
81
|
end
|
82
82
|
|
83
83
|
def name
|
84
|
-
|
84
|
+
param.to_s
|
85
85
|
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
private_attr_reader :occurences, :param
|
86
90
|
end
|
87
91
|
|
88
92
|
# Finds cases of ControlParameter in a particular node for a particular parameter
|
@@ -108,13 +112,15 @@ module Reek
|
|
108
112
|
|
109
113
|
private
|
110
114
|
|
115
|
+
private_attr_reader :node, :param
|
116
|
+
|
111
117
|
def conditional_nodes
|
112
|
-
|
118
|
+
node.body_nodes(CONDITIONAL_NODE_TYPES)
|
113
119
|
end
|
114
120
|
|
115
121
|
def nested_finders
|
116
122
|
@nested_finders ||= conditional_nodes.flat_map do |node|
|
117
|
-
self.class.new(node,
|
123
|
+
self.class.new(node, param)
|
118
124
|
end
|
119
125
|
end
|
120
126
|
|
@@ -129,12 +135,12 @@ module Reek
|
|
129
135
|
|
130
136
|
def uses_of_param_in_condition
|
131
137
|
return [] unless condition
|
132
|
-
condition.each_node(:lvar).select { |inner| inner.var_name ==
|
138
|
+
condition.each_node(:lvar).select { |inner| inner.var_name == param }
|
133
139
|
end
|
134
140
|
|
135
141
|
def condition
|
136
|
-
return nil unless CONDITIONAL_NODE_TYPES.include?
|
137
|
-
|
142
|
+
return nil unless CONDITIONAL_NODE_TYPES.include? node.type
|
143
|
+
node.condition
|
138
144
|
end
|
139
145
|
|
140
146
|
def regular_call_involving_param?(call_node)
|
@@ -150,12 +156,12 @@ module Reek
|
|
150
156
|
end
|
151
157
|
|
152
158
|
def call_involving_param?(call_node)
|
153
|
-
call_node.each_node(:lvar).any? { |it| it.var_name ==
|
159
|
+
call_node.each_node(:lvar).any? { |it| it.var_name == param }
|
154
160
|
end
|
155
161
|
|
156
162
|
def uses_param_in_body?
|
157
|
-
nodes =
|
158
|
-
nodes.any? { |lvar_node| lvar_node.var_name ==
|
163
|
+
nodes = node.body_nodes([:lvar], [:if, :case, :and, :or])
|
164
|
+
nodes.any? { |lvar_node| lvar_node.var_name == param }
|
159
165
|
end
|
160
166
|
end
|
161
167
|
|
@@ -175,12 +181,14 @@ module Reek
|
|
175
181
|
|
176
182
|
private
|
177
183
|
|
184
|
+
private_attr_reader :context
|
185
|
+
|
178
186
|
def potential_parameters
|
179
|
-
|
187
|
+
context.exp.parameter_names
|
180
188
|
end
|
181
189
|
|
182
190
|
def find_matches(param)
|
183
|
-
ControlParameterFinder.new(
|
191
|
+
ControlParameterFinder.new(context.exp, param).find_matches
|
184
192
|
end
|
185
193
|
end
|
186
194
|
end
|