rubocop 1.88.2 → 1.89.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +67 -11
  3. data/lib/rubocop/comment_config.rb +35 -19
  4. data/lib/rubocop/config.rb +53 -0
  5. data/lib/rubocop/config_validator.rb +38 -1
  6. data/lib/rubocop/cop/badge.rb +8 -0
  7. data/lib/rubocop/cop/base.rb +6 -4
  8. data/lib/rubocop/cop/bundler.rb +19 -0
  9. data/lib/rubocop/cop/corrector.rb +1 -1
  10. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  11. data/lib/rubocop/cop/gemspec.rb +22 -0
  12. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  13. data/lib/rubocop/cop/generator.rb +4 -0
  14. data/lib/rubocop/cop/layout/class_structure.rb +8 -2
  15. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  16. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  17. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  18. data/lib/rubocop/cop/layout/line_length.rb +85 -12
  19. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  20. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  21. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
  22. data/lib/rubocop/cop/layout.rb +114 -0
  23. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  24. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  25. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  26. data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
  27. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  28. data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
  29. data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
  30. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  31. data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
  32. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
  33. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  34. data/lib/rubocop/cop/lint/name_typo.rb +223 -0
  35. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
  36. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
  37. data/lib/rubocop/cop/lint/to_enum_arguments.rb +37 -1
  38. data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
  39. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  40. data/lib/rubocop/cop/lint/void.rb +17 -6
  41. data/lib/rubocop/cop/lint.rb +176 -0
  42. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  43. data/lib/rubocop/cop/metrics.rb +31 -0
  44. data/lib/rubocop/cop/migration/department_name.rb +0 -4
  45. data/lib/rubocop/cop/migration.rb +13 -0
  46. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  47. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  48. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  49. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  50. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  51. data/lib/rubocop/cop/mixin/statement_modifier.rb +42 -6
  52. data/lib/rubocop/cop/mixin.rb +78 -77
  53. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  54. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  55. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  56. data/lib/rubocop/cop/naming.rb +31 -0
  57. data/lib/rubocop/cop/offense.rb +10 -6
  58. data/lib/rubocop/cop/registry.rb +153 -43
  59. data/lib/rubocop/cop/security.rb +19 -0
  60. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  61. data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
  62. data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
  63. data/lib/rubocop/cop/style/copyright.rb +6 -1
  64. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
  65. data/lib/rubocop/cop/style/documentation.rb +47 -0
  66. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  67. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -48
  68. data/lib/rubocop/cop/style/lambda.rb +4 -0
  69. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
  70. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  71. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  72. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  73. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  74. data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
  75. data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
  76. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  77. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  78. data/lib/rubocop/cop/style/redundant_format.rb +12 -3
  79. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  80. data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
  81. data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
  82. data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
  83. data/lib/rubocop/cop/style/static_class.rb +21 -0
  84. data/lib/rubocop/cop/style.rb +310 -0
  85. data/lib/rubocop/cop/team.rb +48 -3
  86. data/lib/rubocop/cop/variable_force/variable.rb +18 -6
  87. data/lib/rubocop/cops_documentation_generator.rb +1 -1
  88. data/lib/rubocop/directive_comment.rb +2 -0
  89. data/lib/rubocop/feature_loader.rb +0 -6
  90. data/lib/rubocop/lsp/routes.rb +41 -3
  91. data/lib/rubocop/lsp/runtime.rb +4 -0
  92. data/lib/rubocop/lsp/server.rb +4 -0
  93. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  94. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  95. data/lib/rubocop/project_index_loader.rb +16 -0
  96. data/lib/rubocop/result_cache.rb +6 -4
  97. data/lib/rubocop/rspec/shared_contexts.rb +14 -2
  98. data/lib/rubocop/runner.rb +45 -20
  99. data/lib/rubocop/server/cache.rb +10 -5
  100. data/lib/rubocop/target_ruby.rb +1 -1
  101. data/lib/rubocop/version.rb +1 -1
  102. data/lib/rubocop.rb +17 -626
  103. metadata +18 -3
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'did_you_mean'
5
+ rescue LoadError
6
+ nil
7
+ end
8
+
9
+ module RuboCop
10
+ module Cop
11
+ module Lint
12
+ # Checks for probable typos in constant and method names: a name that
13
+ # does not resolve anywhere in the project, used in a namespace the
14
+ # project does define, with a close-named sibling to suggest instead.
15
+ #
16
+ # The check is powered by the project-wide index, so it only runs when
17
+ # `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed.
18
+ # Without the index the cop does nothing.
19
+ #
20
+ # Constants are checked only in qualified references (`Foo::Bar`) whose
21
+ # namespace resolves in the index; bare names cannot be distinguished
22
+ # from constants provided by gems or the standard library. Methods are
23
+ # checked only in calls on constant receivers (`Foo.bar`) whose entire
24
+ # indexed ancestry is resolved, so methods gained through gem classes or
25
+ # dynamic definitions never produce offenses. In both cases an offense
26
+ # requires a similarly named alternative to exist — an unknown name
27
+ # alone is not reported, since the index does not see gems or the
28
+ # standard library.
29
+ #
30
+ # Names that appear as symbols or inside string literals in the same
31
+ # file are never reported, since they usually belong to runtime
32
+ # definitions the index cannot see (`stub_const`, `const_set`,
33
+ # `define_method`, and the like).
34
+ #
35
+ # @example
36
+ # # bad - Services::UserCraetor is not defined, Services::UserCreator is
37
+ # Services::UserCraetor.new
38
+ #
39
+ # # good
40
+ # Services::UserCreator.new
41
+ #
42
+ # # bad - Report.generate_sumary is not defined, Report.generate_summary is
43
+ # Report.generate_sumary
44
+ #
45
+ # # good
46
+ # Report.generate_summary
47
+ #
48
+ # @example CheckConstants: false
49
+ # # good - constant references are not checked
50
+ # Services::UserCraetor.new
51
+ #
52
+ # @example CheckMethods: false
53
+ # # good - method calls are not checked
54
+ # Report.generate_sumary
55
+ #
56
+ # @example AllowedNames: ['generate_sumary']
57
+ # # good - the name is explicitly allowed
58
+ # Report.generate_sumary
59
+ #
60
+ class NameTypo < Base
61
+ include ProjectIndexHelp
62
+
63
+ CONSTANT_MSG = 'Possible typo: `%<name>s` is not defined in `%<namespace>s`. ' \
64
+ 'Did you mean `%<suggestion>s`?'
65
+ METHOD_MSG = 'Possible typo: `%<receiver>s` does not respond to `%<name>s`. ' \
66
+ 'Did you mean `%<suggestion>s`?'
67
+
68
+ METHOD_MEMBER_REGEXP = /#([a-zA-Z_]\w*[?!]?)\(\)\z/.freeze
69
+ LITERAL_IDENTIFIER_PATTERN = /[a-zA-Z_]\w*[?!]?/.freeze
70
+
71
+ def on_const(node)
72
+ return unless check?('CheckConstants') && checkable_constant?(node)
73
+
74
+ suggestion = constant_typo_suggestion(node)
75
+ return unless suggestion
76
+
77
+ message = format(CONSTANT_MSG, name: node.short_name,
78
+ namespace: node.namespace.const_name,
79
+ suggestion: suggestion)
80
+ add_offense(node.loc.name, message: message)
81
+ end
82
+
83
+ def on_send(node)
84
+ return unless check?('CheckMethods')
85
+ return unless node.receiver&.const_type?
86
+ return if allowed_name?(node.method_name) || defined_check?(node)
87
+
88
+ suggestion = method_typo_suggestion(node)
89
+ return unless suggestion
90
+
91
+ message = format(METHOD_MSG, receiver: node.receiver.const_name,
92
+ name: node.method_name,
93
+ suggestion: suggestion)
94
+ add_offense(node.loc.selector, message: message)
95
+ end
96
+ alias on_csend on_send
97
+
98
+ def on_new_investigation
99
+ @literal_names = nil
100
+ super
101
+ end
102
+
103
+ private
104
+
105
+ def check?(key)
106
+ project_index && defined?(DidYouMean::SpellChecker) && cop_config.fetch(key, true)
107
+ end
108
+
109
+ def checkable_constant?(node)
110
+ node.namespace&.const_type? &&
111
+ !allowed_name?(node.short_name) &&
112
+ !definition_identifier?(node) && !defined_check?(node)
113
+ end
114
+
115
+ def allowed_name?(name)
116
+ cop_config.fetch('AllowedNames', []).include?(name.to_s)
117
+ end
118
+
119
+ # The last segment of a class, module or constant definition is being
120
+ # defined, not referenced.
121
+ def definition_identifier?(node)
122
+ node.parent&.defined_module
123
+ end
124
+
125
+ # `defined?(Foo::Bar)` probes whether a name exists; unknown names
126
+ # there are deliberate.
127
+ def defined_check?(node)
128
+ node.each_ancestor(:defined?).any?
129
+ end
130
+
131
+ def constant_typo_suggestion(node)
132
+ return nil if literal_names.include?(node.short_name.to_s)
133
+
134
+ namespace = resolve_constant_in_index(node.namespace)
135
+ return nil unless namespace.is_a?(Rubydex::Namespace)
136
+ return nil if namespace.find_member(node.short_name.to_s)
137
+
138
+ spell_check(node.short_name, constant_member_names(namespace))
139
+ rescue StandardError
140
+ nil
141
+ end
142
+
143
+ # Writers are indexed under the reader's name, so setter calls are
144
+ # verified and spell-checked through the base name.
145
+ def method_typo_suggestion(node)
146
+ setter = setter_call?(node)
147
+ base = setter ? node.method_name.to_s.delete_suffix('=') : node.method_name.to_s
148
+ return nil if literal_names.include?(base)
149
+
150
+ declaration = unknown_method_owner(node, base)
151
+ return nil unless declaration
152
+
153
+ suggestion = spell_check(base, method_member_names(declaration))
154
+ suggestion && setter ? "#{suggestion}=" : suggestion
155
+ rescue StandardError
156
+ nil
157
+ end
158
+
159
+ # The receiver's declaration when it resolves in the index, its whole
160
+ # ancestry is resolved, and the method is not found — nil otherwise.
161
+ def unknown_method_owner(node, base)
162
+ declaration = resolve_constant_in_index(node.receiver)
163
+ return nil unless declaration.is_a?(Rubydex::Namespace)
164
+ return nil if responds_in_index?(declaration, node.method_name.to_s, base)
165
+ return nil unless fully_resolved_index_ancestry?(declaration)
166
+
167
+ declaration
168
+ end
169
+
170
+ def setter_call?(node)
171
+ node.assignment_method? && !node.operator_method?
172
+ end
173
+
174
+ def responds_in_index?(declaration, name, base)
175
+ # Instance methods of a module are also callable on the module
176
+ # itself when exposed with `module_function`.
177
+ [name, base].uniq.any? do |candidate|
178
+ member_name = "#{candidate}()"
179
+ indexed_singleton_member(declaration, member_name) ||
180
+ declaration.find_member(member_name)
181
+ end
182
+ end
183
+
184
+ # Names mentioned as symbols or inside string literals in the current
185
+ # file belong to runtime definitions (`stub_const`, `const_set`,
186
+ # `define_method`) that the index cannot see.
187
+ def literal_names
188
+ @literal_names ||=
189
+ processed_source.ast.each_descendant(:sym, :str)
190
+ .with_object(Set.new) do |literal, names|
191
+ if literal.sym_type?
192
+ names << literal.value.to_s
193
+ else
194
+ literal.value.scan(LITERAL_IDENTIFIER_PATTERN) { |token| names << token }
195
+ end
196
+ end
197
+ end
198
+
199
+ def constant_member_names(namespace)
200
+ namespace.members.filter_map do |member|
201
+ name = member.name
202
+ name.split('::').last unless name.include?('#') || name.include?('<')
203
+ end
204
+ end
205
+
206
+ def method_member_names(declaration)
207
+ scopes = declaration.ancestors.filter_map { |ancestor| indexed_singleton_of(ancestor) }
208
+ scopes << declaration
209
+
210
+ scopes.flat_map do |scope|
211
+ scope.members.filter_map { |member| member.name[METHOD_MEMBER_REGEXP, 1] }
212
+ end
213
+ end
214
+
215
+ def spell_check(name, dictionary)
216
+ return nil if dictionary.empty?
217
+
218
+ DidYouMean::SpellChecker.new(dictionary: dictionary.uniq).correct(name.to_s).first
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -119,10 +119,6 @@ module RuboCop
119
119
  def regexp_receiver?(send_node)
