rubocop 1.88.0 → 1.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +72 -12
  3. data/lib/rubocop/comment_config.rb +35 -19
  4. data/lib/rubocop/config.rb +53 -0
  5. data/lib/rubocop/config_validator.rb +38 -1
  6. data/lib/rubocop/cop/badge.rb +8 -0
  7. data/lib/rubocop/cop/base.rb +19 -12
  8. data/lib/rubocop/cop/bundler/gem_comment.rb +3 -1
  9. data/lib/rubocop/cop/bundler.rb +19 -0
  10. data/lib/rubocop/cop/commissioner.rb +13 -11
  11. data/lib/rubocop/cop/corrector.rb +1 -1
  12. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  13. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  14. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  15. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  16. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  17. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
  18. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  19. data/lib/rubocop/cop/gemspec.rb +22 -0
  20. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  21. data/lib/rubocop/cop/generator.rb +4 -0
  22. data/lib/rubocop/cop/layout/block_alignment.rb +17 -0
  23. data/lib/rubocop/cop/layout/class_structure.rb +14 -4
  24. data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
  25. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  26. data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
  27. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  28. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  29. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  30. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  31. data/lib/rubocop/cop/layout/indentation_width.rb +31 -1
  32. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  33. data/lib/rubocop/cop/layout/line_length.rb +95 -17
  34. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +18 -3
  35. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  36. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  37. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
  38. data/lib/rubocop/cop/layout.rb +114 -0
  39. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  40. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  41. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  42. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  43. data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
  44. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  45. data/lib/rubocop/cop/lint/debugger.rb +13 -1
  46. data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
  47. data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
  48. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  49. data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
  50. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
  51. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  52. data/lib/rubocop/cop/lint/name_typo.rb +223 -0
  53. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +7 -0
  54. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
  55. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
  56. data/lib/rubocop/cop/lint/to_enum_arguments.rb +44 -2
  57. data/lib/rubocop/cop/lint/to_json.rb +8 -1
  58. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +32 -8
  59. data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
  60. data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
  61. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
  62. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  63. data/lib/rubocop/cop/lint/void.rb +19 -8
  64. data/lib/rubocop/cop/lint.rb +176 -0
  65. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  66. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  67. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  68. data/lib/rubocop/cop/metrics.rb +31 -0
  69. data/lib/rubocop/cop/migration/department_name.rb +0 -4
  70. data/lib/rubocop/cop/migration.rb +13 -0
  71. data/lib/rubocop/cop/mixin/allowed_methods.rb +5 -5
  72. data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
  73. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  74. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  75. data/lib/rubocop/cop/mixin/forbidden_pattern.rb +5 -1
  76. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  77. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  78. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  79. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  80. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  81. data/lib/rubocop/cop/mixin/statement_modifier.rb +43 -7
  82. data/lib/rubocop/cop/mixin.rb +78 -77
  83. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  84. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  85. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  86. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  87. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  88. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  89. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  90. data/lib/rubocop/cop/naming.rb +31 -0
  91. data/lib/rubocop/cop/offense.rb +10 -6
  92. data/lib/rubocop/cop/registry.rb +153 -43
  93. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  94. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  95. data/lib/rubocop/cop/security.rb +19 -0
  96. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  97. data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
  98. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  99. data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
  100. data/lib/rubocop/cop/style/collection_compact.rb +1 -1
  101. data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
  102. data/lib/rubocop/cop/style/copyright.rb +6 -1
  103. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  104. data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
  105. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  106. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
  107. data/lib/rubocop/cop/style/documentation.rb +47 -0
  108. data/lib/rubocop/cop/style/documentation_method.rb +5 -1
  109. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  110. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  111. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  112. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  113. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  114. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  115. data/lib/rubocop/cop/style/env_home.rb +4 -0
  116. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  117. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  118. data/lib/rubocop/cop/style/file_null.rb +4 -2
  119. data/lib/rubocop/cop/style/format_string.rb +13 -1
  120. data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
  121. data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
  122. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  123. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
  124. data/lib/rubocop/cop/style/if_unless_modifier.rb +11 -52
  125. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  126. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  127. data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
  128. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  129. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  130. data/lib/rubocop/cop/style/lambda.rb +11 -1
  131. data/lib/rubocop/cop/style/lambda_call.rb +11 -0
  132. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  133. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  134. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
  135. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +11 -5
  136. data/lib/rubocop/cop/style/method_def_parentheses.rb +5 -1
  137. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  138. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +40 -7
  139. data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
  140. data/lib/rubocop/cop/style/module_function.rb +4 -2
  141. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  142. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  143. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  144. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  145. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  146. data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
  147. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  148. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  149. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  150. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  151. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  152. data/lib/rubocop/cop/style/parallel_assignment.rb +11 -2
  153. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  154. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  155. data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
  156. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  157. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +13 -1
  158. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  159. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  160. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  161. data/lib/rubocop/cop/style/redundant_format.rb +40 -3
  162. data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
  163. data/lib/rubocop/cop/style/redundant_line_continuation.rb +80 -129
  164. data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
  165. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  166. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  167. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  168. data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
  169. data/lib/rubocop/cop/style/semicolon.rb +4 -4
  170. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  171. data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
  172. data/lib/rubocop/cop/style/static_class.rb +21 -0
  173. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  174. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
  175. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  176. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  177. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  178. data/lib/rubocop/cop/style.rb +310 -0
  179. data/lib/rubocop/cop/team.rb +66 -7
  180. data/lib/rubocop/cop/variable_force/variable.rb +18 -6
  181. data/lib/rubocop/cops_documentation_generator.rb +1 -1
  182. data/lib/rubocop/directive_comment.rb +2 -0
  183. data/lib/rubocop/feature_loader.rb +0 -6
  184. data/lib/rubocop/lsp/routes.rb +41 -3
  185. data/lib/rubocop/lsp/runtime.rb +4 -0
  186. data/lib/rubocop/lsp/server.rb +4 -0
  187. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  188. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  189. data/lib/rubocop/project_index_loader.rb +16 -0
  190. data/lib/rubocop/result_cache.rb +6 -4
  191. data/lib/rubocop/rspec/shared_contexts.rb +14 -2
  192. data/lib/rubocop/runner.rb +100 -33
  193. data/lib/rubocop/server/cache.rb +10 -5
  194. data/lib/rubocop/target_ruby.rb +1 -1
  195. data/lib/rubocop/version.rb +1 -1
  196. data/lib/rubocop.rb +17 -626
  197. metadata +18 -3
