reek 4.5.0 → 4.6.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -7
  3. data/.travis.yml +13 -9
  4. data/CHANGELOG.md +36 -3
  5. data/Dockerfile +6 -5
  6. data/Gemfile +8 -7
  7. data/README.md +45 -27
  8. data/docs/How-To-Write-New-Detectors.md +6 -6
  9. data/docs/Irresponsible-Module.md +8 -1
  10. data/docs/Nil-Check.md +1 -1
  11. data/docs/Prima-Donna-Method.md +27 -0
  12. data/docs/RSpec-matchers.md +5 -10
  13. data/docs/Unused-Private-Method.md +27 -0
  14. data/docs/templates/default/docstring/setup.rb +1 -8
  15. data/features/command_line_interface/options.feature +5 -1
  16. data/features/command_line_interface/stdin.feature +1 -1
  17. data/features/configuration_files/exclude_paths_directives.feature +43 -0
  18. data/features/configuration_files/warn_about_multiple_configuration_files.feature +44 -0
  19. data/features/configuration_via_source_comments/erroneous_source_comments.feature +15 -0
  20. data/features/samples.feature +3 -8
  21. data/features/step_definitions/reek_steps.rb +5 -5
  22. data/features/todo_list.feature +1 -2
  23. data/lib/reek/ast/reference_collector.rb +2 -4
  24. data/lib/reek/ast/sexp_extensions/arguments.rb +0 -5
  25. data/lib/reek/ast/sexp_extensions/constant.rb +1 -0
  26. data/lib/reek/cli/application.rb +7 -8
  27. data/lib/reek/cli/command/report_command.rb +3 -1
  28. data/lib/reek/cli/options.rb +29 -13
  29. data/lib/reek/cli/status.rb +10 -0
  30. data/lib/reek/code_comment.rb +48 -6
  31. data/lib/reek/configuration/configuration_file_finder.rb +87 -27
  32. data/lib/reek/context/ghost_context.rb +1 -2
  33. data/lib/reek/context_builder.rb +26 -1
  34. data/lib/reek/detector_repository.rb +64 -0
  35. data/lib/reek/errors/bad_detector_configuration_key_in_comment_error.rb +37 -0
  36. data/lib/reek/errors/bad_detector_in_comment_error.rb +2 -1
  37. data/lib/reek/errors/base_error.rb +9 -0
  38. data/lib/reek/errors/garbage_detector_configuration_in_comment_error.rb +2 -1
  39. data/lib/reek/errors/incomprehensible_source_error.rb +47 -0
  40. data/lib/reek/errors/parse_error.rb +19 -0
  41. data/lib/reek/examiner.rb +17 -41
  42. data/lib/reek/logging_error_handler.rb +15 -0
  43. data/lib/reek/report/code_climate/code_climate_configuration.rb +12 -0
  44. data/lib/reek/report/code_climate/code_climate_configuration.yml +156 -0
  45. data/lib/reek/report/code_climate/code_climate_fingerprint.rb +4 -2
  46. data/lib/reek/report/code_climate/code_climate_formatter.rb +2 -2
  47. data/lib/reek/smell_configuration.rb +64 -0
  48. data/lib/reek/smell_detectors/attribute.rb +0 -2
  49. data/lib/reek/smell_detectors/base_detector.rb +24 -4
  50. data/lib/reek/smell_detectors/boolean_parameter.rb +0 -1
  51. data/lib/reek/smell_detectors/class_variable.rb +0 -1
  52. data/lib/reek/smell_detectors/control_parameter.rb +0 -1
  53. data/lib/reek/smell_detectors/data_clump.rb +0 -1
  54. data/lib/reek/smell_detectors/duplicate_method_call.rb +0 -1
  55. data/lib/reek/smell_detectors/feature_envy.rb +0 -1
  56. data/lib/reek/smell_detectors/instance_variable_assumption.rb +0 -1
  57. data/lib/reek/smell_detectors/irresponsible_module.rb +0 -1
  58. data/lib/reek/smell_detectors/long_parameter_list.rb +0 -2
  59. data/lib/reek/smell_detectors/long_yield_list.rb +0 -1
  60. data/lib/reek/smell_detectors/manual_dispatch.rb +4 -6
  61. data/lib/reek/smell_detectors/module_initialize.rb +5 -8
  62. data/lib/reek/smell_detectors/nested_iterators.rb +0 -1
  63. data/lib/reek/smell_detectors/nil_check.rb +0 -1
  64. data/lib/reek/smell_detectors/prima_donna_method.rb +30 -1
  65. data/lib/reek/smell_detectors/repeated_conditional.rb +0 -1
  66. data/lib/reek/smell_detectors/subclassed_from_core_class.rb +0 -1
  67. data/lib/reek/smell_detectors/too_many_constants.rb +0 -1
  68. data/lib/reek/smell_detectors/too_many_instance_variables.rb +0 -1
  69. data/lib/reek/smell_detectors/too_many_methods.rb +0 -1
  70. data/lib/reek/smell_detectors/too_many_statements.rb +0 -1
  71. data/lib/reek/smell_detectors/uncommunicative_method_name.rb +0 -1
  72. data/lib/reek/smell_detectors/uncommunicative_module_name.rb +0 -1
  73. data/lib/reek/smell_detectors/uncommunicative_parameter_name.rb +0 -1
  74. data/lib/reek/smell_detectors/uncommunicative_variable_name.rb +0 -1
  75. data/lib/reek/smell_detectors/unused_parameters.rb +0 -1
  76. data/lib/reek/smell_detectors/unused_private_method.rb +0 -2
  77. data/lib/reek/smell_detectors/utility_function.rb +0 -1
  78. data/lib/reek/smell_warning.rb +85 -0
  79. data/lib/reek/source/source_code.rb +2 -1
  80. data/lib/reek/source/source_locator.rb +15 -3
  81. data/lib/reek/spec/should_reek_of.rb +1 -1
  82. data/lib/reek/spec.rb +6 -4
  83. data/lib/reek/version.rb +1 -1
  84. data/reek.gemspec +1 -1
  85. data/samples/configuration/more_than_one_configuration_file/todo.reek +0 -0
  86. data/samples/configuration/single_configuration_file/.reek +0 -0
  87. data/spec/factories/factories.rb +2 -10
  88. data/spec/quality/reek_source_spec.rb +5 -3
  89. data/spec/reek/ast/reference_collector_spec.rb +0 -17
  90. data/spec/reek/cli/application_spec.rb +25 -1
  91. data/spec/reek/cli/command/report_command_spec.rb +2 -2
  92. data/spec/reek/cli/command/todo_list_command_spec.rb +14 -71
  93. data/spec/reek/cli/options_spec.rb +4 -0
  94. data/spec/reek/code_comment_spec.rb +47 -0
  95. data/spec/reek/configuration/configuration_file_finder_spec.rb +38 -15
  96. data/spec/reek/context/code_context_spec.rb +10 -10
  97. data/spec/reek/context/method_context_spec.rb +1 -1
  98. data/spec/reek/context/module_context_spec.rb +8 -4
  99. data/spec/reek/{smell_detectors/detector_repository_spec.rb → detector_repository_spec.rb} +3 -3
  100. data/spec/reek/examiner_spec.rb +39 -40
  101. data/spec/reek/logging_error_handler_spec.rb +24 -0
  102. data/spec/reek/report/code_climate/code_climate_configuration_spec.rb +24 -0
  103. data/spec/reek/report/code_climate/code_climate_fingerprint_spec.rb +9 -9
  104. data/spec/reek/report/yaml_report_spec.rb +4 -4
  105. data/spec/reek/{smell_detectors/smell_configuration_spec.rb → smell_configuration_spec.rb} +3 -3
  106. data/spec/reek/smell_detectors/base_detector_spec.rb +18 -0
  107. data/spec/reek/smell_detectors/duplicate_method_call_spec.rb +2 -2
  108. data/spec/reek/smell_detectors/feature_envy_spec.rb +18 -8
  109. data/spec/reek/smell_detectors/manual_dispatch_spec.rb +13 -0
  110. data/spec/reek/smell_detectors/module_initialize_spec.rb +23 -2
  111. data/spec/reek/smell_detectors/nested_iterators_spec.rb +1 -1
  112. data/spec/reek/smell_detectors/prima_donna_method_spec.rb +12 -0
  113. data/spec/reek/smell_detectors/subclassed_from_core_class_spec.rb +0 -5
  114. data/spec/reek/smell_detectors/uncommunicative_variable_name_spec.rb +2 -2
  115. data/spec/reek/smell_detectors/unused_parameters_spec.rb +1 -1
  116. data/spec/reek/{smell_detectors/smell_warning_spec.rb → smell_warning_spec.rb} +9 -9
  117. data/spec/reek/source/source_code_spec.rb +6 -29
  118. data/spec/reek/source/source_locator_spec.rb +48 -16
  119. data/spec/reek/spec/should_reek_of_spec.rb +1 -1
  120. data/spec/reek/spec/should_reek_only_of_spec.rb +4 -4
  121. data/spec/spec_helper.rb +4 -3
  122. data/tasks/configuration.rake +2 -2
  123. metadata +26 -14
  124. data/lib/reek/smell_detectors/detector_repository.rb +0 -66
  125. data/lib/reek/smell_detectors/smell_configuration.rb +0 -66
  126. data/lib/reek/smell_detectors/smell_warning.rb +0 -88
  127. data/tasks/mutant.rake +0 -14
  128. /data/samples/configuration/{.reek → more_than_one_configuration_file/regular.reek} +0 -0
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'smell_detectors'
3
+ require_relative 'smell_detectors/base_detector'
4
+ require_relative 'configuration/app_configuration'
5
+
6
+ module Reek
7
+ #
8
+ # Contains all the existing smell detectors and exposes operations on them.
9
+ #
10
+ class DetectorRepository
11
+ # @return [Array<Reek::SmellDetectors::BaseDetector>] All known SmellDetectors
12
+ # e.g. [Reek::SmellDetectors::BooleanParameter, Reek::SmellDetectors::ClassVariable].
13
+ def self.smell_types
14
+ Reek::SmellDetectors::BaseDetector.descendants.sort_by(&:name)
15
+ end
16
+
17
+ # @param filter_by_smells [Array<String>]
18
+ # List of smell types to filter by, e.g. "DuplicateMethodCall".
19
+ # More precisely it should be whatever is returned by `BaseDetector`.smell_type.
20
+ # This means that you can write the "DuplicateMethodCall" from above also like this:
21
+ # Reek::SmellDetectors::DuplicateMethodCall.smell_type
22
+ # if you want to make sure you do not fat-finger strings.
23
+ #
24
+ # @return [Array<Reek::SmellDetectors::BaseDetector>] All SmellDetectors that we want to filter for
25
+ # e.g. [Reek::SmellDetectors::Attribute].
26
+ def self.eligible_smell_types(filter_by_smells = [])
27
+ return smell_types if filter_by_smells.empty?
28
+ smell_types.select do |klass|
29
+ filter_by_smells.include? klass.smell_type
30
+ end
31
+ end
32
+
33
+ def initialize(smell_types: self.class.smell_types,
34
+ configuration: {})
35
+ @configuration = configuration
36
+ @smell_types = smell_types
37
+ @detectors = smell_types.map { |klass| klass.new configuration_for(klass) }
38
+ end
39
+
40
+ def examine(context)
41
+ smell_detectors_for(context.type).flat_map do |detector|
42
+ detector.run_for(context)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :configuration, :smell_types, :detectors
49
+
50
+ def configuration_for(klass)
51
+ configuration.fetch klass, {}
52
+ end
53
+
54
+ def smell_detectors_for(type)
55
+ enabled_detectors.select do |detector|
56
+ detector.contexts.include? type
57
+ end
58
+ end
59
+
60
+ def enabled_detectors
61
+ detectors.select { |detector| detector.config.enabled? }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'base_error'
3
+
4
+ module Reek
5
+ module Errors
6
+ # Gets raised when trying to configure a detector with an option
7
+ # which is unknown to it.
8
+ class BadDetectorConfigurationKeyInCommentError < BaseError
9
+ UNKNOWN_SMELL_DETECTOR_MESSAGE = <<-EOS.freeze
10
+
11
+ Error: You are trying to configure the smell detector '%s'
12
+ in one of your source code comments with the unknown option %s.
13
+ The source is '%s' and the comment belongs to the expression starting in line %d.
14
+ Here's the original comment:
15
+
16
+ %s
17
+
18
+ Please see the Reek docs for:
19
+ * how to configure Reek via source code comments: https://github.com/troessner/reek/blob/master/docs/Smell-Suppression.md
20
+ * what basic options are available: https://github.com/troessner/reek/blob/master/docs/Basic-Smell-Options.md
21
+ * what custom options are available by checking the detector specific documentation in /docs
22
+ Update the offensive comment (or remove it if no longer applicable) and re-run Reek.
23
+
24
+ EOS
25
+
26
+ def initialize(detector_name:, offensive_keys:, source:, line:, original_comment:)
27
+ message = format(UNKNOWN_SMELL_DETECTOR_MESSAGE,
28
+ detector_name,
29
+ offensive_keys,
30
+ source,
31
+ line,
32
+ original_comment)
33
+ super message
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
+ require_relative 'base_error'
2
3
 
