rubocop 1.88.2 → 1.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +67 -11
  3. data/lib/rubocop/comment_config.rb +35 -19
  4. data/lib/rubocop/config.rb +53 -0
  5. data/lib/rubocop/config_validator.rb +38 -1
  6. data/lib/rubocop/cop/badge.rb +8 -0
  7. data/lib/rubocop/cop/base.rb +6 -4
  8. data/lib/rubocop/cop/bundler.rb +19 -0
  9. data/lib/rubocop/cop/corrector.rb +1 -1
  10. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  11. data/lib/rubocop/cop/gemspec.rb +22 -0
  12. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  13. data/lib/rubocop/cop/generator.rb +4 -0
  14. data/lib/rubocop/cop/layout/class_structure.rb +8 -2
  15. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  16. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  17. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  18. data/lib/rubocop/cop/layout/line_length.rb +85 -12
  19. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  20. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  21. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
  22. data/lib/rubocop/cop/layout.rb +114 -0
  23. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  24. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  25. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  26. data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
  27. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  28. data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
  29. data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
  30. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  31. data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
  32. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
  33. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  34. data/lib/rubocop/cop/lint/name_typo.rb +223 -0
  35. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
  36. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
  37. data/lib/rubocop/cop/lint/to_enum_arguments.rb +37 -1
  38. data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
  39. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  40. data/lib/rubocop/cop/lint/void.rb +17 -6
  41. data/lib/rubocop/cop/lint.rb +176 -0
  42. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  43. data/lib/rubocop/cop/metrics.rb +31 -0
  44. data/lib/rubocop/cop/migration/department_name.rb +0 -4
  45. data/lib/rubocop/cop/migration.rb +13 -0
  46. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  47. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  48. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  49. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  50. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  51. data/lib/rubocop/cop/mixin/statement_modifier.rb +42 -6
  52. data/lib/rubocop/cop/mixin.rb +78 -77
  53. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  54. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  55. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  56. data/lib/rubocop/cop/naming.rb +31 -0
  57. data/lib/rubocop/cop/offense.rb +10 -6
  58. data/lib/rubocop/cop/registry.rb +153 -43
  59. data/lib/rubocop/cop/security.rb +19 -0
  60. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  61. data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
  62. data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
  63. data/lib/rubocop/cop/style/copyright.rb +6 -1
  64. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
  65. data/lib/rubocop/cop/style/documentation.rb +47 -0
  66. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  67. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -48
  68. data/lib/rubocop/cop/style/lambda.rb +4 -0
  69. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
  70. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  71. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  72. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  73. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  74. data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
  75. data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
  76. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  77. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  78. data/lib/rubocop/cop/style/redundant_format.rb +12 -3
  79. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  80. data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
  81. data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
  82. data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
  83. data/lib/rubocop/cop/style/static_class.rb +21 -0
  84. data/lib/rubocop/cop/style.rb +310 -0
  85. data/lib/rubocop/cop/team.rb +48 -3
  86. data/lib/rubocop/cop/variable_force/variable.rb +18 -6
  87. data/lib/rubocop/cops_documentation_generator.rb +1 -1
  88. data/lib/rubocop/directive_comment.rb +2 -0
  89. data/lib/rubocop/feature_loader.rb +0 -6
  90. data/lib/rubocop/lsp/routes.rb +41 -3
  91. data/lib/rubocop/lsp/runtime.rb +4 -0
  92. data/lib/rubocop/lsp/server.rb +4 -0
  93. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  94. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  95. data/lib/rubocop/project_index_loader.rb +16 -0
  96. data/lib/rubocop/result_cache.rb +6 -4
  97. data/lib/rubocop/rspec/shared_contexts.rb +14 -2
  98. data/lib/rubocop/runner.rb +45 -20
  99. data/lib/rubocop/server/cache.rb +10 -5
  100. data/lib/rubocop/target_ruby.rb +1 -1
  101. data/lib/rubocop/version.rb +1 -1
  102. data/lib/rubocop.rb +17 -626
  103. metadata +18 -3
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ class Generator
6
+ # A class that injects a `register_cop` directive into the cop's department module
7
+ # so that the new cop is registered for lazy loading.
8
+ # The directive is injected in alphabetical order among the existing directives without
9
+ # reordering them, since the registration order defines the cop execution order.
10
+ # When the department module file does not exist yet, it is created, and a require for
11
+ # it is injected into the root RuboCop file.
12
+ #
13
+ # Anything unexpected in the department module, such as a commented-out or otherwise
14
+ # unparsable `register_cop` line, raises an error instead of silently producing
15
+ # a broken registration.
16
+ class RegistrationInjector
17
+ EXTEND_PATTERN = /\A(?<indent>\s*)extend LazyLoader\n\z/.freeze
18
+ ENTRY_PATTERN = %r{
19
+ \A(?<indent>\s*)register_cop\s
20
+ :(?<constant>[A-Za-z0-9]+),\s
21
+ "\#\{__dir__\}/(?<path>[a-z0-9_/]+)"\n\z
22
+ }x.freeze
23
+ DEPARTMENT_REQUIRE_PATTERN = %r{\Arequire_relative 'rubocop/cop/[a-z0-9_/]+'\n\z}.freeze
24
+
25
+ def initialize(source_path:, badge:, root_file_path: 'lib/rubocop.rb', output: $stdout)
26
+ @source_path = Pathname(source_path)
27
+ @badge = badge
28
+ @root_file_path = root_file_path
29
+ @department_file_path = Pathname("#{@source_path.dirname}.rb")
30
+ @output = output
31
+ end
32
+
33
+ def inject
34
+ if File.exist?(department_file_path)
35
+ inject_directive
36
+ else
37
+ create_department_file
38
+ inject_department_require
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :source_path, :badge, :root_file_path, :department_file_path, :output
45
+
46
+ def inject_directive
47
+ lines = File.readlines(department_file_path)
48
+ entries = parse_entries(lines)
49
+ return if entries.any? { |entry| entry[:path] == cop_path }
50
+
51
+ line, indent = insertion_point(lines, entries)
52
+ lines.insert(line, "#{indent}#{directive}\n")
53
+
54
+ write_department_file(lines)
55
+ end
56
+
57
+ def create_department_file
58
+ File.write(department_file_path, department_module_source)
59
+
60
+ output.puts "[create] #{department_file_path}"
61
+ end
62
+
63
+ def inject_department_require
64
+ require_path = department_file_path.sub_ext('').to_s.delete_prefix('lib/')
65
+ require_directive = "require_relative '#{require_path}'\n"
66
+
67
+ lines = File.readlines(root_file_path)
68
+ return if lines.include?(require_directive)
69
+
70
+ lines.insert(department_require_index(lines, require_directive), require_directive)
71
+ File.write(root_file_path, lines.join)
72
+
73
+ output.puts "[modify] #{root_file_path} - `#{require_directive.strip}` was injected."
74
+ end
75
+
76
+ def write_department_file(lines)
77
+ File.write(department_file_path, lines.join)
78
+
79
+ output.puts "[modify] #{department_file_path} - `#{directive}` was injected."
80
+ end
81
+
82
+ # @return [Array<Hash>] the `register_cop` entries with their line numbers
83
+ def parse_entries(lines)
84
+ lines.filter_map.with_index do |line, index|
85
+ match = ENTRY_PATTERN.match(line)
86
+
87
+ if match
88
+ { line: index, indent: match[:indent], constant: match[:constant],
89
+ path: match[:path] }
90
+ elsif line.include?('register_cop')
91
+ raise Error, "unexpected `register_cop` line in #{department_file_path}:" \
92
+ "#{index + 1}; fix it or register the cop manually: #{line}"
93
+ end
94
+ end
95
+ end
96
+
97
+ # The directive path is relative to the department module file's directory,
98
+ # which `__dir__` resolves to at load time.
99
+ def cop_path
100
+ source_path.relative_path_from(department_file_path.dirname).to_s.delete_suffix('.rb')
101
+ end
102
+
103
+ def insertion_point(lines, entries)
104
+ if entries.empty?
105
+ first_directive_position(lines)
106
+ else
107
+ successor = entries.find { |entry| entry[:path] > cop_path }
108
+ entry = successor || entries.last
109
+
110
+ [entry[:line] + (successor ? 0 : 1), entry[:indent]]
111
+ end
112
+ end
113
+
114
+ def directive
115
+ "register_cop :#{badge.cop_name}, \"\#{__dir__}/#{cop_path}\""
116
+ end
117
+
118
+ # The department requires form the contiguous block right before
119
+ # the `rubocop/cop/team` require in the root RuboCop file.
120
+ def department_require_index(lines, require_directive)
121
+ anchor = lines.index("require_relative 'rubocop/cop/team'\n")
122
+ unless anchor
123
+ raise Error, "could not find the department require block in #{root_file_path}"
124
+ end
125
+
126
+ block_end = anchor - 1
127
+ block_end -= 1 while lines[block_end] == "\n"
128
+ block_start = block_end
129
+ block_start -= 1 while lines[block_start - 1]&.match?(DEPARTMENT_REQUIRE_PATTERN)
130
+
131
+ (block_start..block_end).find do |index|
132
+ lines[index] > require_directive
133
+ end || (block_end + 1)
134
+ end
135
+
136
+ def first_directive_position(lines)
137
+ extend_lines = lines.filter_map.with_index do |line, index|
138
+ [index, EXTEND_PATTERN.match(line)[:indent]] if EXTEND_PATTERN.match?(line)
139
+ end
140
+
141
+ unless extend_lines.size == 1
142
+ raise Error, "expected `extend LazyLoader` in #{department_file_path}; " \
143
+ 'add it or register the cop manually'
144
+ end
145
+
146
+ index, indent = extend_lines.first
147
+ blank_after_extend = lines[index + 1] == "\n"
148
+
149
+ [index + (blank_after_extend ? 2 : 1), indent]
150
+ end
151
+
152
+ def department_module_source
153
+ modules = %w[RuboCop Cop] + badge.department.to_s.split('/')
154
+ body_indent = ' ' * modules.size
155
+
156
+ lines = ['# frozen_string_literal: true', '']
157
+ lines.concat(module_opening_lines(modules))
158
+ lines << "#{body_indent}extend LazyLoader"
159
+ lines << ''
160
+ lines << "#{body_indent}#{directive}"
161
+ lines.concat(module_closing_lines(modules))
162
+
163
+ "#{lines.join("\n")}\n"
164
+ end
165
+
166
+ def module_opening_lines(modules)
167
+ modules.flat_map.with_index do |mod, nesting|
168
+ indent = ' ' * nesting
169
+ lines = []
170
+
171
+ if mod == modules.last
172
+ lines << "#{indent}# Cops for the `#{badge.department}` department. " \
173
+ "The department's cops are"
174
+ lines << "#{indent}# registered for lazy loading and their files are " \
175
+ 'loaded on demand.'
176
+ end
177
+
178
+ lines << "#{indent}module #{mod}"
179
+ end
180
+ end
181
+
182
+ def module_closing_lines(modules)
183
+ (modules.size - 1).downto(0).map { |nesting| "#{' ' * nesting}end" }
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -136,6 +136,10 @@ module RuboCop
136
136
  RequireFileInjector.new(source_path: source_path, root_file_path: root_file_path).inject
