rubocop 1.87.0 → 1.88.1

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 (162) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +78 -72
  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 +5 -3
  7. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  8. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  9. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  10. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  11. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  12. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  13. data/lib/rubocop/cop/layout/block_alignment.rb +58 -4
  14. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  15. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  16. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  17. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  18. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  19. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  20. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  21. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  22. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  23. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  24. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  25. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  26. data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
  27. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  28. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  29. data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
  30. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  31. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  32. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  33. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  34. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  35. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  36. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
  37. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  38. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  39. data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
  40. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  41. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  42. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  43. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  44. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  45. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
  46. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  47. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  48. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  49. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
  50. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  51. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  52. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  53. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  54. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  55. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  56. data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
  57. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  58. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  59. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  60. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  61. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  62. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  63. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  64. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  65. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  66. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  67. data/lib/rubocop/cop/lint/to_enum_arguments.rb +35 -2
  68. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  69. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  70. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +35 -9
  71. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  72. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
  73. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  74. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  75. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  76. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  77. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  78. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  79. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  80. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  81. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  82. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  83. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  84. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  85. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  86. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  87. data/lib/rubocop/cop/style/alias.rb +1 -1
  88. data/lib/rubocop/cop/style/and_or.rb +1 -1
  89. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  90. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  91. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  92. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  93. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  94. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  95. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  96. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  97. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  98. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  99. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  100. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  101. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  102. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  103. data/lib/rubocop/cop/style/date_time.rb +2 -2
  104. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  105. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  106. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  107. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  108. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  109. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  110. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  111. data/lib/rubocop/cop/style/env_home.rb +4 -0
  112. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  113. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  114. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  115. data/lib/rubocop/cop/style/file_null.rb +4 -2
  116. data/lib/rubocop/cop/style/file_write.rb +17 -14
  117. data/lib/rubocop/cop/style/format_string.rb +13 -1
  118. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  119. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  120. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
  121. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  122. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  123. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  124. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  125. data/lib/rubocop/cop/style/lambda.rb +7 -1
  126. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  127. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  128. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +6 -2
  129. data/lib/rubocop/cop/style/method_def_parentheses.rb +1 -1
  130. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  131. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  132. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  133. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  134. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  135. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  136. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  137. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  138. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  139. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  140. data/lib/rubocop/cop/style/parallel_assignment.rb +19 -3
  141. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  142. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  143. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  144. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  145. data/lib/rubocop/cop/style/redundant_format.rb +29 -0
  146. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  147. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  148. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  149. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  150. data/lib/rubocop/cop/style/semicolon.rb +20 -5
  151. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  152. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  153. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  154. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  155. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  156. data/lib/rubocop/cop/style/word_array.rb +1 -0
  157. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  158. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  159. data/lib/rubocop/runner.rb +5 -3
  160. data/lib/rubocop/server/core.rb +6 -0
  161. data/lib/rubocop/version.rb +1 -1
  162. metadata +3 -3
@@ -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)
@@ -78,10 +78,12 @@ module RuboCop
78
78
 
79
79
  def acceptable?(node)
80
80
  # Using a hardcoded null device is acceptable when inside an array or
81
- # inside a hash to ensure behavior doesn't change.
81
+ # inside a hash to ensure behavior doesn't change. A `str` that is part of
82
+ # an interpolated or concatenated string (`dstr`) is not a standalone null
83
+ # device either, and replacing it would corrupt the surrounding string.
82
84
  return false unless node.parent
83
85
 
84
- node.parent.type?(:array, :pair)
86
+ node.parent.type?(:array, :pair, :dstr)
85
87
  end
86
88
  end
87
89
  end
@@ -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
@@ -144,10 +144,22 @@ module RuboCop
144
144
  end
145
145
 
146
146
  def format_single_parameter(arg)
147
+ # `format(fmt, *args)` is equivalent to `fmt % args`, so unwrap the splat
148
+ # and render the argument it splats.
149
+ return format_single_parameter(arg.children.first) if arg.splat_type?
150
+
147
151
  source = arg.source
148
152
  return "{ #{source} }" if arg.hash_type?
149
153
 
150
- arg.send_type? && arg.operator_method? && !arg.parenthesized? ? "(#{source})" : source
154
+ requires_parentheses?(arg) ? "(#{source})" : source
155
+ end
156
+
157
+ # An argument that binds looser than `%` (a ternary, range, assignment, or
158
+ # operator call) must be parenthesized to keep its meaning.
159
+ def requires_parentheses?(arg)
160
+ return true if arg.assignment? || arg.type?(:if, :and, :or, :range)
161
+
162
+ arg.send_type? && arg.operator_method? && !arg.parenthesized?
151
163
  end
152
164
  end
153
165
  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