120
120
  send_node.receiver&.regexp_type?
121
121
  end
122
-
123
- def nth_ref_receiver?(send_node)
124
- send_node.receiver&.nth_ref_type?
125
- end
126
122
  end
127
123
  end
128
124
  end
@@ -9,6 +9,13 @@ module RuboCop
9
9
  # safe navigation operator after a safe navigation operator.
10
10
  # This cop checks for the problem outlined above.
11
11
  #
12
+ # @safety
13
+ # This cop's autocorrection is unsafe because it changes the behavior of
14
+ # the code: `x&.foo.bar` raises `NoMethodError` when `x` or `x&.foo` is `nil`,
15
+ # whereas the corrected `x&.foo&.bar` returns `nil` instead. The autocorrection
16
+ # also assumes that extending safe navigation through the chain is intended,
17
+ # while removing the first `&.` may be what was intended instead.
18
+ #
12
19
  # @example
13
20
  #
14
21
  # # bad
@@ -71,7 +71,9 @@ module RuboCop
71
71
  send_arg && argument_match?(send_arg, def_arg)
72
72
  end
73
73
 
74
- all_present && !extra_positional_arguments?(arguments, def_node)
74
+ all_present &&
75
+ !extra_positional_arguments?(arguments, def_node) &&
76
+ !extra_keyword_arguments?(arguments, def_node)
75
77
  end