3
4
  module Reek
4
5
  module Errors
5
6
  # Gets raised when trying to configure a detector which is unknown to us.
6
7
  # This might happen for multiple reasons. The users might have a typo in
7
8
  # his comment or he might use a detector that does not exist anymore.
8
- class BadDetectorInCommentError < RuntimeError
9
+ class BadDetectorInCommentError < BaseError
9
10
  UNKNOWN_SMELL_DETECTOR_MESSAGE = <<-EOS.freeze
10
11
 
11
12
  Error: You are trying to configure an unknown smell detector '%s' in one
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reek
4
+ module Errors
5
+ # Base class for all runtime Reek errors
6
+ class BaseError < ::RuntimeError
7
+ end
8
+ end
9
+ end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ require_relative 'base_error'
2
3
 
3
4
  module Reek
4
5
  module Errors
5
6
  # Gets raised when trying to use a configuration for a detector
6
7
  # that can't be parsed into a hash.
7
- class GarbageDetectorConfigurationInCommentError < RuntimeError
8
+ class GarbageDetectorConfigurationInCommentError < BaseError
8
9
  BAD_DETECTOR_CONFIGURATION_MESSAGE = <<-EOS.freeze
9
10
 
10
11
  Error: You are trying to configure the smell detector '%s'.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'base_error'
