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
@@ -23,8 +23,8 @@ module RuboCop
23
23
  global.without_department(:Test).cops
24
24
  end
25
25
 
26
- def self.qualified_cop_name(name, origin, warn: true)
27
- global.qualified_cop_name(name, origin, warn: warn)
26
+ def self.qualified_cop_name(name, origin, warn: true, correct_namespace: true)
27
+ global.qualified_cop_name(name, origin, warn: warn, correct_namespace: correct_namespace)
28
28
  end
29
29
 
30
30
  # Changes momentarily the global registry
@@ -50,21 +50,37 @@ module RuboCop
50
50
 
51
51
  def initialize(cops = [], options = {})
52
52
  @departments = Set.new
53
+ # Maps each badge to its cop class, or to the fully qualified constant name (a `String`)
54
+ # when the cop is registered for lazy loading and its file is not loaded yet.
55
+ # A single insertion-ordered map keeps the registration order stable no matter when
56
+ # a lazy-loaded cop's file gets loaded, because reassigning an existing key preserves
57
+ # its position; the cop execution order depends on this order.
53
58
  @cops_by_badge = {}
54
- @lazy_loaded_cops_by_badge = {}
55
59
 
56
60
  @enrollment_queue = cops
57
61
  @options = options
58
62
 
59
63
  @enabled_cache = {}.compare_by_identity
60
64
  @disabled_cache = {}.compare_by_identity
65
+ @disabled_names_cache = {}.compare_by_identity
61
66
  @warnings = {}
62
67
  end
63
68
 
64
- def lazy_load(cop_name, constant_name)
65
- badge = Badge.parse(cop_name)
69
+ # Registers a cop by its badge and constant name without loading the class.
70
+ # The constant is resolved (loading the cop's file through `autoload`) only
71
+ # when the cop class itself is needed.
72
+ #
73
+ # @param badge [Badge, String] the badge, or the qualified cop name
74
+ # @param constant_name [String] the fully qualified constant name
75
+ def lazy_load(badge, constant_name)
76
+ badge = Badge.parse(badge) if badge.is_a?(String)
77
+
78
+ # File any cops enlisted earlier first, so that the registration
79
+ # order follows the call order.
80
+ clear_enrollment_queue
81
+
66
82
  @departments << badge.department
67
- @lazy_loaded_cops_by_badge[badge] = constant_name
83
+ @cops_by_badge[badge] = constant_name unless @cops_by_badge[badge].is_a?(Class)
68
84
  end
69
85
 
70
86
  def enlist(cop)
@@ -72,7 +88,16 @@ module RuboCop
72
88
  end
73
89
 
74
90
  def dismiss(cop)
75
- raise "Cop #{cop} could not be dismissed" unless @enrollment_queue.delete(cop)
91
+ dismissed = !@enrollment_queue.delete(cop).nil?
92
+
93
+ # A lazy-loaded cop that was not enlisted yet is dismissed by removing its registration.
94
+ # Anonymous cop classes cannot have a badge.
95
+ if !cop.name.nil? && @cops_by_badge[cop.badge].is_a?(String)
96
+ @cops_by_badge.delete(cop.badge)
97
+ dismissed = true
98
+ end
99
+
100
+ raise "Cop #{cop} could not be dismissed" unless dismissed
76
101
  end
77
102
 
78
103
  # @return [Array<Symbol>] list of departments for current cops.
@@ -83,12 +108,26 @@ module RuboCop
83
108
 
84
109
  # @return [Registry] Cops for that specific department.
85
110
  def with_department(department)
86
- with(cops.select { |cop| cop.department == department })
111
+ filter_by_badge { |badge| badge.department == department }
87
112
  end
88
113
 
89
114
  # @return [Registry] Cops not for a specific department.
90
115
  def without_department(department)
