reek 5.0.2 → 5.1.0

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