diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a4b771ded..d44748084 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -4598,8 +4598,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] continue; } const f = item.f; - lowerer.stats.statementsTotal++; - lowerer.bumpFileStat(locOf(f.initializer).file, "total"); + lowerer.countStatement(f.initializer, locOf(f.initializer).file); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -4635,8 +4634,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] out.push({ kind: "assign", localId: f.globalId, value, loc: locOf(f.initializer) }); } catch (e) { if (!(e instanceof PoisonError)) throw e; - lowerer.stats.statementsFailed++; - lowerer.bumpFileStat(locOf(f.initializer).file, "failed"); + lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); if (isJsSourceFile(f.initializer.getSourceFile())) { const fence = lowerer.diagSink === null @@ -7764,8 +7762,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL }); } if (!f.initializer) continue; - lowerer.stats.statementsTotal++; - lowerer.bumpFileStat(locOf(f.initializer).file, "total"); + lowerer.countStatement(f.initializer, locOf(f.initializer).file); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -7780,8 +7777,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL }); } catch (e) { if (!(e instanceof PoisonError)) throw e; - lowerer.stats.statementsFailed++; - lowerer.bumpFileStat(locOf(f.initializer).file, "failed"); + lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); if (isJsSourceFile(f.initializer.getSourceFile())) { const fence = lowerer.diagSink === null diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 43ad3819b..8ded9106b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -689,10 +689,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir entry.index = i; if (unreachable && skipsWhenUnreachable(stmt)) continue; if (inFunction && isAbruptStatement(stmt)) unreachable = true; - if (!lowerer.suppressStats) { - lowerer.stats.statementsTotal++; - if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "total"); - } + if (!lowerer.suppressStats) lowerer.countStatement(stmt, sf?.fileName); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -757,10 +754,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir } else if (!(e instanceof PoisonError)) { throw e; } - if (!lowerer.suppressStats) { - lowerer.stats.statementsFailed++; - if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "failed"); - } + if (!lowerer.suppressStats) lowerer.countStatementFailed(stmt, sf?.fileName); lowerer.noteBlockedBindings(stmt); // The runtime-fence conversion (JS sources, direct diagnostics // only — a capture wrapper in force means collection owns the diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6493bc5f3..a77142cb0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2925,6 +2925,32 @@ export class Lowerer { * pass skips it (attribution describes the build). */ readonly statsByFile = new Map(); + /** Statements and field initializers already in the coverage totals, and + * those already counted as failed. Speculative lowering can lower one + * function body several times: each receiver probe of + * `(function () { ... }).call(this)` lowers it. A source statement counts + * once however many times it lowers. */ + private readonly countedStatements = new Set(); + private readonly failedStatements = new Set(); + + /** Adds `node` to the coverage totals unless it is already counted (the + * caller checks suppressStats). */ + countStatement(node: ts.Node, file: string | undefined): void { + if (this.countedStatements.has(node)) return; + this.countedStatements.add(node); + this.stats.statementsTotal++; + if (file !== undefined) this.bumpFileStat(file, "total"); + } + + /** Counts `node` as failed unless it already is (the caller checks + * suppressStats). */ + countStatementFailed(node: ts.Node, file: string | undefined): void { + if (this.failedStatements.has(node)) return; + this.failedStatements.add(node); + this.stats.statementsFailed++; + if (file !== undefined) this.bumpFileStat(file, "failed"); + } + /** Bumps the per-file attribution counter (no-op unless provenance is * active and this is the emit/discovery lane — mirror the CALLER's * suppressStats guard, this method only gates remainder). */ diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs index fbb1a65fc..3ae9df3dc 100644 --- a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs +++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs @@ -5,8 +5,7 @@ // and `typeof module.exports` compile; a statement with no static lowering // (here `module.exports` passed as a value in the class base) compiles to // its runtime fence instead of vanishing. It never runs, so the binary -// matches Node. Coverage counts that one fenced statement once: the class -// base's body is lowered once. +// matches Node. const verbose = process.argv.length > 99; (function () { var root = this;