rubocop 1.87.0 → 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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +75 -71
  3. data/config/obsoletion.yml +21 -1
  4. data/lib/rubocop/cli/command/auto_generate_config.rb +6 -0
  5. data/lib/rubocop/cop/base.rb +17 -2
  6. data/lib/rubocop/cop/bundler/gem_comment.rb +2 -2
  7. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  8. data/lib/rubocop/cop/layout/block_alignment.rb +41 -4
  9. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  10. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  11. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  12. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  13. data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
  14. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  15. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  16. data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
  17. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  18. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  19. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  20. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  21. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  22. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  23. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
  24. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  25. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  26. data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
  27. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  28. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  29. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  30. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  31. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  32. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
  33. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  34. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  35. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  36. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
  37. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  38. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  39. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  40. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  41. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  42. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  43. data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
  44. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  45. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  46. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  47. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  48. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  49. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  50. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  51. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  52. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  53. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  54. data/lib/rubocop/cop/lint/to_enum_arguments.rb +28 -1
  55. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  56. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  57. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +3 -1
  58. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  59. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
  60. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  61. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  62. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  63. data/lib/rubocop/cop/style/alias.rb +1 -1
  64. data/lib/rubocop/cop/style/and_or.rb +1 -1
  65. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  66. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  67. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  68. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  69. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  70. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  71. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  72. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  73. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  74. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  75. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  76. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  77. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  78. data/lib/rubocop/cop/style/date_time.rb +2 -2
  79. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  80. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  81. data/lib/rubocop/cop/style/file_write.rb +17 -14
  82. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  83. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
  84. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  85. data/lib/rubocop/cop/style/parallel_assignment.rb +8 -1
  86. data/lib/rubocop/cop/style/redundant_format.rb +1 -0
  87. data/lib/rubocop/cop/style/semicolon.rb +16 -1
  88. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  89. data/lib/rubocop/cop/style/word_array.rb +1 -0
  90. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  91. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  92. data/lib/rubocop/server/core.rb +6 -0
  93. data/lib/rubocop/version.rb +1 -1
  94. metadata +3 -3
@@ -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
@@ -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)
@@ -79,6 +79,11 @@ module RuboCop
79
79
  corrector.replace(range, replacement)
80
80
 
81
81
  comments_in_range(node).reverse_each do |comment|
82
+ # Only relocate comments that the replacement destroys. A trailing
83
+ # comment after the chain survives in place, so moving it would
84
+ # duplicate it (and splitting the line drops the indentation).
85
+ next if comment.source_range.begin_pos >= range.end_pos
86
+
82
87
  corrector.insert_before(node, "#{comment.source}\n")
83
88
  end
84
89
  end
@@ -70,7 +70,7 @@ module RuboCop
70
70
 
71
71
  def allowed_var?(node)
72
72
  env_key_node = node.children.last
73
- env_key_node.str_type? && cop_config['AllowedVars'].include?(env_key_node.value)
73
+ env_key_node.str_type? && cop_config['AllowedVariables'].include?(env_key_node.value)
74
74
  end
75
75
 
76
76
  def used_as_flag?(node)
@@ -104,30 +104,33 @@ module RuboCop
104
104
 
105
105
  def replacement(mode, filename, content, write_node)
106
106
  replacement = "#{write_method(mode)}(#{filename.source}, #{content.source})"
107
- heredoc = heredoc_in_write(write_node)
108
- return replacement unless heredoc
107
+ heredocs = removed_heredocs(filename, content, write_node)
108
+ return replacement if heredocs.empty?
109
109
 
110
- <<~REPLACEMENT.chomp
111
- #{replacement}
112
- #{heredoc_range(heredoc).source}
113
- REPLACEMENT
110
+ [replacement, *heredocs.map { |heredoc| heredoc_range(heredoc).source }].join("\n")
114
111
  end
115
112
 
116
- def heredoc_in_write(write_node)
117
- return unless write_node.block_type? && (first_argument = write_node.body.first_argument)
118
-
119
- find_heredoc(first_argument)
113
+ # Heredocs opened in the arguments keep working in the replacement, but their
114
+ # bodies are lost when they lie within the replaced range, so they need to be
115
+ # restored after the replacement.
116
+ def removed_heredocs(filename, content, write_node)
117
+ [filename, content].flat_map { |argument| find_heredocs(argument) }
118
+ .select { |heredoc| removed?(heredoc, write_node) }
119
+ .sort_by { |heredoc| heredoc.loc.heredoc_body.begin_pos }
120
120
  end
121
121
 
122
122
  def heredoc_range(heredoc)
123
123
  range_between(heredoc.loc.heredoc_body.begin_pos, heredoc.loc.heredoc_end.end_pos)
124
124
  end
125
125
 
