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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Lint/RedundantCopDisableDirective
3
+ # rubocop:disable-next Lint/RedundantCopDisableDirective
4
4
  module RuboCop
5
5
  module Cop
6
6
  module Lint
@@ -55,7 +55,7 @@ module RuboCop
55
55
  class MissingCopEnableDirective < Base
56
56
  include RangeHelp
57
57
 
58
- MSG = 'Re-enable %<cop>s %<type>s with `# rubocop:enable` after disabling it.'
58
+ MSG = 'Re-enable %<cop>s %<type>s with `%<directive>s` after disabling it.'
59
59
  MSG_BOUND = 'Re-enable %<cop>s %<type>s within %<max_range>s lines after disabling it.'
60
60
 
61
61
  def on_new_investigation
@@ -72,7 +72,12 @@ module RuboCop
72
72
 
73
73
  def each_missing_enable
74
74
  processed_source.disabled_line_ranges.each do |cop, line_ranges|
75
- line_ranges.each { |line_range| yield cop, line_range }
75
+ line_ranges.each do |line_range|
76
+ # A `disable-next` scope closes itself with its statement.
77
+ next if line_range.respond_to?(:directive) && line_range.directive.disable_next?
78
+
79
+ yield cop, line_range
80
+ end
76
81
  end
77
82
  end
78
83
 
@@ -80,17 +85,22 @@ module RuboCop
80
85
  # This has to remain a strict inequality to handle
81
86
  # the case when max_range is Float::INFINITY
82
87
  return true if line_range.max - line_range.min < max_range + 2
88
+ # A name that is not registered as written (e.g. a wrongly-namespaced
89
+ # cop) suppresses nothing; `Lint/RedundantCopDisableDirective` reports
90
+ # it, so demanding its re-enablement would only add noise.
91
+ return true unless Registry.global.names.include?(cop)
92
+
93
+ expected_open_range?(cop, line_range)
94
+ end
95
+
96
+ def expected_open_range?(cop, line_range)
83
97
  # This cop is disabled in the config, it is not expected to be re-enabled
84
98
  return true if line_range.min == CommentConfig::CONFIG_DISABLED_LINE_RANGE_MIN
85
99
 
86
- cop_class = RuboCop::Cop::Registry.global.find_by_cop_name cop
87
- if cop_class &&
100
+ cop_class = RuboCop::Cop::Registry.global.find_by_cop_name(cop)
101
+ !!(cop_class &&
88
102
  !processed_source.registry.enabled?(cop_class, config) &&
89
- line_range.max == Float::INFINITY
90
- return true
91
- end
92
-
93
- false
103
+ line_range.max == Float::INFINITY)
94
104
  end
95
105
 
96
106
  def max_range
@@ -98,13 +108,17 @@ module RuboCop
98
108
  end
99
109
 
100
110
  def message(cop, comment, type = 'cop')
111
+ directive = DirectiveComment.new(comment)
101
112
  if department_enabled?(cop, comment)
102
113
  type = 'department'
103
114
  cop = cop.split('/').first
104
115
  end
105
116
 
106
117
  if max_range == Float::INFINITY
107
- format(MSG, cop: cop, type: type)
118
+ # A range opened by `rubocop:push` is closed by `rubocop:pop`,
119
+ # not by `rubocop:enable`.
120
+ enabling_directive = directive.push? ? '# rubocop:pop' : '# rubocop:enable'
121
+ format(MSG, cop: cop, type: type, directive: enabling_directive)
108
122
  else
109
123
  format(MSG_BOUND, cop: cop, type: type, max_range: max_range)
110
124
  end
@@ -117,4 +131,3 @@ module RuboCop
117
131
  end
118
132
  end
119
133
  end
120
- # rubocop:enable Lint/RedundantCopDisableDirective
@@ -21,6 +21,13 @@ module RuboCop
21
21
  # `AllowedParentClasses` option to specify which classes should be allowed
22
22
  # *in addition to* `Object` and `BasicObject`.