76
78
 
77
79
  # The enumerator re-invokes the method with these arguments, so passing more
@@ -99,6 +101,40 @@ module RuboCop
99
101
  def_node.arguments.select { |arg| arg.type?(:arg, :optarg) }
100
102
  end
101
103
 
104
+ # The enumerator re-invokes the method with these arguments, so passing
105
+ # unexpected keyword arguments raises `ArgumentError` at runtime.
106
+ def extra_keyword_arguments?(arguments, def_node)
107
+ return false if variadic_keyword_parameters?(def_node)
108
+ return false if expandable_keyword_arguments?(arguments)
109
+
110
+ (passed_keyword_arguments(arguments) - keyword_parameters(def_node)).any?
111
+ end
112
+
113
+ def variadic_keyword_parameters?(def_node)
114
+ def_node.arguments.any? { |arg| arg.type?(:kwrestarg, :forward_arg) }
115
+ end
116
+
117
+ # A call-side keyword splat or argument forwarding can expand to any set of keywords.
118
+ def expandable_keyword_arguments?(arguments)
119
+ arguments.any? do |arg|
120
+ arg.forwarded_args_type? || arg.each_child_node(:kwsplat, :forwarded_kwrestarg).any?
121
+ end
122
+ end
123
+
124
+ def passed_keyword_arguments(arguments)
125
+ arguments.filter_map do |arg|
126
+ next unless arg.hash_type?
127
+
128
+ arg.pairs.filter_map { |pair| pair.key.value if pair.key.sym_type? }
129
+ end.flatten
130
+ end
131
+
132
+ def keyword_parameters(def_node)
133
+ def_node.arguments.filter_map do |arg|
134
+ arg.children[0] if arg.type?(:kwarg, :kwoptarg)
135
+ end
136
+ end
137
+
102
138
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
103
139
  def argument_match?(send_arg, def_arg)
