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
@@ -50,9 +50,11 @@ module RuboCop
50
50
  private
51
51
 
52
52
  def register_offense(node)
53
- add_offense(offense_range(node)) do |corrector|
54
- corrector.remove(node.loc.begin)
55
- corrector.remove(node.loc.end)
53
+ range = offense_range(node)
54
+ return if processed_source.contains_comment?(range)
55
+
56
+ add_offense(range) do |corrector|
57
+ corrector.remove(range)
56
58
  end
57
59
  end
58
60
 
@@ -88,8 +90,12 @@ module RuboCop
88
90
  #
89
91
  def parenthesized_it_method_in_block?(node)
90
92
  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?
93
+ return false unless (block_node = node.each_ancestor(:any_block).first)
94
+ # Inside a numbered/`it` block, a bare `it` is a parse error (it conflicts
95
+ # with the implicit parameter), so `it()` must keep its parentheses.
96
+ if block_node.block_type? && !block_node.arguments.empty_and_without_delimiters?
97
+ return false
98
+ end
93
99
 
94
100
  !node.receiver && node.arguments.empty? && !node.block_literal?
95
101
  end
@@ -102,6 +102,10 @@ module RuboCop
102
102
  MSG_PRESENT = 'Use def without parentheses.'
103
103
  MSG_MISSING = 'Use def with parentheses when there are parameters.'
104
104
 
105
+ def self.autocorrect_incompatible_with
106
+ [Style::ArgumentsForwarding]
107
+ end
108
+
105
109
  def on_def(node)
106
110
  args = node.arguments
107
111
 
@@ -166,7 +170,7 @@ module RuboCop
166
170
 
167
171
  def anonymous_arguments?(node)
168
172
  return true if node.arguments.any? do |arg|
169
- arg.type?(:forward_arg, :restarg, :kwrestarg)
173
+ arg.forward_arg_type? || (arg.type?(:restarg, :kwrestarg) && arg.name.nil?)
170
174
  end
171
175
  return false unless (last_argument = node.last_argument)
172
176
 
@@ -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
 
@@ -65,16 +65,19 @@ module RuboCop
65
65
  private
66
66
 
67
67
  def implements_respond_to_missing?(node)
68
- return false unless (grand_parent = node.parent.parent)
68
+ scope = enclosing_scope(node)
69
+ search_root = scope || node.parent
70
+ return false unless search_root
69
71
 
70
- grand_parent.each_descendant(node.type) do |descendant|
71
- return true if descendant.method?(:respond_to_missing?)
72
-
73
- child = descendant.children.first
74
- return true if child.respond_to?(:method?) && child.method?(:respond_to_missing?)
72
+ search_root.each_descendant(node.type).any? do |descendant|
73
+ descendant.method?(:respond_to_missing?) && enclosing_scope(descendant).equal?(scope)
75
74
  end
75
+ end
76
76
 
77
- false
77
+ # The class/module/`class << self` body that lexically contains `node`,
78
+ # or `nil` when `node` is defined at the top level.
79
+ def enclosing_scope(node)
80
+ node.each_ancestor(:class, :module, :sclass).first
78
81
  end
79
82
  end
80
83
  end
@@ -47,7 +47,7 @@ module RuboCop
47
47
  # @!method include_statement(node)
48
48
  def_node_matcher :include_statement, <<~PATTERN
49
49
  (send nil? ${:include :extend :prepend}
50
- const)
50
+ const+)
51
51
  PATTERN
52
52
 
53
53
  # @!method in_top_level_scope?(node)
@@ -110,9 +110,11 @@ module RuboCop
110
110
  def_node_matcher :private_directive?, '(send nil? :private ...)'
111
111
 
112
112
  def on_module(node)
113
- return unless node.body&.begin_type?
113
+ return unless node.body
114
114
 
115
- each_wrong_style(node.body.children) do |child_node|
115
+ body_nodes = node.body.begin_type? ? node.body.children : [node.body]
116
+
117
+ each_wrong_style(body_nodes) do |child_node|
116
118
  add_offense(child_node) do |corrector|
117
119
  next if style == :forbidden
118
120
 
@@ -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,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
 
@@ -233,14 +233,23 @@ module RuboCop
233
233
  def source(node, loc)