3
+
4
+ module Reek
5
+ module Errors
6
+ # Gets raised when Reek is unable to process the source
7
+ class IncomprehensibleSourceError < BaseError
8
+ INCOMPREHENSIBLE_SOURCE_TEMPLATE = <<-EOS.freeze
9
+ !!!
10
+ Source %s can not be processed by Reek.
11
+
12
+ This is most likely either a bug in your Reek configuration (config file or
13
+ source code comments) or a Reek bug.
14
+
15
+ Please double check your Reek configuration taking the original exception
16
+ below into account - you might have misspelled a smell detector for instance.
17
+ (In the future Reek will handle configuration errors more gracefully, something
18
+ we are working on already).
19
+
20
+ If you feel that this is not a problem with your Reek configuration but with
21
+ Reek itself it would be great if you could report this back to the Reek
22
+ team by opening up a corresponding issue at https://github.com/troessner/reek/issues.
23
+
24
+ Please make sure to include the source in question, the Reek version
25
+ and the original exception below.
26
+
27
+ Exception message:
28
+
29
+ %s
30
+
31
+ Original exception:
32
+
33
+ %s
34
+
35
+ !!!
36
+ EOS
37
+
38
+ def initialize(origin:, original_exception:)
39
+ message = format(INCOMPREHENSIBLE_SOURCE_TEMPLATE,
40
+ origin,
41
+ original_exception.message,
42
+ original_exception.backtrace.join("\n\t"))
43
+ super message
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'base_error'
3
+
4
+ module Reek
5
+ module Errors
6
+ # Gets raised when Reek is unable to process the source
7
+ class ParseError < BaseError
8
+ MESSAGE_TEMPLATE = '%s: %s: %s'.freeze
9
+
10
+ def initialize(origin:, original_exception:)
11
+ message = format(MESSAGE_TEMPLATE,
12
+ origin,
13
+ original_exception.class.name,
14
+ original_exception.message)
15
+ super message
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/reek/examiner.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative 'context_builder'
3
- require_relative 'errors/bad_detector_in_comment_error'
4
- require_relative 'errors/garbage_detector_configuration_in_comment_error'
5
- require_relative 'smell_detectors/detector_repository'
3
+ require_relative 'detector_repository'
4
+ require_relative 'errors/incomprehensible_source_error'
6
5
  require_relative 'source/source_code'
