rubocop 1.88.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +3 -1
  3. data/lib/rubocop/cop/bundler/gem_comment.rb +3 -1
  4. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  5. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  6. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  7. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  8. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  9. data/lib/rubocop/cop/layout/block_alignment.rb +17 -0
  10. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  11. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  12. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  13. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  14. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  15. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  16. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  17. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  18. data/lib/rubocop/cop/lint/to_enum_arguments.rb +7 -1
  19. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +32 -8
  20. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  21. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  22. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  23. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  24. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  25. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  26. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  27. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  28. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  29. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  30. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  31. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  32. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  33. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  34. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  35. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  36. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  37. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  38. data/lib/rubocop/cop/style/env_home.rb +4 -0
  39. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  40. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  41. data/lib/rubocop/cop/style/file_null.rb +4 -2
  42. data/lib/rubocop/cop/style/format_string.rb +13 -1
  43. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  44. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  45. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  46. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  47. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  48. data/lib/rubocop/cop/style/lambda.rb +7 -1
  49. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  50. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  51. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +6 -2
  52. data/lib/rubocop/cop/style/method_def_parentheses.rb +1 -1
  53. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  54. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  55. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  56. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  57. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  58. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  59. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  60. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  61. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  62. data/lib/rubocop/cop/style/parallel_assignment.rb +11 -2
  63. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  64. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  65. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  66. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  67. data/lib/rubocop/cop/style/redundant_format.rb +28 -0
  68. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  69. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  70. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  71. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  72. data/lib/rubocop/cop/style/semicolon.rb +4 -4
  73. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  74. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  75. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  76. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  77. data/lib/rubocop/runner.rb +5 -3
  78. data/lib/rubocop/version.rb +1 -1
  79. metadata +2 -2
@@ -91,6 +91,8 @@ module RuboCop
91
91
  end
92
92
 
93
93
  def on_new_investigation
94
+ return unless @flagged_terms_regex
95
+
94
96
  investigate_filepath if cop_config['CheckFilepaths']
95
97
  investigate_tokens
96
98
  end
@@ -144,7 +146,7 @@ module RuboCop
144
146
  def preprocess_flagged_terms
145
147
  allowed_strings = []
146
148
  flagged_term_strings = []
147
- cop_config['FlaggedTerms'].each do |term, term_definition|
149
+ (cop_config['FlaggedTerms'] || {}).each do |term, term_definition|
148
150
  next if term_definition.nil?
149
151
 
150
152
  allowed_strings.concat(process_allowed_regex(term_definition['AllowedRegex']))
@@ -181,7 +183,11 @@ module RuboCop
181
183
  end
182
184
 
183
185
  def set_regexes(flagged_term_strings, allowed_strings)
184
- @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
186
+ # With no flagged terms an empty regex would match everything, so leave the
187
+ # regex nil and let `on_new_investigation` treat the cop as a no-op.
188
+ unless flagged_term_strings.empty?
189
+ @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
190
+ end
185
191
  @allowed_regex = array_to_ignorecase_regex(allowed_strings) unless allowed_strings.empty?
186
192
  end
187
193
 
@@ -174,6 +174,7 @@ module RuboCop
174
174
 
175
175
  method_node, method_name = find_definition(node)
176
176
  return unless method_node
177
+ return unless nameable_method?(method_name)
177
178
 
178
179
  body = method_node.body
179
180
  return unless body == node || body.children.last == node
@@ -209,6 +210,7 @@ module RuboCop
209
210
 
210
211
  method_node, method_name = find_definition(node)
211
212
  return false unless method_node
213
+ return false unless nameable_method?(method_name)
212
214
 
213
215
  defined_memoized?(method_node.body, arg.name) do |defined_ivar, return_ivar, ivar_assign|
214
216
  return false if matches?(method_name, ivar_assign)
@@ -250,6 +252,13 @@ module RuboCop
250
252
  nil
251
253
  end
252
254
 
255
+ # Operator and other non-word method names (e.g. `[]`, `+`, `<=>`) cannot form a
256
+ # valid instance variable name, so there is no matching ivar to enforce and a
257
+ # suggested correction like `@[]` would be invalid Ruby.
258
+ def nameable_method?(method_name)
259
+ /\A[a-zA-Z_]\w*\z/.match?(method_name.to_s.delete('!?='))
260
+ end
261
+
253
262
  def matches?(method_name, ivar_assign)
254
263
  return true if ivar_assign.nil? || INITIALIZE_METHODS.include?(method_name)
255
264
 
@@ -95,11 +95,14 @@ module RuboCop
95
95
 
96
96
  def autocorrect(corrector, node, range, offending_name, preferred_name)
97
97
  corrector.replace(range, preferred_name)
