rubocop 1.88.2 → 1.90.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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +139 -16
  3. data/exe/rubocop +1 -1
  4. data/lib/rubocop/cached_data.rb +7 -2
  5. data/lib/rubocop/cli/command/execute_runner.rb +31 -0
  6. data/lib/rubocop/cli.rb +2 -4
  7. data/lib/rubocop/comment_config/directive_range.rb +32 -0
  8. data/lib/rubocop/comment_config/disable_next.rb +97 -0
  9. data/lib/rubocop/comment_config.rb +81 -49
  10. data/lib/rubocop/config.rb +76 -5
  11. data/lib/rubocop/config_loader.rb +1 -2
  12. data/lib/rubocop/config_loader_resolver.rb +1 -2
  13. data/lib/rubocop/config_validator.rb +39 -3
  14. data/lib/rubocop/cop/autocorrect_logic.rb +8 -0
  15. data/lib/rubocop/cop/badge.rb +8 -0
  16. data/lib/rubocop/cop/base.rb +40 -6
  17. data/lib/rubocop/cop/bundler.rb +19 -0
  18. data/lib/rubocop/cop/corrector.rb +4 -4
  19. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  20. data/lib/rubocop/cop/gemspec.rb +22 -0
  21. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  22. data/lib/rubocop/cop/generator.rb +4 -0
  23. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -2
  24. data/lib/rubocop/cop/internal_affairs/node_pattern_groups/ast_walker.rb +1 -2
  25. data/lib/rubocop/cop/layout/block_alignment.rb +1 -2
  26. data/lib/rubocop/cop/layout/class_structure.rb +108 -26
  27. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -2
  28. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +63 -5
  29. data/lib/rubocop/cop/layout/end_alignment.rb +1 -2
  30. data/lib/rubocop/cop/layout/extra_spacing.rb +105 -3
  31. data/lib/rubocop/cop/layout/first_argument_indentation.rb +21 -1
  32. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  33. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  34. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  35. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +1 -2
  36. data/lib/rubocop/cop/layout/line_length.rb +84 -12
  37. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +1 -2
  38. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  39. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  40. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +1 -2
  41. data/lib/rubocop/cop/layout/space_around_operators.rb +7 -0
  42. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +31 -34
  43. data/lib/rubocop/cop/layout.rb +114 -0
  44. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  45. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  46. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  47. data/lib/rubocop/cop/lint/ambiguous_range.rb +1 -2
  48. data/lib/rubocop/cop/lint/argument_mismatch.rb +89 -0
  49. data/lib/rubocop/cop/lint/constant_reassignment.rb +18 -21
  50. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  51. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +84 -8
  52. data/lib/rubocop/cop/lint/deprecated_reference.rb +194 -0
  53. data/lib/rubocop/cop/lint/duplicate_methods.rb +300 -22
  54. data/lib/rubocop/cop/lint/implicit_string_concatenation.rb +1 -2
  55. data/lib/rubocop/cop/lint/ineffective_access_modifier.rb +1 -2
  56. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  57. data/lib/rubocop/cop/lint/interpolation_check.rb +1 -2
  58. data/lib/rubocop/cop/lint/literal_as_condition.rb +8 -6
  59. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +4 -7
  60. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +25 -12
  61. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  62. data/lib/rubocop/cop/lint/name_typo.rb +270 -0
  63. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +15 -4
  64. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +98 -19
  65. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +47 -7
  66. data/lib/rubocop/cop/lint/redundant_require_statement.rb +1 -2
  67. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +1 -2
  68. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +1 -2
  69. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +1 -2
  70. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -2
  71. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +8 -2
  72. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +2 -4
  73. data/lib/rubocop/cop/lint/super_argument_mismatch.rb +179 -0
  74. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +1 -2
  75. data/lib/rubocop/cop/lint/to_enum_arguments.rb +38 -3
  76. data/lib/rubocop/cop/lint/unreachable_code.rb +1 -2
  77. data/lib/rubocop/cop/lint/unused_private_method.rb +234 -0
  78. data/lib/rubocop/cop/lint/useless_access_modifier.rb +1 -2
  79. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -2
  80. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +36 -0
  81. data/lib/rubocop/cop/lint/useless_method_definition.rb +1 -1
  82. data/lib/rubocop/cop/lint/useless_rescue.rb +1 -2
  83. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  84. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +61 -7
  85. data/lib/rubocop/cop/lint/void.rb +18 -8
  86. data/lib/rubocop/cop/lint.rb +178 -0
  87. data/lib/rubocop/cop/metrics/perceived_complexity.rb +1 -2
  88. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  89. data/lib/rubocop/cop/metrics.rb +31 -0
  90. data/lib/rubocop/cop/migration/department_name.rb +1 -6
  91. data/lib/rubocop/cop/migration.rb +13 -0
  92. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  93. data/lib/rubocop/cop/mixin/comments_help.rb +1 -2
  94. data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +1 -2
  95. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  96. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  97. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +1 -2
  98. data/lib/rubocop/cop/mixin/hash_subset.rb +1 -2
  99. data/lib/rubocop/cop/mixin/indexed_method_arity.rb +94 -0
  100. data/lib/rubocop/cop/mixin/line_length_help.rb +7 -1
  101. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +51 -6
  102. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  103. data/lib/rubocop/cop/mixin/range_help.rb +1 -2
  104. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  105. data/lib/rubocop/cop/mixin/statement_modifier.rb +46 -4
  106. data/lib/rubocop/cop/mixin/trailing_comma.rb +2 -4
  107. data/lib/rubocop/cop/mixin.rb +82 -77
  108. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  109. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +16 -0
  110. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -4
  111. data/lib/rubocop/cop/naming/method_name.rb +5 -2
  112. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  113. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -2
  114. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  115. data/lib/rubocop/cop/naming.rb +31 -0
  116. data/lib/rubocop/cop/offense.rb +37 -10
  117. data/lib/rubocop/cop/registry.rb +180 -46
  118. data/lib/rubocop/cop/security.rb +19 -0
  119. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -2
  120. data/lib/rubocop/cop/style/arguments_forwarding.rb +3 -6
  121. data/lib/rubocop/cop/style/array_first_last.rb +1 -2
  122. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  123. data/lib/rubocop/cop/style/bitwise_predicate.rb +1 -2
  124. data/lib/rubocop/cop/style/block_delimiters.rb +3 -6
  125. data/lib/rubocop/cop/style/case_like_if.rb +2 -4
  126. data/lib/rubocop/cop/style/class_and_module_children.rb +79 -20
  127. data/lib/rubocop/cop/style/combinable_loops.rb +1 -2
  128. data/lib/rubocop/cop/style/concat_array_literals.rb +1 -2
  129. data/lib/rubocop/cop/style/conditional_assignment.rb +23 -2
  130. data/lib/rubocop/cop/style/constant_visibility.rb +1 -2
  131. data/lib/rubocop/cop/style/copyright.rb +5 -1
  132. data/lib/rubocop/cop/style/directive_scope.rb +171 -0
  133. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +76 -12
  134. data/lib/rubocop/cop/style/documentation.rb +47 -0
  135. data/lib/rubocop/cop/style/empty_case_condition.rb +1 -2
  136. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  137. data/lib/rubocop/cop/style/format_string_token.rb +1 -2
  138. data/lib/rubocop/cop/style/guard_clause.rb +1 -2
  139. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -2
  140. data/lib/rubocop/cop/style/hash_syntax.rb +1 -2
  141. data/lib/rubocop/cop/style/identical_conditional_branches.rb +2 -4
  142. data/lib/rubocop/cop/style/if_inside_else.rb +1 -2
  143. data/lib/rubocop/cop/style/if_unless_modifier.rb +8 -50
  144. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +1 -2
  145. data/lib/rubocop/cop/style/lambda.rb +4 -0
  146. data/lib/rubocop/cop/style/map_to_hash.rb +1 -2
  147. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +22 -7
  148. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +1 -2
  149. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  150. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  151. data/lib/rubocop/cop/style/multiline_method_signature.rb +1 -2
  152. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  153. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  154. data/lib/rubocop/cop/style/negated_if_else_condition.rb +1 -2
  155. data/lib/rubocop/cop/style/negative_array_index.rb +3 -3
  156. data/lib/rubocop/cop/style/nil_comparison.rb +1 -2
  157. data/lib/rubocop/cop/style/operator_method_call.rb +1 -2
  158. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -2
  159. data/lib/rubocop/cop/style/redundant_assignment.rb +12 -2
  160. data/lib/rubocop/cop/style/redundant_condition.rb +2 -4
  161. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  162. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +1 -2
  163. data/lib/rubocop/cop/style/redundant_each.rb +1 -2
  164. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  165. data/lib/rubocop/cop/style/redundant_format.rb +14 -7
  166. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  167. data/lib/rubocop/cop/style/redundant_parentheses.rb +65 -67
  168. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +1 -2
  169. data/lib/rubocop/cop/style/redundant_return.rb +1 -2
  170. data/lib/rubocop/cop/style/redundant_self_assignment.rb +1 -2
  171. data/lib/rubocop/cop/style/redundant_string_escape.rb +1 -2
  172. data/lib/rubocop/cop/style/rescue_modifier.rb +1 -2
  173. data/lib/rubocop/cop/style/safe_navigation.rb +11 -2
  174. data/lib/rubocop/cop/style/sample.rb +11 -6
  175. data/lib/rubocop/cop/style/select_by_regexp.rb +1 -2
  176. data/lib/rubocop/cop/style/semicolon.rb +2 -4
  177. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -2
  178. data/lib/rubocop/cop/style/sole_nested_conditional.rb +21 -5
  179. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +2 -0
  180. data/lib/rubocop/cop/style/static_class.rb +21 -0
  181. data/lib/rubocop/cop/style/string_concatenation.rb +18 -5
  182. data/lib/rubocop/cop/style/super_arguments.rb +1 -2
  183. data/lib/rubocop/cop/style/symbol_proc.rb +1 -2
  184. data/lib/rubocop/cop/style/time_now.rb +41 -0
  185. data/lib/rubocop/cop/style/yoda_condition.rb +1 -2
  186. data/lib/rubocop/cop/style.rb +312 -0
  187. data/lib/rubocop/cop/team.rb +59 -6
  188. data/lib/rubocop/cop/util.rb +2 -4
  189. data/lib/rubocop/cop/variable_force/variable.rb +19 -8
  190. data/lib/rubocop/cops_documentation_generator.rb +4 -7
  191. data/lib/rubocop/directive_comment.rb +39 -3
  192. data/lib/rubocop/feature_loader.rb +0 -6
  193. data/lib/rubocop/formatter/emacs_style_formatter.rb +3 -1
  194. data/lib/rubocop/formatter/html_formatter.rb +2 -4
  195. data/lib/rubocop/formatter/json_formatter.rb +11 -6
  196. data/lib/rubocop/formatter/junit_formatter.rb +26 -9
  197. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -2
  198. data/lib/rubocop/formatter/simple_text_formatter.rb +4 -3
  199. data/lib/rubocop/formatter/tap_formatter.rb +3 -1
  200. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -2
  201. data/lib/rubocop/lockfile.rb +18 -0
  202. data/lib/rubocop/lsp/diagnostic.rb +4 -8
  203. data/lib/rubocop/lsp/routes.rb +89 -22
  204. data/lib/rubocop/lsp/runtime.rb +4 -0
  205. data/lib/rubocop/lsp/server.rb +4 -0
  206. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  207. data/lib/rubocop/mcp/server.rb +1 -2
  208. data/lib/rubocop/options.rb +10 -6
  209. data/lib/rubocop/path_util.rb +1 -2
  210. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  211. data/lib/rubocop/plugin/configuration_integrator.rb +6 -3
  212. data/lib/rubocop/project_index_loader.rb +16 -0
  213. data/lib/rubocop/rake_task.rb +1 -2
  214. data/lib/rubocop/result_cache.rb +6 -4
  215. data/lib/rubocop/rspec/expect_offense.rb +2 -4
  216. data/lib/rubocop/rspec/shared_contexts.rb +16 -2
  217. data/lib/rubocop/runner.rb +71 -43
  218. data/lib/rubocop/server/cache.rb +11 -7
  219. data/lib/rubocop/server/cli.rb +2 -4
  220. data/lib/rubocop/server.rb +14 -0
  221. data/lib/rubocop/target_finder.rb +4 -1
  222. data/lib/rubocop/target_ruby.rb +1 -1
  223. data/lib/rubocop/todo_audit.rb +111 -0
  224. data/lib/rubocop/version.rb +3 -5
  225. data/lib/rubocop.rb +20 -626
  226. data/lib/ruby_lsp/rubocop/addon.rb +1 -2
  227. metadata +28 -5
