reek 4.5.0 → 4.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f44c19009380c9dff7c4d8ef4dcdbe6f788d46b2
4
- data.tar.gz: b9cfe47dc250a613611f414463a6dbca748690c8
3
+ metadata.gz: e73dfbe510f33f5080c142589e06141d394590c4
4
+ data.tar.gz: c97e989ed22ab395217099122515d0b1e02ca8e2
5
5
  SHA512:
6
- metadata.gz: 221c0dbe79b6a6758a30ded23285fd715634843c3ac9bb0b9883aef9a0ebffcf84531dc6e492f091b2fa11cb7087470c96632aa45b53704da831a0a64feb7c1a
7
- data.tar.gz: 51c17f02b1aa2c8ff8cf701ba7bc9d98c058a9c4e62fc2b6f246fd43f3be17ed12b6cc58ccb247f99527f909db50c5b4ab7085b08392a4b2c9e26137c8081bb2
6
+ metadata.gz: 27c163213eee8653498e261dc44b3e6b8a1c7e9a17dfeb19bafdd914a7b7ec741168cc1f1505b71b7f8653c8eff889dbb0cbb3f953220bd46876ca4a12e310d4
7
+ data.tar.gz: 3884c786a8deda2eab21427011f595b0484c61075ab96beb759bbe1ecbefc92f244650a9ef045850ce261783ef397516d5cf8fd16956cbdec16ab712fe86546b
data/.rubocop.yml CHANGED
@@ -74,6 +74,7 @@ RSpec/VerifiedDoubles:
74
74
  # rubocop-rspec expects a CodeClimate namespace to go with the code_climate directory.
75
75
  RSpec/FilePath:
76
76
  Exclude:
77
+ - 'spec/reek/report/code_climate/code_climate_configuration_spec.rb'
77
78
  - 'spec/reek/report/code_climate/code_climate_fingerprint_spec.rb'
78
79
  - 'spec/reek/report/code_climate/code_climate_formatter_spec.rb'
79
80
  - 'spec/reek/report/code_climate/code_climate_report_spec.rb'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Change log
2
2
 
3
+ ## 4.5.1 (2016-10-16)
4
+
5
+ * (troessner) Validate configuration keys in code comments.
6
+
3
7
  ## 4.5.0 (2016-10-12)
4
8
 
5
9
  * (maxjacobson) Emit fingerprints in Code Climate reporter
@@ -51,3 +51,18 @@ Feature: Erroneous source comments are handled properly
51
51
  And it reports the error "Unfortunately we can not parse the configuration you have given."
52
52
  And it reports the error "The source is 'bad_comment.rb'"
53
53
  And it reports the error "the comment belongs to the expression starting in line 3"
54
+
55
+ Scenario: Bad configuration key
56
+ Given a file named "bad_comment.rb" with:
57
+ """
58
+ # Test class
59
+ # exclude -> elude and accept -> accipt are bad keys
60
+ # :reek:UncommunicativeMethodName { elude: 'foo', accipt: 'bar' }
61
+ def x
62
+ end
63
+ """
64
+ When I run reek bad_comment.rb
65
+ Then it reports the error "Error: You are trying to configure the smell detector 'UncommunicativeMethodName'"
66
+ And it reports the error "in one of your source code comments with the unknown option 'elude', 'accipt'"
67
+ And it reports the error "The source is 'bad_comment.rb'"
68
+ And it reports the error "the comment belongs to the expression starting in line 4"
@@ -4,6 +4,7 @@ require 'yaml'
4
4
 
5
5
  require_relative 'smell_detectors/base_detector'
6
6
  require_relative 'errors/bad_detector_in_comment_error'
7
+ require_relative 'errors/bad_detector_configuration_key_in_comment_error'
7
8
  require_relative 'errors/garbage_detector_configuration_in_comment_error'
8
9
 
9
10
  module Reek
@@ -75,10 +76,10 @@ module Reek
75
76
  # 2.) Garbage in the detector configuration like { thats: a: bad: config }
