rubocop 1.88.0 → 1.88.2

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +5 -1
  3. data/lib/rubocop/cop/base.rb +13 -8
  4. data/lib/rubocop/cop/bundler/gem_comment.rb +3 -1
  5. data/lib/rubocop/cop/commissioner.rb +13 -11
  6. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  7. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  8. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  9. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  10. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
  11. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  12. data/lib/rubocop/cop/layout/block_alignment.rb +17 -0
  13. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  14. data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
  15. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  16. data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
  17. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  18. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  19. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  20. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  21. data/lib/rubocop/cop/layout/line_length.rb +10 -5
  22. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +6 -1
  23. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  24. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  25. data/lib/rubocop/cop/lint/debugger.rb +13 -1
  26. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +7 -0
  27. data/lib/rubocop/cop/lint/to_enum_arguments.rb +7 -1
  28. data/lib/rubocop/cop/lint/to_json.rb +8 -1
  29. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +32 -8
  30. data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
  31. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
  32. data/lib/rubocop/cop/lint/void.rb +2 -2
  33. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  34. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  35. data/lib/rubocop/cop/mixin/allowed_methods.rb +5 -5
  36. data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
  37. data/lib/rubocop/cop/mixin/forbidden_pattern.rb +5 -1
  38. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  39. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  40. data/lib/rubocop/cop/mixin/statement_modifier.rb +1 -1
  41. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  42. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  43. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  44. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  45. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  46. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  47. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  48. data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
  49. data/lib/rubocop/cop/style/array_intersect.rb +4 -4
  50. data/lib/rubocop/cop/style/collection_compact.rb +1 -1
  51. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  52. data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
  53. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  54. data/lib/rubocop/cop/style/documentation_method.rb +5 -1
  55. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  56. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  57. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  58. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  59. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  60. data/lib/rubocop/cop/style/env_home.rb +4 -0
  61. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  62. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  63. data/lib/rubocop/cop/style/file_null.rb +4 -2
  64. data/lib/rubocop/cop/style/format_string.rb +13 -1
  65. data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
  66. data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
  67. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  68. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
  69. data/lib/rubocop/cop/style/if_unless_modifier.rb +4 -4
  70. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  71. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  72. data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
  73. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  74. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  75. data/lib/rubocop/cop/style/lambda.rb +7 -1
  76. data/lib/rubocop/cop/style/lambda_call.rb +11 -0
  77. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  78. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  79. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +11 -5
  80. data/lib/rubocop/cop/style/method_def_parentheses.rb +5 -1
  81. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  82. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +10 -7
  83. data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
  84. data/lib/rubocop/cop/style/module_function.rb +4 -2
  85. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  86. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  87. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  88. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  89. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  90. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  91. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  92. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  93. data/lib/rubocop/cop/style/parallel_assignment.rb +11 -2
  94. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  95. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  96. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +13 -1
  97. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  98. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  99. data/lib/rubocop/cop/style/redundant_format.rb +28 -0
  100. data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
  101. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  102. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  103. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  104. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  105. data/lib/rubocop/cop/style/semicolon.rb +4 -4
  106. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  107. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  108. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
  109. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  110. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  111. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  112. data/lib/rubocop/cop/team.rb +18 -4
  113. data/lib/rubocop/runner.rb +55 -13
  114. data/lib/rubocop/version.rb +1 -1
  115. metadata +2 -2
@@ -57,11 +57,15 @@ module RuboCop
57
57
  private
58
58
 
59
59
  def parentheses_required?(node, arguments_range)
60
- return true if node.single_line? && !node.endless?
61
-
62
60
  end_pos = arguments_range.end.end_pos
63
61
  token_after_argument = range_between(end_pos, end_pos + 1).source
64
62
 
63
+ # A `;` after the parentheses (e.g. `def foo(); end`) already separates the
64
+ # signature from the body, so the parentheses can be removed (`def foo; end`).
65
+ return false if token_after_argument == ';'
66
+
67
+ return true if node.single_line? && !node.endless?
68
+
65
69
  token_after_argument == '='
66
70
  end
67
71
  end
@@ -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|
@@ -140,7 +140,11 @@ module RuboCop
140
140
  end
141
141
 
142
142
  def method_allowed?(node)
143
- allowed_methods.include?(node.method_name)
143
+ # For a modifier form like `module_function def foo; end`, `node` is the
144
+ # `module_function`/`ruby2_keywords` send; the real method name is on its
145
+ # `def`/`defs` argument, not the modifier itself.
146
+ method_name = node.send_type? ? node.first_argument.method_name : node.method_name
147
+ allowed_methods.include?(method_name)
144
148
  end
