reek 5.0.2 → 5.1.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -70
  3. data/.rubocop_todo.yml +63 -0
  4. data/.simplecov +4 -1
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +12 -17
  7. data/README.md +21 -29
  8. data/Rakefile +2 -2
  9. data/docs/templates/default/docstring/setup.rb +3 -0
  10. data/features/command_line_interface/options.feature +5 -3
  11. data/features/configuration_files/schema_validation.feature +3 -3
  12. data/features/configuration_files/show_configuration_file.feature +44 -0
  13. data/features/rake_task/rake_task.feature +1 -1
  14. data/features/reports/json.feature +3 -3
  15. data/features/reports/reports.feature +4 -4
  16. data/features/reports/yaml.feature +3 -3
  17. data/features/rspec_matcher.feature +1 -0
  18. data/features/step_definitions/sample_file_steps.rb +6 -8
  19. data/features/todo_list.feature +39 -26
  20. data/lib/reek.rb +7 -0
  21. data/lib/reek/ast/node.rb +4 -0
  22. data/lib/reek/ast/sexp_extensions/if.rb +20 -0
  23. data/lib/reek/ast/sexp_extensions/methods.rb +1 -0
  24. data/lib/reek/cli/application.rb +25 -0
  25. data/lib/reek/cli/command/todo_list_command.rb +17 -7
  26. data/lib/reek/cli/options.rb +21 -14
  27. data/lib/reek/code_comment.rb +2 -0
  28. data/lib/reek/configuration/app_configuration.rb +0 -3
  29. data/lib/reek/configuration/configuration_converter.rb +4 -4
  30. data/lib/reek/configuration/directory_directives.rb +1 -0
  31. data/lib/reek/configuration/schema_validator.rb +1 -0
  32. data/lib/reek/context/method_context.rb +1 -0
  33. data/lib/reek/context/module_context.rb +5 -4
  34. data/lib/reek/context/visibility_tracker.rb +7 -4
  35. data/lib/reek/context_builder.rb +1 -0
  36. data/lib/reek/detector_repository.rb +1 -0
  37. data/lib/reek/errors/incomprehensible_source_error.rb +2 -2
  38. data/lib/reek/errors/syntax_error.rb +4 -0
  39. data/lib/reek/examiner.rb +1 -0
  40. data/lib/reek/report/text_report.rb +1 -0
  41. data/lib/reek/smell_detectors/control_parameter.rb +13 -107
  42. data/lib/reek/smell_detectors/control_parameter_helpers/call_in_condition_finder.rb +91 -0
  43. data/lib/reek/smell_detectors/control_parameter_helpers/candidate.rb +38 -0
  44. data/lib/reek/smell_detectors/control_parameter_helpers/control_parameter_finder.rb +94 -0
  45. data/lib/reek/smell_detectors/duplicate_method_call.rb +1 -0
  46. data/lib/reek/smell_detectors/feature_envy.rb +2 -0
  47. data/lib/reek/smell_detectors/irresponsible_module.rb +1 -0
  48. data/lib/reek/smell_detectors/long_parameter_list.rb +1 -0
  49. data/lib/reek/smell_detectors/manual_dispatch.rb +1 -0
  50. data/lib/reek/smell_detectors/missing_safe_method.rb +1 -0
  51. data/lib/reek/smell_detectors/nested_iterators.rb +1 -0
  52. data/lib/reek/smell_detectors/repeated_conditional.rb +1 -0
  53. data/lib/reek/smell_detectors/too_many_instance_variables.rb +1 -0
  54. data/lib/reek/smell_detectors/too_many_methods.rb +1 -0
  55. data/lib/reek/smell_detectors/too_many_statements.rb +1 -0
  56. data/lib/reek/smell_detectors/uncommunicative_variable_name.rb +2 -0
  57. data/lib/reek/smell_detectors/unused_parameters.rb +1 -0
  58. data/lib/reek/source/source_locator.rb +1 -0
  59. data/lib/reek/spec/should_reek_of.rb +2 -2
  60. data/lib/reek/spec/should_reek_only_of.rb +1 -0
  61. data/lib/reek/spec/smell_matcher.rb +1 -0
  62. data/lib/reek/tree_dresser.rb +1 -0
  63. data/lib/reek/version.rb +1 -1
  64. data/samples/smelly_source/ruby.rb +368 -0
  65. data/spec/factories/factories.rb +10 -9
  66. data/spec/performance/reek/smell_detectors/runtime_speed_spec.rb +17 -0
  67. data/spec/quality/documentation_spec.rb +40 -0
  68. data/spec/reek/ast/sexp_extensions_spec.rb +20 -20
  69. data/spec/reek/cli/application_spec.rb +29 -0
  70. data/spec/reek/cli/command/todo_list_command_spec.rb +64 -46
  71. data/spec/reek/configuration/app_configuration_spec.rb +8 -8
  72. data/spec/reek/configuration/configuration_file_finder_spec.rb +3 -3
  73. data/spec/reek/configuration/schema_validator_spec.rb +10 -10
  74. data/spec/reek/detector_repository_spec.rb +2 -2
  75. data/spec/reek/smell_detectors/control_parameter_spec.rb +17 -0
  76. data/spec/reek/source/source_locator_spec.rb +0 -2
  77. data/spec/spec_helper.rb +2 -0
  78. data/tasks/configuration.rake +2 -1
  79. data/tasks/test.rake +4 -0
  80. metadata +11 -5
  81. data/ataru_setup.rb +0 -13
  82. data/tasks/ataru.rake +0 -5