76
77
  # 3.) Unknown configuration keys (e.g. by doing a simple typo: "exclude" vs. "exlude" )
77
78
  # 4.) Bad data types given as values for those keys
78
- # This class validates [1] and [2] at the moment but will also validate [3]
79
- # and [4] in the future.
79
+ # This class validates [1], [2] and [3] at the moment but will also validate
80
+ # [4] in the future.
80
81
  #
81
- # :reek:TooManyInstanceVariables: { max_instance_variables: 5 }
82
+ # :reek:TooManyInstanceVariables: { max_instance_variables: 7 }
82
83
  class CodeCommentValidator
83
84
  #
84
85
  # @param detector_name [String] - the detector class that was parsed out of the original
@@ -94,16 +95,20 @@ module Reek
94
95
  @line = line
95
96
  @source = source
96
97
  @options = options
98
+ @detector_class = nil # We only know this one after our first initial checks
99
+ @parsed_options = nil # We only know this one after our first initial checks
97
100
  end
98
101
 
99
102
  #
100
103
  # Method can raise the following errors:
101
104
  # * Errors::BadDetectorInCommentError
102
105
  # * Errors::GarbageDetectorConfigurationInCommentError
106
+ # * Errors::BadDetectorConfigurationKeyInCommentError
103
107
  # @return [undefined]
104
108
  def validate
105
109
  escalate_bad_detector
106
110
  escalate_bad_detector_configuration
111
+ escalate_unknown_configuration_key
107
112
  end
108
113
 
109
114
  private
@@ -112,7 +117,9 @@ module Reek
112
117
  :original_comment,
113
118
  :line,
114
119
  :source,
115
- :options
120
+ :options,
121
+ :detector_class,
122
+ :parsed_options
116
123
 
117
124
  def escalate_bad_detector
118
125
  return if SmellDetectors::BaseDetector.valid_detector?(detector_name)
@@ -123,13 +130,46 @@ module Reek
123
130
  end
124
131
 
125
132
  def escalate_bad_detector_configuration
126
- YAML.load(options || CodeComment::DISABLE_DETECTOR_CONFIGURATION)
133
+ @parsed_options = YAML.load(options || CodeComment::DISABLE_DETECTOR_CONFIGURATION)
127
134
  rescue Psych::SyntaxError
128
135
  raise Errors::GarbageDetectorConfigurationInCommentError, detector_name: detector_name,
129
136
  original_comment: original_comment,
130
137
  source: source,
131
138
  line: line
132
139
  end
140
+
141
+ def escalate_unknown_configuration_key
142
+ @detector_class = SmellDetectors::BaseDetector.to_detector(detector_name)
143
+
144
+ return if given_keys_legit?
145
+ raise Errors::BadDetectorConfigurationKeyInCommentError, detector_name: detector_name,
146
+ offensive_keys: configuration_keys_difference,
147
+ original_comment: original_comment,
148
+ source: source,
149
+ line: line
150
+ end
151
+
152
+ # @return [Boolean] - all keys in code comment are applicable to the detector in question
153
+ def given_keys_legit?
154
+ given_configuration_keys.subset? valid_detector_keys
155
+ end
156
+
157
+ # @return [Set] - the configuration keys that are found in the code comment
158
+ def given_configuration_keys
159
+ parsed_options.keys.map(&:to_sym).to_set
160
+ end
161
+
162
+ # @return [String] - all keys from the code comment that look bad
163
+ def configuration_keys_difference
164
+ given_configuration_keys.difference(valid_detector_keys).
165
+ to_a.map { |key| "'#{key}'" }.
166
+ join(', ')
167
+ end
168
+
169
+ # @return [Set] - all keys that are legit for the given detector
170
+ def valid_detector_keys
171
+ detector_class.configuration_keys
172
+ end
133
173
  end
134
174
  end