91
- with(cops.reject { |cop| cop.department == department })
116
+ filter_by_badge { |badge| badge.department != department }
117
+ end
118
+
119
+ # Returns a new registry containing the cops whose badge satisfies the given block,
120
+ # keeping lazy-loaded cops unloaded.
121
+ #
122
+ # @api private
123
+ # @return [Registry]
124
+ def filter_by_badge(options = {})
125
+ clear_enrollment_queue
126
+ copy = self.class.new([], options)
127
+ @cops_by_badge.each do |badge, cop_or_name|
128
+ copy.add_entry(badge, cop_or_name) if yield(badge)
129
+ end
130
+ copy
92
131
  end
93
132
 
94
133
  # @return [Boolean] Checks if given name is department
@@ -97,7 +136,7 @@ module RuboCop
97
136
  end
98
137
 
99
138
  def contains_cop_matching?(names)
100
- cops.any? { |cop| cop.match?(names) }
139
+ registered_badges.any? { |badge| badge.match_name?(names) }
101
140
  end
102
141
 
103
142
  # Convert a user provided cop name into a properly namespaced name
@@ -132,11 +171,19 @@ module RuboCop
132
171
  # @note Emits a warning if the provided name has an incorrect namespace
133
172
  #
134
173
  # @return [String] Qualified cop name
135
- def qualified_cop_name(name, path, warn: true)
174
+ def qualified_cop_name(name, path, warn: true, correct_namespace: true)
136
175
  badge = Badge.parse(name)
137
176
  print_department_missing_warning(name, path) if warn && department_missing?(badge, name)
138
177
  return name if registered?(badge)
178
+ # A name qualified with the wrong department is returned as is when
179
+ # namespace correction is not wanted, so the caller can surface the
180
+ # mistake instead of silently acting on the corrected name.
181
+ return name if badge.qualified? && !correct_namespace
182
+
183
+ resolve_cop_name(badge, name, path, warn)
184
+ end
139
185
 
186
+ def resolve_cop_name(badge, name, path, warn)
140
187
  potential_badges = qualify_badge(badge)
141
188
 
142
189
  case potential_badges.size
@@ -160,9 +207,10 @@ module RuboCop
160
207
 
161
208
  def unqualified_cop_names
162
209
  clear_enrollment_queue
163
- @unqualified_cop_names ||=
164
- (@cops_by_badge.keys | @lazy_loaded_cops_by_badge.keys)
165
- .to_set { |badge| File.basename(badge.to_s) } << 'RedundantCopDisableDirective'
210
+
211
+ @unqualified_cop_names ||= @cops_by_badge.keys.to_set do |badge|
212
+ File.basename(badge.to_s)
213
+ end << 'RedundantCopDisableDirective'
166
214
  end
167
215
 
168
216
  def qualify_badge(badge)
@@ -187,51 +235,64 @@ module RuboCop
187
235
 
188
236
  def length
189
237
  clear_enrollment_queue
190
- @cops_by_badge.size + @lazy_loaded_cops_by_badge.size
238
+ @cops_by_badge.size
191
239
  end
192
240
 
241
+ # Returns the enabled cop classes, loading lazy-loaded cops only when they are enabled.
242
+ #
243
+ # @return [Array<Class>]
193
244
  def enabled(config)
194
- @enabled_cache[config] ||= select { |cop| enabled?(cop, config) }
245
+ @enabled_cache[config] ||= registered_badges.filter_map do |badge|
246
+ next unless enabled_cop_name?(badge.to_s, config)
247
+
248
+ cop_or_name = @cops_by_badge[badge]
249
+ cop_or_name.is_a?(String) ? load_lazy_cop(badge) : cop_or_name
250
+ end
195
251
  end
196
252
 
197
253
  def disabled(config)
198
254
  @disabled_cache[config] ||= reject { |cop| enabled?(cop, config) }
199
255
  end
200
256
 