@@ -91,6 +91,8 @@ module RuboCop
91
91
  end
92
92
 
93
93
  def on_new_investigation
94
+ return unless @flagged_terms_regex
95
+
94
96
  investigate_filepath if cop_config['CheckFilepaths']
95
97
  investigate_tokens
96
98
  end
@@ -144,7 +146,7 @@ module RuboCop
144
146
  def preprocess_flagged_terms
145
147
  allowed_strings = []
146
148
  flagged_term_strings = []
147
- cop_config['FlaggedTerms'].each do |term, term_definition|
149
+ (cop_config['FlaggedTerms'] || {}).each do |term, term_definition|
148
150
  next if term_definition.nil?
149
151
 
150
152
  allowed_strings.concat(process_allowed_regex(term_definition['AllowedRegex']))
@@ -181,7 +183,11 @@ module RuboCop
181
183
  end
182
184
 
183
185
  def set_regexes(flagged_term_strings, allowed_strings)
184
- @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
186
+ # With no flagged terms an empty regex would match everything, so leave the
187
+ # regex nil and let `on_new_investigation` treat the cop as a no-op.
188
+ unless flagged_term_strings.empty?
189
+ @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
190
+ end
185
191
  @allowed_regex = array_to_ignorecase_regex(allowed_strings) unless allowed_strings.empty?
186
192
  end
187
193
 
@@ -174,6 +174,7 @@ module RuboCop
174
174
 
175
175
  method_node, method_name = find_definition(node)
176
176
  return unless method_node
177
+ return unless nameable_method?(method_name)
177
178
 
178
179
  body = method_node.body
179
180
  return unless body == node || body.children.last == node
@@ -209,6 +210,7 @@ module RuboCop
209
210
 
210
211
  method_node, method_name = find_definition(node)
211
212
  return false unless method_node