23
23
  #
24
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed,
25
+ # the constructor check additionally consults the project-wide index: if the
26
+ # class' entire ancestry is resolvable and no ancestor defines `initialize`,
27
+ # no offense is registered, since `super` would only reach the no-op
28
+ # `Object#initialize`. Classes whose ancestry contains an unresolvable
29
+ # superclass or mixin (e.g. one defined in a gem) are still reported.
30
+ #
24
31
  # @example
25
32
  # # bad
26
33
  # class Employee < Person
@@ -83,6 +90,8 @@ module RuboCop
83
90
  # end
84
91
  #
85
92
  class MissingSuper < Base
93
+ include ProjectIndexHelp
94
+
86
95
  CONSTRUCTOR_MSG = 'Call `super` to initialize state of the parent class.'
87
96
  CALLBACK_MSG = 'Call `super` to invoke callback defined in the parent class.'
88
97
 
@@ -140,7 +149,8 @@ module RuboCop
140
149
 
141
150
  !allowed_class?(super_class)
142
151
  elsif (class_node = node.each_ancestor(:class).first)
143
- class_node.parent_class && !allowed_class?(class_node.parent_class)
152
+ class_node.parent_class && !allowed_class?(class_node.parent_class) &&
153
+ !index_verified_stateless_ancestry?(class_node)
144
154
  else
145
155
  false
146
156
  end
@@ -153,6 +163,26 @@ module RuboCop
153
163
  def allowed_classes
154
164
  @allowed_classes ||= STATELESS_CLASSES + cop_config.fetch('AllowedParentClasses', [])
155
165
  end
166
+
167
+ # With the project index, the offense is skipped when the class' whole ancestry
168
+ # is resolvable and none of the inherited ancestors defines `initialize` —
169
+ # `super` would only reach the no-op `Object#initialize`. An unresolvable
170
+ # superclass or mixin anywhere in the chain (e.g. a class from a gem) means
171
+ # the ancestry cannot be verified and the offense is kept.
172
+ def index_verified_stateless_ancestry?(class_node)
173
+ return false unless project_index
174
+
175
+ declaration = resolve_constant_in_index(class_node.identifier)
176
+ return false unless declaration.is_a?(Rubydex::Class)
177
+
178
+ ancestors = declaration.ancestors.to_a
179
+ inherited = ancestors.reject { |ancestor| ancestor.name == declaration.name }
180
+ return false if inherited.any? { |ancestor| ancestor.member('initialize()') }
181
+
182
+ # `extend` affects the singleton class and cannot introduce an
183
+ # inherited `initialize`, so unresolved extends are tolerated.
184
+ fully_resolved_index_ancestry?(declaration, ignore_extend: true)
185
+ end
156
186
  end
157
187
  end
158
188
  end