201
- def enabled?(cop, config)
202
- return true if options[:only]&.include?(cop.cop_name)
203
-
204
- # We need to use `cop_name` in this case, because `for_cop` uses caching
205
- # which expects cop names or cop classes as keys.
206
- cfg = config.for_cop(cop.cop_name)
207
-
208
- cop_enabled = cfg.fetch('Enabled') == true || enabled_pending_cop?(cfg, config)
209
-
210
- if options.fetch(:safe, false)
211
- cop_enabled && cfg.fetch('Safe', true)
212
- else
213
- cop_enabled
257
+ # Returns the names of the disabled cops without loading any of them.
258
+ #
259
+ # @api private
260
+ # @return [Array<String>]
261
+ def disabled_names(config)
262
+ @disabled_names_cache[config] ||= registered_badges.filter_map do |badge|
263
+ name = badge.to_s
264
+ name unless enabled_cop_name?(name, config)
214
265
  end
215
266
  end
216
267
 
217
- def enabled_pending_cop?(cop_cfg, config)
268
+ def enabled?(cop, config)
269
+ enabled_cop_name?(cop.cop_name, config)
270
+ end
271
+
272
+ # @param cop [Class, String, nil] the cop class or the qualified cop name
273
+ def enabled_pending_cop?(cop_cfg, config, cop = nil)
218
274
  return false if @options[:disable_pending_cops]
275
+ return false unless cop_cfg.fetch('Enabled') == 'pending'
276
+ return true if @options[:enable_pending_cops]
277
+ return config.enabled_new_cops? unless cop
219
278
 
220
- cop_cfg.fetch('Enabled') == 'pending' &&
221
- (@options[:enable_pending_cops] || config.enabled_new_cops?)
279
+ cop_name = cop.is_a?(String) ? cop : cop.cop_name
280
+ config.enabled_new_cop?(cop_name)
222
281
  end
223
282
 
224
283
  def names
225
284
  clear_enrollment_queue
226
- @cops_by_badge.keys.map(&:to_s) | @lazy_loaded_cops_by_badge.keys.map(&:to_s)
285
+ @cops_by_badge.keys.map(&:to_s)
227
286
  end
228
287
 
229
288
  def cops_for_department(department)
230
- cops.select { |cop| cop.department == department.to_sym }
289
+ with_department(department.to_sym).cops
231
290
  end
232
291
 
233
292
  def names_for_department(department)
234
- cops_for_department(department).map(&:cop_name)
293
+ department = department.to_sym
294
+
295
+ registered_badges.filter_map { |badge| badge.to_s if badge.department == department }
235
296
  end
236
297
 
237
298
  def ==(other)
@@ -259,7 +320,9 @@ module RuboCop
259
320
  def find_by_cop_name(cop_name)
260
321
  clear_enrollment_queue
261
322
  badge = Badge.parse(cop_name)
262
- @cops_by_badge[badge] || load_lazy_cop(badge)
323
+ cop_or_name = @cops_by_badge[badge]
324
+
325
+ cop_or_name.is_a?(String) ? load_lazy_cop(badge) : cop_or_name
263
326
  end
264
327
 
265
328
  # When a cop name is given returns a single-element array with the cop class.
@@ -270,10 +333,33 @@ module RuboCop
270
333
  cop ? [cop] : cops_for_department(directive)
271
334
  end
272
335
 
336
+ # Loads all cops that are registered for lazy loading. Useful when lazily registered cops
337
+ # must not outlive the current global registry, e.g. before a temporary global registry is
338
+ # discarded in tests.
339
+ #
340
+ # @api private
341
+ def load_all_lazy_cops
342
+ # Take a snapshot because loading a cop can load (and thereby register)
343
+ # another lazy-loaded cop, e.g. its superclass.
344
+ badges = @cops_by_badge.keys
345
+
346
+ badges.each do |badge|
347
+ load_lazy_cop(badge) if @cops_by_badge[badge].is_a?(String)
348
+ end
349
+ end
350
+
273
351
  def freeze
274
352
  clear_enrollment_queue
275
353
  load_all_lazy_cops
276
354
  unqualified_cop_names # build cache