213
+ return false unless nameable_method?(method_name)
212
214
 
213
215
  defined_memoized?(method_node.body, arg.name) do |defined_ivar, return_ivar, ivar_assign|
214
216
  return false if matches?(method_name, ivar_assign)
@@ -250,6 +252,13 @@ module RuboCop
250
252
  nil
251
253
  end
252
254
 
255
+ # Operator and other non-word method names (e.g. `[]`, `+`, `<=>`) cannot form a
256
+ # valid instance variable name, so there is no matching ivar to enforce and a
257
+ # suggested correction like `@[]` would be invalid Ruby.
258
+ def nameable_method?(method_name)
259
+ /\A[a-zA-Z_]\w*\z/.match?(method_name.to_s.delete('!?='))
260
+ end
261
+
253
262
  def matches?(method_name, ivar_assign)
254
263
  return true if ivar_assign.nil? || INITIALIZE_METHODS.include?(method_name)
255
264
 
@@ -21,6 +21,11 @@ module RuboCop
21
21
  # offenses if the method has a Sorbet `sig` with a return type of
22
22
  # `T::Boolean`. Dynamic methods are not supported with this configuration.
23
23
  #
24
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is
25
+ # installed, methods that override a method defined by an ancestor
26
+ # elsewhere in the project are not reported, since renaming an override
27
+ # breaks the inherited contract.
28
+ #
24
29
  # @example NamePrefix: ['is_', 'has_', 'have_'] (default)
25
30
  # # bad
26
31
  # def is_even(value)
@@ -101,6 +106,7 @@ module RuboCop
101
106
  # def_node_matcher(:even?) { |value| }
102
107
  #
103
108
  class PredicatePrefix < Base
109
+ include ProjectIndexHelp
104
110
  include AllowedMethods
105
111
 
106
112
  # @!method dynamic_method_define(node)
@@ -124,18 +130,24 @@ module RuboCop
124
130
  end
125
131
 
126
132
  def on_def(node)
127
- predicate_prefixes.each do |prefix|
128
- method_name = node.method_name.to_s
129
-
130
- next if allowed_method_name?(method_name, prefix)
131
- next if use_sorbet_sigs? && !sorbet_sig?(node, return_type: 'T::Boolean')
133
+ method_name = node.method_name.to_s
134
+ offending_prefixes = offending_prefixes_for(node, method_name)
135
+ return if offending_prefixes.empty? || overrides_inherited_method?(node)
132
136
 
137
+ offending_prefixes.each do |prefix|
133
138
  add_offense(
134
139
  node.loc.name,
135
140
  message: message(method_name, expected_name(method_name, prefix))
136
141
  )
137
142
  end
138
143
  end
144
+
145
+ def offending_prefixes_for(node, method_name)
146
+ predicate_prefixes.reject do |prefix|
147
+ allowed_method_name?(method_name, prefix) ||
148
+ (use_sorbet_sigs? && !sorbet_sig?(node, return_type: 'T::Boolean'))
149
+ end
150
+ end
139
151
  alias on_defs on_def
140
152
 
141
153
  def validate_config
@@ -198,6 +210,22 @@ module RuboCop
198
210
  def method_definition_macro?(macro_name)
199
211
  cop_config['MethodDefinitionMacros'].include?(macro_name.to_s)
200
212
  end
213
+
214
+ # When `AllCops/UseProjectIndex` is enabled, methods that override a
215
+ # method defined by an ancestor elsewhere in the project are not
216
+ # reported: renaming an override breaks the inherited contract.
217
+ def overrides_inherited_method?(node)
218
+ return false unless project_index
219
+ return false unless (namespace_node = node.each_ancestor(:class, :module).first)
220
+
221
+ declaration = resolve_constant_in_index(namespace_node.identifier)
222
+ return false unless declaration.is_a?(Rubydex::Namespace)
223
+
224
+ scope = node.defs_type? ? indexed_singleton_of(declaration) : declaration
225
+ !scope.nil? && inherited_index_member?(scope, "#{node.method_name}()")
226
+ rescue StandardError
227
+ false
228
+ end
201
229
  end
202
230
  end
203
231
  end
@@ -95,11 +95,14 @@ module RuboCop
95
95
 