@@ -0,0 +1,234 @@
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`). An interpolated symbol or string with a literal
18
+ # prefix (e.g. `send(:"format_#{type}")`) counts as a reference to every
19
+ # method whose name starts with that prefix; fully dynamic names cannot
20
+ # be detected. Methods defined in classes or modules with descendants are
21
+ # not checked, since they may be invoked through `super` or inherited
22
+ # dispatch.
23
+ #
24
+ # Methods whose names are composed by a framework (e.g. Rails
25
+ # `attribute_method_suffix` generating `attribute?` methods) can be
26
+ # excluded from the check with `AllowedNames` (exact names) or
27
+ # `AllowedPatterns` (regular expressions).
28
+ #
29
+ # The cop is disabled by default because symbol-based references from
30
+ # *other* files (e.g. a Rails callback declared in a concern) cannot be
31
+ # detected and would be reported as false positives. It is best suited
32
+ # for occasional dead-code sweeps rather than permanent enforcement.
33
+ #
34
+ # @example
35
+ # # bad - `helper` is never referenced anywhere in the project
36
+ # class Service
37
+ # def call
38
+ # do_something
39
+ # end
40
+ #
41
+ # private
42
+ #
43
+ # def helper
44
+ # end
45
+ # end
46
+ #
47
+ # # good
48
+ # class Service
49
+ # def call
50
+ # do_something(helper)
51
+ # end
52
+ #
53
+ # private
54
+ #
55
+ # def helper
56
+ # end
57
+ # end
58
+ #
59
+ # @example AllowedNames: ['attribute?']
60
+ # # good - the name is explicitly allowed
61
+ # class Contact
62
+ # private
63
+ #
64
+ # def attribute?
65
+ # end
66
+ # end
67
+ #
68
+ # @example AllowedPatterns: ['_hook\z']
69
+ # # good - the name matches an allowed pattern
70
+ # class Service
71
+ # private
72
+ #
73
+ # def before_save_hook
74
+ # end
75
+ # end
76
+ #
77
+ class UnusedPrivateMethod < Base
78
+ include AllowedPattern
79
+ include ProjectIndexHelp
80
+
81
+ MSG = 'Private method `%<method>s` appears to be unused.'
82
+
83
+ IDENTIFIER_PATTERN = /[a-zA-Z_]\w*[?!=]?/.freeze
84
+ NAME_PREFIX_PATTERN = /[a-zA-Z_]\w*\z/.freeze
85
+
86
+ # Methods invoked implicitly by the Ruby runtime.
87
+ IMPLICITLY_INVOKED_METHODS = %i[
88
+ initialize initialize_copy initialize_clone initialize_dup
89
+ method_missing respond_to_missing? marshal_dump marshal_load
90
+ coerce instance_variables_to_inspect
91
+ inherited included extended prepended
92
+ append_features extend_object prepend_features
93
+ const_missing const_added
94
+ method_added method_removed method_undefined
95
+ singleton_method_added singleton_method_removed singleton_method_undefined
96
+ ].to_set.freeze
97
+
98
+ class << self
99
+ # The reference-name set is a property of the index, not of the cop
100
+ # instance, so all per-file instances share one computation. A
101
+ # single-entry cache (instead of a hash keyed by index) avoids
102
+ # retaining stale graphs in long-lived processes.
103
+ attr_accessor :cached_reference_names
104
+ end
105
+
106
+ def on_new_investigation
107
+ @literal_names = nil
108
+ @literal_name_prefixes = nil
109
+ super
110
+ end
111
+
112
+ def on_def(node)
113
+ return unless project_index
114
+
115
+ declaration = checkable_declaration(node)
116
+ return unless declaration
117
+ return if referenced?(node.method_name)
118
+ return if owner_with_descendants?(declaration) || override?(declaration, node.method_name)
119
+
120
+ message = format(MSG, method: node.method_name)
121
+ add_offense(node.loc.keyword.join(node.loc.name), message: message)
122
+ end
123
+
124
+ private
125
+
126
+ def checkable_declaration(node)
127
+ return nil if IMPLICITLY_INVOKED_METHODS.include?(node.method_name)
128
+ return nil if allowed_name?(node.method_name)
129
+ return nil if node.each_ancestor(:any_def, :any_block, :sclass).any?
130
+ return nil unless (namespace_node = node.each_ancestor(:class, :module).first)
131
+
132
+ declaration = method_declaration(node, namespace_node)
133
+ declaration if declaration&.private?
134
+ end
135
+
136
+ def allowed_name?(method_name)
137
+ name = method_name.to_s
138
+
139
+ cop_config.fetch('AllowedNames', []).include?(name) || matches_allowed_pattern?(name)
140
+ end
141
+
142
+ def method_declaration(node, namespace_node)
143
+ namespace = resolve_constant_in_index(namespace_node.identifier)
144
+ return nil unless namespace.is_a?(Rubydex::Namespace)
145
+
146
+ namespace.member("#{node.method_name}()")
147
+ end
148
+
149
+ def referenced?(method_name)
150
+ name = method_name.to_s
151
+
152
+ referenced_names.include?(name) || literal_names.include?(name) ||
153
+ literal_name_prefixes.any? { |prefix| name.start_with?(prefix) }
154
+ end
155
+
156
+ def referenced_names
157
+ index, names = self.class.cached_reference_names
158
+ return names if index.equal?(project_index)
159
+
160
+ compute_referenced_names.tap do |computed|
161
+ self.class.cached_reference_names = [project_index, computed]
162
+ end
163
+ end
164
+
165
+ def compute_referenced_names
166
+ project_index.method_references
167
+ .to_set { |reference| reference.name.delete_suffix('()') }
168
+ end
169
+
170
+ # Symbol literals and identifier-like tokens inside string literals in the
171
+ # current file, covering `send(:name)`, declarative DSLs like
172
+ # `before_action :name` and names embedded in strings (e.g. node-pattern
173
+ # `#helper` references).
174
+ def literal_names
175
+ @literal_names ||=
176
+ processed_source.ast.each_descendant(:sym, :str)
177
+ .with_object(Set.new) do |literal, names|
178
+ if literal.sym_type?
179
+ names << literal.value.to_s
180
+ else
181
+ # `scan` raises on binary string literals with invalid byte sequences.
182
+ value = literal.value
183
+ value = value.scrub unless value.valid_encoding?
184
+ value.scan(IDENTIFIER_PATTERN) { |token| names << token }
185
+ end
186
+ end
187
+ end
188
+
189
+ # The leading literal fragment of an interpolated symbol or string acts
190
+ # as a method-name prefix: `send(:"format_#{type}")` may invoke any
191
+ # method whose name starts with `format_`. Only the leading fragment
192
+ # qualifies (a mid-string fragment is not a name prefix), and a fully
193
+ # dynamic name contributes nothing.
194
+ def literal_name_prefixes
195
+ @literal_name_prefixes ||=
196
+ processed_source.ast.each_descendant(:dsym, :dstr)
197
+ .with_object(Set.new) do |literal, prefixes|
198
+ prefix = interpolation_prefix(literal)
199
+ prefixes << prefix if prefix
200
+ end
201
+ end
202
+
203
+ def interpolation_prefix(node)
204
+ return nil if node.children.all?(&:str_type?)
205
+
206
+ leading_fragment = node.children.first
207
+ return nil unless leading_fragment&.str_type?
208
+
209
+ leading_fragment.value[NAME_PREFIX_PATTERN]
210
+ end
211
+
212
+ # A method defined in a class or module with descendants may be invoked
213
+ # through `super` or inherited dispatch, which the index does not track.
214
+ def owner_with_descendants?(declaration)
215
+ owner = declaration.owner
216
+ return false unless owner.is_a?(Rubydex::Namespace)
217
+
218
+ owner.descendants.any? { |descendant| descendant.name != owner.name }
219
+ end
220
+
221
+ # An override of an inherited method may be invoked polymorphically
222
+ # (e.g. a framework hook), even when no direct reference exists.
223
+ def override?(declaration, method_name)
224
+ owner = declaration.owner
225
+ return false unless owner.is_a?(Rubydex::Namespace)
226
+
227
+ owner.ancestors.any? do |ancestor|
228
+ ancestor.name != owner.name && ancestor.member("#{method_name}()")
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -210,7 +210,7 @@ module RuboCop
210
210
  end