234
234
  # __FILE__ is treated as a StrNode but has no begin
235
235
  if node.str_type? && loc.respond_to?(:begin) && loc.begin.nil?
236
- "'#{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)
237
239
  elsif node.sym_type? && !node.loc?(:begin)
238
- ":#{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
239
244
  else
240
245
  node.source
241
246
  end
242
247
  end
243
248
 
249
+ def quote(string)
250
+ "'#{string.gsub(/[\\']/) { |char| "\\#{char}" }}'"
251
+ end
252
+
244
253
  def extract_sources(node)
245
254
  node.children.map(&:source)
246
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
@@ -5,7 +5,7 @@ module RuboCop
5
5
  module Style
6
6
  # Looks for uses of Perl-style regexp match
7
7
  # backreferences and their English versions like
8
- # $1, $2, $&, &+, $MATCH, $PREMATCH, etc.
8
+ # $1, $2, $&, $MATCH, $PREMATCH, etc.
9
9
  #
10
10
  # @example
11
11
  # # bad
@@ -69,6 +69,10 @@ module RuboCop
69
69
  # @return [String, nil]
70
70
  def preferred_expression_to(node)
71
71
  first = node.to_a.first
72
+ # NOTE: `$+` / `$LAST_PAREN_MATCH` is deliberately not converted. It
73
+ # refers to the last group that actually matched, which has no concise
74
+ # `Regexp.last_match` equivalent (`Regexp.last_match(-1)` is the last
75
+ # group in the pattern, which may be `nil`).
72
76
  case first
73
77
  when ::Integer
74
78
  "Regexp.last_match(#{first})"
@@ -78,8 +82,6 @@ module RuboCop
78
82
  'Regexp.last_match.pre_match'
79
83
  when :$', :$POSTMATCH
80
84
  'Regexp.last_match.post_match'
81
- when :$+, :$LAST_PAREN_MATCH
82
- 'Regexp.last_match(-1)'
83
85
  end
84
86
  end
85
87
 
@@ -26,7 +26,9 @@ module RuboCop
26
26
  def on_send(node)
27
27
  return unless (first_argument = node.first_argument)
28
28
  return unless (index = first_argument.source.index(CURRENT_DIRECTORY_PREFIX))
29
- return unless (redundant_length = redundant_path_length(first_argument.str_content))
29
+
30
+ content = leading_path_content(first_argument)
31
+ return unless (redundant_length = redundant_path_length(content))
30
32
 
31
33
  begin_pos = first_argument.source_range.begin.begin_pos + index
32
34
  end_pos = begin_pos + redundant_length
@@ -39,6 +41,16 @@ module RuboCop
39
41
 
40
42
  private
41
43
 
44
+ # The literal text at the start of the path, which is the whole string
45
+ # for a plain string and the leading literal segment for an interpolated
46
+ # one (`nil` when it starts with interpolation).
47
+ def leading_path_content(node)
48
+ return node.str_content if node.str_type?
49
+ return unless node.dstr_type? && (first = node.children.first)&.str_type?
50
+
51
+ first.str_content
52
+ end
53
+
42
54
  def redundant_path_length(path)
43
55
  return unless (match = path&.match(REDUNDANT_CURRENT_DIRECTORY_PREFIX))
44
56
 
@@ -38,6 +38,10 @@ module RuboCop
38
38
 
39
39
  def fix_exploded(node)
40
40
  exploded?(node) do |command, message|
41
+ # `raise RuntimeError, nil` uses the class name as the message, so
42
+ # rewriting it to `raise nil.to_s` (an empty message) would change it.
43
+ next if message.nil_type?
44
+
41
45
  add_offense(node, message: MSG_1) do |corrector|
42
46
  corrector.replace(node, replaced_exploded(node, command, message))
43
47
  end
@@ -56,6 +60,8 @@ module RuboCop
56
60
 
57
61
  def fix_compact(node)
58
62
  compact?(node) do |new_call, message|
63
+ next if message.nil_type?
64
+
59
65
  add_offense(node, message: MSG_2) do |corrector|
60
66
  corrector.replace(new_call, replaced_compact(message))
61
67
  end
@@ -62,7 +62,7 @@ module RuboCop
62
62
  def_node_matcher :select_predicate?, <<~PATTERN