96
96
  def autocorrect(corrector, node, range, offending_name, preferred_name)
97
97
  corrector.replace(range, preferred_name)
98
- correct_node(corrector, node.body, offending_name, preferred_name)
98
+ # Once the exception variable is reassigned, later references point to a
99
+ # different value, so stop correcting after the reassignment - both in the
100
+ # body and in the code following the `begin`/`rescue`.
101
+ return if correct_node(corrector, node.body, offending_name, preferred_name)
99
102
  return unless (kwbegin_node = node.parent.each_ancestor(:kwbegin).first)
100
103
 
101
104
  kwbegin_node.right_siblings.each do |child_node|
102
- correct_node(corrector, child_node, offending_name, preferred_name)
105
+ break if correct_node(corrector, child_node, offending_name, preferred_name)
103
106
  end
104
107
  end
105
108
 
@@ -113,6 +116,8 @@ module RuboCop
113
116
  end
114
117
  end
115
118
 
119
+ # Returns the reassignment node once the exception variable is reassigned (a truthy
120
+ # signal to stop correcting later references), or `nil` when no reassignment is found.
116
121
  # rubocop:disable Metrics/MethodLength
117
122
  def correct_node(corrector, node, offending_name, preferred_name)
118
123
  return unless node
@@ -131,9 +136,10 @@ module RuboCop
131
136
 
132
137
  if child_node.type?(:masgn, :lvasgn)
133
138
  correct_reassignment(corrector, child_node, offending_name, preferred_name)
134
- break
139
+ return child_node
135
140
  end
136
141
  end
142
+ nil
137
143
  end
138
144
  # rubocop:enable Metrics/MethodLength
139
145
 
@@ -132,7 +132,9 @@ module RuboCop
132
132
 
133
133
  def on_sym(node)
134
134
  @node = node
135
- return if allowed_identifier?(node.value)
135
+
136
+ # Prism parses a quoted empty hash key (`{ "": value }`) as an empty symbol node.
137
+ return if node.value.empty? || allowed_identifier?(node.value)
136
138
 
137
139
  check_name(node, node.value, node) if cop_config['CheckSymbols']
138
140
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Cops for the `Naming` department. The department's cops are registered for lazy loading
6
+ # and their files are loaded on demand.
7
+ module Naming
8
+ extend LazyLoader
9
+
10
+ register_cop :AccessorMethodName, "#{__dir__}/naming/accessor_method_name"
11
+ register_cop :AsciiIdentifiers, "#{__dir__}/naming/ascii_identifiers"
12
+ register_cop :BlockForwarding, "#{__dir__}/naming/block_forwarding"
13
+ register_cop :BlockParameterName, "#{__dir__}/naming/block_parameter_name"
14
+ register_cop :ClassAndModuleCamelCase, "#{__dir__}/naming/class_and_module_camel_case"
15
+ register_cop :ConstantName, "#{__dir__}/naming/constant_name"
16
+ register_cop :FileName, "#{__dir__}/naming/file_name"
17
+ register_cop :HeredocDelimiterCase, "#{__dir__}/naming/heredoc_delimiter_case"
18
+ register_cop :HeredocDelimiterNaming, "#{__dir__}/naming/heredoc_delimiter_naming"
19
+ register_cop :InclusiveLanguage, "#{__dir__}/naming/inclusive_language"
20
+ register_cop :MemoizedInstanceVariableName, "#{__dir__}/naming/memoized_instance_variable_name"
21
+ register_cop :MethodName, "#{__dir__}/naming/method_name"
22
+ register_cop :MethodParameterName, "#{__dir__}/naming/method_parameter_name"
23
+ register_cop :BinaryOperatorParameterName, "#{__dir__}/naming/binary_operator_parameter_name"
24
+ register_cop :PredicateMethod, "#{__dir__}/naming/predicate_method"
25
+ register_cop :PredicatePrefix, "#{__dir__}/naming/predicate_prefix"
26
+ register_cop :RescuedExceptionsVariableName, "#{__dir__}/naming/rescued_exceptions_variable_name"
27
+ register_cop :VariableName, "#{__dir__}/naming/variable_name"
28
+ register_cop :VariableNumber, "#{__dir__}/naming/variable_number"
29
+ end
30
+ end
31
+ end
@@ -91,6 +91,12 @@ module RuboCop
91
91
  status = :uncorrected, corrector = nil)