145
149
 
146
150
  def allowed_methods
@@ -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
@@ -121,16 +121,24 @@ module RuboCop
121
121
  end
122
122
 
123
123
  def multi_argument(node)
124
+ # A splat argument can expand to any number of elements, so the pairs
125
+ # can't be built statically and there is no literal hash to suggest.
126
+ return if node.arguments.any?(&:splat_type?)
127
+
124
128
  if node.arguments.count.odd?
125
129
  add_offense(node, message: MSG_LITERAL_MULTI_ARG)
126
130
  else
127
- add_offense(node, message: MSG_LITERAL_MULTI_ARG) do |corrector|
128
- corrector.replace(node, args_to_hash(node.arguments))
131
+ correct_multi_argument(node)
132
+ end
133
+ end
129
134
 
130
- parent = node.parent
131
- if parent&.send_type? && !parent.method?(:to_h) && !parent.parenthesized?
132
- add_parentheses(parent, corrector)
133
- end
135
+ def correct_multi_argument(node)
136
+ add_offense(node, message: MSG_LITERAL_MULTI_ARG) do |corrector|
137
+ corrector.replace(node, args_to_hash(node.arguments))
138
+
139
+ parent = node.parent
140
+ if parent&.send_type? && !parent.method?(:to_h) && !parent.parenthesized?
141
+ add_parentheses(parent, corrector)
134
142
  end
135
143
  end
136
144
  end
@@ -74,7 +74,7 @@ module RuboCop
74
74
 
75
75
  def offense_for_brackets?(node)
76
76
  style == :brackets && node.receiver && node.method?(:fetch) && node.arguments.one? &&
77
- !node.block_literal?
77
+ !node.block_literal? && !node.csend_type?
78
78
  end
79
79
 
80
80
  def offense_for_fetch?(node)
@@ -84,11 +84,7 @@ module RuboCop
84
84
  def correct_fetch_to_brackets(corrector, node)
85
85
  key = node.first_argument.source
86
86
 
87
- if node.csend_type?
88
- corrector.replace(node, "(#{node.receiver.source}[#{key}])")
89
- else
90
- corrector.replace(node.loc.dot.join(node.source_range.end), "[#{key}]")
91
- end
87
+ corrector.replace(node.loc.dot.join(node.source_range.end), "[#{key}]")
92
88
  end
93
89
 
94
90
  def correct_brackets_to_fetch(corrector, node)
@@ -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
@@ -216,23 +216,34 @@ module RuboCop
216
216
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
217
217
 
218
218
  def correct_assignment(corrector, node, expression, insert_position)
219
+ indentation = indentation_of(node.parent)
220
+
219
221
  if insert_position == :after_condition
220
222
  assignment = node.parent.source_range.with(end_pos: node.source_range.begin_pos)
221
223
  corrector.remove(assignment)
222
- corrector.insert_after(node, "\n#{assignment.source}#{expression.source}")
224
+ corrector.insert_after(node, "\n#{indentation}#{assignment.source}#{expression.source}")
223
225
  else
224
- corrector.insert_before(node.parent, "#{expression.source}\n")
226
+ corrector.insert_before(node.parent, "#{expression.source}\n#{indentation}")
225
227
  end
226
228
  end
227
229
 
228
230
  def correct_no_assignment(corrector, node, expression, insert_position)
231
+ indentation = indentation_of(node)
232
+
229
233
  if insert_position == :after_condition
230
- corrector.insert_after(node, "\n#{expression.source}")
234
+ corrector.insert_after(node, "\n#{indentation}#{expression.source}")
231
235
  else
232
- corrector.insert_before(node, "#{expression.source}\n")
236
+ corrector.insert_before(node, "#{expression.source}\n#{indentation}")
233
237
  end
234
238
  end
235
239
 
240
+ # The leading indentation of the line the conditional starts on, so a
241
+ # hoisted expression keeps the surrounding nesting instead of landing
242
+ # at column zero.
243
+ def indentation_of(node)
244
+ ' ' * node.source_range.column
245
+ end
246
+
236
247
  def last_child_of_parent?(node)
237
248
  return true unless (parent = node.parent)
238
249
 
@@ -91,7 +91,7 @@ module RuboCop
91
91
 
92
92
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
93
93
  def on_if(node)
94
- return if endless_method?(node.body) || node.ancestors.any?(&:dstr_type?)
94
+ return if endless_method?(node.body) || node.each_ancestor(:dstr).any?
95
95
 
96
96
  condition = node.condition
97
97
  return if defined_nodes(condition).any? { |n| defined_argument_is_undefined?(node, n) } ||