@@ -122,6 +122,7 @@ module Reek
122
122
 
123
123
  def escalate_bad_detector
124
124
  return if SmellDetectors::BaseDetector.valid_detector?(detector_name)
125
+
125
126
  raise Errors::BadDetectorInCommentError, detector_name: detector_name,
126
127
  original_comment: original_comment,
127
128
  source: source,
@@ -142,6 +143,7 @@ module Reek
142
143
  @detector_class = SmellDetectors::BaseDetector.to_detector(detector_name)
143
144
 
144
145
  return if given_keys_legit?
146
+
145
147
  raise Errors::BadDetectorConfigurationKeyInCommentError, detector_name: detector_name,
146
148
  offensive_keys: configuration_keys_difference,
147
149
  original_comment: original_comment,
@@ -15,9 +15,6 @@ module Reek
15
15
  # @public
16
16
  class AppConfiguration
17
17
  include ConfigurationValidator
18
- EXCLUDE_PATHS_KEY = 'exclude_paths'
19
- DIRECTORIES_KEY = 'directories'
20
- DETECTORS_KEY = 'detectors'
21
18
 
22
19
  # Instantiate a configuration via the given path.
23
20
  #
@@ -71,9 +71,9 @@ module Reek
71
71
  # @quality :reek:NestedIterators { max_allowed_nesting: 3 }
72
72
  # @quality :reek:TooManyStatements { max_statements: 6 }
73
73
  def strings_to_regexes_for_detectors
74
- return unless configuration[AppConfiguration::DETECTORS_KEY]
74
+ return unless configuration[DETECTORS_KEY]
75
75
 
76
- configuration[AppConfiguration::DETECTORS_KEY].tap do |detectors|
76
+ configuration[DETECTORS_KEY].tap do |detectors|
77
77
  detectors.keys.each do |detector|
78
78
  convertible_attributes(detectors[detector]).each do |attribute|
79
79
  detectors[detector][attribute] = detectors[detector][attribute].map do |item|
@@ -91,9 +91,9 @@ module Reek
91
91
  # @quality :reek:NestedIterators { max_allowed_nesting: 4 }
92
92
  # @quality :reek:TooManyStatements { max_statements: 7 }
93
93
  def strings_to_regexes_for_directories
94
- return unless configuration[AppConfiguration::DIRECTORIES_KEY]
94
+ return unless configuration[DIRECTORIES_KEY]
95
95
 
96
- configuration[AppConfiguration::DIRECTORIES_KEY].tap do |directories|
96
+ configuration[DIRECTORIES_KEY].tap do |directories|
97
97
  directories.keys.each do |directory|
98
98
  directories[directory].each do |detector, configuration|
99
99
  convertible_attributes(configuration).each do |attribute|
@@ -17,6 +17,7 @@ module Reek
17
17
  # @return [Hash | nil] the configuration for the source or nil
18
18
  def directive_for(source_via)
19
19
  return unless source_via
20
+
20
21
  source_base_dir = Pathname.new(source_via).dirname
21
22
  hit = best_match_for source_base_dir