211
211
  end
212
212
 
213
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
213
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
214
214
  def check_child_nodes(node, unused, cur_vis)
215
215
  node.child_nodes.each do |child|
216
216
  if child.send_type? && access_modifier?(child)
@@ -228,7 +228,6 @@ module RuboCop
228
228
 
229
229
  [cur_vis, unused]
230
230
  end
231
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
232
231
 
233
232
  def check_send_node(node, cur_vis, unused)
234
233
  if node.bare_access_modifier?
@@ -173,7 +173,7 @@ module RuboCop
173
173
  node.receiver.nil? && !node.arguments?
174
174
  end
175
175
 
176
- # rubocop:disable Metrics/AbcSize
176
+ # rubocop:disable-next Metrics/AbcSize
177
177
  def autocorrect(corrector, assignment)
178
178
  if assignment.exception_assignment?
179
179
  remove_exception_assignment_part(corrector, assignment.node)
@@ -189,7 +189,6 @@ module RuboCop
189
189
  remove_local_variable_assignment_part(corrector, assignment.node)
190
190
  end
191
191
  end
192
- # rubocop:enable Metrics/AbcSize
193
192
 
194
193
  def remove_exception_assignment_part(corrector, node)
195
194
  range = node.parent.children.first&.source_range || node.parent.location.keyword