@@ -263,6 +263,8 @@ module RuboCop
263
263
 
264
264
  hash_node = pair_node.parent
265
265
  return unless hash_node.parent&.return_type? && !hash_node.braces?
266
+ # This runs once per pair, but the hash must only be wrapped once.
267
+ return unless pair_node.equal?(hash_node.pairs.first)
266
268
 
267
269
  corrector.wrap(hash_node, '{', '}')
268
270
  end
@@ -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
@@ -82,7 +82,7 @@ module RuboCop
82
82
 
83
83
  then_code, else_code = else_code, then_code if node.unless?
84
84
 
85
- "#{node.condition.source} ? #{then_code} : #{else_code}"
85
+ "#{ternary_condition(node)} ? #{then_code} : #{else_code}"
86
86
  end
87
87
 
88
88
  def correct_elsif(node)
@@ -103,6 +103,14 @@ module RuboCop
103
103
  "#{method.source}(#{arguments.source})"
104
104
  end
105
105
 
106
+ # An assignment used as the condition must be parenthesized, otherwise the
107
+ # assignment would capture the whole ternary (`a = b ? c : d` instead of
108
+ # `(a = b) ? c : d`), changing what gets assigned.
109
+ def ternary_condition(node)
110
+ condition = node.condition
111
+ condition.assignment? ? "(#{condition.source})" : condition.source
112
+ end
113
+
106
114
  def build_else_branch(second_condition)
107
115
  result = <<~RUBY
108
116
  elsif #{second_condition.condition.source}
@@ -26,7 +26,7 @@ module RuboCop
26
26
  def on_new_investigation
27
27
  processed_source.comments.each do |comment|
28
28
  next if comment_line?(processed_source[comment.loc.line - 1]) ||