104
140
  def_arg_name = def_arg.children[0]
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # Checks for private instance methods that are not referenced anywhere
7
+ # in the project.
8
+ #
9
+ # The check is powered by the project-wide index, so it only runs when
10
+ # `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed.
11
+ # Without the index the cop does nothing.
12
+ #
13
+ # A method counts as referenced when a call with its name appears anywhere
14
+ # in the indexed project (regardless of the receiver), when it is the source
15
+ # of an `alias`, or when its name appears in the same file as a symbol or
16
+ # inside a string literal (covering `send(:name)` and declarative DSLs like
17
+ # `before_action :name`). Methods defined in classes or modules with
18
+ # descendants are not checked, since they may be invoked through `super`
19
+ # or inherited dispatch, and neither are methods whose names are built
20
+ # dynamically (e.g. `send("do_#{action}")`).
21
+ #
22
+ # The cop is disabled by default because symbol-based references from
23
+ # *other* files (e.g. a Rails callback declared in a concern) cannot be
24
+ # detected and would be reported as false positives. It is best suited
25
+ # for occasional dead-code sweeps rather than permanent enforcement.
26
+ #
27
+ # @example
28
+ # # bad - `helper` is never referenced anywhere in the project
29
+ # class Service
30
+ # def call
31
+ # do_something
32
+ # end
33
+ #
34
+ # private
35
+ #
36
+ # def helper
37
+ # end
38
+ # end
39
+ #
40
+ # # good
41
+ # class Service
42
+ # def call
43
+ # do_something(helper)
44
+ # end
45
+ #
46
+ # private
47
+ #
48
+ # def helper
49
+ # end
50
+ # end
51
+ #
52
+ class UnusedPrivateMethod < Base
53
+ include ProjectIndexHelp
54
+
55
+ MSG = 'Private method `%<method>s` appears to be unused.'
56
+
57
+ IDENTIFIER_PATTERN = /[a-zA-Z_]\w*[?!=]?/.freeze
58
+
59
+ # Methods invoked implicitly by the Ruby runtime.
60
+ IMPLICITLY_INVOKED_METHODS = %i[initialize initialize_copy initialize_clone
61
+ initialize_dup method_missing respond_to_missing?
62
+ marshal_dump marshal_load].to_set.freeze
63
+
64
+ class << self
65
+ # The reference-name set is derived once per index and shared by the
66
+ # per-file cop instances.
67
+ def reference_names_cache
68
+ @reference_names_cache ||= {}.compare_by_identity
69
+ end
70
+ end
71
+
72
+ def on_new_investigation
73
+ @literal_names = nil
74
+ super
75
+ end
76
+
77
+ def on_def(node)
78
+ return unless project_index
79
+
80
+ declaration = checkable_declaration(node)
81
+ return unless declaration
82
+ return if referenced?(node.method_name)
83
+ return if owner_with_descendants?(declaration) || override?(declaration, node.method_name)
84
+
85
+ message = format(MSG, method: node.method_name)
86
+ add_offense(node.loc.keyword.join(node.loc.name), message: message)
87
+ end
88
+
89
+ private
90
+
91
+ def checkable_declaration(node)
92
+ return nil if IMPLICITLY_INVOKED_METHODS.include?(node.method_name)
93
+ return nil if node.each_ancestor(:any_def, :any_block, :sclass).any?
94
+ return nil unless (namespace_node = node.each_ancestor(:class, :module).first)
95
+
96
+ declaration = method_declaration(node, namespace_node)
97
+ declaration if declaration&.private?
98
+ end
99
+
100
+ def method_declaration(node, namespace_node)
101
+ namespace = resolve_constant_in_index(namespace_node.identifier)
102
+ return nil unless namespace.is_a?(Rubydex::Namespace)
103
+
104
+ namespace.member("#{node.method_name}()")
105
+ end
106
+
107
+ def referenced?(method_name)
108
+ name = method_name.to_s
109
+
110
+ referenced_names.include?(name) || literal_names.include?(name)
111
+ end
112
+
113
+ def referenced_names
114
+ self.class.reference_names_cache[project_index] ||=
115
+ project_index.method_references
116
+ .to_set { |reference| reference.name.delete_suffix('()') }
117
+ end
118
+
119
+ # Symbol literals and identifier-like tokens inside string literals in the
120
+ # current file, covering `send(:name)`, declarative DSLs like
121
+ # `before_action :name` and names embedded in strings (e.g. node-pattern
122
+ # `#helper` references).
123
+ def literal_names
124
+ @literal_names ||=
125
+ processed_source.ast.each_descendant(:sym, :str)
126
+ .with_object(Set.new) do |literal, names|
127
+ if literal.sym_type?
128
+ names << literal.value.to_s
129
+ else
130
+ literal.value.scan(IDENTIFIER_PATTERN) { |token| names << token }
131
+ end
132
+ end
133
+ end
134
+
135
+ # A method defined in a class or module with descendants may be invoked
136
+ # through `super` or inherited dispatch, which the index does not track.
137
+ def owner_with_descendants?(declaration)
138
+ owner = declaration.owner
139
+ return false unless owner.is_a?(Rubydex::Namespace)
140
+
141
+ owner.descendants.any? { |descendant| descendant.name != owner.name }
142
+ end
143
+
144
+ # An override of an inherited method may be invoked polymorphically
145
+ # (e.g. a framework hook), even when no direct reference exists.
146
+ def override?(declaration, method_name)
147
+ owner = declaration.owner
148
+ return false unless owner.is_a?(Rubydex::Namespace)
149
+
150
+ owner.ancestors.any? do |ancestor|
151
+ ancestor.name != owner.name && ancestor.member("#{method_name}()")
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -116,6 +116,8 @@ module RuboCop
116
116
  # so stop searching once a class/module boundary is crossed without a match.