@@ -10,6 +10,11 @@ module RuboCop
10
10
  # It does not support autocorrection due to behavior change and multiple ways to fix it.
11
11
  # Or a public constant may be intended.
12
12
  #
13
+ # Constant assignments that define classes or modules via `Class.new`, `Module.new`,
14
+ # `Struct.new`, or `Data.define` are allowed. Those forms are class and module definitions
15
+ # written with assignment syntax, and match the common practice of placing nested
16
+ # `class` / `module` bodies after `private` without intending private constant visibility.
17
+ #
13
18
  # @example
14
19
  #
15
20
  # # bad
@@ -29,6 +34,19 @@ module RuboCop
29
34
  # PUBLIC_CONST = 42 # If private scope is not intended.
30
35
  # end
31
36
  #
37
+ # # good - class/module definitions via assignment, same as nested `class`/`module`
38
+ # class Foo
39
+ # private
40
+ #
41
+ # def some_private_method
42
+ # end
43
+ #
44
+ # MyClass = Class.new
45
+ # MyModule = Module.new
46
+ # MyStruct = Struct.new(:name)
47
+ # MyData = Data.define(:name)
48
+ # end
49
+ #
32
50
  class UselessConstantScoping < Base
33
51
  MSG = 'Useless `private` access modifier for constant scope.'