22
23
  self[hit]
@@ -24,6 +24,7 @@ module Reek
24
24
  def validate
25
25
  errors = CLI::Silencer.without_warnings { @validator.validate @configuration }
26
26
  return if !errors || errors.empty?
27
+
27
28
  raise Errors::ConfigFileError, error_message(errors)
28
29
  end
29
30
 
@@ -81,6 +81,7 @@ module Reek
81
81
  own = super
82
82
  return own unless own.empty?
83
83
  return parent_exp.full_comment if parent_exp
84
+
84
85
  ''
85
86
  end
86
87
 
@@ -26,7 +26,7 @@ module Reek
26
26
  #
27
27
  # @param child [CodeContext] the child context to register
28
28
  def append_child_context(child)
29
- visibility_tracker.set_child_visibility(child)
29
+ visibility_tracker.apply_visibility(child)
30
30
  super
31
31
  end
32
32
 
@@ -44,9 +44,9 @@ module Reek
44
44
  end
45
45
 
46
46
  def defined_instance_methods(visibility: :any)
47
- instance_method_children.select do |context|
48
- visibility == :any || context.visibility == visibility
49
- end
47
+ return instance_method_children if visibility == :any
48
+
49
+ instance_method_children.select { |child| child.visibility == visibility }
50
50
  end
51
51
 
52
52
  def instance_method_calls
@@ -76,6 +76,7 @@ module Reek
76
76
  # @quality :reek:FeatureEnvy
77
77
  def namespace_module?
78
78
  return false if exp.type == :casgn
79
+
79
80
  children = exp.direct_children
80
81
  children.any? && children.all? { |child| [:casgn, :class, :module].include? child.type }
81
82
  end
@@ -22,6 +22,7 @@ module Reek
22
22
  #
23
23
  def track_visibility(children:, visibility:, names:)
24
24
  return unless VISIBILITY_MODIFIERS.include? visibility
25
+
25
26
  if names.any?
26
27
  children.each do |child|
27
28
  child.visibility = visibility if names.include?(child.name)
@@ -44,17 +45,19 @@ module Reek
44
45
  #
45
46
  def track_singleton_visibility(children:, visibility:, names:)
46
47
  return if names.empty?
48
+
47
49
  visibility = VISIBILITY_MAP[visibility]
48
50
  return unless visibility
51
+
49
52
  track_visibility children: children, visibility: visibility, names: names
50
53
  end
51
54
 
52
- # Sets the visibility of a child CodeContext to the tracked visibility.
55
+ # Sets the visibility of a CodeContext to the tracked visibility.
53
56
  #
54
- # @param child [CodeContext]
57
+ # @param context [CodeContext]
55
58
  #
56
- def set_child_visibility(child)
57
- child.apply_current_visibility tracked_visibility
59
+ def apply_visibility(context)
60
+ context.apply_current_visibility tracked_visibility
58
61
  end
59
62
 
60
63
  private
@@ -521,6 +521,7 @@ module Reek
521
521
 
522
522
  def register_attributes(exp)
523
523
  return unless exp.attribute_writer?
524
+
524
525
  klass = current_context.attribute_context_class
525
526
  exp.args.each do |arg|
526
527
  append_new_context(klass, arg, exp)
@@ -26,6 +26,7 @@ module Reek
26
26
  # e.g. [Reek::SmellDetectors::Attribute].
27
27
  def self.eligible_smell_types(filter_by_smells = [])
28
28
  return smell_types if filter_by_smells.empty?
29
+
29
30
  smell_types.select do |klass|
30
31
  filter_by_smells.include? klass.smell_type
31
32
  end
@@ -17,8 +17,8 @@ module Reek
17
17
  It would be great if you could report this back to the Reek team by opening a
18
18
  corresponding issue at https://github.com/troessner/reek/issues.
19
19
 
20
- Please make sure to include the source in question, the Reek version and the
21
- original exception below.
20
+ Please make sure to include the source in question, the Reek version,
21
+ and this entire error message, including the original exception below.
22
22
 
23
23
  Exception message:
24
24
 
@@ -15,6 +15,10 @@ module Reek
15
15
  This is a problem that is outside of Reek's scope and should be fixed by you, the
16
16
  user, in order for Reek being able to continue.
17
17
 