7
6
 
8
7
  module Reek
@@ -11,35 +10,13 @@ module Reek
11
10
  #
12
11
  # @public
13
12
  class Examiner
14
- INCOMPREHENSIBLE_SOURCE_TEMPLATE = <<-EOS.freeze
15
- !!!
16
- Source %s can not be processed by Reek.
17
-
18
- This is most likely either a bug in your Reek configuration (config file or
19
- source code comments) or a Reek bug.
20
-
21
- Please double check your Reek configuration taking the original exception
22
- below into account - you might have misspelled a smell detector for instance.
23
- (In the future Reek will handle configuration errors more gracefully, something
24
- we are working on already).
25
-
26
- If you feel that this is not a problem with your Reek configuration but with
27
- Reek itself it would be great if you could report this back to the Reek
28
- team by opening up a corresponding issue at https://github.com/troessner/reek/issues.
29
-
30
- Please make sure to include the source in question, the Reek version
31
- and the original exception below.
32
-
33
- Exception message:
34
-
35
- %s
36
-
37
- Original exception:
38
-
39
- %s
13
+ # Handles no errors
14
+ class NullHandler
15
+ def handle(_exception)
16
+ false
17
+ end
18
+ end
40
19
 
41
- !!!
42
- EOS
43
20
  #
44
21
  # Creates an Examiner which scans the given +source+ for code smells.