98
- correct_node(corrector, node.body, offending_name, preferred_name)
98
+ # Once the exception variable is reassigned, later references point to a
99
+ # different value, so stop correcting after the reassignment - both in the
100
+ # body and in the code following the `begin`/`rescue`.
101
+ return if correct_node(corrector, node.body, offending_name, preferred_name)
99
102
  return unless (kwbegin_node = node.parent.each_ancestor(:kwbegin).first)
100
103
 
101
104
  kwbegin_node.right_siblings.each do |child_node|
102
- correct_node(corrector, child_node, offending_name, preferred_name)
105
+ break if correct_node(corrector, child_node, offending_name, preferred_name)
103
106
  end
104
107
  end
105
108
 
@@ -113,6 +116,8 @@ module RuboCop
113
116
  end
114
117
  end
115
118
 
119
+ # Returns the reassignment node once the exception variable is reassigned (a truthy
120
+ # signal to stop correcting later references), or `nil` when no reassignment is found.
116
121
  # rubocop:disable Metrics/MethodLength
117
122
  def correct_node(corrector, node, offending_name, preferred_name)
118
123
  return unless node
@@ -131,9 +136,10 @@ module RuboCop
131
136
 
132
137
  if child_node.type?(:masgn, :lvasgn)
133
138
  correct_reassignment(corrector, child_node, offending_name, preferred_name)
134
- break
139
+ return child_node
135
140
  end
136
141
  end
142
+ nil
137
143
  end
138
144
  # rubocop:enable Metrics/MethodLength
139
145
 
@@ -37,7 +37,7 @@ module RuboCop
37
37
  return unless (receiver = node.receiver) && receiver.source == 'IO'
38
38
 
39
39
  argument = node.first_argument
40
- return if argument.respond_to?(:value) && argument.value.strip.start_with?('|')
40
+ return if argument&.str_type? && argument.value.strip.start_with?('|')
41
41
 
42
42
  add_offense(node, message: format(MSG, method_name: node.method_name)) do |corrector|
43
43
  corrector.replace(receiver, 'File')
@@ -25,7 +25,7 @@ module RuboCop
25
25
  # @!method marshal_load(node)
26
26
  def_node_matcher :marshal_load, <<~PATTERN
27
27
  (send (const {nil? cbase} :Marshal) ${:load :restore}
28
- !(send (const {nil? cbase} :Marshal) :dump ...))
28
+ !(send (const {nil? cbase} :Marshal) :dump ...) _?)
29
29
  PATTERN
30
30
 
31
31
  def on_send(node)
@@ -203,13 +203,23 @@ module RuboCop
203
203
  end
204
204
 
205
205
  def range_with_trailing_argument_comment(node)
206
- comment = processed_source.ast_with_comments[node.last_argument].last
206
+ comment = trailing_argument_comment(node)
207
207
  if comment
208
208
  add_range(node.source_range, comment.source_range)
209
209
  else
210
210
  node
211
211
  end
212
212
  end
213
+
214
+ # For a single-line declaration the parser associates the trailing
215
+ # comment with the first argument, not `last_argument`, so look through
216
+ # all arguments for a comment that trails the whole node.
217
+ def trailing_argument_comment(node)
218
+ comments = node.arguments.filter_map do |argument|
219
+ processed_source.ast_with_comments[argument].last
220
+ end
221
+ comments.find { |comment| comment.source_range.begin_pos >= node.source_range.end_pos }
222
+ end
213
223
  end
214
224
  end
215
225
  end
@@ -61,6 +61,10 @@ module RuboCop
61
61
 
62
62
  def correct_parent(parent, corrector)
63
63
  if parent.block_type?
64
+ # Convert a brace block to `do`, so the class's own `end` closes it
65
+ # once the closing delimiter is removed; otherwise a dangling `{` is
66
+ # left behind, producing invalid Ruby.
67
+ corrector.replace(parent.loc.begin, 'do') if parent.braces?
64
68
  corrector.remove(range_with_surrounding_space(parent.loc.end, newlines: false))
65
69
  elsif (class_node = parent.parent).body.nil?
66
70
  corrector.remove(range_for_empty_class_body(class_node, parent))
@@ -35,6 +35,10 @@ module RuboCop
35
35
  PATTERN
36
36
 
37
37
  def on_send(node)
38
+ # A trailing block (e.g. `Dir.each_child(path).none? { ... }`) changes
39
+ # the meaning and is not equivalent to `Dir.empty?`.
40
+ return if node.block_literal?
41
+
38
42
  offensive?(node) do |const_node, arg_node|
39
43
  replacement = "#{bang(node)}#{const_node.source}.empty?(#{arg_node.source})"
40
44
  add_offense(node, message: format(MSG, replacement: replacement)) do |corrector|