@@ -0,0 +1,270 @@
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
+ # For the same reason, a namespace whose root segment names a gem in the
31
+ # bundle (`Flipper` for the `flipper` gem) is left alone: a project that
32
+ # reopens it (`module Flipper; module Adapters; ...`) makes it resolve in
33
+ # the index, while the members the gem itself defines stay invisible and
34
+ # would look like typos. Enabling `AllCops/ProjectIndexIncludesGems`
35
+ # indexes those sources and restores the check.
36
+ #
37
+ # Names that appear as symbols or inside string literals in the same
38
+ # file are never reported, since they usually belong to runtime
39
+ # definitions the index cannot see (`stub_const`, `const_set`,
40
+ # `define_method`, and the like).
41
+ #
42
+ # @example
43
+ # # bad - Services::UserCraetor is not defined, Services::UserCreator is
44
+ # Services::UserCraetor.new
45
+ #
46
+ # # good
47
+ # Services::UserCreator.new
48
+ #
49
+ # # bad - Report.generate_sumary is not defined, Report.generate_summary is
50
+ # Report.generate_sumary
51
+ #
52
+ # # good
53
+ # Report.generate_summary
54
+ #
55
+ # @example CheckConstants: false
56
+ # # good - constant references are not checked
57
+ # Services::UserCraetor.new
58
+ #
59
+ # @example CheckMethods: false
60
+ # # good - method calls are not checked
61
+ # Report.generate_sumary
62
+ #
63
+ # @example AllowedNames: ['generate_sumary']
64
+ # # good - the name is explicitly allowed
65
+ # Report.generate_sumary
66
+ #
67
+ class NameTypo < Base
68
+ include ProjectIndexHelp
69
+
70
+ CONSTANT_MSG = 'Possible typo: `%<name>s` is not defined in `%<namespace>s`. ' \
71
+ 'Did you mean `%<suggestion>s`?'
72
+ METHOD_MSG = 'Possible typo: `%<receiver>s` does not respond to `%<name>s`. ' \
73
+ 'Did you mean `%<suggestion>s`?'
74
+
75
+ METHOD_MEMBER_REGEXP = /#([a-zA-Z_]\w*[?!]?)\(\)\z/.freeze
76
+ LITERAL_IDENTIFIER_PATTERN = /[a-zA-Z_]\w*[?!]?/.freeze
77
+
78
+ def on_const(node)
79
+ return unless check?('CheckConstants') && checkable_constant?(node)
80
+
81
+ suggestion = constant_typo_suggestion(node)
82
+ return unless suggestion
83
+
84
+ message = format(CONSTANT_MSG, name: node.short_name,
85
+ namespace: node.namespace.const_name,
86
+ suggestion: suggestion)
87
+ add_offense(node.loc.name, message: message)
88
+ end
89
+
90
+ def on_send(node)
91
+ return unless check?('CheckMethods')
92
+ return unless node.receiver&.const_type?
93
+ return if allowed_name?(node.method_name) || defined_check?(node)
94
+
95
+ suggestion = method_typo_suggestion(node)
96
+ return unless suggestion
97
+
98
+ message = format(METHOD_MSG, receiver: node.receiver.const_name,
99
+ name: node.method_name,
100
+ suggestion: suggestion)
101
+ add_offense(node.loc.selector, message: message)
102
+ end
103
+ alias on_csend on_send
104
+
105
+ def on_new_investigation
106
+ @literal_names = nil
107
+ super
108
+ end
109
+
110
+ private
111
+
112
+ def check?(key)
113
+ project_index && defined?(DidYouMean::SpellChecker) && cop_config.fetch(key, true)
114
+ end
115
+
116
+ def checkable_constant?(node)
117
+ node.namespace&.const_type? &&
118
+ !allowed_name?(node.short_name) &&
119
+ !definition_identifier?(node) && !defined_check?(node)
120
+ end
121
+
122
+ def allowed_name?(name)
123
+ cop_config.fetch('AllowedNames', []).include?(name.to_s)
124
+ end
125
+
126
+ # The last segment of a class, module or constant definition is being
127
+ # defined, not referenced.
128
+ def definition_identifier?(node)
129
+ node.parent&.defined_module
130
+ end
131
+
132
+ # `defined?(Foo::Bar)` probes whether a name exists; unknown names
133
+ # there are deliberate.
134
+ def defined_check?(node)
135
+ node.each_ancestor(:defined?).any?
136
+ end
137
+
138
+ def constant_typo_suggestion(node)
139
+ namespace = resolve_constant_in_index(node.namespace)
140
+ return nil unless namespace.is_a?(Rubydex::Namespace)
141
+ return nil if namespace.find_member(node.short_name.to_s)
142
+ return nil unless complete_index_members?(namespace)
143
+ return nil if literal_names.include?(node.short_name.to_s)
144
+
145
+ spell_check(node.short_name, constant_member_names(namespace))
146
+ rescue StandardError
147
+ nil
148
+ end
149
+
150
+ # Whether the index can be trusted to list everything the namespace
151
+ # defines. A name missing from an incomplete member list is not
152
+ # evidence of a typo, so both gaps have to be ruled out first:
153
+ # an ancestor that does not resolve may contribute the name, and so
154
+ # may a gem that reopens the namespace.
155
+ def complete_index_members?(declaration)
156
+ fully_resolved_index_ancestry?(declaration) && !gem_owned_namespace?(declaration)
157
+ end
158
+
159
+ # Whether the namespace's root segment names a gem in the bundle,
160
+ # which is then free to define members the index never sees: gem
161
+ # sources are only indexed when `AllCops/ProjectIndexIncludesGems`
162
+ # is enabled. A project that reopens such a namespace to add its own
163
+ # members (`module Flipper; module Adapters; ...`) makes it resolve
164
+ # in the index while most of what it holds stays invisible.
165
+ def gem_owned_namespace?(declaration)
166
+ return false if config.for_all_cops['ProjectIndexIncludesGems']
167
+
168
+ root = declaration.name.to_s.split('::').first.to_s
169
+ bundled_gem_namespaces.include?(root.downcase)
170
+ end
171
+
172
+ # Gem names normalized towards the constant they conventionally
173
+ # provide: `flipper` for `Flipper`, `activerecord` for `ActiveRecord`.
174
+ def bundled_gem_namespaces
175
+ @bundled_gem_namespaces ||= bundled_gem_names.to_set { |name| name.delete('-_').downcase }
176
+ end
177
+
178
+ # The bundle's gems except those sourced from a local path, whose code
179
+ # lives in the project and is indexed like the rest of it — among them
180
+ # the project's own gem, which its lockfile lists in the `PATH` section.
181
+ def bundled_gem_names
182
+ (config.gem_versions_in_target || {}).keys - (config.path_sourced_gems_in_target || [])
183
+ end
184
+
185
+ # Writers are indexed under the reader's name, so setter calls are
186
+ # verified and spell-checked through the base name.
187
+ def method_typo_suggestion(node)
188
+ setter = setter_call?(node)
189
+ base = setter ? node.method_name.to_s.delete_suffix('=') : node.method_name.to_s
190
+
191
+ declaration = unknown_method_owner(node, base)
192
+ return nil unless declaration
193
+ return nil if literal_names.include?(base)
194
+
195
+ suggestion = spell_check(base, method_member_names(declaration))
196
+ suggestion && setter ? "#{suggestion}=" : suggestion
197
+ rescue StandardError
198
+ nil
199
+ end
200
+
201
+ # The receiver's declaration when it resolves in the index, everything
202
+ # it can inherit or be reopened with is visible there, and the method
203
+ # is not found — nil otherwise.
204
+ def unknown_method_owner(node, base)
205
+ declaration = resolve_constant_in_index(node.receiver)
206
+ return nil unless declaration.is_a?(Rubydex::Namespace)
207
+ return nil if responds_in_index?(declaration, node.method_name.to_s, base)
208
+ return nil unless fully_resolved_index_ancestry?(declaration)
209
+ return nil if gem_owned_namespace?(declaration)
210
+
211
+ declaration
212
+ end
213
+
214
+ def setter_call?(node)
215
+ node.assignment_method? && !node.operator_method?
216
+ end
217
+
218
+ def responds_in_index?(declaration, name, base)
219
+ # Instance methods of a module are also callable on the module
220
+ # itself when exposed with `module_function`.
221
+ [name, base].uniq.any? do |candidate|
222
+ member_name = "#{candidate}()"
223
+ indexed_singleton_member(declaration, member_name) ||
224
+ declaration.find_member(member_name)
225
+ end
226
+ end
227
+
228
+ # Names mentioned as symbols or inside string literals in the current
229
+ # file belong to runtime definitions (`stub_const`, `const_set`,
230
+ # `define_method`) that the index cannot see.
231
+ def literal_names
232
+ @literal_names ||=
233
+ processed_source.ast.each_descendant(:sym, :str)
234
+ .with_object(Set.new) do |literal, names|
235
+ if literal.sym_type?
236
+ names << literal.value.to_s
237
+ else
238
+ # `scan` raises on binary string literals with invalid byte sequences.
239
+ value = literal.value
240
+ value = value.scrub unless value.valid_encoding?
241
+ value.scan(LITERAL_IDENTIFIER_PATTERN) { |token| names << token }
242
+ end
243
+ end
244
+ end
245
+
246
+ def constant_member_names(namespace)
247
+ namespace.members.filter_map do |member|
248
+ name = member.name
249
+ name.split('::').last unless name.include?('#') || name.include?('<')
250
+ end
251
+ end
252
+
253
+ def method_member_names(declaration)
254
+ scopes = declaration.ancestors.filter_map { |ancestor| indexed_singleton_of(ancestor) }
255
+ scopes << declaration
256
+
257
+ scopes.flat_map do |scope|
258
+ scope.members.filter_map { |member| member.name[METHOD_MEMBER_REGEXP, 1] }
259
+ end
260
+ end
261
+
262
+ def spell_check(name, dictionary)
263
+ return nil if dictionary.empty?
264
+
265
+ DidYouMean::SpellChecker.new(dictionary: dictionary.uniq).correct(name.to_s).first
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
@@ -53,6 +53,8 @@ module RuboCop
53
53
  end