34
52
 
@@ -37,9 +55,27 @@ module RuboCop
37
55
  (send nil? :private_constant $...)
38
56
  PATTERN
39
57
 
58
+ # Matches class/module-like constant assignments. Nested `class` / `module`
59
+ # keyword definitions are not visited by this cop; these assignment forms are
60
+ # the equivalent syntax and should be treated the same way.
61
+ # @!method class_or_module_definition_assignment?(node)
62
+ def_node_matcher :class_or_module_definition_assignment?, <<~PATTERN
63
+ {
64
+ (send (const {nil? cbase} {:Class :Module :Struct}) :new ...)
65
+ (send (const {nil? cbase} :Data) :define ...)
66
+ (any_block
67
+ {
68
+ (send (const {nil? cbase} {:Class :Module :Struct}) :new ...)
69
+ (send (const {nil? cbase} :Data) :define ...)
70
+ }
71
+ ...)
72
+ }
73
+ PATTERN
74
+
40
75
  def on_casgn(node)
41
76
  return unless after_private_modifier?(node.left_siblings)
42
77
  return if private_constantize?(node.right_siblings, node.name)
78
+ return if class_or_module_definition_assignment?(node.expression)
43
79
 
44
80
  add_offense(node)
45
81
  end
@@ -59,7 +59,7 @@ module RuboCop
59
59
  end