18
+ Things you can try:
19
+ - Check the syntax of the problematic file
20
+ - If the file is not in fact a Ruby file, exclude it in your .reek.yml file
21
+
18
22
  Exception message:
19
23
 
20
24
  %<exception>s
@@ -95,6 +95,7 @@ module Reek
95
95
  end
96
96
  rescue StandardError => exception
97
97
  raise unless @error_handler.handle exception
98
+
98
99
  []
99
100
  end
100
101
 
@@ -43,6 +43,7 @@ module Reek
43
43
 
44
44
  def display_total_smell_count
45
45
  return unless examiners.size > 1
46
+
46
47
  print total_smell_count_message
47
48
  end
48
49
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base_detector'
4
+ require_relative 'control_parameter_helpers/candidate'
5
+ require_relative 'control_parameter_helpers/control_parameter_finder'
4
6
 
5
7
  module Reek
6
8
  module SmellDetectors
@@ -61,125 +63,29 @@ module Reek
61
63
 
62
64
  private
63
65
 
66
+ #
67
+ # @return [Array<ControlParameterHelpers::Candidate>]
68
+ #
64
69
  def control_parameters
65
70
  potential_parameters.
66
- map { |param| FoundControlParameter.new(param, find_matches(param)) }.
71
+ map { |parameter| ControlParameterHelpers::Candidate.new(parameter, find_matches(parameter)) }.
67
72
  select(&:smells?)
68
73
  end
69
74
 
75
+ #
76
+ # @return [Array<Symbol>] e.g. [:bravo, :charlie]
77
+ #
70
78
  def potential_parameters
71
79
  expression.parameter_names
72
80
  end
73
81
 
74
- def find_matches(param)
75
- ControlParameterFinder.new(expression, param).find_matches
76
- end
77
-
78
82
  #
79
- # Collects information about a single control parameter.
83
+ # @param parameter [Symbol] the name of the parameter
84
+ # @return [Array<Reek::AST::Node>]
80
85
  #
81
- class FoundControlParameter
82
- def initialize(param, occurences)
83
- @param = param
84
- @occurences = occurences
85
- end
86
-
87
- def smells?
88
- occurences.any?
89
- end
90
-
91
- def lines
92
- occurences.map(&:line)
93
- end
94
-
95
- def name
96
- param.to_s
97
- end
98
-
99
- private
100
-
101
- attr_reader :occurences, :param
86
+ def find_matches(parameter)
87
+ ControlParameterHelpers::ControlParameterFinder.new(expression, parameter).find_matches
102
88
  end
103
-
104
- private_constant :FoundControlParameter
105
-
106
- # Finds cases of ControlParameter in a particular node for a particular parameter
107
- class ControlParameterFinder
108
- CONDITIONAL_NODE_TYPES = [:if, :case, :and, :or].freeze
109
-
110
- def initialize(node, param)
111
- @node = node
112
- @param = param
113
- end
114
-
115
- def find_matches
116
- return [] if legitimite_uses?
117
- nested_finders.flat_map(&:find_matches) + uses_of_param_in_condition
118
- end
119
-
120
- def legitimite_uses?
121
- return true if uses_param_in_body?
122
- return true if uses_param_in_call_in_condition?
123
- return true if nested_finders.any?(&:legitimite_uses?)
124
- false
125
- end
126
-
127
- private
128
-
129
- attr_reader :node, :param
130
-
131
- def conditional_nodes
132
- node.body_nodes(CONDITIONAL_NODE_TYPES)
133
- end
134
-
135
- def nested_finders
136
- @nested_finders ||= conditional_nodes.flat_map do |node|
137
- self.class.new(node, param)
138
- end
139
- end
140
-
141
- def uses_param_in_call_in_condition?
142
- return unless condition
143
- condition.each_node(:send) do |inner|
144
- next unless regular_call_involving_param? inner
145
- return true
146
- end
147
- false
148
- end
149
-
150
- def uses_of_param_in_condition
151
- return [] unless condition
152
- condition.each_node(:lvar).select { |inner| inner.var_name == param }
153
- end
154
-
155
- def condition
156
- return nil unless CONDITIONAL_NODE_TYPES.include? node.type
157
- node.condition
158
- end
159
-
160
- def regular_call_involving_param?(call_node)
161
- call_involving_param?(call_node) && !comparison_call?(call_node)
162
- end
163
-
164
- def comparison_call?(call_node)
165
- comparison_method_names.include? call_node.name
166
- end
167
-
168
- def comparison_method_names
169
- [:==, :!=, :=~]
170
- end
171
-
172
- def call_involving_param?(call_node)
173
- call_node.each_node(:lvar).any? { |it| it.var_name == param }
174
- end
175
-
176
- def uses_param_in_body?
177
- nodes = node.body_nodes([:lvar], CONDITIONAL_NODE_TYPES)
178
- nodes.any? { |lvar_node| lvar_node.var_name == param }
179
- end
180
- end
181
-
182
- private_constant :ControlParameterFinder
183
89
  end