126
- def find_heredoc(node)
127
- return node if node.respond_to?(:heredoc?) && node.heredoc?
128
- return if node.send_type? && !(receiver = node.receiver)
126
+ def find_heredocs(node)
127
+ [node, *node.each_descendant(:any_str)].select do |child|
128
+ child.respond_to?(:heredoc?) && child.heredoc?
129
+ end
130
+ end
129
131
 
130
- find_heredoc(receiver)
132
+ def removed?(heredoc, write_node)
133
+ heredoc.loc.heredoc_end.end_pos <= write_node.source_range.end_pos
131
134
  end
132
135
  end
133
136
  end
@@ -19,6 +19,22 @@ module RuboCop
19
19
  # This cop is unsafe because it cannot be guaranteed that the receiver
20
20
  # is a `Hash` or responds to the replacement method.
21
21
  #
22
+ # Additionally, the replacement may change the order of the resulting
23
+ # hash: `Hash#slice` returns entries in the order the keys are given,
24
+ # whereas `select`, `filter`, and `reject` preserve the entry order of
25
+ # the receiver.
26
+ #
27
+ # For example:
28
+ #
29
+ # [source,ruby]
30
+ # ----
31
+ # hash = {foo: 1, bar: 2, baz: 3}
32
+ # keys = %i[baz foo]
33
+ #
34
+ # hash.select { |k, _v| keys.include?(k) } # => {foo: 1, baz: 3}
35
+ # hash.slice(*keys) # => {baz: 3, foo: 1}
36
+ # ----
37
+ #
22
38
  # @example
23
39
  #
24
40
  # # bad
@@ -86,7 +86,7 @@ module RuboCop
86
86
  MSG_USE_NORMAL = 'Modifier form of `%<keyword>s` makes the line too long.'
87
87
 
88
88
  def self.autocorrect_incompatible_with
89
- [Style::SoleNestedConditional]
89
+ [Style::Next, Style::SoleNestedConditional]
90
90
  end
91
91
 
92
92
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -6,6 +6,14 @@ module RuboCop
6
6
  # Checks whether some constant value isn't a
7
7
  # mutable literal (e.g. array or hash).
8
8
  #
9
+ # When the `Recursive` option is enabled, mutable literals nested inside
10
+ # arrays and hashes are also frozen, so an offense on the outermost
11
+ # unfrozen literal will autocorrect every nested mutable literal as well.
12
+ # When the outer literal already has `.freeze` appended, the cop descends
13
+ # into it and reports each outermost unfrozen literal underneath. The
14
+ # option is disabled by default to preserve existing behavior; opt in to
15
+ # get strict nested freezing.
16
+ #
9
17
  # Strict mode can be used to freeze all constants, rather than
10
18
  # just literals.
11
19
  # Strict mode is considered an experimental feature. It has not been
@@ -49,6 +57,17 @@ module RuboCop
49
57
  # CONST = Something.new
50
58
  #
51
59
  #
60
+ # @example Recursive: false (default)
61
+ # # good - only the outer container needs to be frozen
62
+ # CONST = [{ a: [], b: 'foo' }].freeze
63
+ #
64
+ # @example Recursive: true
65
+ # # bad - nested mutable literals must be frozen too
66
+ # CONST = [{ a: [], b: 'foo' }].freeze
67
+ #
68
+ # # good
69
+ # CONST = [{ a: [].freeze, b: 'foo'.freeze }.freeze].freeze
70
+ #
52
71
  # @example EnforcedStyle: strict
53
72
  # # bad
54
73
  # CONST = Something.new
@@ -138,10 +157,30 @@ module RuboCop
138
157
  private
139
158
 
140
159
  def on_assignment(value)
141
- if style == :strict
142
- strict_check(value)
160
+ nodes = mutable_nodes(value) do |node|
161
+ if style == :strict
162
+ strict_check(node)
163
+ else
164
+ literal_check(node)
165
+ end
166
+ end
167
+
168
+ nodes.each do |node|
169
+ add_offense(node) { |corrector| autocorrect(corrector, node) }
170
+ end
171
+ end
172
+
173
+ def mutable_nodes(value, &block)
174
+ if recursive? && explicitly_frozen_literal?(value)
175
+ literal_children(value.receiver).flat_map { |c| mutable_nodes(c, &block) }
143
176
  else
144
- check(value)
177
+ node_offending = yield(value)
178
+
179
+ if node_offending
180
+ [value]
181
+ else
182
+ []
183
+ end
145
184
  end
146
185
  end
147
186
 
@@ -151,18 +190,20 @@ module RuboCop
151
190
  return if frozen_string_literal?(value)
152
191
  return if shareable_constant_value?(value)
153
192
 
154
- add_offense(value) { |corrector| autocorrect(corrector, value) }
193
+ true
155
194
  end
156
195
 