135
175
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reek
4
+ module Errors
5
+ # Gets raised when trying to configure a detector with an option
6
+ # which is unknown to it.
7
+ class BadDetectorConfigurationKeyInCommentError < RuntimeError
8
+ UNKNOWN_SMELL_DETECTOR_MESSAGE = <<-EOS.freeze
9
+
10
+ Error: You are trying to configure the smell detector '%s'
11
+ in one of your source code comments with the unknown option %s.
12
+ The source is '%s' and the comment belongs to the expression starting in line %d.
13
+ Here's the original comment:
14
+
15
+ %s
16
+
17
+ Please see the Reek docs for:
18
+ * how to configure Reek via source code comments: https://github.com/troessner/reek/blob/master/docs/Smell-Suppression.md
19
+ * what basic options are available: https://github.com/troessner/reek/blob/master/docs/Basic-Smell-Options.md
20
+ * what custom options are available by checking the detector specific documentation in /docs
21
+ Update the offensive comment (or remove it if no longer applicable) and re-run Reek.
22
+
23
+ EOS
24
+
25
+ def initialize(detector_name:, offensive_keys:, source:, line:, original_comment:)
26
+ message = format(UNKNOWN_SMELL_DETECTOR_MESSAGE,
27
+ detector_name,
28
+ offensive_keys,
29
+ source,
30
+ line,
31
+ original_comment)
32
+ super message
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/reek/examiner.rb CHANGED
@@ -121,7 +121,8 @@ module Reek
121
121
  begin
122
122
  examine_tree
123
123
  rescue Errors::BadDetectorInCommentError,
124
- Errors::GarbageDetectorConfigurationInCommentError => exception
124
+ Errors::GarbageDetectorConfigurationInCommentError,
125
+ Errors::BadDetectorConfigurationKeyInCommentError => exception
125
126
  warn exception
126
127
  []