29
- comment.text.match?(/\A# rubocop:(enable|disable)/)
29
+ comment.text.match?(/\A# rubocop:(enable|disable|todo)/)
30
30
 
31
31
  add_offense(comment)
32
32
  end
@@ -37,6 +37,10 @@ module RuboCop
37
37
  return unless (ancestor = node.parent&.parent)
38
38
 
39
39
  merge_kwargs?(ancestor) do |merge_node, hash_node, other_hash_node|
40
+ # A block-pass argument (e.g. `merge(other, &block)`) has no keyword
41
+ # equivalent, so spreading it would produce invalid Ruby (`**&block`).
42
+ next if other_hash_node.any?(&:block_pass_type?)
43
+
40
44
  add_offense(merge_node) do |corrector|
41
45
  autocorrect(corrector, node, hash_node, other_hash_node)
42
46
  end
@@ -62,11 +62,15 @@ module RuboCop
62
62
  end
63
63
 
64
64
  def append_newline_to_last_kwoptarg(arguments, corrector)
65
- last_argument = arguments.last
66
- return if last_argument.type?(:kwrestarg, :blockarg)
65
+ # The newline only needs restoring when the moved keyword argument was
66
+ # the last parameter, so removing it also consumes the line break before
67
+ # the body. When a `kwoptarg` already trails the list, the body stays
68
+ # separated and inserting a newline would leave a spurious blank line.
69
+ return unless arguments.last.kwarg_type?
70
+ return if arguments.parent.block_type?
67
71
 
68
72
  last_kwoptarg = arguments.reverse.find(&:kwoptarg_type?)
69
- corrector.insert_after(last_kwoptarg, "\n") unless arguments.parent.block_type?
73
+ corrector.insert_after(last_kwoptarg, "\n")
70
74
  end
71
75
 
72
76
  def remove_kwargs(kwarg_nodes, corrector)
@@ -118,7 +118,13 @@ module RuboCop
118
118
  end
119
119
 
120
120
  def lambda_arg_string(args)
121
- args.children.map(&:source).join(', ')
121
+ # Block-local (shadow) arguments are separated from regular arguments by a
122
+ # `;`; joining everything with `,` would turn them into extra parameters
123
+ # and change the lambda's arity.
124
+ regular, shadow = args.children.partition { |arg| !arg.shadowarg_type? }
125
+ arg_string = regular.map(&:source).join(', ')
126
+ arg_string += "; #{shadow.map(&:source).join(', ')}" unless shadow.empty?
127
+ arg_string
122
128
  end
123
129
  end
124
130
  end
@@ -6,6 +6,17 @@ module RuboCop
6
6
  # Prefer `select` or `reject` over `map { ... }.compact`.
7
7
  # This cop also handles `filter_map { ... }`, similar to `map { ... }.compact`.
8
8
  #
9
+ # @safety
10
+ # This cop is unsafe because `compact` also removes `nil` elements that
11
+ # were already present in the receiver, whereas `select`/`reject` keep
12
+ # them. The result therefore differs when the collection contains `nil`:
13
+ #
14
+ # [source,ruby]
15
+ # ----
16
+ # [nil, 1].map { |e| e if e }.compact # => [1]
17
+ # [nil, 1].select { |e| e } # => [nil, 1]
18
+ # ----
19
+ #
9
20
  # @example
10
21
  #
11
22
  # # bad
@@ -65,7 +65,7 @@ module RuboCop
65
65
 
66
66
  # @!method suitable_argument_node?(node)
67
67
  def_node_matcher :suitable_argument_node?, <<-PATTERN
68
- !{splat forwarded-restarg forwarded-args (hash (forwarded-kwrestarg)) (block-pass nil?)}
68
+ !{splat forwarded-restarg forwarded-args (hash (forwarded-kwrestarg)) block-pass}
69
69
  PATTERN
70
70
 
71
71
  # @!method each_block_with_push?(node)
@@ -88,8 +88,12 @@ module RuboCop
88
88
  #
89
89
  def parenthesized_it_method_in_block?(node)
90
90
  return false unless node.method?(:it)
91
- return false unless (block_node = node.each_ancestor(:block).first)
92
- return false unless block_node.arguments.empty_and_without_delimiters?
91
+ return false unless (block_node = node.each_ancestor(:any_block).first)
92
+ # Inside a numbered/`it` block, a bare `it` is a parse error (it conflicts
93
+ # with the implicit parameter), so `it()` must keep its parentheses.
94
+ if block_node.block_type? && !block_node.arguments.empty_and_without_delimiters?
95
+ return false
96
+ end
93
97
 
94
98
  !node.receiver && node.arguments.empty? && !node.block_literal?
95
99
  end
@@ -166,7 +166,7 @@ module RuboCop
166
166
 
167
167
  def anonymous_arguments?(node)
168
168
  return true if node.arguments.any? do |arg|
169
- arg.type?(:forward_arg, :restarg, :kwrestarg)
169
+ arg.forward_arg_type? || (arg.type?(:restarg, :kwrestarg) && arg.name.nil?)
170
170
  end
171
171
  return false unless (last_argument = node.last_argument)
172
172
 
@@ -55,8 +55,11 @@ module RuboCop
55
55
  lhs, operator, rhs = comparison_condition(node.condition)
56
56
  return unless operator
57
57
 
58
+ # For `unless`, the branches run opposite to an `if`, so swap them to
59
+ # keep the `max`/`min` decision correct.
58
60
  if_branch = node.if_branch
59
61
  else_branch = node.else_branch
62
+ if_branch, else_branch = else_branch, if_branch if node.unless?
60
63
  preferred_method = preferred_method(operator, lhs, rhs, if_branch, else_branch)
61
64
  return unless preferred_method
62
65
 
@@ -36,7 +36,7 @@ module RuboCop
36
36
  private
37
37
 
38
38
  def non_modifier_then?(node)
39
- node.then? && node.loc.begin.line != node.if_branch&.loc&.line
39
+ node.multiline? && node.then? && node.loc.begin.line != node.if_branch&.loc&.line
40
40
  end
41
41
  end
42
42
  end
@@ -65,10 +65,16 @@ module RuboCop
65
65
  if style == :keyword
66
66
  rhs.begin_type?
67
67
  else
68
- rhs.kwbegin_type?
68
+ # A `begin` block with `rescue`/`ensure` cannot be expressed with
69
+ # parentheses, so wrapping it in `(` and `)` is not possible.
70
+ rhs.kwbegin_type? && !contains_rescue_or_ensure?(rhs)
69
71
  end
70
72
  end
71
73
 
74
+ def contains_rescue_or_ensure?(node)
75
+ node.each_child_node(:rescue, :ensure).any?
76
+ end
77
+
72
78
  def keyword_autocorrect(node, corrector)
73
79
  node_buf = node.source_range.source_buffer
74
80
  corrector.replace(node.loc.begin, keyword_begin_str(node, node_buf))
@@ -51,10 +51,12 @@ module RuboCop
51
51
  end
52
52
 
53
53
  arguments_range = range_with_surrounding_space(arguments_range(node), side: :left)
54
- # If the method name isn't on the same line as def, move it directly after def
54
+ # If the method name isn't on the same line as `def`, pull the name and
55
+ # the opening parenthesis up next to `def` so the collapsed signature
56
+ # stays on a single line and remains valid Ruby.
55
57
  if arguments_range.first_line != opening_line(node)
56
- corrector.remove(node.loc.name)
57
- corrector.insert_after(node.loc.keyword, " #{node.loc.name.source}")
58
+ prefix_range = range_between(node.loc.keyword.end_pos, begin_of_arguments.begin_pos)
59
+ corrector.replace(prefix_range, " #{prefix_range.source.strip}")
58
60
  end
59
61
 
60
62
  corrector.remove(arguments_range)
@@ -85,7 +87,12 @@ module RuboCop
85
87
  end
86
88
 
87
89
  def definition_width(node)
88
- node.source_range.begin.join(node.arguments.source_range.end).length
90
+ # Measure the collapsed single-line width the autocorrect would
91
+ # produce, not the multi-line source length, so a signature that
92
+ # would fit on one line is not skipped.
93
+ signature = node.source_range.begin.join(node.arguments.source_range.end).source
94
+
95
+ signature.gsub(/\s+/, ' ').length
89
96
  end
90
97
  end
91
98
  end
@@ -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)
@@ -6,6 +6,11 @@ module RuboCop
6
6
  # Checks for lambdas and procs that always return nil,
7
7
  # which can be replaced with an empty lambda or proc instead.
8
8
  #
9
+ # NOTE: A `proc` that returns nil via an explicit `return` is allowed,
10
+ # because in a `proc` `return` exits the enclosing method, so removing it
11
+ # would change behavior. A lambda is still reported, since there `return`
12
+ # only exits the lambda itself.
13
+ #
9
14
  # @example
10
15
  # # bad
11
16
  # -> { nil }
@@ -46,6 +51,9 @@ module RuboCop
46
51
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
47
52
  return unless node.lambda_or_proc?
48
53
  return unless nil_return?(node.body)
54
+ # `return` inside a non-lambda proc returns from the enclosing method,
55
+ # so dropping it changes behavior; only a lambda can omit it safely.
56
+ return if node.body.return_type? && !node.lambda?
49
57
 
50
58
  message = format(MSG, type: node.lambda? ? 'lambda' : 'proc')
51
59
  add_offense(node, message: message) do |corrector|
@@ -92,7 +92,7 @@ module RuboCop
92
92
  return unless numeric
93
93
 
94
94
  return if allowed_method_name?(node.method_name) ||
95
- node.each_ancestor(:send, :block).any? do |ancestor|
95
+ node.each_ancestor(:send, :any_block).any? do |ancestor|
96
96
  allowed_method_name?(ancestor.method_name)
97
97
  end
98
98
 
@@ -61,7 +61,7 @@ module RuboCop
61
61
  def custom_class_or_module_definition?(node)
62
62
  parent = node.parent
63
63
 
64
- parent.type?(:class, :module) && node.left_siblings.empty?
64
+ parent&.type?(:class, :module) && node.left_siblings.empty?
65
65
  end
66
66
  end
67
67
  end
@@ -46,7 +46,7 @@ module RuboCop
46
46
  end
47
47
 
48
48
  def super_used?(node)
49
- node.parent.each_node(:zsuper).any?
49
+ node.parent.each_node(:zsuper, :super).any?
50
50
  end
51
51
  end
52
52
  end
@@ -27,6 +27,7 @@ module RuboCop
27
27
  def on_def(node)
28
28
  each_misplaced_optional_arg(node.arguments) { |argument| add_offense(argument) }
29
29
  end
30
+ alias on_defs on_def
30
31
 
31
32
  private
32
33
 
@@ -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
@@ -226,14 +233,23 @@ module RuboCop
226
233
  def source(node, loc)
227
234
  # __FILE__ is treated as a StrNode but has no begin
228
235
  if node.str_type? && loc.respond_to?(:begin) && loc.begin.nil?
229
- "'#{node.source}'"
236
+ # `%w` elements have no per-element delimiter, so the value must be
237
+ # quoted and escaped to stay valid (e.g. `%w(it's)` -> `'it\'s'`).
238
+ quote(node.value)
230
239
  elsif node.sym_type? && !node.loc?(:begin)
231
- ":#{node.source}"
240
+ # `%i` elements have no per-element delimiter, so a symbol that needs
241
+ # quoting must be emitted as `:"..."` (e.g. `%i(foo-bar)` -> `:"foo-bar"`),
242
+ # otherwise `:foo-bar` would parse as `:foo.-(bar)`.
243
+ node.value.inspect
232
244
  else
233
245
  node.source
234
246
  end
235
247
  end
236
248
 
249
+ def quote(string)
250
+ "'#{string.gsub(/[\\']/) { |char| "\\#{char}" }}'"
251
+ end
252
+
237
253
  def extract_sources(node)
238
254
  node.children.map(&:source)
239
255
  end
@@ -101,6 +101,8 @@ module RuboCop
101
101
  def string_source(node)
102
102
  if node.is_a?(String)
103
103
  node.scrub
104
+ elsif node.is_a?(Symbol)
105
+ node.to_s
104
106
  elsif node.respond_to?(:type) && node.type?(:str, :sym)
105
107
  node.source
106
108
  end