60
60
 
61
61
  def use_rest_or_optional_args?(node)
62
- node.arguments.any? { |arg| arg.type?(:restarg, :optarg, :kwoptarg) }
62
+ node.arguments.any? { |arg| arg.type?(:restarg, :optarg, :kwoptarg, :kwrestarg) }
63
63
  end
64
64
 
65
65
  def delegating?(node, def_node)
@@ -56,7 +56,7 @@ module RuboCop
56
56
 
57
57
  private
58
58
 
59
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
59
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
60
60
  def only_reraising?(resbody_node)
61
61
  return false if use_exception_variable_in_ensure?(resbody_node)
62
62
 
@@ -70,7 +70,6 @@ module RuboCop
70
70
 
71
71
  exception_objects(resbody_node).include?(exception_name)
72
72
  end
73
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
74
73
 
75
74
  def use_exception_variable_in_ensure?(resbody_node)
76
75
  return false unless (exception_variable = resbody_node.exception_variable)
@@ -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
@@ -12,6 +12,8 @@ module RuboCop
12
12
  @receiver = receiver
13
13
  @additional_nil_methods = additional_nil_methods
14
14
  @checked_nodes = {}.compare_by_identity
15
+ @receiver_binding_name = binding_name(receiver)
16
+ @receiver_binding_scope = binding_scope(receiver) if @receiver_binding_name
15
17
  end