54
54
 
55
55
  def after_send(node)
56
+ return if backrefs_unaffected?(node)
57
+
56
58
  @valid_ref = nil
57
59
 
58
60
  if regexp_first_argument?(node)
@@ -111,6 +113,19 @@ module RuboCop
111
113
  end
112
114
  end
113
115
 
116
+ # A call such as `hash[:key]` or `array[0]` cannot involve a regexp match:
117
+ # a non-string literal cannot act as a pattern (whereas `sub`, `gsub`,
118
+ # and `match` treat a string one as a pattern and do set backreferences),
119
+ # and `$~` is frame-local, so a user-defined method cannot change
120
+ # the caller's backreferences either.
121
+ def backrefs_unaffected?(send_node)
122
+ first_argument = send_node.first_argument
123
+ return false unless first_argument&.basic_literal?
124
+ return false if first_argument.str_type?
125
+
126
+ !regexp_receiver?(send_node)
127
+ end
128
+
114
129
  def regexp_first_argument?(send_node)
115
130
  send_node.first_argument&.regexp_type? \
116
131
  && REGEXP_ARGUMENT_METHODS.include?(send_node.method_name)
@@ -119,10 +134,6 @@ module RuboCop
119
134
  def regexp_receiver?(send_node)
120
135
  send_node.receiver&.regexp_type?
