reek 5.6.0 → 6.0.0

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