137
137
  end
138
138
 
139
+ def inject_registration
140
+ RegistrationInjector.new(source_path: source_path, badge: badge).inject
141
+ end
142
+
139
143
  def inject_config(config_file_path: 'config/default.yml',
140
144
  version_added: '<<next>>')
141
145
  injector =
@@ -251,7 +251,7 @@ module RuboCop
251
251
  key = category || name
252
252
  visibility_key =
253
253
  if node.def_modifier?
254
- "#{name}_methods"
254
+ name.end_with?('_class_method') ? "#{name}s" : "#{name}_methods"
255
255
  else
256
256
  "#{node_visibility(node)}_#{key}"
257
257
  end
@@ -283,7 +283,7 @@ module RuboCop
283
283
  # Exploding such a node into its children would yield non-node values (e.g. a
284
284
  # method-name `Symbol` from a `csend`) and crash later checks.
285
285
  if class_def.type?(:begin, :kwbegin)
286
- class_def.children.compact
286
+ flatten_class_elements(class_def)
287
287
  else
288
288
  [class_def]
289
289
  end
@@ -296,6 +296,12 @@ module RuboCop
296
296
  private_constant?(node)
297
297
  end
298
298
 
299
+ def flatten_class_elements(node)
300
+ node.children.compact.flat_map do |child|
301
+ child.kwbegin_type? ? flatten_class_elements(child) : [child]
302
+ end
303
+ end
304
+
299
305
  def ignore_for_autocorrect?(node, sibling)