92
92
  @severity = RuboCop::Cop::Severity.new(severity)
93
93
  @location = location
94
+
95
+ # Pre-compute the position eagerly because the offense is frozen and
96
+ # `location.line` / `location.column` are expensive to compute; sorting
97
+ # many offenses calls them repeatedly through `#<=>`.
98
+ @line = location.line
99
+ @column = location.column
94
100
  @message = message.freeze
95
101
  @cop_name = cop_name.freeze
96
102
  @status = status
@@ -104,6 +110,8 @@ module RuboCop
104
110
 
105
111
  def marshal_load(array)
106
112
  @severity, @location, @message, @cop_name, @status = array
113
+ @line = @location.line
114
+ @column = @location.column
107
115
  end
108
116
 
109
117
  # @api public
@@ -167,14 +175,10 @@ module RuboCop
167
175
  end
168
176
 
169
177
  # @api private
170
- def line
171
- location.line
172
- end
178
+ attr_reader :line
173
179
 
174
180
  # @api private
175
- def column
176
- location.column
177
- end
181
+ attr_reader :column
178
182
 
179
183
  # @api private
180
184
  def source_line
@@ -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
@@ -160,9 +199,10 @@ module RuboCop
160
199
 
161
200
  def unqualified_cop_names
162
201
  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'
202
+
203
+ @unqualified_cop_names ||= @cops_by_badge.keys.to_set do |badge|
204
+ File.basename(badge.to_s)
205
+ end << 'RedundantCopDisableDirective'
166
206
  end
167
207
 
168
208
  def qualify_badge(badge)
@@ -187,51 +227,64 @@ module RuboCop
187
227
 
188
228
  def length
189
229
  clear_enrollment_queue
190
- @cops_by_badge.size + @lazy_loaded_cops_by_badge.size
230
+ @cops_by_badge.size
191
231
  end
192
232
 
233
+ # Returns the enabled cop classes, loading lazy-loaded cops only when they are enabled.
234
+ #
235
+ # @return [Array<Class>]
193
236
  def enabled(config)
194
- @enabled_cache[config] ||= select { |cop| enabled?(cop, config) }
237
+ @enabled_cache[config] ||= registered_badges.filter_map do |badge|
238
+ next unless enabled_cop_name?(badge.to_s, config)
239
+
240
+ cop_or_name = @cops_by_badge[badge]
241
+ cop_or_name.is_a?(String) ? load_lazy_cop(badge) : cop_or_name
242
+ end
195
243
  end
196
244
 
197
245
  def disabled(config)
198
246
  @disabled_cache[config] ||= reject { |cop| enabled?(cop, config) }
199
247
  end
200
248
 
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
249
+ # Returns the names of the disabled cops without loading any of them.
250
+ #
251
+ # @api private
252
+ # @return [Array<String>]
253
+ def disabled_names(config)
254
+ @disabled_names_cache[config] ||= registered_badges.filter_map do |badge|
255
+ name = badge.to_s
256
+ name unless enabled_cop_name?(name, config)
214
257
  end
215
258
  end
216
259
 
217
- def enabled_pending_cop?(cop_cfg, config)
260
+ def enabled?(cop, config)
261
+ enabled_cop_name?(cop.cop_name, config)
262
+ end
263
+
264
+ # @param cop [Class, String, nil] the cop class or the qualified cop name
265
+ def enabled_pending_cop?(cop_cfg, config, cop = nil)
218
266
  return false if @options[:disable_pending_cops]
267
+ return false unless cop_cfg.fetch('Enabled') == 'pending'
268
+ return true if @options[:enable_pending_cops]
269
+ return config.enabled_new_cops? unless cop
219
270
 
220
- cop_cfg.fetch('Enabled') == 'pending' &&
221
- (@options[:enable_pending_cops] || config.enabled_new_cops?)
271
+ cop_name = cop.is_a?(String) ? cop : cop.cop_name
272
+ config.enabled_new_cop?(cop_name)
222
273
  end