@@ -119,7 +119,7 @@ module RuboCop
119
119
  if condition.defined_type?
120
120
  [condition]
121
121
  else
122
- condition.each_descendant.select(&:defined_type?)
122
+ condition.each_descendant(:defined?)
123
123
  end
124
124
  end
125
125
 
@@ -137,7 +137,7 @@ module RuboCop
137
137
  if condition.any_match_pattern_type?
138
138
  [condition]
139
139
  else
140
- condition.each_descendant.select(&:any_match_pattern_type?)
140
+ condition.each_descendant(:any_match_pattern)
141
141
  end
142
142
  end
143
143
 
@@ -350,7 +350,7 @@ module RuboCop
350
350
  end
351
351
 
352
352
  def comment_on_node_line(node)
353
- processed_source.comments.find { |c| same_line?(c, node) }
353
+ processed_source.comment_at_line(node.first_line)
354
354
  end
355
355
 
356
356
  def remove_comment(corrector, _node, comment)
@@ -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
@@ -75,7 +75,9 @@ module RuboCop
75
75
  def invertible?(node) # rubocop:disable Metrics/CyclomaticComplexity
76
76
  case node&.type
77
77
  when :begin
78
- invertible?(node.children.first)
78
+ # A multi-statement `begin` evaluates to its last expression, so it
79
+ # cannot be inverted by negating a single child.
80
+ node.children.one? && invertible?(node.children.first)
79
81
  when :send
80
82
  return false if inheritance_check?(node)
81
83
 
@@ -124,12 +126,17 @@ module RuboCop
124
126
  end
125
127
 
126
128
  def preferred_logical_condition(node)
127
- preferred_lhs = preferred_condition(node.lhs)
128
- preferred_rhs = preferred_condition(node.rhs)
129
+ preferred_lhs = preferred_operand(node, node.lhs)
130
+ preferred_rhs = preferred_operand(node, node.rhs)
129
131
 
130
132
  "#{preferred_lhs} #{node.inverse_operator} #{preferred_rhs}"
131
133
  end
132
134
 
135
+ def preferred_operand(node, operand)
136
+ preferred = preferred_condition(operand)
137
+ parenthesize_inverted_operand?(node, operand) ? "(#{preferred})" : preferred
138
+ end
139
+
133
140
  def autocorrect(corrector, node)
134
141
  case node.type
135
142
  when :begin
@@ -138,11 +145,24 @@ module RuboCop
138
145
  autocorrect_send_node(corrector, node)
139
146
  when :or, :and
140
147
  corrector.replace(node.loc.operator, node.inverse_operator)
141
- autocorrect(corrector, node.lhs)
142
- autocorrect(corrector, node.rhs)
148
+ autocorrect_operand(corrector, node, node.lhs)
149
+ autocorrect_operand(corrector, node, node.rhs)
143
150
  end
144
151
  end
145
152
 
153
+ def autocorrect_operand(corrector, node, operand)
154
+ autocorrect(corrector, operand)
155
+ corrector.wrap(operand, '(', ')') if parenthesize_inverted_operand?(node, operand)
156
+ end
157
+
158
+ # When an `and` is nested in an `or`, inverting both operators turns the
159
+ # `and` into an `or` that now binds looser than its parent, so it must be
160
+ # parenthesized to keep the original grouping (`a && b || c` inverts to
161
+ # `(!a || !b) && !c`, not `!a || !b && !c`).
162
+ def parenthesize_inverted_operand?(node, operand)
163
+ node.or_type? && operand.and_type?
164
+ end
165
+
146
166
  def autocorrect_send_node(corrector, node)
147
167
  if node.method?(:!)
148
168
  corrector.remove(node.loc.selector)
@@ -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
@@ -27,6 +27,9 @@ module RuboCop
27
27
 
28
28
  def on_send(node)
29
29
  return unless node.receiver
30
+ # Rewriting the call rebuilds it as a single expression, which would drop
31
+ # any comments inside the argument list, so leave it to be fixed manually.
32
+ return if comments_in_node?(node)
30
33
 
31
34
  if offense?(node)
32
35
  prefer = prefer(node)
@@ -48,6 +51,14 @@ module RuboCop
48
51
 
49
52
  private
50
53
 
54
+ def comments_in_node?(node)
55
+ range = node.source_range
56
+ processed_source.comments.any? do |comment|
57
+ range.begin_pos <= comment.source_range.begin_pos &&
58
+ comment.source_range.end_pos <= range.end_pos
59
+ end
60
+ end
61
+
51
62
  def offense?(node)
52
63
  (explicit_style? && node.implicit_call?) || (implicit_style? && !node.implicit_call?)
53
64
  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)