117
117
  return nil if ancestor.type?(:class, :module, :sclass)
118
118
  end
119
+
120
+ nil
119
121
  end
120
122
 
121
123
  # `ruby2_keywords` is only allowed if there's a `restarg` and no keyword arguments
@@ -16,11 +16,14 @@ module RuboCop
16
16
  # enumerator.each { |item| item >= 2 } #=> [2, 3]
17
17
  # ----
18
18
  #
19
- # NOTE: Return values in assignment method definitions such as `def foo=(arg)` are
20
- # detected because they are in a void context. However, autocorrection does not remove
21
- # the return value, as that would change behavior. In such cases, whether to remove
22
- # the return value or rename the method to something more appropriate should be left to
23
- # the user.
19
+ # NOTE: The last expression in an assignment method definition such as `def foo=(arg)`
20
+ # is not flagged. Ruby discards it (the method returns its argument), but the method can
21
+ # still be called directly and its return value relied upon, so flagging it would be a
22
+ # false positive for this lint.
23
+ #
24
+ # NOTE: A constant used in a void context is flagged but not autocorrected, since
25
+ # referencing a constant can trigger autoloading side effects (e.g. forcing a file to
26
+ # load before a monkey-patch), so removing it may change behavior.
24
27
  #
25
28
  # @example CheckForMethodsWithNoSideEffects: false (default)
