rubocop 1.86.2 → 1.88.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +82 -71
  3. data/config/obsoletion.yml +21 -1
  4. data/lib/rubocop/cli/command/auto_generate_config.rb +6 -0
  5. data/lib/rubocop/cli.rb +2 -0
  6. data/lib/rubocop/config_loader.rb +17 -2
  7. data/lib/rubocop/config_loader_resolver.rb +11 -3
  8. data/lib/rubocop/config_store.rb +1 -1
  9. data/lib/rubocop/cop/base.rb +25 -4
  10. data/lib/rubocop/cop/bundler/gem_comment.rb +2 -2
  11. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +33 -2
  12. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +2 -2
  13. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  14. data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +3 -3
  15. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  16. data/lib/rubocop/cop/layout/begin_end_alignment.rb +1 -1
  17. data/lib/rubocop/cop/layout/block_alignment.rb +41 -4
  18. data/lib/rubocop/cop/layout/class_structure.rb +1 -1
  19. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +14 -5
  20. data/lib/rubocop/cop/layout/end_alignment.rb +2 -2
  21. data/lib/rubocop/cop/layout/indentation_width.rb +13 -1
  22. data/lib/rubocop/cop/layout/redundant_line_break.rb +3 -1
  23. data/lib/rubocop/cop/layout/space_before_brackets.rb +1 -1
  24. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  25. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +1 -1
  26. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  27. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  28. data/lib/rubocop/cop/lint/constant_overwritten_in_rescue.rb +1 -1
  29. data/lib/rubocop/cop/lint/constant_reassignment.rb +36 -4
  30. data/lib/rubocop/cop/lint/constant_resolution.rb +5 -5
  31. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  32. data/lib/rubocop/cop/lint/deprecated_constants.rb +2 -8
  33. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  34. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  35. data/lib/rubocop/cop/lint/erb_new_arguments.rb +4 -2
  36. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  37. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  38. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  39. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  40. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  41. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  42. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +5 -5
  43. data/lib/rubocop/cop/lint/multiple_comparison.rb +2 -2
  44. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  45. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +1 -1
  46. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  47. data/lib/rubocop/cop/lint/number_conversion.rb +18 -9
  48. data/lib/rubocop/cop/lint/numbered_parameter_assignment.rb +1 -1
  49. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  50. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  51. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +1 -1
  52. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  53. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  54. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  55. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +6 -3
  56. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  57. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  58. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  59. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +10 -3
  60. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  61. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  62. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  63. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  64. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  65. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  66. data/lib/rubocop/cop/lint/require_relative_self_path.rb +6 -6
  67. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  68. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  69. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  70. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  71. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  72. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  73. data/lib/rubocop/cop/lint/shadowed_exception.rb +1 -1
  74. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  75. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  76. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  77. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  78. data/lib/rubocop/cop/lint/to_enum_arguments.rb +28 -1
  79. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  80. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  81. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +4 -2
  82. data/lib/rubocop/cop/lint/unreachable_code.rb +2 -2
  83. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  84. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +8 -4
  85. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  86. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  87. data/lib/rubocop/cop/metrics/block_length.rb +1 -1
  88. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  89. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  90. data/lib/rubocop/cop/metrics/utils/iterating_block.rb +1 -1
  91. data/lib/rubocop/cop/mixin/project_index_help.rb +48 -0
  92. data/lib/rubocop/cop/mixin.rb +1 -0
  93. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +1 -1
  94. data/lib/rubocop/cop/naming/predicate_method.rb +2 -2
  95. data/lib/rubocop/cop/naming/predicate_prefix.rb +1 -1
  96. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -1
  97. data/lib/rubocop/cop/registry.rb +28 -6
  98. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  99. data/lib/rubocop/cop/style/alias.rb +11 -2
  100. data/lib/rubocop/cop/style/and_or.rb +1 -1
  101. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  102. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  103. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  104. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  105. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  106. data/lib/rubocop/cop/style/character_literal.rb +2 -2
  107. data/lib/rubocop/cop/style/class_and_module_children.rb +8 -0
  108. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  109. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  110. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  111. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  112. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  113. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  114. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  115. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  116. data/lib/rubocop/cop/style/date_time.rb +2 -2
  117. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  118. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +1 -1
  119. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  120. data/lib/rubocop/cop/style/file_write.rb +21 -16
  121. data/lib/rubocop/cop/style/format_string.rb +4 -3
  122. data/lib/rubocop/cop/style/hash_conversion.rb +1 -1
  123. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  124. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
  125. data/lib/rubocop/cop/style/magic_comment_format.rb +1 -1
  126. data/lib/rubocop/cop/style/min_max_comparison.rb +1 -1
  127. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  128. data/lib/rubocop/cop/style/parallel_assignment.rb +8 -1
  129. data/lib/rubocop/cop/style/redundant_array_constructor.rb +2 -2
  130. data/lib/rubocop/cop/style/redundant_constant_base.rb +5 -5
  131. data/lib/rubocop/cop/style/redundant_format.rb +1 -0
  132. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +2 -2
  133. data/lib/rubocop/cop/style/regexp_literal.rb +2 -2
  134. data/lib/rubocop/cop/style/rescue_modifier.rb +3 -3
  135. data/lib/rubocop/cop/style/self_assignment.rb +1 -1
  136. data/lib/rubocop/cop/style/semicolon.rb +16 -1
  137. data/lib/rubocop/cop/style/struct_inheritance.rb +13 -0
  138. data/lib/rubocop/cop/style/top_level_method_definition.rb +2 -2
  139. data/lib/rubocop/cop/style/unless_logical_operators.rb +3 -3
  140. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  141. data/lib/rubocop/cop/style/word_array.rb +1 -0
  142. data/lib/rubocop/cop/style/yoda_condition.rb +1 -1
  143. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  144. data/lib/rubocop/file_patterns.rb +9 -1
  145. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  146. data/lib/rubocop/options.rb +18 -0
  147. data/lib/rubocop/project_index_loader.rb +66 -0
  148. data/lib/rubocop/runner.rb +47 -3
  149. data/lib/rubocop/server/core.rb +6 -0
  150. data/lib/rubocop/version.rb +20 -2
  151. data/lib/rubocop.rb +1 -0
  152. metadata +5 -3
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Common helpers for cops that consult the project-wide static-analysis index
6
+ # via `Cop::Base#project_index`.
7
+ #
8
+ # Mixed-in cops gain the `external_dependency_checksum` override that invalidates
9
+ # the `ResultCache` whenever the indexed project files change on disk.
10
+ # To run index-backed analysis, cops should simply check whether `project_index` is non-nil;
11
+ # the runner only exposes a non-nil index when the user opted in via `AllCops/UseProjectIndex`
12
+ # and the underlying gem is available.
13
+ module ProjectIndexHelp
14
+ BUILTIN_DOCUMENT_URI = 'rubydex:built-in'
15
+ FILE_URI_PREFIX = 'file://'
16
+ # Matches the spurious leading slash before a Windows drive letter that
17
+ # remains after stripping `file://` from a `file:///C:/...` URI.
18
+ WINDOWS_DRIVE_PREFIX = %r{\A/(?=[A-Za-z]:[/\\])}.freeze
19
+
20
+ def external_dependency_checksum
21
+ return nil unless project_index
22
+
23
+ @external_dependency_checksum ||= Digest::SHA1.hexdigest(
24
+ project_index_signature.join("\n")
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def project_index_signature
31
+ project_index.documents.filter_map do |doc|
32
+ uri = doc.uri
33
+ next if uri == BUILTIN_DOCUMENT_URI
34
+
35
+ path = uri.delete_prefix(FILE_URI_PREFIX).sub(WINDOWS_DRIVE_PREFIX, '')
36
+ mtime, size = begin
37
+ stat = File.stat(path)
38
+ [stat.mtime.to_f, stat.size]
39
+ rescue StandardError
40
+ [0, 0]
41
+ end
42
+
43
+ "#{path}:#{mtime}:#{size}"
44
+ end.sort
45
+ end
46
+ end
47
+ end
48
+ end
@@ -28,6 +28,7 @@ module RuboCop
28
28
  autoload :DocumentationComment, 'rubocop/cop/mixin/documentation_comment'
29
29
  autoload :Duplication, 'rubocop/cop/mixin/duplication'
30
30
  autoload :RangeHelp, 'rubocop/cop/mixin/range_help'
31
+ autoload :ProjectIndexHelp, 'rubocop/cop/mixin/project_index_help'
31
32
  autoload :AnnotationComment, 'rubocop/cop/mixin/annotation_comment'
32
33
  autoload :EmptyParameter, 'rubocop/cop/mixin/empty_parameter'
33
34
  autoload :EndKeywordAlignment, 'rubocop/cop/mixin/end_keyword_alignment'
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Cop
5
5
  module Naming
6
6
  # Makes sure that certain binary operator methods have their
7
- # sole parameter named `other`.
7
+ # sole parameter named `other`.
8
8
  #
9
9
  # @example
10
10
  #
@@ -6,7 +6,7 @@ module RuboCop
6
6
  # Checks that predicate methods end with `?` and non-predicate methods do not.
7
7
  #
8
8
  # The names of predicate methods (methods that return a boolean value) should end
9
- # in a question mark. Methods that don't return a boolean, shouldn't
9
+ # in a question mark. Methods that don't return a boolean shouldn't
10
10
  # end in a question mark.
11
11
  #
12
12
  # The cop assesses a predicate method as one that returns boolean values. Likewise,
@@ -22,7 +22,7 @@ module RuboCop
22
22
  # return values are detected.
23
23
  #
24
24
  # The cop also has `AllowedMethods` configuration in order to prevent the cop from
25
- # registering an offense from a method name that does not confirm to the naming
25
+ # registering an offense from a method name that does not conform to the naming
26
26
  # guidelines. By default, `call` is allowed. The cop also has `AllowedPatterns`
27
27
  # configuration to allow method names by regular expression.
28
28
  #
@@ -17,7 +17,7 @@ module RuboCop
17
17
  # they end with a `?`. These methods should be changed to remove the
18
18
  # prefix.
19
19
  #
20
- # When `UseSorbetSigs` set to true (optional), the cop will only report
20
+ # When `UseSorbetSigs` is set to true (optional), the cop will only report
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
  #
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Naming
6
- # Makes sure that rescued exceptions variables are named as
6
+ # Makes sure that rescued exception variables are named as
7
7
  # expected.
8
8
  #
9
9
  # The `PreferredName` config option takes a `String`. It represents
@@ -51,6 +51,7 @@ module RuboCop
51
51
  def initialize(cops = [], options = {})
52
52
  @departments = Set.new
53
53
  @cops_by_badge = {}
54
+ @lazy_loaded_cops_by_badge = {}
54
55
 
55
56
  @enrollment_queue = cops
56
57
  @options = options
@@ -60,6 +61,12 @@ module RuboCop
60
61
  @warnings = {}
61
62
  end
62
63
 
64
+ def lazy_load(cop_name, constant_name)
65
+ badge = Badge.parse(cop_name)
66
+ @departments << badge.department
67
+ @lazy_loaded_cops_by_badge[badge] = constant_name
68
+ end
69
+
63
70
  def enlist(cop)
64
71
  @enrollment_queue << cop
65
72
  end
@@ -154,8 +161,8 @@ module RuboCop
154
161
  def unqualified_cop_names
155
162
  clear_enrollment_queue
156
163
  @unqualified_cop_names ||=
157
- Set.new(@cops_by_badge.keys.map { |badge| File.basename(badge.to_s) }) <<
158
- 'RedundantCopDisableDirective'
164
+ (@cops_by_badge.keys | @lazy_loaded_cops_by_badge.keys)
165
+ .to_set { |badge| File.basename(badge.to_s) } << 'RedundantCopDisableDirective'
159
166
  end
160
167
 
161
168
  def qualify_badge(badge)
@@ -168,17 +175,19 @@ module RuboCop
168
175
  # @return [Hash{String => Array<Class>}]
169
176
  def to_h
170
177
  clear_enrollment_queue
178
+ load_all_lazy_cops
171
179
  @cops_by_badge.to_h { |_badge, cop| [cop.cop_name, [cop]] }
172
180
  end
173
181
 
174
182
  def cops
175
183
  clear_enrollment_queue
184
+ load_all_lazy_cops
176
185
  @cops_by_badge.values
177
186
  end
178
187
 
179
188
  def length
180
189
  clear_enrollment_queue
181
- @cops_by_badge.size
190
+ @cops_by_badge.size + @lazy_loaded_cops_by_badge.size
182
191
  end
183
192
 
184
193
  def enabled(config)
@@ -214,7 +223,7 @@ module RuboCop
214
223
 
215
224
  def names
216
225
  clear_enrollment_queue
217
- @cops_by_badge.keys.map(&:to_s)
226
+ @cops_by_badge.keys.map(&:to_s) | @lazy_loaded_cops_by_badge.keys.map(&:to_s)
218
227
  end
219
228
 
220
229
  def cops_for_department(department)
@@ -231,6 +240,7 @@ module RuboCop
231
240
 
232
241
  def sort!
233
242
  clear_enrollment_queue
243
+ load_all_lazy_cops
234
244
  @cops_by_badge = @cops_by_badge.sort_by { |badge, _cop| badge.cop_name }.to_h
235
245
 
236
246
  self
@@ -249,7 +259,7 @@ module RuboCop
249
259
  def find_by_cop_name(cop_name)
250
260
  clear_enrollment_queue
251
261
  badge = Badge.parse(cop_name)
252
- @cops_by_badge[badge]
262
+ @cops_by_badge[badge] || load_lazy_cop(badge)
253
263
  end
254
264
 
255
265
  # When a cop name is given returns a single-element array with the cop class.
@@ -262,6 +272,7 @@ module RuboCop
262
272
 
263
273
  def freeze
264
274
  clear_enrollment_queue
275
+ load_all_lazy_cops
265
276
  unqualified_cop_names # build cache
266
277
  super
267
278
  end
@@ -292,6 +303,17 @@ module RuboCop
292
303
  @enrollment_queue = []
293
304
  end
294
305
 
306
+ def load_all_lazy_cops
307
+ @lazy_loaded_cops_by_badge.each_key { |badge| load_lazy_cop(badge) }
308
+ end
309
+
310
+ def load_lazy_cop(badge)
311
+ constant_name = @lazy_loaded_cops_by_badge.delete(badge)
312
+ return unless constant_name
313
+
314
+ @cops_by_badge[badge] = Kernel.const_get(constant_name)
315
+ end
316
+
295
317
  def with(cops)
296
318
  self.class.new(cops)
297
319
  end
@@ -313,7 +335,7 @@ module RuboCop
313
335
 
314
336
  def registered?(badge)
315
337
  clear_enrollment_queue
316
- @cops_by_badge.key?(badge)
338
+ @cops_by_badge.key?(badge) || @lazy_loaded_cops_by_badge.key?(badge)
317
339
  end
318
340
  end
319
341
  end
@@ -10,7 +10,7 @@ module RuboCop
10
10
  # a subprocess is created in the same way as `Kernel#open`, and its output is returned.
11
11
  # `Kernel#open` may allow unintentional command injection, which is the reason these
12
12
  # `IO` methods are a security risk.
13
- # Consider to use `File.read` to disable the behavior of subprocess invocation.
13
+ # Consider using `File.read` to disable the behavior of subprocess invocation.
14
14
  #
15
15
  # @safety
16
16
  # This cop is unsafe because false positive will occur if the variable passed as
@@ -10,7 +10,7 @@ module RuboCop
10
10
  # is resolved at runtime).
11
11
  # It also flags uses of `alias :symbol` rather than `alias bareword`.
12
12
  #
13
- # However, it will always enforce `method_alias` when used `alias`
13
+ # However, it will always enforce `alias_method` when `alias` is used
14
14
  # in an instance method definition and in a singleton method definition.
15
15
  # If used in a block, always enforce `alias_method`
16
16
  # unless it is an `instance_eval` block.
@@ -45,6 +45,7 @@ module RuboCop
45
45
  return unless node.command?(:alias_method)
46
46
  return unless style == :prefer_alias && alias_keyword_possible?(node)
47
47
  return unless node.arguments.count == 2
48
+ return if alias_method_value_used?(node)
48
49
 
49
50
  msg = format(MSG_ALIAS_METHOD, current: lexical_scope_type(node))
50
51
  add_offense(node.loc.selector, message: msg) do |corrector|
@@ -80,6 +81,14 @@ module RuboCop
80
81
  scope_type(node) != :dynamic && node.arguments.all?(&:sym_type?)
81
82
  end
82
83
 
84
+ # `alias_method` is a method call whose return value can be used
85
+ # (e.g., as an argument to `public`/`module_function`, or as an assignment),
86
+ # but `alias` is a keyword statement that cannot appear in such positions.
87
+ # Detect these positions so the conversion does not produce a syntax error.
88
+ def alias_method_value_used?(node)
89
+ node.argument? || node.parent&.assignment?
90
+ end
91
+
83
92
  def alias_method_possible?(node)
84
93
  scope_type(node) != :instance_eval &&
85
94
  node.children.none?(&:gvar_type?) &&
@@ -104,7 +113,7 @@ module RuboCop
104
113
  return :lexical
105
114
  when :def, :defs
106
115
  return :dynamic
107
- when :block
116
+ when :block, :numblock, :itblock
108
117
  return :instance_eval if parent.method?(:instance_eval)
109
118
 
110
119
  return :dynamic
@@ -71,7 +71,7 @@ module RuboCop
71
71
  node.each_child_node do |expr|
72
72
  if expr.send_type?
73
73
  correct_send(expr, corrector)
74
- elsif expr.return_type? || expr.assignment?
74
+ elsif expr.type?(:return, :next, :break, :yield) || expr.assignment?
75
75
  correct_other(expr, corrector)
76
76
  end
77
77
  end
@@ -40,8 +40,9 @@ module RuboCop
40
40
 
41
41
  node = innermost_braces_node(node)
42
42
  return if node.parent && brace_method?(node.parent)
43
+ return if compound_assignment_target?(node)
43
44
 
44
- preferred = (value.zero? ? 'first' : 'last')
45
+ preferred = preferred_name(value)
45
46
  offense_range = find_offense_range(node)
46
47
 
47
48
  add_offense(offense_range, message: format(MSG, preferred: preferred)) do |corrector|
@@ -53,6 +54,10 @@ module RuboCop
53
54
 
54
55
  private
55
56
 
57
+ def preferred_name(value)
58
+ value.zero? ? 'first' : 'last'
59
+ end
60
+
56
61
  def preferred_value(node, value)
57
62
  value = ".#{value}" unless node.loc.dot
58
63
  value
@@ -74,6 +79,12 @@ module RuboCop
74
79
  def brace_method?(node)
75
80
  node.send_type? && (node.method?(:[]) || node.method?(:[]=))
76
81
  end
82
+
83
+ # `arr[0] += 1` etc. would autocorrect to `arr.first += 1`, which calls the
84
+ # nonexistent `first=`/`last=` setter and raises `NoMethodError`.
85
+ def compound_assignment_target?(node)
86
+ node.parent&.type?(:op_asgn, :or_asgn, :and_asgn) && node.parent.children.first == node
87
+ end
77
88
  end
78
89
  end
79
90
  end
@@ -145,6 +145,10 @@ module RuboCop
145
145
 
146
146
  dot = dot_node.loc.dot.source
147
147
  bang = straight?(method_name) ? '' : '!'
148
+ # `a&.intersection(b)&.none?` returns `nil` when `a` is `nil`, but the negated
149
+ # rewrite `!a&.intersect?(b)` returns `true` there, flipping the result.
150
+ return if bang == '!' && dot == '&.'
151
+
148
152
  replacement = "#{bang}#{receiver.source}#{dot}intersect?(#{argument.source})"
149
153
 
150
154
  register_offense(node, replacement)
@@ -29,6 +29,9 @@ module RuboCop
29
29
  def on_send(node)
30
30
  array, element = single_element(node)
31
31
  return unless array
32
+ # `[*foo]` is not a single element: the splat can expand to any number of
33
+ # elements, so `intersect?([*foo])` is not equivalent to `include?(*foo)`.
34
+ return if element.splat_type?
32
35
 
33
36
  add_offense(
34
37
  node.source_range.with(begin_pos: node.loc.selector.begin_pos)
@@ -389,9 +389,23 @@ module RuboCop
389
389
 
390
390
  def require_do_end?(node)
391
391
  return false if node.braces? || node.multiline?
392
- return false unless (resbody = node.each_descendant(:resbody).first)
393
392
 
394
- resbody.children.first&.array_type?
393
+ body = node.body
394
+ return false unless body
395
+ # `ensure` and a block-level `rescue` are illegal inside `{ }`; only a
396
+ # bare modifier rescue (`expr rescue expr`) can be written with braces.
397
+ return true if body.ensure_type?
398
+ return false unless body.rescue_type?
399
+
400
+ !modifier_rescue?(body)
401
+ end
402
+
403
+ def modifier_rescue?(rescue_node)
404
+ return false if rescue_node.body.nil? || rescue_node.else_branch
405
+ return false unless rescue_node.resbody_branches.one?
406
+
407
+ resbody = rescue_node.resbody_branches.first
408
+ resbody.exceptions.empty? && resbody.exception_variable.nil?
395
409
  end
396
410
 
397
411
  def braces_required_method?(method_name)
@@ -96,13 +96,25 @@ module RuboCop
96
96
  end
97
97
 
98
98
  def const_replacement(lhs, rhs)
99
- "#{rhs.source}.is_a?(#{lhs.source})"
99
+ "#{parenthesize_if_needed(rhs)}.is_a?(#{lhs.source})"
100
100
  end
101
101
 
102
102
  def send_replacement(lhs, rhs)
103
103
  return unless self_class?(lhs)
104
104
 
105
- "#{rhs.source}.is_a?(#{lhs.source})"
105
+ "#{parenthesize_if_needed(rhs)}.is_a?(#{lhs.source})"
106
+ end
107
+
108
+ # `Array === a + b` must become `(a + b).is_a?(Array)`, not
109
+ # `a + b.is_a?(Array)` (which parses as `a + (b.is_a?(Array))`).
110
+ def parenthesize_if_needed(node)
111
+ requires_parentheses?(node) ? "(#{node.source})" : node.source
112
+ end
113
+
114
+ def requires_parentheses?(node)
115
+ return true if node.type?(:and, :or, :if, :range) || node.assignment?
116
+
117
+ node.send_type? && (node.operator_method? || node.unary_operation?)
106
118
  end
107
119
  end
108
120
  end
@@ -8,8 +8,8 @@ module RuboCop
8
8
  # essentially one-character strings, so this syntax
9
9
  # is mostly redundant at this point.
10
10
  #
11
- # ? character literal can be used to express meta and control character.
12
- # That's a good use case of ? literal so it doesn't count it as an offense.
11
+ # A `?` character literal can be used to express meta and control characters.
12
+ # That's a good use case of a `?` literal so it doesn't count as an offense.
13
13
  #
14
14
  # @example
15
15
  # # bad
@@ -181,6 +181,7 @@ module RuboCop
181
181
 
182
182
  def check_style(node, body, style)
183
183
  return if node.identifier.namespace&.cbase_type?
184
+ return unless const_namespace?(node.identifier.namespace)
184
185
 
185
186
  if style == :nested
186
187
  check_nested_style(node)
@@ -189,6 +190,13 @@ module RuboCop
189
190
  end
190
191
  end
191
192
 
193
+ def const_namespace?(node)
194
+ return true if node.nil? || node.cbase_type?
195
+ return false unless node.const_type?
196
+
197
+ const_namespace?(node.namespace)
198
+ end
199
+
192
200
  def check_nested_style(node)
193
201
  return unless compact_node_name?(node)
194
202
  return if node.parent&.type?(:class, :module)
@@ -90,25 +90,33 @@ module RuboCop
90
90
  private
91
91
 
92
92
  def class_name(class_node, node)
93
- if class_name_method?(node.children.first.method_name)
94
- if (receiver = class_node.receiver) && class_name_method?(class_node.method_name)
95
- return receiver.source
96
- end
93
+ unless class_name_method?(node.children.first.method_name)
94
+ # `var.class == 'Foo'` compares a `Class` to a `String` (always false) and
95
+ # has no valid `instance_of?` rewrite, so don't suggest one.
96
+ return if class_node.str_type?
97
97
 
98
- if class_node.str_type?
99
- value = trim_string_quotes(class_node)
100
- value.prepend('::') if require_cbase?(class_node)
101
- return value
102
- elsif unable_to_determine_type?(class_node)
103
- # When a variable or return value of a method is used, it returns nil
104
- # because the type is not known and cannot be suggested.
105
- return
106
- end
98
+ return class_node.source
99
+ end
100
+
101
+ if (receiver = class_node.receiver) && class_name_method?(class_node.method_name)
102
+ return receiver.source
107
103
  end
108
104
 
105
+ return string_class_name(class_node) if class_node.str_type?
106
+ # When a variable or return value of a method is used, the type is not known
107
+ # and cannot be suggested.
108
+ return if unable_to_determine_type?(class_node)
109
+
109
110
  class_node.source
110
111
  end
111
112
 
113
+ def string_class_name(class_node)
114
+ value = trim_string_quotes(class_node)
115
+ # Avoid `::::Foo` when the name is already fully qualified.
116
+ value.prepend('::') if require_cbase?(class_node) && !value.start_with?('::')
117
+ value
118
+ end
119
+
112
120
  def class_name_method?(method_name)
113
121
  CLASS_NAME_METHODS.include?(method_name)
114
122
  end
@@ -140,15 +140,21 @@ module RuboCop
140
140
 
141
141
  def extract_def_from_sclass(def_node, sclass_node)
142
142
  range = source_range_with_comment(def_node)
143
- source = range.source.sub!(
144
- "def #{def_node.method_name}",
145
- "def self.#{def_node.method_name}"
146
- )
147
-
143
+ source = prefix_def_with_self(range, def_node)
148
144
  source = source.gsub(/^ {#{indentation_diff(def_node, sclass_node)}}/, '')
149
145
  [range, source.chomp]
150
146
  end
151
147
 
148
+ # Splice in `self.` at the actual `def` keyword rather than substituting the
149
+ # first textual `def <name>`, which may appear inside a preceding comment.
150
+ def prefix_def_with_self(range, def_node)
151
+ keyword_offset = def_node.loc.keyword.begin_pos - range.begin_pos
152
+ name_end_offset = def_node.loc.name.end_pos - range.begin_pos
153
+ source = range.source.dup
154
+ source[keyword_offset...name_end_offset] = "def self.#{def_node.method_name}"
155
+ source
156
+ end
157
+
152
158
  def indentation_diff(node1, node2)
153
159
  node1.loc.column - node2.loc.column
154
160
  end
@@ -24,10 +24,9 @@ module RuboCop
24
24
 
25
25
  MSG = 'Do not use `::` for method calls.'
26
26
 
27
- # @!method java_type_node?(node)
28
- def_node_matcher :java_type_node?, <<~PATTERN
29
- (send
30
- (const nil? :Java) _)
27
+ # @!method java_root?(node)
28
+ def_node_matcher :java_root?, <<~PATTERN
29
+ (const nil? :Java)
31
30
  PATTERN
32
31
 
33
32
  def self.autocorrect_incompatible_with
@@ -37,11 +36,19 @@ module RuboCop
37
36
  def on_send(node)
38
37
  return unless node.receiver && node.double_colon?
39
38
  return if node.camel_case_method?
40
- # ignore Java interop code like Java::int
41
- return if java_type_node?(node)
39
+ # ignore Java interop code like `Java::int` or `Java::com::method`
40
+ return if java_interop?(node)
42
41
 
43
42
  add_offense(node.loc.dot) { |corrector| corrector.replace(node.loc.dot, '.') }
44
43
  end
44
+
45
+ private
46
+
47
+ def java_interop?(node)
48
+ receiver = node.receiver
49
+ receiver = receiver.receiver while receiver.respond_to?(:receiver) && receiver.receiver
50
+ java_root?(receiver)
51
+ end
45
52
  end
46
53
  end
47
54
  end
@@ -85,8 +85,13 @@ module RuboCop
85
85
  def on_for(node)
86
86
  return unless node.parent&.begin_type?
87
87
  return unless same_collection_looping_for?(node, node.left_sibling)
88
+ return unless node.body && node.left_sibling.body
88
89
 
89
90
  add_offense(node) do |corrector|
91
+ # Combining loops with different iteration variables would leave the second
92
+ # body referencing an undefined variable, so only autocorrect when they match.
93
+ next unless node.variable == node.left_sibling.variable
94
+
90
95
  combine_with_left_sibling(corrector, node)
91
96
  end
92
97
  end
@@ -90,7 +90,7 @@ module RuboCop
90
90
  max = if_body.source
91
91
  end
92
92
 
93
- prefer = "#{else_body_source}.clamp(#{min}, #{max})"
93
+ prefer = "#{parenthesize_if_needed(else_body)}.clamp(#{min}, #{max})"
94
94
 
95
95
  add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
96
96
  autocorrect(corrector, node, prefer)
@@ -119,6 +119,17 @@ module RuboCop
119
119
 
120
120
  (lhs.source == else_body && op == :<) || (rhs.source == else_body && op == :>)
121
121
  end
122
+
123
+ # `a + b` must become `(a + b).clamp(low, high)`, not `a + b.clamp(low, high)`
124
+ # (which parses as `a + (b.clamp(low, high))`).
125
+ def parenthesize_if_needed(node)
126
+ if node.type?(:and, :or, :if, :range) || node.assignment? ||
127
+ (node.send_type? && (node.operator_method? || node.unary_operation?))
128
+ "(#{node.source})"
129
+ else
130
+ node.source
131
+ end
132
+ end
122
133
  end
123
134
  end
124
135
  end
@@ -55,6 +55,10 @@ module RuboCop
55
55
  next unless prefer
56
56
 
57
57
  corrector.replace(offense, prefer)
58
+ elsif node.arguments.any? { |argument| argument.children.empty? }
59
+ # In-place bracket removal would leave dangling commas (e.g.
60
+ # `concat([], [b])` -> `push(, b)`), so rebuild the call instead.
61
+ corrector.replace(offense, preferred_method(node))
58
62
  else
59
63
  corrector.replace(node.loc.selector, 'push')
60
64
  node.arguments.each do |argument|
@@ -75,7 +79,7 @@ module RuboCop
75
79
 
76
80
  def preferred_method(node)
77
81
  new_arguments =
78
- node.arguments.map do |arg|
82
+ node.arguments.flat_map do |arg|
79
83
  if arg.percent_literal?
80
84
  arg.children.map { |child| child.value.inspect }
81
85
  else
@@ -283,7 +283,10 @@ module RuboCop
283
283
 
284
284
  _condition, *branches, else_branch = *assignment
285
285
 
286
- return unless else_branch
286
+ # Use the node accessor rather than the raw destructured branch: for
287
+ # `x = unless cond; body; end` (no `else`) the parser puts `body` in the
288
+ # else slot, but `else_branch` correctly reports there is no `else` clause.
289
+ return unless assignment.else_branch
287
290
  return if allowed_single_line?([*branches, else_branch])
288
291
 
289
292
  add_offense(node, message: ASSIGN_TO_CONDITION_MSG) do |corrector|
@@ -657,6 +660,8 @@ module RuboCop
657
660
  remove_whitespace_in_branches(corrector, branch, condition, column)
658
661
 
659
662
  parent_keyword = branch.parent.loc.keyword
663
+ return if same_line?(parent_keyword, condition)
664
+
660
665
  corrector.remove_preceding(parent_keyword, parent_keyword.column - column)
661
666
  end
662
667
  end
@@ -89,7 +89,10 @@ module RuboCop
89
89
 
90
90
  arguments = arguments.first.children.first.to_a if arguments.first&.splat_type?
91
91
  constant_values = arguments.map do |argument|
92
- argument.value.to_sym if argument.respond_to?(:value)
92
+ # `respond_to?(:value)` is too broad: `int`/`float` nodes respond to it
93
+ # but their value is a `Numeric`, which has no `to_sym` (e.g.
94
+ # `private_constant 42`). Only symbol/string arguments are real names.
95
+ argument.value.to_sym if argument.type?(:sym, :str)
93
96
  end
94
97
 
95
98
  constant_values.include?(node.name)
@@ -59,12 +59,12 @@ module RuboCop
59
59
 
60
60
  # @!method historic_date?(node)
61
61
  def_node_matcher :historic_date?, <<~PATTERN
62
- (send _ _ _ (const (const {nil? (cbase)} :Date) _))
62
+ (call _ _ _ (const (const {nil? (cbase)} :Date) _))
63
63
  PATTERN
64
64
 
65
65
  # @!method to_datetime?(node)
66
66
  def_node_matcher :to_datetime?, <<~PATTERN
67
- (call _ :to_datetime)
67
+ (call !nil? :to_datetime)
68
68
  PATTERN
69
69
 
70
70
  def on_send(node)