From 1720963081ff59ad85212dad37ea8668ee21c738 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 8 Sep 2026 00:07:24 +0200 Subject: [PATCH 1/6] Run newline_test.rb only on Ruby 3.4+ * Older versions have known bugs in this area, like not emitting a :line event for `nil`, and there is no value to replicate them. --- test/prism/newline_test.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index ed797db965..69b68c06f3 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -2,7 +2,9 @@ require_relative "test_helper" -return unless defined?(RubyVM::InstructionSequence) +# There have also been changes made in other versions of Ruby, so we only want +# to test on the most recent versions. +return if !defined?(RubyVM::InstructionSequence) || RUBY_VERSION < "3.4.0" module Prism class NewlineTest < TestCase From 178297c8d2ba66c9dd2c60f40b37399391f923cc Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 7 Sep 2026 23:15:52 +0200 Subject: [PATCH 2/6] Remove unnecessary skips in newline_test.rb --- test/prism/newline_test.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index 69b68c06f3..74eb9981c0 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -11,7 +11,6 @@ class NewlineTest < TestCase skips = %w[ errors_test.rb locals_test.rb - regexp_test.rb test_helper.rb unescape_test.rb api/parse_stream_test.rb @@ -24,9 +23,6 @@ class NewlineTest < TestCase ruby/find_fixtures.rb ruby/find_test.rb ruby/parser_test.rb - ruby/ripper_test.rb - ruby/ruby_parser_test.rb - ruby/parameters_signature_test.rb ] base = __dir__ From cbe1cbbcdd832848551b676ee1f18411b8adbacf Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 7 Sep 2026 23:38:33 +0200 Subject: [PATCH 3/6] Remove all remaining skips in newline_test.rb Fix two mismatches between prism's newline flags and RubyVM's line events in the Newlines visitor: * def, class, module, and singleton class nodes compile to their own ISeqs with independent line-event tracking, so reset the line table for them like blocks and lambdas already do. This matches one-line definitions like `def foo; bar; end`, where the bytecode emits two line events on the same line. The body of an endless method definition never emits newline events, so in that case mark every line as already seen instead. * Statements inside string interpolation do not emit line events, so mark every line as already seen while visiting embedded statements. Nested scopes (blocks, lambdas, defs, etc.) still reset the lines and emit events again. The remaining divergences are bytecode artifacts: for statements like `foo = [` or `foo =` where the value continues on the following lines, the line event is emitted on the line of the first sub-expression of the value instead of on the first line of the statement. Replace the two ad-hoc compensations in the test with a single count-based rule that moves or drops the newline flag accordingly. Co-Authored-By: Claude Fable 5 --- lib/prism/parse_result/newlines.rb | 75 +++++++++++++++++++ rbi/generated/prism/parse_result/newlines.rbi | 25 +++++++ sig/generated/prism/parse_result/newlines.rbs | 30 ++++++++ test/prism/newline_test.rb | 59 +++++++-------- 4 files changed, 155 insertions(+), 34 deletions(-) diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb index ad8d8b6f55..8361c11864 100644 --- a/lib/prism/parse_result/newlines.rb +++ b/lib/prism/parse_result/newlines.rb @@ -65,6 +65,81 @@ def visit_lambda_node(node) end end + # Permit def nodes to mark newlines within themselves. The body of an + # endless method definition never emits newline events, so in that case + # mark every line as already seen while visiting it instead. Nested + # scopes (blocks, lambdas, etc.) reset the lines and emit events again. + # + #: (DefNode node) -> void + def visit_def_node(node) + old_lines = @lines + @lines = Array.new(old_lines.size, !node.equal_loc.nil?) + + begin + super(node) + ensure + @lines = old_lines + end + end + + # Permit class nodes to mark newlines within themselves. + # + #: (ClassNode node) -> void + def visit_class_node(node) + old_lines = @lines + @lines = Array.new(old_lines.size, false) + + begin + super(node) + ensure + @lines = old_lines + end + end + + # Permit module nodes to mark newlines within themselves. + # + #: (ModuleNode node) -> void + def visit_module_node(node) + old_lines = @lines + @lines = Array.new(old_lines.size, false) + + begin + super(node) + ensure + @lines = old_lines + end + end + + # Permit singleton class nodes to mark newlines within themselves. + # + #: (SingletonClassNode node) -> void + def visit_singleton_class_node(node) + old_lines = @lines + @lines = Array.new(old_lines.size, false) + + begin + super(node) + ensure + @lines = old_lines + end + end + + # Statements inside string interpolation do not emit newline events, so + # mark every line as already seen while visiting them. Nested scopes + # (blocks, lambdas, defs, etc.) reset the lines and emit events again. + # + #: (EmbeddedStatementsNode node) -> void + def visit_embedded_statements_node(node) + old_lines = @lines + @lines = Array.new(old_lines.size, true) + + begin + super(node) + ensure + @lines = old_lines + end + end + # Mark if nodes as newlines. # #: (IfNode node) -> void diff --git a/rbi/generated/prism/parse_result/newlines.rbi b/rbi/generated/prism/parse_result/newlines.rbi index f77dee3bbf..85e37cd832 100644 --- a/rbi/generated/prism/parse_result/newlines.rbi +++ b/rbi/generated/prism/parse_result/newlines.rbi @@ -35,6 +35,31 @@ module Prism sig { params(node: LambdaNode).void } def visit_lambda_node(node); end + # Permit def nodes to mark newlines within themselves. The body of an + # endless method definition never emits newline events, so in that case + # mark every line as already seen while visiting it instead. Nested + # scopes (blocks, lambdas, etc.) reset the lines and emit events again. + sig { params(node: DefNode).void } + def visit_def_node(node); end + + # Permit class nodes to mark newlines within themselves. + sig { params(node: ClassNode).void } + def visit_class_node(node); end + + # Permit module nodes to mark newlines within themselves. + sig { params(node: ModuleNode).void } + def visit_module_node(node); end + + # Permit singleton class nodes to mark newlines within themselves. + sig { params(node: SingletonClassNode).void } + def visit_singleton_class_node(node); end + + # Statements inside string interpolation do not emit newline events, so + # mark every line as already seen while visiting them. Nested scopes + # (blocks, lambdas, defs, etc.) reset the lines and emit events again. + sig { params(node: EmbeddedStatementsNode).void } + def visit_embedded_statements_node(node); end + # Mark if nodes as newlines. sig { params(node: IfNode).void } def visit_if_node(node); end diff --git a/sig/generated/prism/parse_result/newlines.rbs b/sig/generated/prism/parse_result/newlines.rbs index 9a505fa961..8619e7871c 100644 --- a/sig/generated/prism/parse_result/newlines.rbs +++ b/sig/generated/prism/parse_result/newlines.rbs @@ -40,6 +40,36 @@ module Prism # : (LambdaNode node) -> void def visit_lambda_node: (LambdaNode node) -> void + # Permit def nodes to mark newlines within themselves. The body of an + # endless method definition never emits newline events, so in that case + # mark every line as already seen while visiting it instead. Nested + # scopes (blocks, lambdas, etc.) reset the lines and emit events again. + # + # : (DefNode node) -> void + def visit_def_node: (DefNode node) -> void + + # Permit class nodes to mark newlines within themselves. + # + # : (ClassNode node) -> void + def visit_class_node: (ClassNode node) -> void + + # Permit module nodes to mark newlines within themselves. + # + # : (ModuleNode node) -> void + def visit_module_node: (ModuleNode node) -> void + + # Permit singleton class nodes to mark newlines within themselves. + # + # : (SingletonClassNode node) -> void + def visit_singleton_class_node: (SingletonClassNode node) -> void + + # Statements inside string interpolation do not emit newline events, so + # mark every line as already seen while visiting them. Nested scopes + # (blocks, lambdas, defs, etc.) reset the lines and emit events again. + # + # : (EmbeddedStatementsNode node) -> void + def visit_embedded_statements_node: (EmbeddedStatementsNode node) -> void + # Mark if nodes as newlines. # # : (IfNode node) -> void diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index 74eb9981c0..702a28b898 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -8,25 +8,8 @@ module Prism class NewlineTest < TestCase - skips = %w[ - errors_test.rb - locals_test.rb - test_helper.rb - unescape_test.rb - api/parse_stream_test.rb - api/raise_error_test.rb - encoding/regular_expression_encoding_test.rb - encoding/string_encoding_test.rb - result/breadth_first_search_test.rb - result/static_literals_test.rb - result/warnings_test.rb - ruby/find_fixtures.rb - ruby/find_test.rb - ruby/parser_test.rb - ] - base = __dir__ - (Dir["{,api/,encoding/,result/,ruby/}*.rb", base: base] - skips).each do |relative| + Dir["{,api/,encoding/,result/,ruby/}*.rb", base: base].each do |relative| define_method(:"test_#{relative}") do assert_newlines(base, relative) end @@ -43,7 +26,8 @@ def assert_newlines(base, relative) assert_empty result.errors actual = prism_lines(result) - source.each_line.with_index(1) do |line, line_number| + lines = source.lines + lines.each.with_index(1) do |line, line_number| # Lines like `while (foo = bar)` result in two line flags in the # bytecode but only one newline flag in the AST. We need to remove the # extra line flag from the bytecode to make the test pass. @@ -52,25 +36,32 @@ def assert_newlines(base, relative) expected.delete_at(index) if index end - # Lines like `foo =` where the value is on the next line result in - # another line flag in the bytecode but only one newline flag in the - # AST. - if line.match?(/^\s+\w+ =$/) - if source.lines[line_number].match?(/^\s+case/) - actual[actual.index(line_number)] += 1 - else - actual.delete_at(actual.index(line_number)) - end - end - - if line.match?(/^\s+\w+ = \[$/) - if !expected.include?(line_number) && !expected.include?(line_number + 2) - actual[actual.index(line_number)] += 1 + # For statements like `foo = [` or `foo =` where the value continues + # on the following lines, the line event in the bytecode is emitted on + # the line of the first sub-expression of the value (e.g., the first + # array element) instead of on the first line of the statement, while + # prism marks the newline flag on the node that starts the statement. + # The same is true for statements that begin with a multi-line array + # or hash literal, like `[` alone on a line. To compensate, move the + # newline flag to the line the bytecode uses, or drop it if another + # node already has a newline flag on that line. + if line.match?(/[\w\])"'] =( \[| \{| begin)?$/) || line.match?(/\A\s*[\[{]$/) + if actual.count(line_number) > expected.count(line_number) + target = ((line_number + 1)..lines.length).find do |candidate| + !lines[candidate - 1].match?(/\A\s*(#|\z)/) + end + + index = actual.index(line_number) #: Integer + if target && expected.count(target) > actual.count(target) + actual[index] = target + else + actual.delete_at(index) + end end end end - assert_equal expected, actual + assert_equal expected, actual.sort end def rubyvm_lines(source) From e5162a40cf40266a0d61d7a03c83bafbcea4d009 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 7 Sep 2026 23:58:16 +0200 Subject: [PATCH 4/6] Match RubyVM newlines for parenthesized while/until predicates Lines like `while (foo = bar)` result in two line events in the bytecode: parentheses make the inner expression a statement with its own line event, and the predicate of a while or until loop is compiled at the end of the loop, after the body, so that event is emitted again in addition to the one for the loop statement itself. This also mirrors runtime behavior, since the predicate line fires on each iteration. Match this in the Newlines visitor by marking the loop node itself when a prefix loop has a parenthesized predicate, and by visiting the predicate with a fresh set of lines so that its statements can mark lines that were already seen. This removes the corresponding compensation in newline_test.rb. The remaining compensation for assignments whose value continues on the following lines is kept: the line event is emitted on the line of the statement's first compiled instruction, which depends on constant folding (for example, an array of static literals compiles to a single instruction on the line of the literal, and string literals are only static under `# frozen_string_literal: true`). That is a property of the compiler rather than of the AST, so it does not belong in the newline flags. Co-Authored-By: Claude Fable 5 --- lib/prism/parse_result/newlines.rb | 53 ++++++++++++++++++- rbi/generated/prism/parse_result/newlines.rbi | 12 +++++ sig/generated/prism/parse_result/newlines.rbs | 14 +++++ test/prism/newline_test.rb | 24 ++++----- 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb index 8361c11864..874258b1b0 100644 --- a/lib/prism/parse_result/newlines.rb +++ b/lib/prism/parse_result/newlines.rb @@ -140,6 +140,42 @@ def visit_embedded_statements_node(node) end end + # The predicate of a while loop is compiled at the end of the loop, + # after the body, so any statements it contains (from parentheses) + # emit their line events again even if the lines were already seen. + # + #: (WhileNode node) -> void + def visit_while_node(node) + old_lines = @lines + @lines = Array.new(old_lines.size, false) + + begin + visit(node.predicate) + ensure + @lines = old_lines + end + + visit(node.statements) + end + + # The predicate of an until loop is compiled at the end of the loop, + # after the body, so any statements it contains (from parentheses) + # emit their line events again even if the lines were already seen. + # + #: (UntilNode node) -> void + def visit_until_node(node) + old_lines = @lines + @lines = Array.new(old_lines.size, false) + + begin + visit(node.predicate) + ensure + @lines = old_lines + end + + visit(node.statements) + end + # Mark if nodes as newlines. # #: (IfNode node) -> void @@ -219,14 +255,26 @@ def newline_flag!(lines) # :nodoc: class UntilNode < Node #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: - predicate.newline_flag!(lines) + if location.start_offset == keyword_loc.start_offset && predicate.is_a?(ParenthesesNode) + # A parenthesized predicate emits its own line event when it is + # compiled at the end of the loop, in addition to this one. + super + else + predicate.newline_flag!(lines) + end end end class WhileNode < Node #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: - predicate.newline_flag!(lines) + if location.start_offset == keyword_loc.start_offset && predicate.is_a?(ParenthesesNode) + # A parenthesized predicate emits its own line event when it is + # compiled at the end of the loop, in addition to this one. + super + else + predicate.newline_flag!(lines) + end end end @@ -237,6 +285,7 @@ def newline_flag!(lines) # :nodoc: end end + class InterpolatedMatchLastLineNode < Node #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: diff --git a/rbi/generated/prism/parse_result/newlines.rbi b/rbi/generated/prism/parse_result/newlines.rbi index 85e37cd832..427d0cb2b7 100644 --- a/rbi/generated/prism/parse_result/newlines.rbi +++ b/rbi/generated/prism/parse_result/newlines.rbi @@ -60,6 +60,18 @@ module Prism sig { params(node: EmbeddedStatementsNode).void } def visit_embedded_statements_node(node); end + # The predicate of a while loop is compiled at the end of the loop, + # after the body, so any statements it contains (from parentheses) + # emit their line events again even if the lines were already seen. + sig { params(node: WhileNode).void } + def visit_while_node(node); end + + # The predicate of an until loop is compiled at the end of the loop, + # after the body, so any statements it contains (from parentheses) + # emit their line events again even if the lines were already seen. + sig { params(node: UntilNode).void } + def visit_until_node(node); end + # Mark if nodes as newlines. sig { params(node: IfNode).void } def visit_if_node(node); end diff --git a/sig/generated/prism/parse_result/newlines.rbs b/sig/generated/prism/parse_result/newlines.rbs index 8619e7871c..6f3c0ee3b8 100644 --- a/sig/generated/prism/parse_result/newlines.rbs +++ b/sig/generated/prism/parse_result/newlines.rbs @@ -70,6 +70,20 @@ module Prism # : (EmbeddedStatementsNode node) -> void def visit_embedded_statements_node: (EmbeddedStatementsNode node) -> void + # The predicate of a while loop is compiled at the end of the loop, + # after the body, so any statements it contains (from parentheses) + # emit their line events again even if the lines were already seen. + # + # : (WhileNode node) -> void + def visit_while_node: (WhileNode node) -> void + + # The predicate of an until loop is compiled at the end of the loop, + # after the body, so any statements it contains (from parentheses) + # emit their line events again even if the lines were already seen. + # + # : (UntilNode node) -> void + def visit_until_node: (UntilNode node) -> void + # Mark if nodes as newlines. # # : (IfNode node) -> void diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index 702a28b898..b92381e991 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -28,23 +28,17 @@ def assert_newlines(base, relative) lines = source.lines lines.each.with_index(1) do |line, line_number| - # Lines like `while (foo = bar)` result in two line flags in the - # bytecode but only one newline flag in the AST. We need to remove the - # extra line flag from the bytecode to make the test pass. - if line.match?(/while \(/) - index = expected.index(line_number) - expected.delete_at(index) if index - end - # For statements like `foo = [` or `foo =` where the value continues # on the following lines, the line event in the bytecode is emitted on - # the line of the first sub-expression of the value (e.g., the first - # array element) instead of on the first line of the statement, while - # prism marks the newline flag on the node that starts the statement. - # The same is true for statements that begin with a multi-line array - # or hash literal, like `[` alone on a line. To compensate, move the - # newline flag to the line the bytecode uses, or drop it if another - # node already has a newline flag on that line. + # the line of its first instruction (e.g., the first array element) + # instead of on the first line of the statement, while prism marks the + # newline flag on the node that starts the statement. The same is true + # for statements that begin with a multi-line array or hash literal, + # like `[` alone on a line. The exact line depends on constant folding + # (e.g., an array of literals compiles to a single instruction on the + # first line), so to compensate, move the newline flag to the line the + # bytecode uses, or drop it if another node already has a newline flag + # on that line. if line.match?(/[\w\])"'] =( \[| \{| begin)?$/) || line.match?(/\A\s*[\[{]$/) if actual.count(line_number) > expected.count(line_number) target = ((line_number + 1)..lines.length).find do |candidate| From fed449b08c6b14fc28132bb5b9e1db7bf8f2aa53 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 8 Sep 2026 00:03:07 +0200 Subject: [PATCH 5/6] Match RubyVM newlines exactly, removing all compensations in newline_test.rb The line event for a statement is emitted where its first instruction is compiled, so make nodes whose first instruction comes from a sub-expression delegate their newline flag to that sub-expression: assignments to their value, calls to their receiver, and array, hash, and interpolated string literals to their first element. Static literals are the exception: they are compiled to a single instruction on the first line of the literal, so they do not delegate. The static literal flag captures the folding boundary exactly, including that string literals are only static under `# frozen_string_literal: true`, both in arrays and in the parts of heredocs. With this, prism's newline flags match RubyVM's line events exactly on every file in the test suite and newline_test.rb needs no compensation logic at all. Co-Authored-By: Claude Fable 5 --- lib/prism/parse_result/newlines.rb | 98 ++++++++++++++++++- rbi/generated/prism/parse_result/newlines.rbi | 50 ++++++++++ sig/generated/prism/parse_result/newlines.rbs | 50 ++++++++++ test/prism/newline_test.rb | 31 +----- 4 files changed, 198 insertions(+), 31 deletions(-) diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb index 874258b1b0..85b35d66e3 100644 --- a/lib/prism/parse_result/newlines.rb +++ b/lib/prism/parse_result/newlines.rb @@ -285,6 +285,98 @@ def newline_flag!(lines) # :nodoc: end end + # The line event for a statement is emitted where its first instruction is + # compiled, so nodes whose first instruction comes from a sub-expression + # delegate their newline flag to that sub-expression: assignments to their + # value, calls to their receiver, and array, hash, and interpolated string + # literals to their first element. Static literals are the exception: they + # are compiled to a single instruction on the first line of the literal, so + # they do not delegate. + + class LocalVariableWriteNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + value.newline_flag!(lines) + end + end + + class InstanceVariableWriteNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + value.newline_flag!(lines) + end + end + + class ClassVariableWriteNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + value.newline_flag!(lines) + end + end + + class GlobalVariableWriteNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + value.newline_flag!(lines) + end + end + + class ConstantWriteNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + value.newline_flag!(lines) + end + end + + class ConstantPathWriteNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + value.newline_flag!(lines) + end + end + + class MultiWriteNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + value.newline_flag!(lines) + end + end + + class CallNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + if (receiver = self.receiver) + receiver.newline_flag!(lines) + else + super + end + end + end + + class ArrayNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + first = elements.first + if first && !static_literal? + first.newline_flag!(lines) + else + super + end + end + end + + class HashNode < Node + #: (Array[bool] lines) -> void + def newline_flag!(lines) # :nodoc: + first = elements.first + if first && !static_literal? + first.newline_flag!(lines) + else + super + end + end + end + class InterpolatedMatchLastLineNode < Node #: (Array[bool] lines) -> void @@ -306,7 +398,11 @@ class InterpolatedStringNode < Node #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: first = parts.first - first.newline_flag!(lines) if first + if first && !static_literal? + first.newline_flag!(lines) + else + super + end end end diff --git a/rbi/generated/prism/parse_result/newlines.rbi b/rbi/generated/prism/parse_result/newlines.rbi index 427d0cb2b7..3e83f4757a 100644 --- a/rbi/generated/prism/parse_result/newlines.rbi +++ b/rbi/generated/prism/parse_result/newlines.rbi @@ -131,6 +131,56 @@ module Prism def newline_flag!(lines); end end + class LocalVariableWriteNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class InstanceVariableWriteNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class ClassVariableWriteNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class GlobalVariableWriteNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class ConstantWriteNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class ConstantPathWriteNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class MultiWriteNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class CallNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class ArrayNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + + class HashNode < Node + sig { params(lines: T::Array[T::Boolean]).void } + def newline_flag!(lines); end + end + class InterpolatedMatchLastLineNode < Node sig { params(lines: T::Array[T::Boolean]).void } def newline_flag!(lines); end diff --git a/sig/generated/prism/parse_result/newlines.rbs b/sig/generated/prism/parse_result/newlines.rbs index 6f3c0ee3b8..2a9cf13c54 100644 --- a/sig/generated/prism/parse_result/newlines.rbs +++ b/sig/generated/prism/parse_result/newlines.rbs @@ -146,6 +146,56 @@ module Prism def newline_flag!: (Array[bool] lines) -> void end + class LocalVariableWriteNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class InstanceVariableWriteNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class ClassVariableWriteNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class GlobalVariableWriteNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class ConstantWriteNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class ConstantPathWriteNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class MultiWriteNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class CallNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class ArrayNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + + class HashNode < Node + # : (Array[bool] lines) -> void + def newline_flag!: (Array[bool] lines) -> void + end + class InterpolatedMatchLastLineNode < Node # : (Array[bool] lines) -> void def newline_flag!: (Array[bool] lines) -> void diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index b92381e991..7d48d5297e 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -26,36 +26,7 @@ def assert_newlines(base, relative) assert_empty result.errors actual = prism_lines(result) - lines = source.lines - lines.each.with_index(1) do |line, line_number| - # For statements like `foo = [` or `foo =` where the value continues - # on the following lines, the line event in the bytecode is emitted on - # the line of its first instruction (e.g., the first array element) - # instead of on the first line of the statement, while prism marks the - # newline flag on the node that starts the statement. The same is true - # for statements that begin with a multi-line array or hash literal, - # like `[` alone on a line. The exact line depends on constant folding - # (e.g., an array of literals compiles to a single instruction on the - # first line), so to compensate, move the newline flag to the line the - # bytecode uses, or drop it if another node already has a newline flag - # on that line. - if line.match?(/[\w\])"'] =( \[| \{| begin)?$/) || line.match?(/\A\s*[\[{]$/) - if actual.count(line_number) > expected.count(line_number) - target = ((line_number + 1)..lines.length).find do |candidate| - !lines[candidate - 1].match?(/\A\s*(#|\z)/) - end - - index = actual.index(line_number) #: Integer - if target && expected.count(target) > actual.count(target) - actual[index] = target - else - actual.delete_at(index) - end - end - end - end - - assert_equal expected, actual.sort + assert_equal expected, actual end def rubyvm_lines(source) From d9fb2b85799594999b355dcc548e0471498dd697 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 8 Sep 2026 23:42:34 +0200 Subject: [PATCH 6/6] Add a note about what a skip means in newline_test.rb --- test/prism/newline_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index 7d48d5297e..7fb7a548a8 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -8,6 +8,7 @@ module Prism class NewlineTest < TestCase + # Do not add skips here, every change is a regression in TracePoint :line events base = __dir__ Dir["{,api/,encoding/,result/,ruby/}*.rb", base: base].each do |relative| define_method(:"test_#{relative}") do