300
306
  classification = classify(node)
301
307
  sibling_class = classify(sibling)
@@ -172,7 +172,7 @@ module RuboCop
172
172
  'Indent the right bracket the same as the first position ' \
173
173
  'after the preceding left parenthesis.'
174
174
  when :parent_hash_key
175
- 'Indent the right bracket the same as the parent hash key.' \
175
+ 'Indent the right bracket the same as the parent hash key.'
176
176
  else
177
177
  'Indent the right bracket the same as the start of the line ' \
178
178
  'where the left bracket is.'
@@ -206,7 +206,9 @@ module RuboCop
206
206
  def autocorrect(corrector, node)
207
207
  return unless node
208
208
 
209
- AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
209
+ AlignmentCorrector.correct(
210
+ corrector, processed_source, node, @column_delta, tab_indentation: true
211
+ )
210
212
  end
211
213
 
212
214
  # Returns a range at the first non-space column of the line the opening parenthesis is on,
@@ -98,6 +98,24 @@ module RuboCop
98
98
  #
99
99
  # name = 'John' #: String
100
100
  #
101
+ # @example AllowYARDCommentBlockSeparator: false (default)
102
+ #
103
+ # # bad
104
+ #
105
+ # # Copyright (c) Example Corp
106
+ # #-
107
+ # class Client
108
+ # end
109
+ #
110
+ # @example AllowYARDCommentBlockSeparator: true
111
+ #
112
+ # # good
113
+ #
114
+ # # Copyright (c) Example Corp
115
+ # #-
116
+ # class Client
117
+ # end
118
+ #
101
119
  class LeadingCommentSpace < Base