223
274
 
224
275
  def names
225
276
  clear_enrollment_queue
226
- @cops_by_badge.keys.map(&:to_s) | @lazy_loaded_cops_by_badge.keys.map(&:to_s)
277
+ @cops_by_badge.keys.map(&:to_s)
227
278
  end
228
279
 
229
280
  def cops_for_department(department)
230
- cops.select { |cop| cop.department == department.to_sym }
281
+ with_department(department.to_sym).cops
231
282
  end
232
283
 
233
284
  def names_for_department(department)
234
- cops_for_department(department).map(&:cop_name)
285
+ department = department.to_sym
286
+
287
+ registered_badges.filter_map { |badge| badge.to_s if badge.department == department }
235
288
  end
236
289
 
237
290
  def ==(other)
@@ -259,7 +312,9 @@ module RuboCop
259
312
  def find_by_cop_name(cop_name)
260
313
  clear_enrollment_queue
261
314
  badge = Badge.parse(cop_name)
262
- @cops_by_badge[badge] || load_lazy_cop(badge)
315
+ cop_or_name = @cops_by_badge[badge]
316
+
317
+ cop_or_name.is_a?(String) ? load_lazy_cop(badge) : cop_or_name
263
318
  end
264
319
 
265
320
  # When a cop name is given returns a single-element array with the cop class.
@@ -287,35 +342,90 @@ module RuboCop
287
342
  @warnings[path]
288
343
  end
289
344
 
345
+ protected
346
+
347
+ # Adds an already-loaded cop class or a lazy-load constant name under the given badge,
348
+ # used to build filtered copies.
349
+ def add_entry(badge, cop_or_name)
350
+ @departments << badge.department
351
+ @cops_by_badge[badge] = cop_or_name
352
+ end
353
+
290
354
  private
291
355
 
292
356
  def initialize_copy(reg)
293
- initialize(reg.cops, reg.options)
357
+ super
358
+ @cops_by_badge = @cops_by_badge.dup
359
+ @enrollment_queue = @enrollment_queue.dup
360
+ @departments = @departments.dup
361
+ @enabled_cache = {}.compare_by_identity
362
+ @disabled_cache = {}.compare_by_identity
363
+ @disabled_names_cache = {}.compare_by_identity
364
+ @warnings = {}
365
+ @unqualified_cop_names = nil
294
366
  end
295
367
 
296
368
  def clear_enrollment_queue
297
369
  return if @enrollment_queue.empty?
298
370
 
299
371
  @enrollment_queue.each do |cop|
372
+ # Reassigning a badge that was registered for lazy loading keeps
373
+ # its original position in the insertion-ordered map.
300
374
  @cops_by_badge[cop.badge] = cop
301
375
  @departments << cop.department
302
376
  end
303
377
  @enrollment_queue = []
304
378
  end
305
379
 
306
- def load_all_lazy_cops
307
- @lazy_loaded_cops_by_badge.each_key { |badge| load_lazy_cop(badge) }
380
+ def registered_badges
381
+ clear_enrollment_queue
382
+ @cops_by_badge.keys
308
383
  end
309
384
 
310
- def load_lazy_cop(badge)
311
- constant_name = @lazy_loaded_cops_by_badge.delete(badge)
312
- return unless constant_name
385
+ def enabled_cop_name?(cop_name, config)
386
+ return true if options[:only]&.include?(cop_name)
313
387
 
314
- @cops_by_badge[badge] = Kernel.const_get(constant_name)
388
+ # We need to use the cop name in this case, because `for_cop` uses
389
+ # caching which expects cop names or cop classes as keys.
390
+ cfg = config.for_cop(cop_name)
391
+
392
+ cop_enabled = cfg.fetch('Enabled') == true || enabled_pending_cop?(cfg, config, cop_name)
393
+
394
+ if options.fetch(:safe, false)
395
+ cop_enabled && cfg.fetch('Safe', true)
396
+ else
397
+ cop_enabled
398
+ end
315
399
  end
316
400
 