16
18
 
17
19
  def cant_be_nil?
@@ -20,7 +22,7 @@ module RuboCop
20
22
 
21
23
  private
22
24
 
23
- # rubocop:disable Metrics
25
+ # rubocop:disable-next Metrics
24
26
  def _cant_be_nil?(node, receiver)
25
27
  return false unless node
26
28
 
@@ -34,7 +36,9 @@ module RuboCop
34
36
  when :def, :defs, :class, :module, :sclass
35
37
  return false
36
38
  when :send
37
- return non_nil_method?(node.method_name) if node.receiver == receiver
39
+ if node.receiver == receiver && same_binding_as_receiver?(node.receiver)
40
+ return non_nil_method?(node.method_name)
41
+ end
38
42
 
39
43
  node.arguments.each do |argument|
40
44
  return true if _cant_be_nil?(argument, receiver)
@@ -74,13 +78,12 @@ module RuboCop
74
78
  false
75
79
  end
76
80
  end
77
- # rubocop:enable Metrics
78
81
 
79
82
  def non_nil_method?(method_name)
80
83
  !NIL_METHODS.include?(method_name) && !@additional_nil_methods.include?(method_name)
81
84
  end
82
85
 
83
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
86
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
84
87
  def sole_condition_of_parent_if?(node)
85
88
  child = node
86
89
  parent = node.parent
@@ -104,12 +107,63 @@ module RuboCop
104
107
 
105
108
  false
106
109
  end
107
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
108
110
 
109
111
  def non_nil_condition?(condition, node)
