reek 5.6.0 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|