102
120
  include RangeHelp
103
121
  extend AutoCorrector
@@ -113,6 +131,7 @@ module RuboCop
113
131
  next if gemfile_ruby_comment?(comment)
114
132
  next if rbs_inline_annotation?(comment)
115
133
  next if steep_annotation?(comment)
134
+ next if yard_comment_block_separator?(comment)
116
135
 
117
136
  add_offense(comment) do |corrector|
118
137
  expr = comment.source_range
@@ -197,6 +216,14 @@ module RuboCop
197
216
  def steep_annotation?(comment)
198
217
  allow_steep_annotation? && comment.text.start_with?(/#[$:]/)
199
218
  end
219
+
220
+ def allow_yard_comment_block_separator?
221
+ cop_config['AllowYARDCommentBlockSeparator']
222
+ end
223
+
224
+ def yard_comment_block_separator?(comment)
225
+ allow_yard_comment_block_separator? && comment.text.match?(/\A#-\s*\z/)
226
+ end
200
227
  end
201
228
  end
202
229
  end
@@ -65,6 +65,7 @@ module RuboCop
65
65
  include AllowedPattern
66
66
  include RangeHelp
67
67
  include LineLengthHelp
68
+ include EndlessMethodRewriter
68
69
  extend AutoCorrector
69
70
 
70
71
  exclude_limit 'Max'
@@ -92,8 +93,15 @@ module RuboCop
92
93
  alias on_hash on_potential_breakable_node
93
94
  alias on_send on_potential_breakable_node
94
95
  alias on_csend on_potential_breakable_node
95
- alias on_def on_potential_breakable_node
96
- alias on_defs on_potential_breakable_node
96
+
97
+ def on_def(node)
98
+ if node.endless?
99
+ track_endless_method(node)
100
+ else
101
+ check_for_breakable_node(node)
102
+ end
103
+ end
104
+ alias on_defs on_def
97
105
 
98
106
  def on_new_investigation
99
107
  return unless processed_source.raw_source.include?(';')
@@ -111,6 +119,70 @@ module RuboCop
111
119
 
112
120
  attr_accessor :breakable_range
113
121
 
122
+ def track_endless_method(node)
123
+ line_index = node.first_line - processed_source.buffer.first_line
124
+ endless_methods_by_line[line_index] = node
125
+ end
126
+
127
+ def handle_endless_method_line(line, line_index)
128
+ if require_endless_methods?
129
+ return register_required_endless_method_offense(line, line_index)
130
+ end
131
+
132
+ register_endless_method_offense(line, line_index)
133
+ end
134
+
135
+ def require_endless_methods?
136
+ config.cop_enabled?('Style/EndlessMethod') &&
137
+ config.for_cop('Style/EndlessMethod')['EnforcedStyle'] == 'require_always'
138
+ end
139
+
140
+ def register_endless_method_offense(line, line_index)
141
+ message = format(MSG, length: line_length(line), max: max)
142
+ loc = excess_range(nil, line, line_index)
143
+
144
+ add_offense(loc, message: message) do |corrector|
145
+ self.max = line_length(line)
146
+
147
+ correct_to_multiline(corrector, endless_methods_by_line[line_index])
148
+ end
149
+ end
150
+
151
+ def register_required_endless_method_offense(line, line_index)
152
+ node = endless_methods_by_line[line_index]
153
+ loc = excess_range(nil, line, line_index)
154
+
155
+ add_offense(loc, message: format(MSG, length: line_length(line), max: max)) do |corrector|
156
+ self.max = line_length(line)
157
+
158
+ if correctable_endless_method_block?(node)
159
+ correct_endless_method_block_to_multiline(corrector, node)
160
+ end
161
+ end
162
+ end
163
+
164
+ def correctable_endless_method_block?(node)
165
+ block_node = node.body
166
+
167
+ block_node&.type?(:any_block) &&
168
+ block_node.braces? &&
169
+ block_node.single_line? &&
170
+ block_node.body &&
171
+ !receiver_contains_heredoc?(block_node)
172
+ end
173
+
174
+ def correct_endless_method_block_to_multiline(corrector, node)
175
+ block_node = node.body
176
+ block_arguments = block_node.arguments? ? " #{block_node.arguments.source}" : ''
177
+ replacement = [
178
+ "#{block_node.send_node.source} do#{block_arguments}",
179
+ "#{indent(node, offset: 2)}#{block_node.body.source}",
180
+ "#{indent(node)}end"
181
+ ].join("\n")
182
+
183
+ corrector.replace(block_node, replacement)
184
+ end
185
+
114
186
  def check_for_breakable_node(node)
115
187
  breakable_node = extract_breakable_node(node, max)
116
188
  return if breakable_node.nil?
@@ -237,6 +309,10 @@ module RuboCop
237
309
  @breakable_range_by_line_index ||= {}
238
310
  end
239
311
 
312
+ def endless_methods_by_line
313
+ @endless_methods_by_line ||= {}
314
+ end
315
+
240
316
  def breakable_string_delimiters
241
317
  @breakable_string_delimiters ||= {}
242
318
  end
@@ -252,10 +328,15 @@ module RuboCop
252
328
  [max - indentation_difference(line), 0].max
253
329
  end
254
330
 
255
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
331
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
256
332
  def check_line(line, line_index)
257
333
  return if line_length(line) <= max
258
334
  return if allowed_line?(line, line_index)
335
+
336
+ if endless_methods_by_line.key?(line_index)
337
+ return handle_endless_method_line(line, line_index)
338
+ end
339
+
259
340
  if allow_rbs_inline_annotation? && rbs_inline_annotation_on_source_line?(line_index)
260
341
  return
261
342
  end
@@ -267,7 +348,7 @@ module RuboCop
267
348
 
268
349
  register_offense(excess_range(nil, line, line_index), line, line_index)
269
350
  end
270
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
351
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
271
352
 
272
353
  def allowed_line?(line, line_index)
273
354
  matches_allowed_pattern?(line) ||
@@ -313,10 +394,6 @@ module RuboCop
313
394
  end
314
395
  alias max_line_length max
315
396
 
316
- def allow_heredoc?
317
- allowed_heredoc
318
- end
319
-
320
397
  def allowed_heredoc
321
398
  cop_config['AllowHeredoc']
322
399
  end
@@ -344,10 +421,6 @@ module RuboCop
344
421
  end
345
422
  end
346
423
 
347
- def line_in_heredoc?(line_number)
348
- heredocs.any? { |range, _delimiter| range.cover?(line_number) }
349
- end
350
-
351
424
  def receiver_contains_heredoc?(node)
352
425
  return false unless (receiver = node.receiver)
353
426
  return true if receiver.any_str_type? && receiver.heredoc?
@@ -410,7 +410,7 @@ module RuboCop
410
410
 
411
411
  def find_continuation_node(node)
412
412
  receiver = node.receiver
413
- return receiver.send_node if single_line_block_receiver?(receiver)
413
+ return leftmost_call_on_same_line(receiver) if single_line_block_receiver?(receiver)
414
414
  return unless receiver.call_type? && receiver.loc.dot
415
415
  return receiver if receiver.receiver.begin_type? && node.block_node.single_line?
416
416
  return unless receiver.loc.dot.line > receiver.receiver.last_line
@@ -422,9 +422,19 @@ module RuboCop
422
422
  receiver.single_line? && receiver.any_block_type?
423
423
  end
424
424
 
425
+ def leftmost_call_on_same_line(node)
426
+ current = node.any_block_type? ? node.send_node : node
427
+ while current.receiver&.call_type? &&
428
+ current.receiver.loc?(:dot) &&
429
+ current.receiver.loc.dot.line == current.loc.dot.line
430
+ current = current.receiver
431
+ end
432
+ current
433
+ end
434
+
425
435
  def handle_descendant_block(node)
426
436
  receiver = node.receiver
427
- return receiver.send_node if single_line_block_receiver?(receiver)
437
+ return leftmost_call_on_same_line(receiver) if single_line_block_receiver?(receiver)
428
438
 
429
439
  block_node = node.each_descendant(:any_block).first
430
440
  return unless block_node&.multiline?
@@ -49,6 +49,7 @@ module RuboCop
49
49
  class RedundantLineBreak < Base
50
50
  include CheckAssignment
51
51
  include CheckSingleLineSuitability
52
+ include ReparsedEquivalence
52
53
  extend AutoCorrector
53
54
 
54
55
  MSG = 'Redundant line break detected.'
@@ -82,12 +83,88 @@ module RuboCop
82
83
  end
83
84
 
84
85
  def register_offense(node)
86
+ # The exact single-line correction is verified to parse equivalently
87
+ # before the offense is registered, so a join that would change how
88
+ # the code parses is never reported or offered.
89
+ return if verified_by_reparse([node], oversized: :verify).empty?
90
+
85
91
  add_offense(node) do |corrector|
86
92
  corrector.replace(node, to_single_line(node.source).strip)
87
93
  end
88
94
  ignore_node(node)
89
95
  end
90
96
 
97
+ def apply_reparse_correction(corrector, node)
98
+ corrector.replace(node, to_single_line(node.source).strip)
99
+ end
100
+
101
+ # Joining lines shifts the value of any later `__LINE__`, exactly as
102
+ # any other line-removing correction does; neutralize it on both sides
103
+ # with an identifier, which stays valid in every position the keyword
104
+ # can appear in.
105
+ def preprocess_reparsed_source(source)
106
+ source.gsub(/\b__LINE__\b/, '__LINE0__')
107
+ end
108
+
109
+ def normalize_reparsed_ast(node)
110
+ fold_string_concatenation(node)
111
+ end
112
+
113
+ # Joining lines merges split string literals (`"a" \<newline> "b"` into
114
+ # `"ab"`) and turns mixed-quote pairs into `+` concatenation, which
115
+ # changes the tree but not the resulting string. Normalize all string
116
+ # concatenation to a canonical form before comparing: nested
117
+ # concatenations are flattened and adjacent literal parts merged.
118
+ def fold_string_concatenation(node)
119
+ return node unless node.is_a?(::Parser::AST::Node)
120
+
121
+ children = node.children.map { |child| fold_string_concatenation(child) }
122
+ node = node.updated(nil, children)
123
+
124
+ parts = string_concatenation_parts(node)
125
+ return node unless parts
126
+
127
+ merged = merge_string_parts(parts)
128
+ if merged.one? && merged.first.str_type?
129
+ node.updated(:str, merged.first.children)
130
+ else
131
+ node.updated(:dstr, merged)
132
+ end
133
+ end
134
+
135
+ def string_concatenation_parts(node)
136
+ case node.type
137
+ when :dstr
138
+ node.children
139
+ when :send
140
+ parts = [node.receiver, *node.arguments]
141
+ parts if node.method?(:+) && parts.all? { |part| stringish?(part) }
142
+ end
143
+ end
144
+
145
+ def merge_string_parts(parts)
146
+ flattened = parts.flat_map do |part|
147
+ part.is_a?(::Parser::AST::Node) && part.dstr_type? ? part.children : [part]
148
+ end
149
+
150
+ flattened.chunk_while { |left, right| plain_string?(left) && plain_string?(right) }
151
+ .map { |chunk| merge_plain_strings(chunk) }
152
+ end
153
+
154
+ def merge_plain_strings(chunk)
155
+ return chunk.first if chunk.one?
156
+
157
+ chunk.first.updated(:str, [chunk.map { |part| part.children.first }.join])
158
+ end
159
+
160
+ def stringish?(node)
161
+ node.is_a?(::Parser::AST::Node) && %i[str dstr].include?(node.type)
162
+ end
163
+
164
+ def plain_string?(node)
165
+ node.is_a?(::Parser::AST::Node) && node.str_type? && node.children.one?
166
+ end
167
+
91
168
  def offense?(node)
92
169
  return false unless node.multiline? && suitable_as_single_line?(node)
93
170
  return require_backslash?(node) if node.operator_keyword?