184
90
  end
185
91
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reek
4
+ module SmellDetectors
5
+ module ControlParameterHelpers
6
+ #
7
+ # CallInConditionFinder finds usages of the given parameter
8
+ # in the context of a method call in a condition, e.g.:
9
+ #
10
+ # def alfa(bravo)
11
+ # if charlie(bravo)
12
+ # delta
13
+ # end
14
+ # end
15
+ #
16
+ # or
17
+ #
18
+ # def alfa(bravo)
19
+ # if bravo.charlie?
20
+ # delta
21
+ # end
22
+ # end
23
+ #
24
+ # Those usages are legit and should not trigger the ControlParameter smell warning.
25
+ #
26
+ class CallInConditionFinder
27
+ COMPARISON_METHOD_NAMES = [:==, :!=, :=~].freeze
28
+
29
+ #
30
+ # @param node [Reek::AST::Node] the node in our current scope,
31
+ # e.g. s(:def, :alfa,
32
+ # s(:args,
33
+ # s(:arg, :bravo),
34
+ # @param parameter [Symbol] the parameter name in question
35
+ # e.g. in the example above this would be :bravo
36
+ #
37
+ def initialize(node, parameter)
38
+ @node = node
39
+ @parameter = parameter
40
+ end
41
+
42
+ #
43
+ # @return [Boolean] if the parameter in question has been used in the context of a
44
+ # method call in a condition
45
+ #
46
+ def uses_param_in_call_in_condition?
47
+ condition = node.condition
48
+ return false unless condition
49
+
50
+ condition.each_node(:send) do |inner|
51
+ return true if regular_call_involving_param?(inner)
52
+ end
53
+ false
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :node, :parameter
59
+
60
+ #
61
+ # @return [Boolean] if the parameter is used in a method call that is not a comparison call
62
+ # e.g. this would return true given that "bravo" is the parameter in question:
63
+ #
64
+ # if charlie(bravo) then delta end
65
+ #
66
+ # while this would return false (since its a comparison):
67
+ #
68
+ # if bravo == charlie then charlie end
69
+ #
70
+ def regular_call_involving_param?(call_node)
71
+ call_involving_param?(call_node) && !comparison_call?(call_node)
72
+ end
73
+
74
+ #
75
+ # @return [Boolean] if the parameter is used in the given method call
76
+ #
77
+ def call_involving_param?(call_node)
78
+ call_node.each_node(:lvar).any? { |it| it.var_name == parameter }
79
+ end
80
+
81
+ #
82
+ # @return [Boolean] if the given method call is a comparison call
83
+ #
84
+ # :reek:UtilityFunction
85
+ def comparison_call?(call_node)
86
+ COMPARISON_METHOD_NAMES.include? call_node.name
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reek
4
+ module SmellDetectors
5
+ module ControlParameterHelpers
6
+ #
7
+ # Collects information about a single control parameter.
8
+ #
9
+ class Candidate
10
+ #
11
+ # @param parameter [Symbol] the parameter name
12
+ # @param occurences [Array<Reek::AST::Node>] the occurences of the ControlParameter smell
13
+ # e.g. [s(:lvar, :bravo), s(:lvar, :bravo)]
14
+ #
15
+ def initialize(parameter, occurences)
16
+ @parameter = parameter
17
+ @occurences = occurences
18
+ end
19
+
20
+ def smells?
21
+ occurences.any?
22
+ end
23
+
24
+ def lines
25
+ occurences.map(&:line)
26
+ end
27
+
28
+ def name
29
+ parameter.to_s
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :occurences, :parameter
35
+ end
36
+ end
37
+ end
38
+ end