45
22
  #
@@ -57,11 +34,13 @@ module Reek
57
34
  def initialize(source,
58
35
  filter_by_smells: [],
59
36
  configuration: Configuration::AppConfiguration.default,
60
- detector_repository_class: SmellDetectors::DetectorRepository)
37
+ detector_repository_class: DetectorRepository,
38
+ error_handler: NullHandler.new)
61
39
  @source = Source::SourceCode.from(source)
62
40
  @smell_types = detector_repository_class.eligible_smell_types(filter_by_smells)
63
41
  @detector_repository = detector_repository_class.new(smell_types: @smell_types,
64
42
  configuration: configuration.directive_for(description))
43
+ @error_handler = error_handler
65
44
  end
66
45
 
67
46
  # @return [String] origin of the source being analysed
@@ -120,15 +99,8 @@ module Reek
120
99
  return [] unless syntax_tree
121
100
  begin
122
101
  examine_tree
123
- rescue Errors::BadDetectorInCommentError,
124
- Errors::GarbageDetectorConfigurationInCommentError => exception
125
- warn exception
126
- []
127
- rescue StandardError => exception
128
- warn format(INCOMPREHENSIBLE_SOURCE_TEMPLATE,
129
- origin,
130
- exception.message,
131
- exception.backtrace.join("\n\t"))
102
+ rescue Errors::BaseError => exception
103
+ raise unless @error_handler.handle exception
132
104
  []
133
105
  end
134
106
  end
@@ -141,6 +113,10 @@ module Reek
141
113
  ContextBuilder.new(syntax_tree).context_tree.flat_map do |element|
142
114
  detector_repository.examine(element)
143
115
  end
116
+ rescue Errors::BaseError
117
+ raise
118
+ rescue StandardError => exception
119
+ raise Errors::IncomprehensibleSourceError, origin: origin, original_exception: exception
144
120
  end
145
121
  end