110
- return true if condition == node
112
+ return true if condition == node && same_binding_as_receiver?(condition)
113
+ return false unless condition.csend_type?
114
+
115
+ root_receiver = csend_root_receiver(condition)
116
+
117
+ root_receiver == node && same_binding_as_receiver?(root_receiver)
118
+ end
119
+
120
+ # Structurally equal occurrences can be different variables: `it` binds to
121
+ # its innermost block, and a block parameter can shadow an outer variable of
122
+ # the same name. Evidence about such a receiver only holds when both occurrences
123
+ # resolve to the same binding.
124
+ def same_binding_as_receiver?(occurrence)
125
+ return true unless @receiver_binding_name
126
+
127
+ binding_scope(occurrence).equal?(@receiver_binding_scope)
128
+ end
129
+
130
+ # The name whose binding can differ between structurally equal occurrences:
131
+ # a local variable (block parameters included), or a bare `it` call, which is
132
+ # how `it` parses when the target Ruby version is below 3.4 even though the code
133
+ # may run on 3.4+ where `it` is the implicit block parameter.
134
+ def binding_name(node)
135
+ if node.lvar_type?
136
+ node.children.first
137
+ elsif node.send_type? && !node.receiver && node.method?(:it) && node.arguments.empty?
138
+ :it
139
+ end
140
+ end
141
+
142
+ def binding_scope(occurrence)
143
+ node = occurrence
144
+
145
+ while (parent = node.parent)
146
+ case parent.type
147
+ when :block, :numblock, :itblock
148
+ # Only the body is inside the block's scope; the send node and the arguments of
149
+ # the block node evaluate in the outer scope.
150
+ return parent if parent.body.equal?(node) && block_binds_name?(parent)
151
+ when :def, :defs, :class, :module, :sclass
152
+ return parent
153
+ end
154
+
155
+ node = parent
156
+ end
157
+
158
+ nil
159
+ end
160
+
161
+ def block_binds_name?(block_node)
162
+ name = @receiver_binding_name
163
+
164
+ return true if block_node.argument_list.any? { |argument| argument.name == name }
111
165
 
112
- condition.csend_type? && csend_root_receiver(condition) == node
166
+ name == :it && block_node.arguments.empty?
113
167
  end
114
168
 
115
169
  # Whether control reaches `node` by falling through its left siblings rather than by
@@ -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)
@@ -150,7 +153,7 @@ module RuboCop
150
153
  check_expression(case_node.else_branch) if case_node.else_branch
151
154
  end
152
155
 
153
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
156
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
154
157
  def check_void_op(node, &block)
155
158
  node = node.children.first while node&.begin_type?
156
159
  return unless node&.call_type? && OPERATORS.include?(node.method_name)
@@ -165,7 +168,6 @@ module RuboCop
165
168
  autocorrect_void_op(corrector, node)
166
169
  end
167
170
  end
168
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
169
171
 
170
172
  def check_var(node)
171
173
  return unless node.variable? || node.const_type?
@@ -244,6 +246,10 @@ module RuboCop
244
246
  parent.respond_to?(:void_context?) && parent.void_context?
245
247
  end
246
248
 
249
+ def setter_method?(node)
250
+ node.parent&.any_def_type? && node.parent.assignment_method?
251
+ end
252
+
247
253
  def autocorrect_void_op(corrector, node)
248
254
  if node.arguments.empty?
249
255
  corrector.replace(node, node.receiver.source)
@@ -258,6 +264,10 @@ module RuboCop
258
264
  end
259
265
 
260
266
  def autocorrect_void_expression(corrector, node)
267
+ # Referencing a constant can trigger autoloading side effects (e.g. forcing a file
268
+ # to load before a monkey-patch), so removing it may change behavior. Flag but don't
269
+ # autocorrect, leaving it to the user.
270
+ return if node.const_type? && !node.special_keyword?
261
271
  return if node.parent.type?(:if, :case, :when, :case_match, :in_pattern)
262
272
  return if (def_node = node.each_ancestor(:any_def).first) && def_node.assignment_method?
263
273