355
+
356
+ # Freeze the structural collections as well, so that a cop class defined after
357
+ # the freeze fails fast at its `enlist`/`lazy_load` call site instead of corrupting
358
+ # the registry and failing much later.
359
+ @cops_by_badge.freeze
360
+ @departments.freeze
361
+ @enrollment_queue.freeze
362
+
277
363
  super
278
364
  end
279
365
 
@@ -287,35 +373,83 @@ module RuboCop
287
373
  @warnings[path]
288
374
  end
289
375
 
376
+ protected
377
+
378
+ # Adds an already-loaded cop class or a lazy-load constant name under the given badge,
379
+ # used to build filtered copies.
380
+ def add_entry(badge, cop_or_name)
381
+ @departments << badge.department
382
+ @cops_by_badge[badge] = cop_or_name
383
+ end
384
+
290
385
  private
291
386
 
292
387
  def initialize_copy(reg)
293
- initialize(reg.cops, reg.options)
388
+ super
389
+ @cops_by_badge = @cops_by_badge.dup
390
+ @enrollment_queue = @enrollment_queue.dup
391
+ @departments = @departments.dup
392
+ @enabled_cache = {}.compare_by_identity
393
+ @disabled_cache = {}.compare_by_identity
394
+ @disabled_names_cache = {}.compare_by_identity
395
+ @warnings = {}
396
+ @unqualified_cop_names = nil
294
397
  end
295
398
 
296
399
  def clear_enrollment_queue
297
400
  return if @enrollment_queue.empty?
298
401
 
299
402
  @enrollment_queue.each do |cop|
403
+ # Reassigning a badge that was registered for lazy loading keeps
404
+ # its original position in the insertion-ordered map.
300
405
  @cops_by_badge[cop.badge] = cop
301
406
  @departments << cop.department
302
407
  end
303
408
  @enrollment_queue = []
304
409
  end
305
410
 
306
- def load_all_lazy_cops
307
- @lazy_loaded_cops_by_badge.each_key { |badge| load_lazy_cop(badge) }
411
+ def registered_badges
412
+ clear_enrollment_queue
413
+ @cops_by_badge.keys
308
414
  end
309
415
 
310
- def load_lazy_cop(badge)
311
- constant_name = @lazy_loaded_cops_by_badge.delete(badge)
312
- return unless constant_name
416
+ def enabled_cop_name?(cop_name, config)
417
+ return true if options[:only]&.include?(cop_name)
418
+
419
+ # We need to use the cop name in this case, because `for_cop` uses
420
+ # caching which expects cop names or cop classes as keys.
421
+ cfg = config.for_cop(cop_name)
313
422
 
314
- @cops_by_badge[badge] = Kernel.const_get(constant_name)
423
+ cop_enabled = cfg.fetch('Enabled') == true || enabled_pending_cop?(cfg, config, cop_name)
424
+
425
+ if options.fetch(:safe, false)
426
+ cop_enabled && cfg.fetch('Safe', true)
427
+ else
428
+ cop_enabled
429
+ end
315
430
  end
316
431
 
317
- def with(cops)
318
- self.class.new(cops)
432
+ # @return [Class, nil] the loaded cop class, or nil if the cop excluded
433
+ # itself from the registry while being loaded
434
+ def load_lazy_cop(badge)
435
+ constant_name = @cops_by_badge[badge]
436
+ return constant_name unless constant_name.is_a?(String)
437
+
438
+ cop = Kernel.const_get(constant_name)
439
+ if equal?(self.class.global)
440
+ # Resolving the constant fires the autoload, which makes
441
+ # `Base.inherited` enlist the class into the global registry.
442
+ # Enrolling through the queue keeps `exclude_from_registry` honored:
443
+ # a cop that dismissed itself leaves a dangling constant name behind
444
+ # and is removed from the registry.
445
+ clear_enrollment_queue
446
+ cop_or_name = @cops_by_badge[badge]
447
+ @cops_by_badge.delete(badge) if cop_or_name.is_a?(String)
448
+ cop_or_name.is_a?(Class) ? cop_or_name : nil
449
+ else
450
+ # `Base.inherited` enlists into the global registry, not this copy.
451
+ @cops_by_badge[badge] = cop
452
+ end
319
453
  end
