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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +1 -1
  4. data/.simplecov +1 -0
  5. data/.travis.yml +12 -11
  6. data/CHANGELOG.md +8 -0
  7. data/Dockerfile +1 -0
  8. data/Gemfile +15 -17
  9. data/README.md +1 -7
  10. data/bin/code_climate_reek +12 -2
  11. data/docs/Attribute.md +1 -1
  12. data/docs/Control-Couple.md +1 -1
  13. data/docs/Nil-Check.md +4 -1
  14. data/features/command_line_interface/options.feature +2 -3
  15. data/features/reports/codeclimate.feature +2 -2
  16. data/features/reports/json.feature +3 -3
  17. data/features/reports/reports.feature +4 -4
  18. data/features/reports/yaml.feature +3 -3
  19. data/features/step_definitions/reek_steps.rb +4 -0
  20. data/lib/reek/ast/sexp_extensions/arguments.rb +11 -0
  21. data/lib/reek/cli/options.rb +2 -2
  22. data/lib/reek/code_comment.rb +36 -29
  23. data/lib/reek/errors/legacy_comment_separator_error.rb +36 -0
  24. data/lib/reek/report.rb +5 -7
  25. data/lib/reek/report/code_climate/code_climate_report.rb +2 -1
  26. data/lib/reek/report/simple_warning_formatter.rb +0 -7
  27. data/lib/reek/smell_detectors/base_detector.rb +0 -9
  28. data/lib/reek/smell_detectors/data_clump.rb +23 -56
  29. data/lib/reek/smell_detectors/nil_check.rb +1 -12
  30. data/lib/reek/version.rb +1 -1
  31. data/reek.gemspec +5 -6
  32. data/spec/reek/ast/sexp_extensions_spec.rb +15 -33
  33. data/spec/reek/code_comment_spec.rb +23 -21
  34. data/spec/reek/context_builder_spec.rb +110 -113
  35. data/spec/reek/smell_detectors/base_detector_spec.rb +0 -10
  36. data/spec/reek/smell_detectors/data_clump_spec.rb +14 -0
  37. data/spec/reek/smell_detectors/missing_safe_method_spec.rb +8 -2
  38. data/spec/reek/smell_detectors/nil_check_spec.rb +3 -3
  39. data/spec/reek/source/source_code_spec.rb +13 -0
  40. data/spec/spec_helper.rb +1 -0
  41. 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
@@ -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: YAMLReport,
21
- json: JSONReport,
22
- html: HTMLReport,
23
- xml: XMLReport,
24
- text: TextReport,
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 warning_formatter.format_code_climate_hash(smell)
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
- MethodGroup.new(context, min_clump_size, max_copies).clumps.map do |clump, methods|
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
- # Represents a group of methods
85
- # @private
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
- def candidate_clumps
96
- candidate_methods.each_cons(max_copies + 1).map do |methods|
97
- common_argument_names_for(methods)
98
- end.select do |clump|
99
- clump.length >= min_clump_size
100
- end.uniq
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
- attr_reader :candidate_methods, :max_copies, :min_clump_size
121
- end
122
-
123
- # A method definition and a copy of its parameters
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
- def initialize(defn_node)
131
- @defn = defn_node
132
- end
99
+ def methods_containing_clump(clump)
100
+ candidate_methods.select { |method| clump & method.arg_names == clump }
101
+ end
133
102
 
134
- def arg_names
135
- # TODO: Is all this sorting still needed?
136
- @arg_names ||= defn.arg_names.compact.sort
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
@@ -8,6 +8,6 @@ module Reek
8
8
  # @public
9
9
  module Version
10
10
  # @public
11
- STRING = '5.6.0'
11
+ STRING = '6.0.0'
12
12
  end
13
13
  end
@@ -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.3.0'
19
+ s.required_ruby_version = '>= 2.4.0'
20
20
  s.summary = 'Code smell detector for Ruby'
21
21
 
22
- s.add_runtime_dependency 'codeclimate-engine-rb', '~> 0.4.0'
23
- s.add_runtime_dependency 'kwalify', '~> 0.7.0'
24
- s.add_runtime_dependency 'parser', '< 2.8', '>= 2.5.0.0', '!= 2.5.1.1'
25
- s.add_runtime_dependency 'psych', '~> 3.1.0'
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
- context 'with single assignment' do
447
- it 'does not define a module' do
448
- exp = sexp(:casgn, nil, :Foo)
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
- context 'with implicit receiver to new' do
454
- it 'does not define a module' do
455
- exp = sexp(:casgn, nil, :Foo, sexp(:send, nil, :new))
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
- context 'with implicit receiver to new' do
461
- it 'does not define a module' do
462
- exp = Reek::Source::SourceCode.from('Foo = Class.new(Bar)').syntax_tree
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
- context 'when assigning a lambda to a constant' do
469
- it 'does not define a module' do
470
- exp = Reek::Source::SourceCode.from('C = ->{}').syntax_tree
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
- context 'when assigning a string to a constant' do
477
- it 'does not define a module' do
478
- exp = Reek::Source::SourceCode.from('C = "hello"').syntax_tree
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 { enabled: false }'
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 include('enabled')
47
- expect(config['DuplicateMethodCall']['enabled']).to be_falsey
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 { enabled: false }
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 include('enabled')
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 { enabled: false } and :reek:NestedIterators { enabled: true }
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 include('enabled')
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 { enabled: false }
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