121
136
  end
122
-
123
- def nth_ref_receiver?(send_node)
124
- send_node.receiver&.nth_ref_type?
125
- end
126
137
  end
127
138
  end
128
139
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # The Lint/RedundantCopDisableDirective cop needs to be disabled so as
4
4
  # to be able to provide a (bad) example of a redundant disable.
5
- # rubocop:disable Lint/RedundantCopDisableDirective
5
+ # rubocop:disable-next Lint/RedundantCopDisableDirective
6
6
  module RuboCop
7
7
  module Cop
8
8
  module Lint
@@ -104,17 +104,35 @@ module RuboCop
104
104
  end
105
105
 
106
106
  def each_redundant_disable(&block)
107
+ each_detached_next_directive(&block)
108
+
107
109
  cop_disabled_line_ranges.each do |cop, line_ranges|
110
+ # A pending cop that is not enabled in this run produces no
111
+ # offenses, so its directives cannot be judged - they typically
112
+ # prepare the code for the moment the cop gets enabled.
113
+ next if pending_cop_not_run?(cop)
114
+
108
115
  each_already_disabled(cop, line_ranges, &block)
109
116
  each_line_range(cop, line_ranges, &block)
110
117
  end
111
118
  end
112
119
 
120
+ def pending_cop_not_run?(cop)
121
+ # `Config#for_cop` corrects wrongly-namespaced names, but a name that
122
+ # is not registered as written never suppresses anything, so it is
123
+ # reported as unknown rather than exempted via the corrected cop.
124
+ return false unless all_cop_names.include?(cop)
125
+
126
+ cop_cfg = config.for_cop(cop)
127
+ cop_cfg['Enabled'] == 'pending' &&
128
+ !processed_source.registry.enabled_pending_cop?(cop_cfg, config, cop)
129
+ end
130
+
113
131
  def each_line_range(cop, line_ranges)