320
454
 
321
455
  def resolve_badge(given_badge, real_badge, source_path, warn: true)
@@ -335,7 +469,7 @@ module RuboCop
335
469
 
336
470
  def registered?(badge)
337
471
  clear_enrollment_queue
338
- @cops_by_badge.key?(badge) || @lazy_loaded_cops_by_badge.key?(badge)
472
+ @cops_by_badge.key?(badge)
339
473
  end
340
474
  end
341
475
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Cops for the `Security` department. The department's cops are registered for lazy loading
6
+ # and their files are loaded on demand.
7
+ module Security
8
+ extend LazyLoader
9
+
10
+ register_cop :CompoundHash, "#{__dir__}/security/compound_hash"
11
+ register_cop :Eval, "#{__dir__}/security/eval"
12
+ register_cop :IoMethods, "#{__dir__}/security/io_methods"
13
+ register_cop :JSONLoad, "#{__dir__}/security/json_load"
14
+ register_cop :MarshalLoad, "#{__dir__}/security/marshal_load"
15
+ register_cop :Open, "#{__dir__}/security/open"
16
+ register_cop :YAMLLoad, "#{__dir__}/security/yaml_load"
17
+ end
18
+ end
19
+ end
@@ -100,7 +100,7 @@ module RuboCop
100
100
  comment_line?(processed_source[node.first_line - 2])
101
101
  end
102
102
 
103
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
103
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
104
104
  def groupable_accessor?(node)
105
105
  return true unless (previous_expression = node.left_siblings.last)
106
106
 
@@ -122,7 +122,6 @@ module RuboCop
122
122
  previous_expression.access_modifier? ||
123
123
  node.first_line - previous_expression.last_line > 1 # there is a space between nodes
124
124
  end
125
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
126
125
 
127
126
  def class_send_elements(class_node)
128
127
  class_def = class_node.body
@@ -197,7 +197,7 @@ module RuboCop
197
197
  send_classifications.all? { |_, c, _, _| all_classifications.include?(c) }
198
198
  end
199
199
 
200
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
200
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
201
201
  def add_forward_all_offenses(node, send_classifications, forwardable_args)
202
202
  rest_arg, kwrest_arg, block_arg = *forwardable_args
203
203
  registered_block_arg_offense = false
@@ -220,13 +220,12 @@ module RuboCop
220
220
 
221
221
  register_forward_all_offense(node, node.arguments, rest_arg || kwrest_arg)
222
222
  end
223
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
224
223
 
225
224
  def forward_all_first_argument(node)
226
225
  node.arguments.reverse_each.find(&:forwarded_restarg_type?)
227
226
  end
228
227
 
229
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
228
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
230
229
  def add_post_ruby_32_offenses(def_node, send_classifications, forwardable_args)
231
230
  return unless use_anonymous_forwarding?
232
231
  return unless all_forwarding_offenses_correctable?(send_classifications)
@@ -250,7 +249,6 @@ module RuboCop
250
249
  end
251
250
  end
252
251
  end
253
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
254
252
 
255
253
  def non_splat_or_block_pass_lvar_references(body)
256
254
  body.each_descendant(:lvar, :lvasgn).filter_map do |lvar|
@@ -473,7 +471,7 @@ module RuboCop
473
471
 
474
472
  private
475
473
 
476
- # rubocop:disable Metrics/CyclomaticComplexity
474
+ # rubocop:disable-next Metrics/CyclomaticComplexity
477
475
  def can_forward_all?
478
476
  return false if any_arg_referenced?
479
477
  return false if ruby_30_or_lower_optarg?
@@ -483,7 +481,6 @@ module RuboCop
483
481
 
484
482
  no_additional_args? || (target_ruby_version >= 3.0 && no_post_splat_args?)
485
483
  end
486
- # rubocop:enable Metrics/CyclomaticComplexity
487
484
 
