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
Give Coverage.line_stub an entry for every line of the file
The loop that padded the stub with nils compared against lines.size(),
which grows as the loop appends, so it stopped about halfway to the end:
a five-line method whose last statement is on line 2 got a four-entry
stub. A last line with no newline was not counted at all. Pad until the
stub has one entry per line, counting lines as File.foreach does, which
is what CRuby's line_stub sizes its array by.

SimpleCov builds the line coverage of files that were never loaded from
line_stub, so those files were reported with lines missing.

An empty file could still get a stub of [0]. Every parse shares the
NilImplicitNode.NIL singleton, and an if with an empty else as the last
statement of a block marked it as a newline through fixpos, so from then
on the implicit nil at the root of an empty file counted as a line. Make
NIL ignore setNewline and setLine so no parse can change it for later
ones.
  • Loading branch information
sferik committed Oct 2, 2026
commit 281bcb23b6d98fab8d295d28a749a5ce94e40a1e
9 changes: 9 additions & 0 deletions core/src/main/java/org/jruby/ast/NilImplicitNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ public NilImplicitNode() {
public boolean isNil() {
return true;
}

// NIL is shared by every parse, so marking it for one AST would mark it in all later ones.
@Override
public void setNewline() {
}

@Override
public void setLine(int line) {
}
}
2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/parser/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public IRubyObject getLineStub(ThreadContext context, ParseResult result, int li
LineStubVisitor lineVisitor = new LineStubVisitor(context.runtime, lines);
lineVisitor.visitRootNode(((RootNode) result));

for (int i = 0; i <= lineCount - lines.size(); i++) {
while (lines.size() < lineCount) {
lines.append(context, context.nil);
}
return lines;
Expand Down
5 changes: 4 additions & 1 deletion core/src/main/java/org/jruby/parser/ParserManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,13 @@ public IRubyObject getLineStub(ThreadContext context, IRubyObject arg) {
int length = contents.realSize();
byte[] bytes = contents.unsafeBytes();

// One entry per line, as File.foreach yields them: a last line without a newline still counts.
int end = begin + length;
int lineCount = 0;
for (int i = begin; i < length; i++) {
for (int i = begin; i < end; i++) {
if (bytes[i] == '\n') lineCount++;
}
if (length > 0 && bytes[end - 1] != '\n') lineCount++;

// FIXME: Semantic problem. Linenumber affects both differently due to prism being 1-indexed and AST being 0-indexed.
ParseResult result = parseFile("", Options.PARSER_PRISM.load() ? 0 : -1, new LoadServiceResourceInputStream(contents.bytes()), contents.getEncoding());
Expand Down
27 changes: 27 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 @@ -672,6 +672,33 @@ def test_ternary_arms_are_line_events
RUBY
end

def test_line_stub_has_an_entry_for_every_line
source = <<~'RUBY'
def show
@product = base_scope
.includes(colors_products: :color)
.find(params[:id])
end
RUBY
with_source(source) { |path| assert_equal [0, 0, nil, nil, nil], Coverage.line_stub(path) }
with_source(source.chomp) { |path| assert_equal [0, 0, nil, nil, nil], Coverage.line_stub(path) }
with_source("x = 1\n\n\n\n") { |path| assert_equal [0, nil, nil, nil], Coverage.line_stub(path) }
with_source("") { |path| assert_equal [], Coverage.line_stub(path) }
end

def test_line_stub_of_an_empty_file_after_an_empty_else
source = <<~'RUBY'
x.each do
if x
1
else
end
end
RUBY
with_source(source) { |path| Coverage.line_stub(path) }
with_source("") { |path| assert_equal [], Coverage.line_stub(path) }
end

private

def with_source(source)
Expand Down