317
- def with(cops)
318
- self.class.new(cops)
401
+ def load_all_lazy_cops
402
+ # Take a snapshot because loading a cop can load (and thereby register)
403
+ # another lazy-loaded cop, e.g. its superclass.
404
+ badges = @cops_by_badge.keys
405
+ badges.each { |badge| load_lazy_cop(badge) if @cops_by_badge[badge].is_a?(String) }
406
+ end
407
+
408
+ # @return [Class, nil] the loaded cop class, or nil if the cop excluded
409
+ # itself from the registry while being loaded
410
+ def load_lazy_cop(badge)
411
+ constant_name = @cops_by_badge[badge]
412
+ return constant_name unless constant_name.is_a?(String)
413
+
414
+ cop = Kernel.const_get(constant_name)
415
+ if equal?(self.class.global)
416
+ # Resolving the constant fires the autoload, which makes
417
+ # `Base.inherited` enlist the class into the global registry.
418
+ # Enrolling through the queue keeps `exclude_from_registry` honored:
419
+ # a cop that dismissed itself leaves a dangling constant name behind
420
+ # and is removed from the registry.
421
+ clear_enrollment_queue
422
+ cop_or_name = @cops_by_badge[badge]
423
+ @cops_by_badge.delete(badge) if cop_or_name.is_a?(String)
424
+ cop_or_name.is_a?(Class) ? cop_or_name : nil
425
+ else
426
+ # `Base.inherited` enlists into the global registry, not this copy.
427
+ @cops_by_badge[badge] = cop
428
+ end
319
429
  end
320
430
 
321
431
  def resolve_badge(given_badge, real_badge, source_path, warn: true)
@@ -335,7 +445,7 @@ module RuboCop
335
445
 
336
446
  def registered?(badge)
337
447
  clear_enrollment_queue
338
- @cops_by_badge.key?(badge) || @lazy_loaded_cops_by_badge.key?(badge)
448
+ @cops_by_badge.key?(badge)
339
449
  end
340
450
  end
341
451
  end
@@ -37,7 +37,7 @@ module RuboCop
37
37
  return unless (receiver = node.receiver) && receiver.source == 'IO'
38
38
 
39
39
  argument = node.first_argument
40
- return if argument.respond_to?(:value) && argument.value.strip.start_with?('|')
40
+ return if argument&.str_type? && argument.value.strip.start_with?('|')
41
41
 
42
42
  add_offense(node, message: format(MSG, method_name: node.method_name)) do |corrector|
43
43
  corrector.replace(receiver, 'File')
@@ -25,7 +25,7 @@ module RuboCop
25
25
  # @!method marshal_load(node)
26
26
  def_node_matcher :marshal_load, <<~PATTERN
27
27
  (send (const {nil? cbase} :Marshal) ${:load :restore}
28
- !(send (const {nil? cbase} :Marshal) :dump ...))
28
+ !(send (const {nil? cbase} :Marshal) :dump ...) _?)
29
29
  PATTERN
30
30
 
31
31
  def on_send(node)
@@ -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
@@ -203,13 +203,23 @@ module RuboCop
203
203
  end
204
204
 
205
205
  def range_with_trailing_argument_comment(node)
206
- comment = processed_source.ast_with_comments[node.last_argument].last
206
+ comment = trailing_argument_comment(node)
207
207
  if comment
208
208
  add_range(node.source_range, comment.source_range)
209
209
  else
210
210
  node
211
211
  end
212
212
  end
213
+
214
+ # For a single-line declaration the parser associates the trailing
215
+ # comment with the first argument, not `last_argument`, so look through
216
+ # all arguments for a comment that trails the whole node.
217
+ def trailing_argument_comment(node)
218
+ comments = node.arguments.filter_map do |argument|
219
+ processed_source.ast_with_comments[argument].last
220
+ end
221
+ comments.find { |comment| comment.source_range.begin_pos >= node.source_range.end_pos }
222
+ end
213
223
  end
214
224
  end
215
225
  end
@@ -153,7 +153,7 @@ module RuboCop
153
153
  BLOCK_MSG = 'Use anonymous block arguments forwarding (`&`).'
154
154
 
155
155
  def self.autocorrect_incompatible_with
156
- [Naming::BlockForwarding]
156
+ [Naming::BlockForwarding, Style::MethodDefParentheses]
157
157
  end
158
158
 
159
159
  def on_def(node)