@@ -40,7 +40,7 @@ module RuboCop
40
40
  extend AutoCorrector
41
41
 
42
42
  MSG = 'Do not use empty `case` condition, instead use an `if` expression.'
43
- NOT_SUPPORTED_PARENT_TYPES = %i[return break next send csend].freeze
43
+ NOT_SUPPORTED_PARENT_TYPES = %i[return break next send csend yield super].freeze
44
44
 
45
45
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
46
46
  def on_case(case_node)
@@ -90,7 +90,17 @@ module RuboCop
90
90
  range = range_between(conditions.first.source_range.begin_pos,
91
91
  conditions.last.source_range.end_pos)
92
92
 
93
- corrector.replace(range, conditions.map(&:source).join(' || '))
93
+ corrector.replace(range, conditions.map { |c| parenthesize_condition(c) }.join(' || '))
94
+ end
95
+ end
96
+
97
+ # A condition that binds looser than `||` (e.g. a ternary, range, or
98
+ # assignment) must be parenthesized so the joined `||` keeps its meaning.
99
+ def parenthesize_condition(condition)
100
+ if condition.assignment? || condition.type?(:if, :and, :or, :range)
101
+ "(#{condition.source})"
102
+ else
103
+ condition.source
94
104
  end
95
105
  end
96
106
 
@@ -101,12 +101,19 @@ module RuboCop
101
101
 
102
102
  def autocorrect_class_new(corrector, node, class_new_node)
103
103
  indent = ' ' * node.loc.column
104
- class_name = node.name
104
+ class_name = constant_name(node)
105
105
  parent_class_name = class_new_node.first_argument.source
106
106
 
107
107
  corrector.replace(node, "class #{class_name} < #{parent_class_name}\n#{indent}end")
108
108
  end
109
109
 
110
+ # Preserve any namespace on the assigned constant (e.g. `Foo::Bar`),
111
+ # which `node.name` drops.
112
+ def constant_name(node)
113
+ namespace = node.namespace
114
+ namespace ? "#{namespace.source}::#{node.name}" : node.name
115
+ end
116
+
110
117
  def autocorrect_class_definition(corrector, node)
111
118
  class_name = node.identifier.source
112
119
  parent_class_name = node.parent_class.source
@@ -42,6 +42,10 @@ module RuboCop
42
42
  MSG = 'Use an empty string literal instead of heredoc.'
43
43
 
44
44
  def on_heredoc(node)
45
+ # A backtick heredoc (`<<~`CMD``) executes a command, so it cannot be
46
+ # replaced with an empty string literal.
47
+ return if node.xstr_type?
48
+
45
49
  heredoc_body = node.loc.heredoc_body
46
50
 
47
51
  return unless heredoc_body.source.empty?
@@ -44,13 +44,18 @@ module RuboCop
44
44
  def_node_matcher :str_node, '(send (const {nil? cbase} :String) :new)'
45
45
 
46
46
  # @!method array_with_block(node)
47
- def_node_matcher :array_with_block, '(block (send (const {nil? cbase} :Array) :new) args _)'
47
+ def_node_matcher :array_with_block, <<~PATTERN
48
+ {
49
+ (block (send (const {nil? cbase} :Array) :new) args _)
50
+ ({numblock itblock} (send (const {nil? cbase} :Array) :new) ...)
51
+ }
52
+ PATTERN
48
53
 
49
54
  # @!method hash_with_block(node)
50
55
  def_node_matcher :hash_with_block, <<~PATTERN
51
56
  {
52
57
  (block (send (const {nil? cbase} :Hash) :new) args _)
53
- (numblock (send (const {nil? cbase} :Hash) :new) ...)
58
+ ({numblock itblock} (send (const {nil? cbase} :Hash) :new) ...)
54
59
  }
55
60
  PATTERN
56
61
 
@@ -45,36 +45,46 @@ module RuboCop
45
45
  MSG_TRAILING_CONDITIONAL = 'Do not use trailing conditionals in string interpolation.'
46
46
  MSG_TERNARY = 'Do not return empty strings in string interpolation.'
47
47
 
48
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
49
48
  def on_interpolation(node)
50
49
  node.each_child_node(:if) do |child_node|
51
50
  if style == :trailing_conditional
52
- if empty_if_outcome?(child_node)
53
- ternary_style_autocorrect(child_node, child_node.else_branch.source, 'unless')
54
- end
55
-
56
- if empty_else_outcome?(child_node)
57
- ternary_style_autocorrect(child_node, child_node.if_branch.source, 'if')
58
- end
51
+ trailing_conditional_correction(child_node)
59
52
  elsif style == :ternary