26
29
  # # bad
@@ -109,7 +112,7 @@ module RuboCop
109
112
  def check_begin(node)
110
113
  expressions = *node
111
114
  inside_each_block = node.each_ancestor(:any_block).first&.method?(:each)
112
- expressions.pop if !in_void_context?(node) || inside_each_block
115
+ expressions.pop if !in_void_context?(node) || inside_each_block || setter_method?(node)
113
116
  expressions.each do |expr|
114
117
  check_void_op(expr) { inside_each_block }
115
118
  check_expression(expr)
@@ -244,6 +247,10 @@ module RuboCop
244
247
  parent.respond_to?(:void_context?) && parent.void_context?
245
248
  end
246
249
 
250
+ def setter_method?(node)
251
+ node.parent&.any_def_type? && node.parent.assignment_method?
252
+ end
253
+
247
254
  def autocorrect_void_op(corrector, node)
248
255
  if node.arguments.empty?
249
256
  corrector.replace(node, node.receiver.source)
@@ -258,6 +265,10 @@ module RuboCop
258
265
  end
259
266
 
260
267
  def autocorrect_void_expression(corrector, node)
268
+ # Referencing a constant can trigger autoloading side effects (e.g. forcing a file
269
+ # to load before a monkey-patch), so removing it may change behavior. Flag but don't
270
+ # autocorrect, leaving it to the user.
271
+ return if node.const_type? && !node.special_keyword?
261
272
  return if node.parent.type?(:if, :case, :when, :case_match, :in_pattern)
262
273
  return if (def_node = node.each_ancestor(:any_def).first) && def_node.assignment_method?
263
274