reek 5.6.0 → 6.0.0
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/.gitignore +1 -0
- data/.rubocop.yml +1 -1
- data/.simplecov +1 -0
- data/.travis.yml +12 -11
- data/CHANGELOG.md +8 -0
- data/Dockerfile +1 -0
- data/Gemfile +15 -17
- data/README.md +1 -7
- data/bin/code_climate_reek +12 -2
- data/docs/Attribute.md +1 -1
- data/docs/Control-Couple.md +1 -1
- data/docs/Nil-Check.md +4 -1
- data/features/command_line_interface/options.feature +2 -3
- data/features/reports/codeclimate.feature +2 -2
- data/features/reports/json.feature +3 -3
- data/features/reports/reports.feature +4 -4
- data/features/reports/yaml.feature +3 -3
- data/features/step_definitions/reek_steps.rb +4 -0
- data/lib/reek/ast/sexp_extensions/arguments.rb +11 -0
- data/lib/reek/cli/options.rb +2 -2
- data/lib/reek/code_comment.rb +36 -29
- data/lib/reek/errors/legacy_comment_separator_error.rb +36 -0
- data/lib/reek/report.rb +5 -7
- data/lib/reek/report/code_climate/code_climate_report.rb +2 -1
- data/lib/reek/report/simple_warning_formatter.rb +0 -7
- data/lib/reek/smell_detectors/base_detector.rb +0 -9
- data/lib/reek/smell_detectors/data_clump.rb +23 -56
- data/lib/reek/smell_detectors/nil_check.rb +1 -12
- data/lib/reek/version.rb +1 -1
- data/reek.gemspec +5 -6
- data/spec/reek/ast/sexp_extensions_spec.rb +15 -33
- data/spec/reek/code_comment_spec.rb +23 -21
- data/spec/reek/context_builder_spec.rb +110 -113
- data/spec/reek/smell_detectors/base_detector_spec.rb +0 -10
- data/spec/reek/smell_detectors/data_clump_spec.rb +14 -0
- data/spec/reek/smell_detectors/missing_safe_method_spec.rb +8 -2
- data/spec/reek/smell_detectors/nil_check_spec.rb +3 -3
- data/spec/reek/source/source_code_spec.rb +13 -0
- data/spec/spec_helper.rb +1 -0
- metadata +6 -18
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_error'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Errors
|
7
|
+
# Gets raised for old-style comment configuration format.
|
8
|
+
class LegacyCommentSeparatorError < BaseError
|
9
|
+
MESSAGE = <<-MESSAGE
|
10
|
+
Error: You are using the legacy configuration format (including three
|
11
|
+
colons) to configure Reek in one your source code comments.
|
12
|
+
|
13
|
+
The source is '%<source>s' and the comment belongs to the expression
|
14
|
+
starting in line %<line>d.
|
15
|
+
|
16
|
+
Here's the original comment:
|
17
|
+
|
18
|
+
%<comment>s
|
19
|
+
|
20
|
+
Please see the Reek docs for information on how to configure Reek via
|
21
|
+
source code comments: #{DocumentationLink.build('Smell Suppression')}
|
22
|
+
|
23
|
+
Update the offensive comment and re-run Reek.
|
24
|
+
|
25
|
+
MESSAGE
|
26
|
+
|
27
|
+
def initialize(source:, line:, original_comment:)
|
28
|
+
message = format(MESSAGE,
|
29
|
+
source: source,
|
30
|
+
line: line,
|
31
|
+
comment: original_comment)
|
32
|
+
super message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/reek/report.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'report/code_climate'
|
4
3
|
require_relative 'report/html_report'
|
5
4
|
require_relative 'report/json_report'
|
6
5
|
require_relative 'report/text_report'
|
@@ -17,12 +16,11 @@ module Reek
|
|
17
16
|
# Reek reporting functionality.
|
18
17
|
module Report
|
19
18
|
REPORT_CLASSES = {
|
20
|
-
yaml:
|
21
|
-
json:
|
22
|
-
html:
|
23
|
-
xml:
|
24
|
-
text:
|
25
|
-
code_climate: CodeClimateReport
|
19
|
+
yaml: YAMLReport,
|
20
|
+
json: JSONReport,
|
21
|
+
html: HTMLReport,
|
22
|
+
xml: XMLReport,
|
23
|
+
text: TextReport
|
26
24
|
}.freeze
|
27
25
|
|
28
26
|
LOCATION_FORMATTERS = {
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../base_report'
|
4
|
+
require_relative 'code_climate_formatter'
|
4
5
|
|
5
6
|
module Reek
|
6
7
|
module Report
|
@@ -12,7 +13,7 @@ module Reek
|
|
12
13
|
class CodeClimateReport < BaseReport
|
13
14
|
def show(out = $stdout)
|
14
15
|
smells.map do |smell|
|
15
|
-
out.print
|
16
|
+
out.print CodeClimateFormatter.new(smell).render
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'code_climate/code_climate_formatter'
|
4
|
-
|
5
3
|
module Reek
|
6
4
|
module Report
|
7
5
|
#
|
@@ -17,11 +15,6 @@ module Reek
|
|
17
15
|
"#{location_formatter.format(warning)}#{warning.base_message}"
|
18
16
|
end
|
19
17
|
|
20
|
-
# @quality :reek:UtilityFunction
|
21
|
-
def format_code_climate_hash(warning)
|
22
|
-
CodeClimateFormatter.new(warning).render
|
23
|
-
end
|
24
|
-
|
25
18
|
def format_list(warnings)
|
26
19
|
warnings.map { |warning| " #{format(warning)}" }.join("\n")
|
27
20
|
end
|
@@ -121,15 +121,6 @@ module Reek
|
|
121
121
|
@descendants ||= []
|
122
122
|
end
|
123
123
|
|
124
|
-
#
|
125
|
-
# @param detector [String] the detector in question, e.g. 'DuplicateMethodCall'
|
126
|
-
# @return [Boolean]
|
127
|
-
#
|
128
|
-
def valid_detector?(detector)
|
129
|
-
descendants.map { |descendant| descendant.to_s.split('::').last }.
|
130
|
-
include?(detector)
|
131
|
-
end
|
132
|
-
|
133
124
|
#
|
134
125
|
# Transform a detector name to the corresponding constant.
|
135
126
|
# Note that we assume a valid name - exceptions are not handled here.
|
@@ -51,7 +51,7 @@ module Reek
|
|
51
51
|
# @return [Array<SmellWarning>]
|
52
52
|
#
|
53
53
|
def sniff
|
54
|
-
|
54
|
+
clumps.map do |clump, methods|
|
55
55
|
methods_length = methods.length
|
56
56
|
smell_warning(
|
57
57
|
lines: methods.map(&:line),
|
@@ -72,72 +72,39 @@ module Reek
|
|
72
72
|
private
|
73
73
|
|
74
74
|
def max_copies
|
75
|
-
value(MAX_COPIES_KEY, context)
|
75
|
+
@max_copies ||= value(MAX_COPIES_KEY, context)
|
76
76
|
end
|
77
77
|
|
78
78
|
def min_clump_size
|
79
|
-
value(MIN_CLUMP_SIZE_KEY, context)
|
79
|
+
@min_clump_size ||= value(MIN_CLUMP_SIZE_KEY, context)
|
80
80
|
end
|
81
|
-
end
|
82
|
-
end
|
83
81
|
|
84
|
-
|
85
|
-
|
86
|
-
class MethodGroup
|
87
|
-
def initialize(ctx, min_clump_size, max_copies)
|
88
|
-
@min_clump_size = min_clump_size
|
89
|
-
@max_copies = max_copies
|
90
|
-
@candidate_methods = ctx.node_instance_methods.map do |defn_node|
|
91
|
-
CandidateMethod.new(defn_node)
|
82
|
+
def candidate_methods
|
83
|
+
@candidate_methods ||= context.node_instance_methods
|
92
84
|
end
|
93
|
-
end
|
94
85
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
end
|
102
|
-
|
103
|
-
# @quality :reek:UtilityFunction
|
104
|
-
def common_argument_names_for(methods)
|
105
|
-
methods.map(&:arg_names).inject(:&)
|
106
|
-
end
|
107
|
-
|
108
|
-
def methods_containing_clump(clump)
|
109
|
-
candidate_methods.select { |method| clump & method.arg_names == clump }
|
110
|
-
end
|
111
|
-
|
112
|
-
def clumps
|
113
|
-
candidate_clumps.map do |clump|
|
114
|
-
[clump, methods_containing_clump(clump)]
|
86
|
+
def candidate_clumps
|
87
|
+
candidate_methods.each_cons(max_copies + 1).map do |methods|
|
88
|
+
common_argument_names_for(methods)
|
89
|
+
end.select do |clump|
|
90
|
+
clump.length >= min_clump_size
|
91
|
+
end.uniq
|
115
92
|
end
|
116
|
-
end
|
117
|
-
|
118
|
-
private
|
119
93
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
# @private
|
125
|
-
class CandidateMethod
|
126
|
-
extend Forwardable
|
127
|
-
|
128
|
-
def_delegators :defn, :line, :name
|
94
|
+
# @quality :reek:UtilityFunction
|
95
|
+
def common_argument_names_for(methods)
|
96
|
+
methods.map(&:arg_names).inject(:&).compact.sort
|
97
|
+
end
|
129
98
|
|
130
|
-
|
131
|
-
|
132
|
-
|
99
|
+
def methods_containing_clump(clump)
|
100
|
+
candidate_methods.select { |method| clump & method.arg_names == clump }
|
101
|
+
end
|
133
102
|
|
134
|
-
|
135
|
-
|
136
|
-
|
103
|
+
def clumps
|
104
|
+
candidate_clumps.map do |clump|
|
105
|
+
[clump, methods_containing_clump(clump)]
|
106
|
+
end
|
107
|
+
end
|
137
108
|
end
|
138
|
-
|
139
|
-
private
|
140
|
-
|
141
|
-
attr_reader :defn
|
142
109
|
end
|
143
110
|
end
|
@@ -24,8 +24,7 @@ module Reek
|
|
24
24
|
|
25
25
|
def detect_nodes
|
26
26
|
finders = [NodeFinder.new(context, :send, NilCallNodeDetector),
|
27
|
-
NodeFinder.new(context, :when, NilWhenNodeDetector)
|
28
|
-
NodeFinder.new(context, :csend, SafeNavigationNodeDetector)]
|
27
|
+
NodeFinder.new(context, :when, NilWhenNodeDetector)]
|
29
28
|
finders.flat_map(&:smelly_nodes)
|
30
29
|
end
|
31
30
|
|
@@ -88,16 +87,6 @@ module Reek
|
|
88
87
|
node.condition_list.any? { |it| it.type == :nil }
|
89
88
|
end
|
90
89
|
end
|
91
|
-
|
92
|
-
# Detect safe navigation. Returns true for all nodes, since all :csend
|
93
|
-
# nodes are considered smelly.
|
94
|
-
module SafeNavigationNodeDetector
|
95
|
-
module_function
|
96
|
-
|
97
|
-
def detect(_node)
|
98
|
-
true
|
99
|
-
end
|
100
|
-
end
|
101
90
|
end
|
102
91
|
end
|
103
92
|
end
|
data/lib/reek/version.rb
CHANGED
data/reek.gemspec
CHANGED
@@ -16,12 +16,11 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.executables = s.files.grep(%r{^bin/}).map { |path| File.basename(path) }
|
17
17
|
s.homepage = 'https://github.com/troessner/reek'
|
18
18
|
s.rdoc_options = %w(--main README.md -x assets/|bin/|config/|features/|spec/|tasks/)
|
19
|
-
s.required_ruby_version = '>= 2.
|
19
|
+
s.required_ruby_version = '>= 2.4.0'
|
20
20
|
s.summary = 'Code smell detector for Ruby'
|
21
21
|
|
22
|
-
s.add_runtime_dependency '
|
23
|
-
s.add_runtime_dependency '
|
24
|
-
s.add_runtime_dependency '
|
25
|
-
s.add_runtime_dependency '
|
26
|
-
s.add_runtime_dependency 'rainbow', '>= 2.0', '< 4.0'
|
22
|
+
s.add_runtime_dependency 'kwalify', '~> 0.7.0'
|
23
|
+
s.add_runtime_dependency 'parser', '< 2.8', '>= 2.5.0.0', '!= 2.5.1.1'
|
24
|
+
s.add_runtime_dependency 'psych', '~> 3.1.0'
|
25
|
+
s.add_runtime_dependency 'rainbow', '>= 2.0', '< 4.0'
|
27
26
|
end
|
@@ -443,73 +443,55 @@ end
|
|
443
443
|
|
444
444
|
RSpec.describe Reek::AST::SexpExtensions::CasgnNode do
|
445
445
|
describe '#defines_module?' do
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
expect(exp).not_to be_defines_module
|
450
|
-
end
|
446
|
+
it 'is false for single assignment' do
|
447
|
+
exp = sexp(:casgn, nil, :Foo)
|
448
|
+
expect(exp).not_to be_defines_module
|
451
449
|
end
|
452
450
|
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
expect(exp).not_to be_defines_module
|
457
|
-
end
|
451
|
+
it 'is false for implicit receiver to new' do
|
452
|
+
exp = sexp(:casgn, nil, :Foo, sexp(:send, nil, :new))
|
453
|
+
expect(exp).not_to be_defines_module
|
458
454
|
end
|
459
455
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
expect(exp).to be_defines_module
|
465
|
-
end
|
456
|
+
it 'is true for explicit receiver to new' do
|
457
|
+
exp = Reek::Source::SourceCode.from('Foo = Class.new(Bar)').syntax_tree
|
458
|
+
expect(exp).to be_defines_module
|
466
459
|
end
|
467
460
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
expect(exp).not_to be_defines_module
|
473
|
-
end
|
461
|
+
it 'is false for assigning a lambda to a constant' do
|
462
|
+
exp = Reek::Source::SourceCode.from('C = ->{}').syntax_tree
|
463
|
+
expect(exp).not_to be_defines_module
|
474
464
|
end
|
475
465
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
expect(exp).not_to be_defines_module
|
481
|
-
end
|
466
|
+
it 'is false for assigning a string to a constant' do
|
467
|
+
exp = Reek::Source::SourceCode.from('C = "hello"').syntax_tree
|
468
|
+
expect(exp).not_to be_defines_module
|
482
469
|
end
|
483
470
|
end
|
484
471
|
|
485
472
|
describe '#superclass' do
|
486
473
|
it 'returns the superclass from the class definition' do
|
487
474
|
exp = Reek::Source::SourceCode.from('Foo = Class.new(Bar)').syntax_tree
|
488
|
-
|
489
475
|
expect(exp.superclass).to eq sexp(:const, nil, :Bar)
|
490
476
|
end
|
491
477
|
|
492
478
|
it 'returns nil in case of no class definition' do
|
493
479
|
exp = Reek::Source::SourceCode.from('Foo = 23').syntax_tree
|
494
|
-
|
495
480
|
expect(exp.superclass).to be_nil
|
496
481
|
end
|
497
482
|
|
498
483
|
it 'returns nil in case of no superclass' do
|
499
484
|
exp = Reek::Source::SourceCode.from('Foo = Class.new').syntax_tree
|
500
|
-
|
501
485
|
expect(exp.superclass).to be_nil
|
502
486
|
end
|
503
487
|
|
504
488
|
it 'returns nothing for a class definition using Struct.new' do
|
505
489
|
exp = Reek::Source::SourceCode.from('Foo = Struct.new("Bar")').syntax_tree
|
506
|
-
|
507
490
|
expect(exp.superclass).to be_nil
|
508
491
|
end
|
509
492
|
|
510
493
|
it 'returns nothing for a constant assigned with a bare method call' do
|
511
494
|
exp = Reek::Source::SourceCode.from('Foo = foo("Bar")').syntax_tree
|
512
|
-
|
513
495
|
expect(exp.superclass).to be_nil
|
514
496
|
end
|
515
497
|
end
|
@@ -38,48 +38,35 @@ RSpec.describe Reek::CodeComment do
|
|
38
38
|
|
39
39
|
describe 'good comment config' do
|
40
40
|
it 'parses hashed options' do
|
41
|
-
comment = '# :reek:DuplicateMethodCall {
|
41
|
+
comment = '# :reek:DuplicateMethodCall { max_calls: 3 }'
|
42
42
|
config = build(:code_comment,
|
43
43
|
comment: comment).config
|
44
44
|
|
45
45
|
expect(config).to include('DuplicateMethodCall')
|
46
|
-
expect(config['DuplicateMethodCall']).to
|
47
|
-
expect(config['DuplicateMethodCall']['
|
48
|
-
end
|
49
|
-
|
50
|
-
it "supports hashed options with the legacy separator ':' after the smell detector" do
|
51
|
-
comment = '# :reek:DuplicateMethodCall: { enabled: false }'
|
52
|
-
config = build(:code_comment,
|
53
|
-
comment: comment).config
|
54
|
-
|
55
|
-
expect(config).to include('DuplicateMethodCall')
|
56
|
-
expect(config['DuplicateMethodCall']).to include('enabled')
|
57
|
-
expect(config['DuplicateMethodCall']['enabled']).to be_falsey
|
46
|
+
expect(config['DuplicateMethodCall']).to have_key 'max_calls'
|
47
|
+
expect(config['DuplicateMethodCall']['max_calls']).to eq 3
|
58
48
|
end
|
59
49
|
|
60
50
|
it 'parses multiple hashed options' do
|
61
51
|
comment = <<-RUBY
|
62
|
-
# :reek:DuplicateMethodCall {
|
52
|
+
# :reek:DuplicateMethodCall { max_calls: 3 }
|
63
53
|
# :reek:NestedIterators { enabled: true }
|
64
54
|
RUBY
|
65
55
|
config = build(:code_comment, comment: comment).config
|
66
56
|
|
67
57
|
expect(config).to include('DuplicateMethodCall', 'NestedIterators')
|
68
|
-
expect(config['DuplicateMethodCall']).to
|
69
|
-
expect(config['DuplicateMethodCall']['enabled']).to be_falsey
|
70
|
-
expect(config['NestedIterators']).to include('enabled')
|
58
|
+
expect(config['DuplicateMethodCall']['max_calls']).to eq 3
|
71
59
|
expect(config['NestedIterators']['enabled']).to be_truthy
|
72
60
|
end
|
73
61
|
|
74
62
|
it 'parses multiple hashed options on the same line' do
|
75
63
|
comment = <<-RUBY
|
76
|
-
#:reek:DuplicateMethodCall {
|
64
|
+
#:reek:DuplicateMethodCall { max_calls: 3 } and :reek:NestedIterators { enabled: true }
|
77
65
|
RUBY
|
78
66
|
config = build(:code_comment, comment: comment).config
|
79
67
|
|
80
68
|
expect(config).to include('DuplicateMethodCall', 'NestedIterators')
|
81
|
-
expect(config['DuplicateMethodCall']).to
|
82
|
-
expect(config['DuplicateMethodCall']['enabled']).to be_falsey
|
69
|
+
expect(config['DuplicateMethodCall']['max_calls']).to eq 3
|
83
70
|
expect(config['NestedIterators']).to include('enabled')
|
84
71
|
expect(config['NestedIterators']['enabled']).to be_truthy
|
85
72
|
end
|
@@ -104,6 +91,13 @@ RSpec.describe Reek::CodeComment do
|
|
104
91
|
expect(config['DuplicateMethodCall']['enabled']).to be_falsey
|
105
92
|
end
|
106
93
|
|
94
|
+
it 'does not disable the smell if options are specifed' do
|
95
|
+
comment = '# :reek:DuplicateMethodCall { max_calls: 3 }'
|
96
|
+
config = build(:code_comment, comment: comment).config
|
97
|
+
|
98
|
+
expect(config['DuplicateMethodCall']).not_to include('enabled')
|
99
|
+
end
|
100
|
+
|
107
101
|
it 'ignores smells after a space' do
|
108
102
|
config = build(:code_comment,
|
109
103
|
comment: '# :reek: DuplicateMethodCall').config
|
@@ -113,7 +107,7 @@ RSpec.describe Reek::CodeComment do
|
|
113
107
|
it 'removes the configuration options from the comment' do
|
114
108
|
original_comment = <<-RUBY
|
115
109
|
# Actual
|
116
|
-
# :reek:DuplicateMethodCall {
|
110
|
+
# :reek:DuplicateMethodCall { max_calls: 3 }
|
117
111
|
# :reek:NestedIterators { enabled: true }
|
118
112
|
# comment
|
119
113
|
RUBY
|
@@ -143,6 +137,14 @@ RSpec.describe Reek::CodeComment::CodeCommentValidator do
|
|
143
137
|
end
|
144
138
|
end
|
145
139
|
|
140
|
+
context 'when the legacy comment format was used' do
|
141
|
+
it 'raises LegacyCommentSeparatorError' do
|
142
|
+
comment = '# :reek:DuplicateMethodCall:'
|
143
|
+
expect { build(:code_comment, comment: comment) }.
|
144
|
+
to raise_error Reek::Errors::LegacyCommentSeparatorError
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
146
148
|
describe 'validating configuration keys' do
|
147
149
|
context 'when basic options are mispelled' do
|
148
150
|
it 'raises BadDetectorConfigurationKeyInCommentError' do
|