63
63
  (call
64
64
  {
65
- (block $(call _ {:select :filter :find_all}) ...)
65
+ (any_block $(call _ {:select :filter :find_all}) ...)
66
66
  $(call _ {:select :filter :find_all} block_pass_type?)
67
67
  }
68
68
  ${:#{RESTRICT_ON_SEND.join(' :')}})
@@ -89,6 +89,8 @@ module RuboCop
89
89
 
90
90
  def on_send(node)
91
91
  format_without_additional_args?(node) do |value|
92
+ next if string_with_format_sequence?(value)
93
+
92
94
  replacement = escape_control_chars(value.source)
93
95
 
94
96
  add_offense(node, message: message(node, replacement)) do |corrector|
@@ -102,6 +104,24 @@ module RuboCop
102
104
 
103
105
  private
104
106
 
107
+ # A single-argument `format` whose string still contains a format sequence is
108
+ # not redundant: `format('%s')` raises at runtime, and `format('%%')` returns
109
+ # `'%'`, so replacing it with the literal would change behavior.
110
+ def string_with_format_sequence?(node)
111
+ string = static_string_value(node)
112
+ return false unless string
113
+
114
+ RuboCop::Cop::Utils::FormatString.new(string).format_sequences.any?
115
+ end
116
+
117
+ def static_string_value(node)
118
+ if node.str_type?
119
+ node.value
120
+ elsif node.dstr_type?
121
+ node.children.select(&:str_type?).map(&:value).join
122
+ end
123
+ end
124
+
105
125
  def message(node, prefer)
106
126
  format(MSG, prefer: prefer, method_name: node.method_name)
107
127
  end
@@ -246,6 +266,14 @@ module RuboCop
246
266
  def argument_value(argument)
247
267
  argument = argument.children.first if argument.begin_type?
248
268
 
269
+ # `nil` formats to an empty string (`format('%s', nil) == ''`), not the
270
+ # literal `'nil'` that `argument.source` would return.
271
+ return if argument.nil_type?
272
+
273
+ typed_argument_value(argument)
274
+ end
275
+
276
+ def typed_argument_value(argument)
249
277
  if argument.dsym_type?
250
278
  dsym_value(argument)
251
279
  elsif argument.hash_type?
@@ -48,8 +48,13 @@ module RuboCop
48
48
  def need_heredoc_delimiter_quotes?(node)
49
49
  heredoc_delimiter = node.source.delete(heredoc_type(node))
50
50
  return true unless heredoc_delimiter.start_with?("'", '"')
51
+ return true if node.loc.heredoc_end.source.strip.match?(/\W/)
51
52
 
52
- node.loc.heredoc_end.source.strip.match?(/\W/) ||
53
+ # A double-quoted delimiter interpolates exactly like an unquoted one,
54
+ # so its quotes are always redundant. A single-quoted delimiter is
55
+ # required when the body contains interpolation or escapes that would
56
+ # otherwise be evaluated.
57
+ heredoc_delimiter.start_with?("'") &&
53
58
  node.loc.heredoc_body.source.match?(STRING_INTERPOLATION_OR_ESCAPED_CHARACTER_PATTERN)
54
59
  end
55
60
  end
@@ -165,11 +165,11 @@ module RuboCop
165
165
  end
166
166
 
167
167
  def inspect_end_of_ruby_code_line_continuation
168
- last_line = processed_source.lines[processed_source.ast.last_line - 1]
168
+ last_line_number = processed_source.ast.last_line
169
+ last_line = processed_source.lines[last_line_number - 1]
169
170
  return unless code_ends_with_continuation?(last_line)
170
171
 
171
- last_column = last_line.length
172
- line_continuation_range = range_between(last_column - 1, last_column)
172
+ line_continuation_range = trailing_line_continuation_range(last_line_number)
173
173
 
174
174
  add_offense(line_continuation_range) do |corrector|
175
175
  corrector.remove_trailing(line_continuation_range, 1)
@@ -182,6 +182,14 @@ module RuboCop
182
182
  last_line.end_with?(LINE_CONTINUATION)
183
183
  end
184
184
 
185
+ # The backslash is the last character of the line; locate it by the line's
186
+ # position in the buffer rather than treating the column as an absolute offset
187
+ # (which corrupts an earlier line in multi-line files).
188
+ def trailing_line_continuation_range(line_number)
189
+ line_range = processed_source.buffer.line_range(line_number)
190
+ range_between(line_range.end_pos - 1, line_range.end_pos)
191
+ end
192
+
185
193
  def inside_string_literal?(range, token)
186
194
  ALLOWED_STRING_TOKENS.include?(token.type) && token.pos.overlaps?(range)
187
195
  end
@@ -65,7 +65,7 @@ module RuboCop
65
65
  # different versions of Ruby so that e.g. /\i/ != /i/
66
66
  return true if /[[:alnum:]]/.match?(char)
67
67
  return true if ALLOWED_ALWAYS_ESCAPES.include?(char) || delimiter?(node, char)
68
- return true if requires_escape_to_avoid_interpolation?(node.source[index], char)
68
+ return true if requires_escape_to_avoid_interpolation?(node, index, char)
69
69
 
70
70
  if within_character_class
71
71
  ALLOWED_WITHIN_CHAR_CLASS_METACHAR_ESCAPES.include?(char) &&
@@ -97,10 +97,14 @@ module RuboCop
97
97
  delimiters.include?(char)
98
98
  end
99
99
 
100
- def requires_escape_to_avoid_interpolation?(char_before_escape, escaped_char)
100
+ def requires_escape_to_avoid_interpolation?(node, index, escaped_char)
101
101
  # Preserve escapes after '#' that would otherwise trigger interpolation:
102
- # '#@ivar', '#@@cvar', and '#$gvar'.
103
- char_before_escape == '#' && INTERPOLATION_SIGILS.include?(escaped_char)
102
+ # '#@ivar', '#@@cvar', and '#$gvar'. `index` is relative to the regexp
103
+ # contents, so index into those rather than `node.source` (which also
104
+ # includes the opening delimiter, e.g. `%r{`).
105
+ return false unless index.positive? && INTERPOLATION_SIGILS.include?(escaped_char)
106
+
107
+ contents_range(node).source[index - 1] == '#'
104
108
  end
105
109
 
106
110
  def each_escape(node)
@@ -100,6 +100,15 @@ module RuboCop
100
100
  add_lhs_to_local_variables_scopes(node.rhs, node.lhs)
101
101
  end
102
102
 
103
+ # Register the exception variable of `rescue => e` so that `self.e` in the
104
+ # body is not treated as redundant (it disambiguates the local variable).
105
+ def on_resbody(node)
106
+ exception_variable = node.exception_variable
107
+ return unless exception_variable&.lvasgn_type?
108
+
109
+ @local_variables_scopes[node] << exception_variable.name
110
+ end
111
+
103
112
  def on_in_pattern(node)
104
113
  add_match_var_scopes(node)
105
114
  end
@@ -100,12 +100,31 @@ module RuboCop
100
100
  end
101
101
 
102
102
  def range(redundant_keyword_init)
103
- if redundant_keyword_init.parent.left_siblings.last.is_a?(AST::Node)
104
- beginning_of_range = redundant_keyword_init.parent.left_siblings.last.source_range.end
103
+ if redundant_keyword_init.parent.pairs.all? { |pair| keyword_init?(pair) }
104
+ # The hash holds only `keyword_init` pairs, so it is emptied; anchor the
105
+ # removal before the hash to also drop the comma that precedes it.
106
+ range_emptying_hash(redundant_keyword_init)
107
+ else
108
+ # Other pairs remain, so just drop this pair and one adjacent comma.
109
+ range_within_hash(redundant_keyword_init)
110
+ end
111
+ end
112
+
113
+ def range_within_hash(pair)
114
+ if (left = pair.left_sibling)
115
+ left.source_range.end.join(pair.source_range.end)
116
+ elsif (right = pair.right_sibling)
117
+ pair.source_range.begin.join(right.source_range.begin)
118
+ else
119
+ pair.source_range
120
+ end
121
+ end
105
122
 
106
- beginning_of_range.join(redundant_keyword_init.source_range.end)
123
+ def range_emptying_hash(pair)
124
+ if (preceding = pair.parent.left_sibling).is_a?(AST::Node)
125
+ preceding.source_range.end.join(pair.source_range.end)
107
126
  else
108
- redundant_keyword_init
127
+ pair.source_range
109
128
  end
110
129
  end
111
130
  end
@@ -170,10 +170,10 @@ module RuboCop
170
170
  end
171
171
 
172
172
  def find_semicolon_positions(line)
173
- # Scan for all the semicolons on the line
174
- semicolons = processed_source[line - 1].enum_for(:scan, ';')
175
- semicolons.each do
176
- yield Regexp.last_match.begin(0)
173
+ # Scan for all the semicolon tokens on the line. Iterating tokens rather
174
+ # than the raw source skips `;` characters inside string/regexp literals.
175
+ processed_source.tokens.each do |token|
176
+ yield token.column if token.line == line && token.semicolon?
177
177
  end
178
178
  end
179
179
 
@@ -44,11 +44,12 @@ module RuboCop
44
44
  add_offense(node) do |corrector|
45
45
  corrector.insert_after(do_line(node), "\n")
46
46
 
47
- node_body = node.body
48
-
49
- if node_body.respond_to?(:heredoc?) && node_body.heredoc?
47
+ if (heredoc = trailing_heredoc(node.body))
48
+ # The heredoc body extends past the `end` on the source, so the
49
+ # `end` has to be moved after it rather than before, which would
50
+ # otherwise move it into the heredoc body and break the syntax.
50
51
  corrector.remove(node.loc.end)
51
- corrector.insert_after(node_body.loc.heredoc_end, "\nend")
52
+ corrector.insert_after(heredoc.loc.heredoc_end, "\nend")
52
53
  else
53
54
  corrector.insert_before(node.loc.end, "\n")
54
55
  end
@@ -60,6 +61,18 @@ module RuboCop
60
61
 
61
62
  private
62
63
 
64
+ # Returns the heredoc opened on the block's line whose body extends the
65
+ # furthest down, whether it is the block body itself or nested within it.
66
+ def trailing_heredoc(node_body)
67
+ return unless node_body
68
+
69
+ heredocs = [node_body, *node_body.each_descendant].select do |node|
70
+ node.respond_to?(:heredoc?) && node.heredoc?
71
+ end
72
+
73
+ heredocs.max_by { |heredoc| heredoc.loc.heredoc_end.line }
74
+ end
75
+
63
76
  def do_line(node)
64
77
  if node.type?(:numblock, :itblock) ||
65
78
  node.arguments.children.empty? || node.send_node.lambda_literal?
@@ -41,6 +41,7 @@ module RuboCop
41
41
 
42
42
  def on_pair(node)
43
43
  return unless string_hash_key?(node)
44
+ return if node.key.heredoc?
44
45
 
45
46
  key_content = node.key.str_content
46
47
  return unless key_content.valid_encoding?
@@ -41,7 +41,9 @@ module RuboCop
41
41
  return unless struct_constructor?(node.parent_class)
42
42
 
43
43
  add_offense(node.parent_class) do |corrector|
44
- corrector.remove(range_with_surrounding_space(node.loc.keyword, newlines: false))
44
+ corrector.remove(
45
+ range_with_surrounding_space(node.loc.keyword, side: :right, newlines: false)
46
+ )
45
47
  corrector.replace(node.loc.operator, '=')
46
48
 
47
49
  correct_parent(node.parent_class, corrector)
@@ -113,6 +113,10 @@ module RuboCop
113
113
  def offense?(node)
114
114
  condition = node.condition
115
115
 
116
+ # A modifier `if`/`unless` requires the parentheses, e.g. `(a if b) ? x : y`,
117
+ # so removing them would change the meaning. Don't flag it.
118
+ return false if parenthesized_modifier_condition?(condition)
119
+
116
120
  if safe_assignment?(condition)
117
121
  !safe_assignment_allowed?
118
122
  else
@@ -174,6 +178,13 @@ module RuboCop
174
178
  condition.children.any? { |child| below_ternary_precedence?(child) }
175
179
  end
176
180
 
181
+ def parenthesized_modifier_condition?(condition)
182
+ return false unless condition.begin_type?
183
+
184
+ inner = condition.children.first
185
+ inner&.if_type? && inner.modifier_form?
186
+ end
187
+
177
188
  def unparenthesized_method_call?(child)
178
189
  /^[a-z]/i.match?(method_name(child)) && !child.parenthesized?
179
190
  end