114
132
  line_ranges.each_with_index do |line_range, line_range_index|
115
133
  next if should_skip_line_range?(cop, line_range)
116
134
 
117
- comment = processed_source.comment_at_line(line_range.begin)
135
+ comment = directive_comment(line_range)
118
136
  next if skip_directive?(comment)
119
137
 
120
138
  next_range = line_ranges[line_range_index + 1]
@@ -127,6 +145,24 @@ module RuboCop
127
145
  ignore_offense?(line_range) || expected_final_disable?(cop, line_range)
128
146
  end
129
147
 
148
+ # A range opened by a `disable-next` directive starts at the statement,
149
+ # not at the directive comment above it, so ask the range itself.
150
+ def directive_comment(line_range)
151
+ if line_range.respond_to?(:directive)
152
+ line_range.directive.comment
153
+ else
154
+ processed_source.comment_at_line(line_range.begin)
155
+ end
156
+ end
157
+
158
+ # A `disable-next` with no statement attached (or misplaced at the end
159
+ # of a code line) suppresses nothing, so it is redundant by definition.
160
+ def each_detached_next_directive
161
+ processed_source.comment_config.detached_next_directives.each do |directive|
162
+ directive.raw_cop_names.each { |cop| yield directive.comment, cop }
163
+ end
164
+ end
165
+
130
166
  def skip_directive?(comment)
131
167
  directive = DirectiveComment.new(comment)
132
168
  directive.push? || directive.pop?
@@ -142,7 +178,7 @@ module RuboCop
142
178
  end
143
179
  end
144
180
 
145
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
181
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
146
182
  def each_already_disabled(cop, line_ranges)
147
183
  line_ranges.each_cons(2) do |previous_range, range|
148
184
  next if ignore_offense?(range)
@@ -152,7 +188,7 @@ module RuboCop
152
188
  # whether there are offenses or not.
153
189
  next unless followed_ranges?(previous_range, range)
154
190
 
155
- comment = processed_source.comment_at_line(range.begin)
191
+ comment = directive_comment(range)
156
192
 
157
193
  next unless comment
158
194
  # Comments disabling all cops don't count since it's reasonable
@@ -170,7 +206,6 @@ module RuboCop
170
206
  yield comment, redundant if redundant
171
207
  end
172
208
  end
173
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
174
209
 
175
210
  def find_redundant_cop(cop, range)
176
211
  cop_offenses = offenses_to_check.select { |offense| offense.cop_name == cop }
@@ -198,7 +233,11 @@ module RuboCop
198
233
  end
199
234
 
200
235
  def range_with_offense?(range, offenses = offenses_to_check)
201
- offenses.none? { |offense| range.cover?(offense.line) }
236
+ # An offense is covered when the disabled range overlaps any line of
237
+ # its span, mirroring how offenses are suppressed.
238
+ offenses.none? do |offense|
239
+ offense.line <= range.end && offense.location.last_line >= range.begin
240
+ end
202
241
  end
203
242
 
204
243
  def all_disabled?(comment)
@@ -244,14 +283,28 @@ module RuboCop
244
283
  location = DirectiveComment.new(comment).range
245
284
  cop_names = cops.sort.map { |c| describe(c) }.join(', ')
246
285
 
