Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
bc973d8311 fix(coverage): correct line coverage for compile-time constant conditions
When code coverage is enabled, JSC's ControlFlowProfiler incorrectly marks
the body of if-statements with constant conditions (like `if(true)`,
`if(2 === 2)`) as not executed. This happens because JSC's bytecode compiler
doesn't create proper basic blocks for constant-condition branches.

The fix unwraps if-statements with compile-time constant conditions during
parsing when coverage mode is active:
- `if(true) { body }` → `body`
- `if(false) { body } else { alt }` → `alt`

This ensures JSC's ControlFlowProfiler sees straight-line code instead of
branches it can't properly track.

Changes:
- Add `code_coverage` feature flag to RuntimeFeatures and wire it through
  from transpiler options
- In the `s_if` visit handler, detect constant-condition if-statements
  using `toBooleanWithoutDCECheck` (since DCE is disabled in coverage mode)
  and replace them with just their body
- Make `toBooleanWithoutDCECheck` public so it can be called from visitStmt

Closes #20141

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 10:09:31 +00:00
5 changed files with 188 additions and 1 deletions

View File

@@ -772,7 +772,7 @@ pub const SideEffects = enum(u1) {
// Avoid passing through *P
// This is a very recursive function.
fn toBooleanWithoutDCECheck(exp: Expr.Data) Result {
pub fn toBooleanWithoutDCECheck(exp: Expr.Data) Result {
switch (exp) {
.e_null, .e_undefined => {
return Result{ .ok = true, .value = false, .side_effects = .no_side_effects };

View File

@@ -1115,6 +1115,31 @@ pub fn VisitStmt(
}
}
// When code coverage is enabled, unwrap if-statements with compile-time
// constant conditions. JSC's ControlFlowProfiler splits basic blocks at
// branch points but marks the body of constant-condition branches as not
// executed, causing incorrect coverage reports. By replacing the if-statement
// with just its body (when the condition is known truthy) or removing it
// (when known falsy), we ensure JSC tracks coverage correctly.
//
// We can't use SideEffects.toBoolean() here because dead_code_elimination
// is disabled in coverage mode. Instead, check the condition directly using
// toBooleanWithoutDCECheck which doesn't require DCE to be enabled.
if (p.options.features.code_coverage) {
const cov_effects = SideEffects.toBooleanWithoutDCECheck(data.test_.data);
if (cov_effects.ok and cov_effects.side_effects != .could_have_side_effects) {
if (cov_effects.value) {
return try p.appendIfBodyPreservingScope(stmts, data.yes);
} else {
// The condition is always false — emit the else branch if present
if (data.no) |no| {
return try p.appendIfBodyPreservingScope(stmts, no);
}
return;
}
}
}
try stmts.append(stmt.*);
}
pub fn s_for(noalias p: *P, noalias stmts: *ListManaged(Stmt), noalias stmt: *Stmt, noalias data: *S.For) !void {

View File

@@ -223,6 +223,10 @@ pub const Runtime = struct {
/// - Assigns functions to context for persistence
repl_mode: bool = false,
/// Code coverage mode: unwrap constant-condition if statements so JSC's
/// ControlFlowProfiler can correctly track coverage of their bodies.
code_coverage: bool = false,
pub const empty_bundler_feature_flags: bun.StringSet = bun.StringSet.initComptime();
/// Initialize bundler feature flags for dead-code elimination via `import { feature } from "bun:bundle"`.

View File

@@ -1134,6 +1134,7 @@ pub const Transpiler = struct {
opts.features.bundler_feature_flags = transpiler.options.bundler_feature_flags;
opts.features.repl_mode = transpiler.options.repl_mode;
opts.repl_mode = transpiler.options.repl_mode;
opts.features.code_coverage = transpiler.options.code_coverage;
if (transpiler.macro_context == null) {
transpiler.macro_context = js_ast.Macro.MacroContext.init(transpiler);

View File

@@ -0,0 +1,157 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
// https://github.com/oven-sh/bun/issues/20141
// Test coverage was incorrect for compile-time constant conditions.
// The transpiler folds equality expressions like `2 === 2` into `true`,
// then JSC's ControlFlowProfiler marks the if-body as not executed because
// it doesn't create proper basic blocks for constant-condition branches.
// Fix: when coverage is enabled, unwrap if-statements with constant
// conditions so JSC doesn't create a branch in the first place.
test("coverage: while(true) + if(2 === 2) shows 100%", () => {
const dir = tempDirWithFiles("cov-20141", {
"src.ts": `
export function repro(): undefined {
while(true) {
if(2 === 2) {
break;
}
}
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { repro } from "./src";
test("repro", () => {
repro();
expect(true).toBe(true);
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: String(dir),
env: { ...bunEnv },
stdio: [null, null, "pipe"],
});
const stderr = result.stderr.toString("utf-8");
// The coverage output should show 100% line coverage for src.ts
// Previously it showed 66.67% with lines 4-5 uncovered
expect(stderr).toContain("src.ts");
expect(stderr).toMatch(/src\.ts\s*\|\s*100\.00\s*\|\s*100\.00/);
expect(result.exitCode).toBe(0);
});
test("coverage: if(true) with else shows 100%", () => {
const dir = tempDirWithFiles("cov-20141b", {
"src.ts": `
export function repro(): string {
if(true) {
return "yes";
} else {
return "no";
}
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { repro } from "./src";
test("repro", () => {
expect(repro()).toBe("yes");
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: String(dir),
env: { ...bunEnv },
stdio: [null, null, "pipe"],
});
const stderr = result.stderr.toString("utf-8");
// Previously showed 60.00% with body marked uncovered
expect(stderr).toContain("src.ts");
expect(stderr).toMatch(/src\.ts\s*\|\s*100\.00\s*\|\s*100\.00/);
expect(result.exitCode).toBe(0);
});
test("coverage: const x = 2; if(x === 2) shows 100%", () => {
const dir = tempDirWithFiles("cov-20141c", {
"src.ts": `
export function repro(): undefined {
const x = 2;
if(x === 2) {
return;
} else {
return;
}
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { repro } from "./src";
test("repro", () => {
repro();
expect(true).toBe(true);
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: String(dir),
env: { ...bunEnv },
stdio: [null, null, "pipe"],
});
const stderr = result.stderr.toString("utf-8");
// Previously showed 66.67% with body marked uncovered
expect(stderr).toContain("src.ts");
expect(stderr).toMatch(/src\.ts\s*\|\s*100\.00\s*\|\s*100\.00/);
expect(result.exitCode).toBe(0);
});
test("coverage: runtime conditions still report correctly", () => {
const dir = tempDirWithFiles("cov-20141d", {
"src.ts": `
export function repro(): undefined {
let i = 0;
while(true) {
i++;
if(i >= 3) {
break;
}
}
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { repro } from "./src";
test("repro", () => {
repro();
expect(true).toBe(true);
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: String(dir),
env: { ...bunEnv },
stdio: [null, null, "pipe"],
});
const stderr = result.stderr.toString("utf-8");
// Runtime conditions should still show 100% when fully exercised
expect(stderr).toContain("src.ts");
expect(stderr).toMatch(/src\.ts\s*\|\s*100\.00\s*\|\s*100\.00/);
expect(result.exitCode).toBe(0);
});