488
485
  # def foo(a = 41, ...) is a syntax error in 3.0.
489
486
  def ruby_30_or_lower_optarg?
@@ -31,7 +31,7 @@ module RuboCop
31
31
  MSG = 'Use `%<preferred>s`.'
32
32
  RESTRICT_ON_SEND = %i[[]].freeze
33
33
 
34
- # rubocop:disable Metrics/AbcSize
34
+ # rubocop:disable-next Metrics/AbcSize
35
35
  def on_send(node)
36
36
  return unless node.arguments.size == 1 && node.first_argument.int_type?
37
37
 
@@ -49,7 +49,6 @@ module RuboCop
49
49
  corrector.replace(offense_range, preferred_value(node, preferred))
50
50
  end
51
51
  end
52
- # rubocop:enable Metrics/AbcSize
53
52
  alias on_csend on_send
54
53
 
55
54
  private
@@ -30,6 +30,11 @@ module RuboCop
30
30
  # only cases where exactly one argument is provided can be replaced with
31
31
  # `Array#intersect?` and are handled by this cop.
32
32
  #
33
+ # NOTE: In the block form, `include?` is only detected when its receiver is
34
+ # an array literal, because `include?` is defined with different semantics
35
+ # on many non-array classes (e.g. `String#include?` checks for substrings).
36
+ # `member?` does not have this restriction.
37
+ #
33
38
  # @safety
34
39
  # This cop cannot guarantee that `array1` and `array2` are
35
40
  # actually arrays while method `intersect?` is for arrays only.
@@ -47,7 +52,7 @@ module RuboCop
47
52
  #
48
53
  # # bad
49
54
  # array1.any? { |elem| array2.member?(elem) }
50
- # array1.none? { |elem| array2.include?(elem) }
55
+ # array1.none? { |elem| [1, 2].include?(elem) }
51
56
  #
52
57
  # # good
53
58
  # array1.intersect?(array2)
@@ -121,15 +126,15 @@ module RuboCop
121
126
  (block
122
127
  (call $_receiver ${:any? :none?})
123
128
  (args (arg _key))
124
- (send $_argument {:member? :include?} (lvar _key))
129
+ (send $!nil? ${:member? :include?} (lvar _key))
125
130
  )
126
131
  (numblock
127
132
  (call $_receiver ${:any? :none?}) 1
128
- (send $_argument {:member? :include?} (lvar :_1))
133
+ (send $!nil? ${:member? :include?} (lvar :_1))
129
134
  )
130
135
  (itblock
131
136
  (call $_receiver ${:any? :none?}) :it
132
- (send $_argument {:member? :include?} (lvar :it))
137
+ (send $!nil? ${:member? :include?} (lvar :it))
133
138
  )
134
139
  }
135
140
  PATTERN
@@ -156,7 +161,10 @@ module RuboCop
156
161
  alias on_csend on_send
157
162
 
158
163
  def on_block(node)
159
- return unless (receiver, method_name, argument = any_none_block_intersection(node))
164
+ return unless (captures = any_none_block_intersection(node))
165
+
166
+ receiver, method_name, argument, block_method = captures
167
+ return if uncorrectable_block_intersection?(node, method_name, argument, block_method)
160
168
 
161
169
  dot = node.send_node.loc.dot.source
162
170
  bang = method_name == :any? ? '' : '!'
@@ -186,6 +194,17 @@ module RuboCop
186
194
  STRAIGHT_METHODS.include?(method_name.to_sym)
187
195
  end
188
196
 
197
+ def uncorrectable_block_intersection?(node, method_name, argument, block_method)
198
+ # `a&.none? { |elem| b.include?(elem) }` returns `nil` when `a` is `nil`,
199
+ # but the negated rewrite `!a&.intersect?(b)` returns `true` there, flipping the result.
200
+ return true if method_name == :none? && node.send_node.safe_navigation?
201
+
202
+ # `include?` is defined with different semantics on many non-array classes
203
+ # (e.g. `String#include?` checks for substrings), so it is only a reliable
204
+ # signal of an intersection check when the receiver is an array literal.
205
+ block_method == :include? && !argument.array_type?
206
+ end
207
+
189
208
  def register_offense(node, replacement)
