Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Only uncover a line the lone interpolated statement marked itself
Setting the interpolation's line back to nil also wiped out a mark left by
another statement on that line, such as a statement in a block earlier on
the same line or the first of several statements in one interpolation.
Undo only the mark the lone statement's newline_node added, and only when
no earlier statement had already marked the line.
  • Loading branch information
sferik committed Oct 2, 2026
commit 23e7904fd6181e277f4ed13dd5aa204252b48532
8 changes: 5 additions & 3 deletions core/src/main/java/org/jruby/parser/RubyParser.java
Comment thread
headius marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -5988,8 +5988,10 @@ public Object yyparse (RubyLexer yyLex) throws java.io.IOException {
if (((Node)yyVals[-1+yyTop].value) != null) {
/* A lone statement in an interpolation is not a line event of its own (MRI); the*/
/* string it is part of is. Several statements in one interpolation each remain one.*/
/* MRI's compiler marks coverable lines from the newline flag, but newline_node marked*/
/* this one already, so undo that too (it was the last line newline_node marked).*/
if (((Node)yyVals[-1+yyTop].value).isNewline()) p.uncoverLastLine();
((Node)yyVals[-1+yyTop].value).unsetNewline();
p.uncoverLine(yyVals[yyTop - count + 6].start());
}
yyVal = p.newEvStrNode(yyVals[yyTop - count + 6].start(), ((Node)yyVals[-1+yyTop].value));
/*% %*/
Expand Down Expand Up @@ -6913,7 +6915,7 @@ public Object yyparse (RubyLexer yyLex) throws java.io.IOException {
return yyVal;
};
}
// line 4909 "parse.y"
// line 4911 "parse.y"

}
// line 15251 "-"
// line 15253 "-"
4 changes: 3 additions & 1 deletion core/src/main/java/org/jruby/parser/RubyParser.y
Original file line number Diff line number Diff line change
Expand Up @@ -4083,8 +4083,10 @@ string_content : tSTRING_CONTENT {
if ($6 != null) {
// A lone statement in an interpolation is not a line event of its own (MRI); the
// string it is part of is. Several statements in one interpolation each remain one.
// MRI's compiler marks coverable lines from the newline flag, but newline_node marked
// this one already, so undo that too (it was the last line newline_node marked).
if ($6.isNewline()) p.uncoverLastLine();
$6.unsetNewline();
p.uncoverLine(@6.start());
}
$$ = p.newEvStrNode(@6.start(), $6);
/*% %*/
Expand Down
16 changes: 11 additions & 5 deletions core/src/main/java/org/jruby/parser/RubyParserBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ public abstract class RubyParserBase {

private int[] coverage = EMPTY_COVERAGE;

// The line the most recent coverLine call marked, or -1 if an earlier statement had already marked it.
private int lastNewlyCoveredLine = -1;

private static final int[] EMPTY_COVERAGE = new int[0];

private StringStyle stringStyle = Chilled;
Expand Down Expand Up @@ -2682,22 +2685,25 @@ private boolean isLineCountingEnabled() {
* Zero out coverable lines as they're encountered
*/
public void coverLine(int i) {
lastNewlyCoveredLine = -1;
// We had an overflow so we cannot mark whatever line this is as covered.
if (i < 0) return;
if (isLineCountingEnabled()) {
growCoverageLines(i);
if (coverage[i] != 0) lastNewlyCoveredLine = i;
coverage[i] = 0;
}
}

/**
* Forget that a line has code on it (a statement marked by newline_node that turned out not to be a line
* Undo the most recent coverLine (a statement marked by newline_node that turned out not to be a line
* event, such as a lone statement inside a string interpolation): the line reads as nil in the results
* unless another statement marks it.
* unless another statement marks it. A line some earlier statement had already marked stays marked.
*/
public void uncoverLine(int i) {
if (i < 0 || coverage == null || i >= coverage.length) return;
if (isCoverageEnabled()) coverage[i] = -1;
public void uncoverLastLine() {
if (lastNewlyCoveredLine < 0) return;
coverage[lastNewlyCoveredLine] = -1;
lastNewlyCoveredLine = -1;
}

/**
Expand Down
25 changes: 25 additions & 0 deletions test/jruby/test_coverage.rb
Comment thread
headius marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,23 @@ def dead(x) = x ? 1 : 2
Object.send(:remove_method, :folds) if Object.private_method_defined?(:folds)
end

def test_lone_statement_in_interpolation_keeps_earlier_statement_on_its_line
assert_equal [1, 1, 1], line_coverage(<<~'RUBY')
def foo(*) = nil
foo([1].map {
y = 1; nil }, "#{3}")
RUBY
end

def test_several_statements_in_interpolation_each_cover_their_line
assert_equal 1, line_coverage(<<~'RUBY')[1]
x = "#{
a = 1
a
}"
RUBY
end

private

def with_source(source)
Expand Down Expand Up @@ -592,4 +609,12 @@ def span(line, from, to)
def source_line(line)
@source.lines[line - 1]
end

def line_coverage(code)
with_source(code) do |path|
Coverage.start(lines: true)
load path
Coverage.result.fetch(path)[:lines]
end
end
end