127
128
  rescue StandardError => exception
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module Reek
3
+ module Report
4
+ # loads the smell type metadata to present in Code Climate
5
+ module CodeClimateConfiguration
6
+ def self.load
7
+ config_file = File.expand_path('../code_climate_configuration.yml', __FILE__)
8
+ YAML.load_file config_file
9
+ end
10
+ end
11
+ end
12
+ end
@@ -222,6 +222,135 @@ FeatureEnvy:
222
222
  ```
223
223
 
224
224
  belongs to the Item class, not the Warehouse.
225
+ InstanceVariableAssumption:
226
+ remediation_points: 350_000
227
+ content: |
228
+ Classes should not assume that instance variables are set or present outside of the current class definition.
229
+
230
+ Good:
231
+
232
+ ```Ruby
233
+ class Foo
234
+ def initialize
235
+ @bar = :foo
236
+ end
237
+
238
+ def foo?
239
+ @bar == :foo
240
+ end
241
+ end
242
+ ```
243
+
244
+ Good as well:
245
+
246
+ ```Ruby
247
+ class Foo
248
+ def foo?
249
+ bar == :foo
250
+ end
251
+
252
+ def bar
253
+ @bar ||= :foo
254
+ end
255
+ end
256
+ ```
257
+
258
+ Bad:
259
+
260
+ ```Ruby
261
+ class Foo
262
+ def go_foo!
263
+ @bar = :foo
264
+ end
265
+
266
+ def foo?
267
+ @bar == :foo
268
+ end
269
+ end
270
+ ```
271
+
272
+ ## Example
273
+
274
+ Running Reek on:
275
+
276
+ ```Ruby
277
+ class Dummy
278
+ def test
279
+ @ivar
280
+ end
281
+ end
282
+ ```
283
+
284
+ would report:
285
+
286
+ ```Bash
287
+ [1]:InstanceVariableAssumption: Dummy assumes too much for instance variable @ivar [https://github.com/troessner/reek/blob/master/docs/Instance-Variable-Assumption.md]
288
+ ```
289
+
290
+ Note that this example would trigger this smell warning as well:
291
+
292
+ ```Ruby
293
+ class Parent
294
+ def initialize(omg)
295
+ @omg = omg
296
+ end
297
+ end
298
+
299
+ class Child < Parent
300
+ def foo
301
+ @omg
302
+ end
303
+ end
304
+ ```
305
+
306
+ The way to address the smell warning is that you should create an `attr_reader` to use `@omg` in the subclass and not access `@omg` directly like this:
307
+
308
+ ```Ruby
309
+ class Parent
310
+ attr_reader :omg
311
+
312
+ def initialize(omg)
313
+ @omg = omg
314
+ end
315
+ end
316
+
317
+ class Child < Parent
318
+ def foo
319
+ omg
320
+ end
321
+ end
322
+ ```
323
+
324
+ Directly accessing instance variables is considered a smell because it [breaks encapsulation](http://designisrefactoring.com/2015/03/29/organizing-data-self-encapsulation/) and makes it harder to reason about code.
325
+
326
+ If you don't want to expose those methods as public API just make them private like this:
327
+
328
+ ```Ruby
329
+ class Parent
330
+ def initialize(omg)
331
+ @omg = omg
332
+ end
333
+
334
+ private
335
+ attr_reader :omg
336
+ end
337
+
338
+ class Child < Parent
339
+ def foo
340
+ omg
341
+ end
342
+ end
343
+ ```
344
+
345
+
346
+ ## Current Support in Reek
347
+
348
+ An instance variable must:
349
+
350
+ * be set in the constructor
351
+ * or be accessed through a method with lazy initialization / memoization.
352
+
353
+ If not, _Instance Variable Assumption_ will be reported.
225
354
  IrresponsibleModule:
226
355
  remediation_points: 350_000
227
356
  content: |
@@ -300,6 +429,33 @@ LongYieldList:
300
429
  ```
301
430
 
302
431
  A common solution to this problem would be the introduction of parameter objects.
432
+ ManualDispatch:
433
+ remediation_points: 350_000
434
+ content: |
435
+ Reek reports a _Manual Dispatch_ smell if it finds source code that manually checks whether an object responds to a method before that method is called. Manual dispatch is a type of [Simulated Polymorphism](Simulated-Polymorphism.md) which leads to code that is harder to reason about, debug, and refactor.
436
+
437
+ ## Example
438
+
439
+ ```Ruby
440
+ class MyManualDispatcher
441
+ attr_reader :foo
442
+
443
+ def initialize(foo)
444
+ @foo = foo
445
+ end
446
+
447
+ def call
448
+ foo.bar if foo.respond_to?(:bar)
449
+ end
450
+ end
451
+ ```
452
+
453
+ Reek would emit the following warning:
454
+
455
+ ```
456
+ test.rb -- 1 warning:
457
+ [9]: MyManualDispatcher manually dispatches method call (ManualDispatch)
458
+ ```
303
459
  ModuleInitialize:
304
460
  remediation_points: 350_000
305
461
  content: |
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'digest'
3
+
2
4
  module Reek
3
5
  module Report
4
6
  # Generates a string to uniquely identify a smell
@@ -14,7 +16,7 @@ module Reek
14
16
 
15
17
  identify_warning
16
18
 
17
- identifying_aspects.hexdigest
19
+ identifying_aspects.hexdigest.freeze
18
20
  end
19
21
 
20
22
  private
@@ -33,7 +35,7 @@ module Reek
33
35
  end
34
36
 
35
37
  def parameters
36
- warning.parameters.except(*NON_IDENTIFYING_PARAMETERS).sort.to_s
38
+ warning.parameters.reject { |key, _| NON_IDENTIFYING_PARAMETERS.include?(key) }.sort.to_s
37
39
  end
38
40
 
39
41
  def warning_uniquely_identifiable?
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'codeclimate_engine'
3
+ require_relative 'code_climate_configuration'
3
4
 
4
5
  module Reek
5
6
  module Report
@@ -56,8 +57,7 @@ module Reek
56
57
 
57
58
  def configuration
58
59
  @configuration ||= begin
59
- config_file = File.expand_path('../code_climate_configuration.yml', __FILE__)
60
- YAML.load_file config_file
60
+ CodeClimateConfiguration.load
61
61
  end
62
62
  end
63
63
  end
@@ -14,6 +14,7 @@ module Reek
14
14
  # for details.
15
15
  #
16
16
  # :reek:UnusedPrivateMethod: { exclude: [ smell_warning ] }
17
+ # :reek:TooManyMethods: { max_methods: 18 }
17
18
  class BaseDetector
18
19
  attr_reader :config
19
20
  # The name of the config field that lists the names of code contexts
@@ -122,6 +123,24 @@ module Reek
122
123
  descendants.map { |descendant| descendant.to_s.split('::').last }.
123
124
  include?(detector)
124
125
  end
126
+
127
+ #
128
+ # Transform a detector name to the corresponding constant.
129
+ # Note that we assume a valid name - exceptions are not handled here.
130
+ #
131
+ # @param detector_name [String] the detector in question, e.g. 'DuplicateMethodCall'
132
+ # @return [SmellDetector] - this will return the class, not an instance
133
+ #
134
+ def to_detector(detector_name)
135
+ SmellDetectors.const_get detector_name
136
+ end
137
+
138
+ #
139
+ # @return [Set<Symbol>] - all configuration keys that are available for this detector
140
+ #
141
+ def configuration_keys
142
+ Set.new(default_config.keys.map(&:to_sym))
143
+ end
125
144
  end
126
145
  end
127
146
  end
data/lib/reek/version.rb CHANGED
@@ -7,6 +7,6 @@ module Reek
7
7
  # @public
8
8
  module Version
9
9
  # @public
10
- STRING = '4.5.0'.freeze
10
+ STRING = '4.5.1'.freeze
11
11
  end
12
12
  end
@@ -142,4 +142,44 @@ RSpec.describe Reek::CodeComment::CodeCommentValidator do
142
142
  end.to raise_error(Reek::Errors::GarbageDetectorConfigurationInCommentError)
143
143
  end
144
144
  end
145
+
146
+ describe 'validating configuration keys' do
147
+ context 'basic options mispelled' do
148
+ it 'raises BadDetectorConfigurationKeyInCommentError' do
149
+ expect do
150
+ # exclude -> exlude and enabled -> nabled
151
+ comment = '# :reek:UncommunicativeMethodName { exlude: alfa, nabled: true }'
152
+ FactoryGirl.build(:code_comment, comment: comment)
153
+ end.to raise_error(Reek::Errors::BadDetectorConfigurationKeyInCommentError)
154
+ end
155
+ end
156
+
157
+ context 'basic options not mispelled' do
158
+ it 'does not raise' do
159
+ expect do
160
+ comment = '# :reek:UncommunicativeMethodName { exclude: alfa, enabled: true }'
161
+ FactoryGirl.build(:code_comment, comment: comment)
162
+ end.not_to raise_error
163
+ end
164
+ end
165
+
166
+ context 'unknown custom options' do
167
+ it 'raises BadDetectorConfigurationKeyInCommentError' do
168
+ expect do
169
+ # max_copies -> mx_copies and min_clump_size -> mn_clump_size
170
+ comment = '# :reek:DataClump { mx_copies: 4, mn_clump_size: 3 }'
171
+ FactoryGirl.build(:code_comment, comment: comment)
172
+ end.to raise_error(Reek::Errors::BadDetectorConfigurationKeyInCommentError)
173
+ end
174
+ end
175
+
176
+ context 'valid custom options' do
177
+ it 'does not raise' do
178
+ expect do
179
+ comment = '# :reek:DataClump { max_copies: 4, min_clump_size: 3 }'
180
+ FactoryGirl.build(:code_comment, comment: comment)
181
+ end.not_to raise_error
182
+ end
183
+ end
184
+ end
145
185
  end
@@ -0,0 +1,24 @@
1
+ require_relative '../../../spec_helper'
2
+ require_lib 'reek/report/code_climate/code_climate_configuration'
3
+
4
+ RSpec.describe Reek::Report::CodeClimateConfiguration do
5
+ yml = described_class.load
6
+ smell_types = Reek::SmellDetectors::BaseDetector.descendants.map do |descendant|
7
+ descendant.name.demodulize
8
+ end
9
+
10
+ smell_types.each do |name|
11
+ config = yml.fetch(name)
12
+ it "provides remediation_points for #{name}" do
13
+ expect(config['remediation_points']).to be_a Fixnum
14
+ end
15
+
16
+ it "provides content for #{name}" do
17
+ expect(config['content']).to be_a String
18
+ end
19
+ end
20
+
21
+ it 'does not include extraneous configuration' do
22
+ expect(smell_types).to match_array(yml.keys)
23
+ end
24
+ end
@@ -1,5 +1,6 @@
1
1
  require_relative '../../spec_helper'
2
2
  require_lib 'reek/smell_detectors/base_detector'
3
+ require_lib 'reek/smell_detectors/duplicate_method_call'
3
4
 
4
5
  RSpec.describe Reek::SmellDetectors::BaseDetector do
5
6
  describe '.todo_configuration_for' do
@@ -43,4 +44,21 @@ RSpec.describe Reek::SmellDetectors::BaseDetector do
43
44
  expect(described_class.valid_detector?('Unknown')).to be false
44
45
  end
45
46
  end
47
+
48
+ describe '.to_detector' do
49
+ it 'returns the right detector' do
50
+ expect(described_class.to_detector('DuplicateMethodCall')).to eq(Reek::SmellDetectors::DuplicateMethodCall)
51
+ end
52
+
53
+ it 'raise NameError for an invalid detector name' do
54
+ expect { described_class.to_detector('Unknown') }.to raise_error(NameError)
55
+ end
56
+ end
57
+
58
+ describe '.configuration_keys' do
59
+ it 'returns the right keys' do
60
+ expected_keys = Reek::SmellDetectors::DuplicateMethodCall.configuration_keys.to_a
61
+ expect(expected_keys).to eq([:enabled, :exclude, :max_calls, :allow_calls])
62
+ end
63
+ end
46
64
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reek
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.0
4
+ version: 4.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Rutherford
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2016-10-13 00:00:00.000000000 Z
14
+ date: 2016-10-16 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: codeclimate-engine-rb
@@ -220,6 +220,7 @@ files:
220
220
  - lib/reek/context/statement_counter.rb
221
221
  - lib/reek/context/visibility_tracker.rb
222
222
  - lib/reek/context_builder.rb
223
+ - lib/reek/errors/bad_detector_configuration_key_in_comment_error.rb
223
224
  - lib/reek/errors/bad_detector_in_comment_error.rb
224
225
  - lib/reek/errors/garbage_detector_configuration_in_comment_error.rb
225
226
  - lib/reek/examiner.rb
@@ -227,6 +228,7 @@ files:
227
228
  - lib/reek/report.rb
228
229
  - lib/reek/report/base_report.rb
229
230
  - lib/reek/report/code_climate.rb
231
+ - lib/reek/report/code_climate/code_climate_configuration.rb
230
232
  - lib/reek/report/code_climate/code_climate_configuration.yml
231
233
  - lib/reek/report/code_climate/code_climate_fingerprint.rb
232
234
  - lib/reek/report/code_climate/code_climate_formatter.rb
@@ -344,6 +346,7 @@ files:
344
346
  - spec/reek/context_builder_spec.rb
345
347
  - spec/reek/examiner_spec.rb
346
348
  - spec/reek/rake/task_spec.rb
349
+ - spec/reek/report/code_climate/code_climate_configuration_spec.rb
347
350
  - spec/reek/report/code_climate/code_climate_fingerprint_spec.rb
348
351
  - spec/reek/report/code_climate/code_climate_formatter_spec.rb
349
352
  - spec/reek/report/code_climate/code_climate_report_spec.rb