190
209
  message = format(MSG, replacement: replacement, existing: node.source)
191
210
 
@@ -70,7 +70,7 @@ module RuboCop
70
70
  (send _ :& _))
71
71
  PATTERN
72
72
 
73
- # rubocop:disable Metrics/AbcSize
73
+ # rubocop:disable-next Metrics/AbcSize
74
74
  def on_send(node)
75
75
  return unless node.receiver&.begin_type?
76
76
  return unless (preferred_method = preferred_method(node))
@@ -88,7 +88,6 @@ module RuboCop
88
88
  corrector.replace(node, preferred)
89
89
  end
90
90
  end
91
- # rubocop:enable Metrics/AbcSize
92
91
 
93
92
  private
94
93
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/ClassLength
3
+ # rubocop:disable-next Metrics/ClassLength
4
4
  module RuboCop
5
5
  module Cop
6
6
  module Style
@@ -344,7 +344,7 @@ module RuboCop
344
344
  node.respond_to?(:block_node) && node.block_node
345
345
  end
346
346
 
347
- # rubocop:disable Metrics/CyclomaticComplexity
347
+ # rubocop:disable-next Metrics/CyclomaticComplexity
348
348
  def get_blocks(node, &block)
349
349
  case node.type
350
350
  when :block, :numblock, :itblock
@@ -368,9 +368,8 @@ module RuboCop
368
368
  node.each_child_node { |child| get_blocks(child, &block) }
369
369
  end
370
370
  end
371
- # rubocop:enable Metrics/CyclomaticComplexity
372
371
 
373
- # rubocop:disable Metrics/CyclomaticComplexity -- inlined special_method checks to avoid double evaluation
372
+ # rubocop:disable-next Metrics/CyclomaticComplexity -- inlined special_method checks to avoid double evaluation
374
373
  def proper_block_style?(node)
375
374
  return true if require_do_end?(node)
376
375
 
@@ -385,7 +384,6 @@ module RuboCop
385
384
  when :always_braces then node.braces?
386
385
  end
387
386
  end
388
- # rubocop:enable Metrics/CyclomaticComplexity
389
387
 
390
388
  def require_do_end?(node)
391
389
  return false if node.braces? || node.multiline?
@@ -507,4 +505,3 @@ module RuboCop
507
505
  end
508
506
  end
509
507
  end
510
- # rubocop:enable Metrics/ClassLength
@@ -98,7 +98,7 @@ module RuboCop
98
98
  node.elsif_conditional? && min_branches_count?(node)
99
99
  end
100
100
 
101
- # rubocop:disable Metrics/MethodLength
101
+ # rubocop:disable-next Metrics/MethodLength
102
102
  def find_target(node)
103
103
  case node.type
104
104
  when :begin
@@ -116,7 +116,6 @@ module RuboCop
116
116
  find_target_in_send_node(node)
117
117
  end
118
118
  end
119
- # rubocop:enable Metrics/MethodLength
120
119
 
121
120
  def find_target_in_send_node(node)
122
121
  case node.method_name
@@ -181,7 +180,7 @@ module RuboCop
181
180
  conditions << condition if condition
182
181
  end
183
182
 
184
- # rubocop:disable Metrics/CyclomaticComplexity
183
+ # rubocop:disable-next Metrics/CyclomaticComplexity
185
184
  def condition_from_send_node(node, target)
186
185
  case node.method_name
187
186
  when :is_a?
@@ -196,7 +195,6 @@ module RuboCop
196
195
  condition_from_include_or_cover_node(node, target)
197
196
  end
198
197
  end
199
- # rubocop:enable Metrics/CyclomaticComplexity
200
198
 
201
199
  def condition_from_equality_node(node, target)
202
200
  condition = condition_from_binary_op(node.receiver, node.first_argument, target)