157
- def check(value)
158
- range_enclosed_in_parentheses = range_enclosed_in_parentheses?(value)
159
- return unless mutable_literal?(value) ||
160
- (target_ruby_version <= 2.7 && range_enclosed_in_parentheses)
161
-
196
+ def literal_check(value)
197
+ return unless mutable_or_unfrozen_range?(value)
162
198
  return if frozen_string_literal?(value)
163
199
  return if shareable_constant_value?(value)
164
200
 
165
- add_offense(value) { |corrector| autocorrect(corrector, value) }
201
+ true
202
+ end
203
+
204
+ def mutable_or_unfrozen_range?(value)
205
+ mutable_literal?(value) ||
206
+ (target_ruby_version <= 2.7 && range_enclosed_in_parentheses?(value))
166
207
  end
167
208
 
168
209
  def autocorrect(corrector, node)
@@ -171,13 +212,66 @@ module RuboCop
171
212
  splat_value = splat_value(node)
172
213
  if splat_value
173
214
  correct_splat_expansion(corrector, expr, splat_value)
174
- elsif node.array_type? && !node.bracketed?
215
+ corrector.insert_after(expr, '.freeze')
216
+ return
217
+ end
218
+
219
+ if node.array_type? && !node.bracketed?
175
220
  corrector.wrap(expr, '[', ']')
176
221
  elsif requires_parentheses?(node)
177
222
  corrector.wrap(expr, '(', ')')
178
223
  end
179
224
 
180
225
  corrector.insert_after(expr, '.freeze')
226
+
227
+ freeze_nested_literals(corrector, node) if recursive?
228
+ end
229
+
230
+ # Recursively freezes every nested mutable literal inside an array or
231
+ # hash literal. Already-frozen subtrees are not re-frozen, but their
232
+ # children are still inspected for unfrozen literals deeper down.
233
+ def freeze_nested_literals(corrector, node)
234
+ literal_children(node).each do |child|
235
+ if explicitly_frozen_literal?(child)
236
+ freeze_nested_literals(corrector, child.receiver)
237
+ elsif freezable_nested_literal?(child)
238
+ autocorrect(corrector, child)
239
+ end
240
+ end
241
+ end
242
+
243
+ def freezable_nested_literal?(node)
244
+ return false if frozen_string_literal?(node)
245
+ return false if shareable_constant_value?(node)
246
+
247
+ mutable_literal?(node)
248
+ end
249
+
250
+ # Returns the child literals of an array or hash node that may
251
+ # themselves need freezing. For hashes, both keys and values are
252
+ # included. Percent-literal arrays (e.g. `%w(a b)`) are skipped because
253
+ # `.freeze` cannot be appended to their contents.
254
+ def literal_children(node)
255
+ case node.type
256
+ when :array
257
+ return [] if node.percent_literal?
258
+
259
+ node.children
260
+ when :hash
261
+ node.children.flat_map { |child| child.pair_type? ? child.children : [] }
262
+ else
263
+ []
264
+ end
265
+ end
266
+
267
+ def explicitly_frozen_literal?(node)
268
+ return false unless node.send_type? && node.method?(:freeze)
269
+
270
+ node.receiver && mutable_literal?(node.receiver)
271
+ end
272
+
273
+ def recursive?
274
+ cop_config.fetch('Recursive', false)
181
275
  end
182
276
 
183
277
  def mutable_literal?(value)
@@ -38,7 +38,7 @@ module RuboCop
38
38
  rhs_elements = Array(rhs).compact # edge case for one constant
39
39
 
40
40
  return if allowed_lhs?(node.assignments) || allowed_rhs?(rhs) ||
41
- allowed_masign?(node.assignments, rhs_elements)
41
+ allowed_masign?(node.assignments, rhs_elements) || contains_heredoc?(rhs)
42
42
 
43
43
  range = node.source_range.begin.join(rhs.source_range.end)
44
44
 
@@ -77,6 +77,13 @@ module RuboCop
77
77
  !node.array_type? || elements.any?(&:splat_type?)
78
78
  end
79
79
 
80
+ # Autocorrection splits the assignment into single assignments on
81
+ # consecutive lines, which would put following assignments into the
82
+ # heredoc body unless the heredoc bodies were moved along.
83
+ def contains_heredoc?(node)
84
+ node.each_descendant(:any_str).any?(&:heredoc?)
85
+ end
86
+
80
87
  def assignment_corrector(node, rhs, order)
81
88
  if node.parent&.rescue_type?
82
89
  _assignment, modifier = *node.parent
@@ -108,6 +108,7 @@ module RuboCop
108
108
 
109
109
  def detect_unnecessary_fields(node)
110
110
  return unless node.first_argument&.str_type?
111
+ return if node.first_argument.heredoc?
111
112
 
112
113
  string = node.first_argument.value
113
114
  arguments = node.arguments[1..]