60
- next unless child_node.modifier_form?
61
-
62
- ternary_component = if child_node.unless?
63
- "'' : #{child_node.if_branch.source}"
64
- else
65
- "#{child_node.if_branch.source} : ''"
66
- end
67
-
68
- add_offense(node, message: MSG_TRAILING_CONDITIONAL) do |corrector|
69
- corrector.replace(node, "\#{#{child_node.condition.source} ? #{ternary_component}}")
70
- end
53
+ ternary_correction(node, child_node)
71
54
  end
72
55
  end
73
56
  end
74
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
75
57
 
76
58
  private
77
59
 
60
+ def trailing_conditional_correction(child_node)
61
+ # A modifier `if`/`unless` is already a trailing conditional and has
62
+ # no `else` branch, so the ternary-to-trailing rewrite does not apply.
63
+ return if child_node.modifier_form?
64
+
65
+ if empty_if_outcome?(child_node)
66
+ ternary_style_autocorrect(child_node, child_node.else_branch.source, 'unless')
67
+ end
68
+
69
+ return unless empty_else_outcome?(child_node)
70
+
71
+ ternary_style_autocorrect(child_node, child_node.if_branch.source, 'if')
72
+ end
73
+
74
+ def ternary_correction(node, child_node)
75
+ return unless child_node.modifier_form?
76
+
77
+ ternary_component = if child_node.unless?
78
+ "'' : #{child_node.if_branch.source}"
79
+ else
80
+ "#{child_node.if_branch.source} : ''"
81
+ end
82
+
83
+ add_offense(node, message: MSG_TRAILING_CONDITIONAL) do |corrector|
84
+ corrector.replace(node, "\#{#{child_node.condition.source} ? #{ternary_component}}")
85
+ end
86
+ end
87
+
78
88
  def empty_if_outcome?(node)
79
89
  empty_branch_outcome?(node.if_branch)
80
90
  end
@@ -45,6 +45,10 @@ module RuboCop
45
45
  def on_send(node)
46
46
  return unless env_home?(node)
47
47
  return if node.arguments.count == 2 && !node.arguments[1].nil_type?
48
+ # `ENV.fetch('HOME') { default }` supplies a fallback, just like
49
+ # `ENV.fetch('HOME', default)`. `Dir.home` ignores the block, so
50
+ # converting would silently drop it.
51
+ return if node.block_node
48
52
 
49
53
  add_offense(node) do |corrector|
50
54
  corrector.replace(node, 'Dir.home')
@@ -34,7 +34,7 @@ module RuboCop
34
34
  even_odd_candidate?(node) do |base_number, method, arg|
35
35
  replacement_method = replacement_method(arg, method)
36
36
  add_offense(node, message: format(MSG, method: replacement_method)) do |corrector|
37
- correction = "#{base_number.source}.#{replacement_method}?"
37
+ correction = "#{receiver_source(base_number)}.#{replacement_method}?"
38
38
  corrector.replace(node, correction)
39
39
  end
40
40
  end
@@ -42,6 +42,16 @@ module RuboCop
42
42
 
43
43
  private
44
44
 
45
+ def receiver_source(node)
46
+ # A binary or unary operator receiver (e.g. `a * b`, `-a`) binds looser
47
+ # than the appended method call, so it must be wrapped in parentheses.
48
+ if node.send_type? && node.operator_method? && !node.method?(:[])
49
+ "(#{node.source})"
50
+ else
51
+ node.source
52
+ end
53
+ end
54
+
45
55
  def replacement_method(arg, method)
46
56
  case arg
47
57
  when 0
@@ -43,7 +43,8 @@ module RuboCop
43
43
  return unless (parsed_regexp = parse_regexp(regexp))
44
44
  return unless exact_match_pattern?(parsed_regexp)
45
45
 
46
- prefer = "#{receiver.source} #{new_method(node)} '#{parsed_regexp[1].text}'"
46
+ string = escape_single_quotes(parsed_regexp[1].text)
47
+ prefer = "#{receiver.source} #{new_method(node)} '#{string}'"
47
48
 
48
49
  add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
49
50
  corrector.replace(node, prefer)
@@ -53,6 +54,12 @@ module RuboCop
53
54
 
54
55
  private
55
56
 
57
+ # Escape characters that are special inside a single-quoted string so the
58
+ # generated literal (e.g. for `/\Afoo'bar\z/`) stays valid Ruby.
59
+ def escape_single_quotes(text)
60
+ text.gsub(/['\\]/) { |char| "\\#{char}" }
61
+ end
62
+
56
63
  def exact_match_pattern?(parsed_regexp)
57
64
  tokens = parsed_regexp.map(&:token)
58
65
  return false unless tokens[0] == :bos && tokens[1] == :literal && tokens[2] == :eos
@@ -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
@@ -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
@@ -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
@@ -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