286
+ # An unknown cop may just not be loaded in this run (e.g. a custom
287
+ # cop whose configuration failed to load) - removing its directive
288
+ # would destroy something that cannot be restored, so only report.
289
+ # A misplaced EOL `disable-next` should be moved, not deleted.
290
+ if any_unknown_cop?(cops) || misplaced_next_directive?(comment)
291
+ return add_offense(location, message: message(cop_names))
292
+ end
293
+
247
294
  add_offense(location, message: message(cop_names)) do |corrector|
248
- range = comment_range_with_surrounding_space(location, comment.source_range)
295
+ remove_entire_comment(corrector, comment)
296
+ end
297
+ end
249
298
 
250
- if leave_free_comment?(comment, range)
251
- corrector.replace(range, ' # ')
252
- else
253
- corrector.remove(range)
254
- end
299
+ def remove_entire_comment(corrector, comment)
300
+ # Take the `--` reason with the directive; `leave_free_comment?` keeps anything else.
301
+ directive_range = DirectiveComment.new(comment).range_with_reason
302
+ range = comment_range_with_surrounding_space(directive_range, comment.source_range)
303
+
304
+ if leave_free_comment?(comment, range)
305
+ corrector.replace(range, ' # ')
306
+ else
307
+ corrector.remove(range)
255
308
  end
256
309
  end
257
310
 
@@ -261,14 +314,35 @@ module RuboCop
261
314
  ranges = cop_ranges.map { |_, r| r }
262
315
 
263
316
  cop_ranges.each do |cop, range|
264
- cop_name = describe(cop)
265
- add_offense(range, message: message(cop_name)) do |corrector|
266
- range = directive_range_in_list(range, ranges)
267
- corrector.remove(range)
268
- end
317
+ add_offense_for_cop_in_list(cop, range, ranges)
318
+ end
319
+ end
320
+
321
+ def add_offense_for_cop_in_list(cop, range, ranges)
322
+ cop_name = describe(cop)
323
+ return add_offense(range, message: message(cop_name)) if unknown_cop?(cop)
324
+
325
+ add_offense(range, message: message(cop_name)) do |corrector|
326
+ corrector.remove(directive_range_in_list(range, ranges))
269
327
  end
270
328
  end
271
329
 
330
+ def any_unknown_cop?(cops)
331
+ cops.any? { |cop| unknown_cop?(cop) }
332
+ end
333
+
334
+ def misplaced_next_directive?(comment)
335
+ directive = DirectiveComment.new(comment)
336
+ directive.disable_next? &&
337
+ !processed_source.comment_config.comment_only_line?(directive.line_number)
338
+ end
339
+
340
+ def unknown_cop?(cop)
341
+ return false if cop == 'all' || department_marker?(cop)
342
+
343
+ !all_cop_names.include?(cop)
344
+ end
345
+
272
346
  def leave_free_comment?(comment, range)
273
347
  free_comment = comment.text.gsub(range.source.strip, '')
274
348
 
@@ -303,7 +377,13 @@ module RuboCop
303
377
 
304
378
  SIMILAR_COP_NAMES_CACHE = Hash.new do |hash, cop_name|
305
379
  hash[:all_cop_names] = Registry.global.names unless hash.key?(:all_cop_names)
306
- hash[cop_name] = NameSimilarity.find_similar_name(cop_name, hash[:all_cop_names])
380
+ # A registered cop with the same base name is a better suggestion
381
+ # than anything spelling similarity can find - the most common
382
+ # mistake is qualifying a cop with the wrong department.
383
+ basename = "/#{cop_name.split('/').last}"
384
+ same_basename = hash[:all_cop_names].find { |name| name.end_with?(basename) }
385
+ hash[cop_name] =
386
+ same_basename || NameSimilarity.find_similar_name(cop_name, hash[:all_cop_names])
307
387
  end
308
388
  private_constant :SIMILAR_COP_NAMES_CACHE
309
389
 
@@ -344,4 +424,3 @@ module RuboCop
344
424
  end
345
425
  end
346
426
  end
347
- # rubocop:enable Lint/RedundantCopDisableDirective