146
122
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'errors/bad_detector_configuration_key_in_comment_error'
3
+ require_relative 'errors/bad_detector_in_comment_error'
4
+ require_relative 'errors/garbage_detector_configuration_in_comment_error'
5
+ require_relative 'errors/incomprehensible_source_error'
6
+
7
+ module Reek
8
+ # Handles errors by logging to stderr
9
+ class LoggingErrorHandler
10
+ def handle(exception)
11
+ warn exception
12
+ true
13
+ end
14
+ end
15
+ end
@@ -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 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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ module Reek
3
+ #
4
+ # Represents a single set of configuration options for a smell detector
5
+ #
6
+ class SmellConfiguration
7
+ # The name of the config field that specifies whether a smell is
8
+ # enabled. Set to +true+ or +false+.
9
+ ENABLED_KEY = 'enabled'.freeze
10
+
11
+ # The name of the config field that sets scope-specific overrides
12
+ # for other values in the current smell detector's configuration.
13
+ OVERRIDES_KEY = 'overrides'.freeze
14
+
15
+ def initialize(hash)
16
+ @options = hash
17
+ end
18
+
19
+ def merge(new_options)
20
+ options.merge!(new_options)
21
+ end
22
+
23
+ def enabled?
24
+ options[ENABLED_KEY]
25
+ end
26
+
27
+ def overrides_for(context)
28
+ Overrides.new(options.fetch(OVERRIDES_KEY, {})).for_context(context)
29
+ end
30
+
31
+ # Retrieves the value, if any, for the given +key+ in the given +context+.
32
+ #
33
+ # Raises an error if neither the context nor this config have a value for
34
+ # the key.
35
+ #
36
+ def value(key, context)
37
+ overrides_for(context).each { |conf| return conf[key] if conf.key?(key) }
38
+ options.fetch(key)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :options
44
+ end
45
+
46
+ #
47
+ # A set of context-specific overrides for smell detectors.
48
+ #
49
+ class Overrides
50
+ def initialize(hash)
51
+ @hash = hash
52
+ end
53
+
54
+ # Find any overrides that match the supplied context
55
+ def for_context(context)
56
+ contexts = hash.keys.select { |ckey| context.matches?([ckey]) }
57
+ contexts.map { |exc| hash[exc] }
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :hash
63
+ end
64
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require_relative 'smell_configuration'
3
2
  require_relative 'base_detector'
4
- require_relative 'smell_warning'
5
3
 
6
4
  module Reek
7
5
  module SmellDetectors
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require 'set'
3
- require_relative 'smell_configuration'
3
+ require_relative '../smell_warning'
4
+ require_relative '../smell_configuration'
4
5
 
5
6
  module Reek
6
7
  module SmellDetectors
@@ -14,6 +15,7 @@ module Reek
14
15
  # for details.
15
16
  #
16
17
  # :reek:UnusedPrivateMethod: { exclude: [ smell_warning ] }
18
+ # :reek:TooManyMethods: { max_methods: 18 }
17
19
  class BaseDetector
18
20
  attr_reader :config
19
21
  # The name of the config field that lists the names of code contexts
@@ -105,9 +107,9 @@ module Reek
105
107
  # Returns all descendants of BaseDetector
106
108
  #
107
109
  # @return [Array<Constant>], e.g.:
108
- # [Reek::Smells::Attribute,
109
- # Reek::Smells::BooleanParameter,
110
- # Reek::Smells::ClassVariable,
110
+ # [Reek::SmellDetectors::Attribute,
111
+ # Reek::SmellDetectors::BooleanParameter,
112
+ # Reek::SmellDetectors::ClassVariable,
111
113
  # ...]
112
114
  #
113
115
  def descendants
@@ -122,6 +124,24 @@ module Reek
122
124
  descendants.map { |descendant| descendant.to_s.split('::').last }.
123
125
  include?(detector)
124
126
  end
127
+
128
+ #
129
+ # Transform a detector name to the corresponding constant.
130
+ # Note that we assume a valid name - exceptions are not handled here.
131
+ #
132
+ # @param detector_name [String] the detector in question, e.g. 'DuplicateMethodCall'
133
+ # @return [SmellDetector] - this will return the class, not an instance
134
+ #
135
+ def to_detector(detector_name)
136
+ SmellDetectors.const_get detector_name
137
+ end
138
+
139
+ #
140
+ # @return [Set<Symbol>] - all configuration keys that are available for this detector
141
+ #
142
+ def configuration_keys
143
+ Set.new(default_config.keys.map(&:to_sym))
144
+ end
125
145
  end
126
146
  end
127
147
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative 'base_detector'
3
- require_relative 'smell_warning'
4
3
 
5
4
  module Reek
6
5
  module SmellDetectors
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'set'
3
3
  require_relative 'base_detector'
4
- require_relative 'smell_warning'
5
4
 
6
5
  module Reek
7
6
  module SmellDetectors