Skip to content

Make line coverage follow the innermost JSC block - #41528

Closed
robobun wants to merge 10 commits into
mainfrom
robobun/ba64acf1/coverage-innermost-range
Closed

robobun wants to merge 10 commits into
mainfrom
robobun/ba64acf1/coverage-innermost-range

Conversation

@robobun

@robobun robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #5307
Fixes #7025
Fixes #8290
Fixes #9008
Fixes #29691
Fixes #31484
Fixes #33957

Blocked on oven-sh/WebKit#570. The WebKit pin in this PR points at that PR's preview build and moves to the merged sha once it lands.

Problem

Fix

Background

  • JSC marks the start of each basic block with op_profile_control_flow. CodeBlock::insertBasicBlockBoundariesForControlFlowProfiler turns consecutive marks into [start, end] text ranges, end inclusive, and carves nested functions out as gaps. Bun reads those pieces plus FunctionHasExecutedCache's [start, end] -> executed function ranges.
  • A never-called function has no blocks at all. Only its function range says it is dead, so it must win over the enclosing executed block that contains it.
  • BUN_DEBUG_coverage=1 on a debug build prints the raw ranges per file.

Related PRs

Notes

Raw ranges for class Foo { x = 1; dead() { return 2 } } function neverCalled() {...} if (false) {...} new Foo() on main: block [0, 137] executed=true (the field initializer), function [1, 15] executed=false (the builtin (function () { }) default constructor, in the program's coordinates), plus the real ranges. With an explicit constructor the shared first block turns into pieces [0, -1] and [293, 29] because the initializer's whole-file span is inserted as a gap into the program's block.

Snapshot changes in coverage.test.ts: hit counts are per block now (DA:2,28 becomes DA:2,1), comment lines inside a dead function are no longer listed (the transpiler strips them, so they never map), return lines of never-called functions are now 0, and class Y { #hello } reports FNF:0 because the default constructor is no longer a function. bundler_bytecode_portable.test.ts's snapshot moves because a bare yield now carries text positions in the cached bytecode.

Line percentages can drop for projects whose dead code was hidden before, so coverageThreshold gates may start to fail. That is the report becoming accurate.

Not changed here: JSC offsets are UTF-16 units and bun builds the line table from the UTF-8 source, so files with non-ASCII text before a range skew (#38710 covers that).

parallel.test.ts "each worker has a unique JEST_WORKER_ID" flakes on main in this container and is tracked separately.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bundler_bytecode_portable.test.ts

@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 14 days. After that, they cost $0.25 per reviewed file.

Or wait 6 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 4b955bab-be51-4bc9-bc5e-c3a03e6fc1bc

📥 Commits

Reviewing files that changed from the base of the PR and between ae7b8f4 and f9348c6.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • test/cli/test/__snapshots__/coverage.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • scripts/build/deps/webkit.ts
  • src/sourcemap_jsc/Cargo.toml
  • src/sourcemap_jsc/CodeCoverage.rs
  • test/bundler/bundler_bytecode_portable.test.ts
  • test/cli/test/coverage.test.ts

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the claude label Sep 6, 2026
@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:06 AM PT - Sep 6th, 2026

❌ @robobun, your commit f9348c6 has 3 failures in Build #111405 (All Failures):

  • test/js/third_party/grpc-js/test-tonic.test.ts - code 1 on 🍎 any aarch64
  • test/js/node/test/parallel/test-crypto-dh-leak.js - code 1 on 🐧 13 x64-asan
  • 📦 Binary size — 8 over 0.50 MB
  • targetthis build canary: main #111080
    sizeΔ
    bun-darwin-aarch6459.46 MB59.25 MB+209.8 KB
    bun-darwin-x6465.75 MB65.29 MB+468.3 KB
    bun-linux-aarch6475.80 MB75.80 MB+1.0 KB
    bun-linux-x6475.92 MB75.89 MB+36.5 KB
    ❌ bun-linux-aarch64-musl69.26 MB68.09 MB+1.16 MB
    ❌ bun-linux-x64-musl70.17 MB68.67 MB+1.50 MB
    ❌ bun-linux-aarch64-android82.84 MB70.18 MB+12.66 MB
    ❌ bun-linux-x64-android85.39 MB72.72 MB+12.66 MB
    ❌ bun-freebsd-x6487.43 MB74.69 MB+12.74 MB
    ❌ bun-freebsd-aarch6490.58 MB77.84 MB+12.74 MB
    ❌ bun-windows-x6482.23 MB81.52 MB+730.5 KB
    ❌ bun-windows-aarch6474.07 MB70.66 MB+3.41 MB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 41528

That installs a local version of the PR into your bun-41528 executable, so you can run:

bun-41528 --bun

@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Related open work, found after opening this PR:

This PR and those can not both land as they are. A maintainer can pick the shape.

A bare yield now carries its text positions, so the serialized bytecode of
files that contain one changed on every platform. The preview build is
not the tagged release build, so its binary sizes differ.

[skip size check]
Comment on lines +257 to +260
/// lines do not: a line counts as executable only if it executed somewhere or
/// every report agrees it is executable, so a worker that saw a coarser view
/// of a function cannot show a fully executed function as partially covered
/// (#39930).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +620 to +623
/// One bit per source byte: set for bytes that are not whitespace and not
/// a closing brace. Only these bytes make a line executable, so the `}`
/// that ends a function, and the newline an executed block shares with the
/// last line of a dead one, do not count.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +730 to +732
// The function ranges include the program itself: the one that starts
// at 0 and reaches farthest. It is the outermost owner of every byte
// and is not a function in the report.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +740 to +741
// Every range JSC reported, function bodies first so that a block with
// the same span is painted after it and owns the bytes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +773 to +777
// Paint the ranges widest first. The last one to touch a byte is the
// innermost range that contains it, and that range decides whether the
// byte executed. This is the same rule as JSC's
// `ControlFlowProfiler::findBasicBlockAtTextOffset`. It is what keeps a
// never-called function dead when an enclosing block executed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +792 to +793
// Bytes whose owner mapped to a source line. Only meaningful with a
// source map: a function none of whose bytes map is not reported.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +919 to +921
// JSC's function range starts at `function`, not at the `export`
// in front of it. Skip the keyword so that the function decides
// its declaration line.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +941 to +942
/// The length of an `export` or `default` keyword at the start of `rest`, if
/// one is there. Whitespace must follow, so `default:` in a switch is not one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +956 to +957
/// One range JSC reported for a file: a basic block piece or a function body.
/// `end` is inclusive, as JSC reports it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +967 to +968
// JSC reports an empty piece as `end < start` when a nested function
// starts exactly where the block does. A negative offset maps to nothing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/sourcemap_jsc/CodeCoverage.rs Outdated
Comment on lines +985 to +987
/// The first significant byte on a line decides the line: that is the
/// statement which starts there. Later bytes may belong to a dead branch
/// on the same line, or to the statement that follows a dead one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +1095 to +1097
/// A basic block or function body as `[start, end]` byte offsets (end
/// inclusive) into the generated source. Offsets are what JSC reports, so they
/// identify the same block across processes that loaded the same file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

[skip size check]
Comment on lines +620 to +621
/// One bit per source byte: not whitespace, not `}`, not a leading
/// `export`/`default`. Only these bytes can make a line executable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +728 to +729
// The program's own range starts at 0 and reaches farthest. It is not
// a function in the report.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +769 to +770
// Widest first, so the innermost range that contains a byte owns it
// (JSC's `ControlFlowProfiler::findBasicBlockAtTextOffset` rule).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +931 to +932
/// Length of an `export` or `default` keyword followed by whitespace at the
/// start of `rest`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +957 to +958
// `end < start` is an empty piece (a nested function starts where the
// block does). A negative offset maps to nothing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment on lines +975 to +976
/// The first significant byte on a line decides the line: that is the
/// statement which starts there.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread scripts/build/deps/webkit.ts Outdated
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "2e2aa2290fac856d6f451ceacb58f7f5b44dd057";
export const WEBKIT_VERSION = "autobuild-preview-pr-570-c685afa2";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 WEBKIT_VERSION is pinned to autobuild-preview-pr-570-c685afa2, an ephemeral preview-PR artifact; merging this makes every fresh checkout's build depend on a release that will be deleted once oven-sh/WebKit#570 lands or is closed. Fix: land oven-sh/WebKit#570 first and pin WEBKIT_VERSION to the resulting merged commit SHA before this PR merges (per Dependencies & vendoring: never merge a pin to an ephemeral artifact).

Extended reasoning...

The prebuilt WebKit dep resolves its download URL from WEBKIT_VERSION. autobuild-preview-* releases on oven-sh/WebKit are transient CI artifacts for unmerged PRs and are garbage-collected; once PR 570 merges or the preview is pruned, bun bd on a clean cache fails at the WebKit fetch step for every developer and CI lane. The base branch pins a stable 40-char merged SHA (2e2aa2290fac856d6f451ceacb58f7f5b44dd057). The PR description acknowledges this is a preview build; the conventions block explicitly calls this out as must-swap-before-merge.

Verification: normal — scripts/build/deps/webkit.ts:6 changes WEBKIT_VERSION from the stable 40-char merged SHA "2e2aa2290fac856d6f451ceacb58f7f5b44dd057" to "autobuild-preview-pr-570-c685afa2". The codebase itself documents that these tags are ephemeral — scripts/build/download.ts:314-331: "The autobuild-preview-pr-* WebKit tags are the sharp edge: GitHub deletes the preview release when the PR…

Comment thread test/cli/test/coverage.test.ts Outdated
@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

On the two review points:

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread test/cli/test/coverage.test.ts Outdated
Comment thread test/cli/test/coverage.test.ts Outdated
@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Both test nits applied in bca35c5: lineHits drains stdout too, and the stderr substring check is gone (the exit code assertion carries stdout and stderr as its message instead).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no new issues

No new issues were found in this update; 1 finding from earlier reviews is still open above.

Still open from earlier reviews (1):

  • Unresolved: 1 blocking on lines changed since (possibly already fixed).

robobun added a commit that referenced this pull request Sep 6, 2026
Drop the CodeCoverage.rs line attribution changes and their tests and
snapshot updates. #38282 and #41528 rewrite that loop and cover the
same cases. What remains: coverageThreshold key validation, enforcing
only the listed metrics, the error message that names the missed
threshold, and the docs.
@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Overlap check against the other open PRs that patch generate_report_from_blocks. I built this branch and #38282 (each merged onto main at d316760) and ran the tests the narrower PRs add against both builds.

Test cases from #38282 this PR
#33959: no DA records for comment and blank lines in an uncalled function (#33957) pass pass
#33959: every line of an uncalled function reports DA:<n>,0, including the last (#31484) pass pass
#33959: text reporter prints 1,4 (a run that starts on line 1, a trailing single line) pass pass
#32849: a never-called multi-line function reports its whole body as 0 pass pass
#32849: export const f = (a) => a * 2; never called: line reported uncovered pass no: the line is decided by const, which ran (same as main, and what istanbul reports)
#32849: DA hit counts are execution counts no (byte counts, deferred to #35725) pass (DA:4,5 for a loop body that ran 5 times)
#36603: report generation time vs file size (--coverage minus plain, debug build, 48/96/193 KB) 1.4 s / 2.0 s / 8.1 s, quadratic 0.26 s / 0.53 s / 1.0 s, linear
#38518: six class field initializer and default constructor tests 0 of 6 pass (the default constructor is filtered, the field initializer is not) 6 of 6 pass

Where a narrow PR's inline snapshot differs only in the hit counts of covered lines (byte counts vs per-block counts), I counted the symptom as covered.

Outcome: #33959 is closed as covered by both. #32849 is trimmed to its coverageThreshold half, which neither PR carries. #36603 and #38518 stay open: this PR covers them, #38282 does not, so they are only redundant if this PR is the one that lands.

Trade-off between the two umbrella PRs: this PR needs oven-sh/WebKit#570 to land first (the pin points at its preview build). #38282 has no WebKit dependency and fixes the attribution symptoms on the current pin, but keeps the per-byte loop (quadratic on large files) and does not handle the class field initializer ranges, which it leaves to #38518.

@ThomasK33

Copy link
Copy Markdown

Real-world data point for the class field case (#5307, #9008), in case it helps prioritize this.

Minimal repro on 1.4.3 (same on 1.2.15 through 1.4.2):

// m.ts
export function check(x: number): number {
  if (x > 0) {
    return 1;
  }
  const y = -x;
  return y;
}

export class Box {
  value = 0;
}
// m.test.ts
import { expect, test } from "bun:test";
import { check } from "./m";

test("check", () => {
  expect(check(5)).toBe(1);
});

bun test --coverage --coverage-reporter=lcov reports DA:5,15 and DA:6,11, so the file reads 100% lines, although lines 5 and 6 never run. With declare value: number; lines 4-6 read 0.

Impact in one TypeScript codebase (coder/xum): 237 of 1,872 source files had an emitted class field, static field, #private field or static {} block. Our CI scores PRs with a CRAP metric computed from this lcov, so untested branches in those files looked covered.

Workaround for others: an explicit "useDefineForClassFields": false in tsconfig.json makes Bun lower plain instance fields and parameter properties to constructor assignments, and the coverage of those files is then correct. In our repo it fixed 226 of the 237 files and left the production output of tsc and esbuild byte-identical (both already use assign semantics at target ES2020). Static fields, #private fields and static {} blocks still trigger the bug, so this PR is still needed for those.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

yermakoffivan pushed a commit to yermakoffivan/mux that referenced this pull request Oct 18, 2026
…coverage is honest (coder#6926)

## Summary

Bun's coverage marks every line of a file as hit when the file has a
class with a field that Bun emits natively. coder#6422 fixed instance fields
with `useDefineForClassFields: false`. Static fields and `#private`
fields stay native, so 9 production files still read near 100% line
coverage and the CRAP gate scored their functions too low. This PR moves
those fields out of the classes. Nothing changes at runtime, and the 9
files now get honest coverage.

## Background

- Bun 1.2.15 to 1.4.3 bug: an evaluated class with an emitted field
reports one basic block with start > end, and Bun's LCOV writer turns it
into "the whole file ran". Only functions that never ran read 0. Untaken
branches read hit. Node's coverage is correct. Upstream fix:
oven-sh/bun#41528.
- coder#6422 covered instance fields. A scan of `src/` (TypeScript AST:
`static` or `#private` property declarations, `accessor` fields, `static
{}` blocks, not `declare`) found 11 files left on main. Two are
test-only (`desktopRfb.test-fixture.ts`, `stories/mocks/desktopRfb.ts`)
and the CRAP gate does not score them. This PR fixes the other 9.

## Implementation

- `private static readonly X = ...` becomes a module-level `const X`
placed just before the class, with its doc comment. Every `ClassName.X`
becomes `X`. The value is evaluated in the same order as before: static
initializers ran when the class was defined, during module evaluation.
- Two statics were public and used outside their files:
`DraftService.CHANGE_EVENT` is now the exported `DRAFT_CHANGE_EVENT`
(router subscription and tests), and `BackupService.RETAINED_SNAPSHOTS`
is the exported `RETAINED_SNAPSHOTS` (one test).
`BackupService.RETAINED_INACTIVE_CACHES` had no outside user, so it is
module-private now.
- `ExactResourceURL` in `mcpOauthService.ts` kept its configured string
in a `#raw` field. A module `WeakMap` keyed by the instance now holds
it. The value stays off the object, as with the `#` field (a TS
`private` field would add an own enumerable `raw` property to a `URL`).

## Validation

I ran the tests of the 9 files (34 test files, 1407 tests) with
`--coverage` on main and on this branch, then scored both with
`scripts/crap/crapper_report.py`:

| File | main LH/LF | branch LH/LF |
| --- | --- | --- |
| PowerModeEngine.ts | 408/408 (100%) | 400/408 (98.0%) |
| secretsStore.ts | 285/295 (96.6%) | 255/288 (88.5%) |
| ExtensionMetadataService.ts | 1004/1014 (99.0%) | 872/1016 (85.8%) |
| historyService.ts | 4101/4532 (90.5%) | 3800/4518 (84.1%) |
| StreamingMessageAggregator.ts | 2361/2488 (94.9%) | 2300/2510 (91.6%)
|
| mcpOauthService.ts | 1597/1651 (96.7%) | 1521/1655 (91.9%) |
| backupService.ts | 833/854 (97.5%) | 808/854 (94.6%) |
| draftService.ts | 523/528 (99.1%) | 509/526 (96.8%) |

- The new zero lines are scattered untaken branches inside functions
that ran, not a block. So this is real coverage, not Bun's inlined-const
bug (the adapter flags 0 functions as `bunInlinedConstSuspect` on both
sides).
- With this test subset, 4 functions newly score above CRAP 30 (for
example `HistoryService.getSubagentTranscript` CRAP 26 -> 136). CI runs
the whole suite, so CI's numbers will differ. The gate is in warn mode,
so they block nothing.
- The AST scan finds no production file with a Bun-native class field
after this change.

## Risks

Low. The moved values are constants that nothing reassigns. Renaming the
two public statics is a compile-time change, and `make typecheck` covers
every user.

## Follow-ups

- Nothing stops new code from adding a `static` or `#private` field,
which would bring the bug back for that file. A lint rule could block
these fields until oven-sh/bun#41528 ships in the pinned Bun. I left it
out to keep this PR small.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
@robobun

robobun commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Closing. This PR is superseded.

I ran the four test cases of this PR on the canary build of main (1.4.4-canary.1+963452075). Dead methods, functions, accessors and if bodies next to a class field have DA 0. The line after an await or a yield that never resumes has 0, and the line after a short-circuited await has 1. A loop body has its execution count. The default constructor is not counted as a function.

One line differs from what this PR expected. With coverageIgnoreSourcemaps = true, the line export function b() { of a function that never ran has DA 1, and its body has 0. With source maps the same line has 0.

@ThomasK33 your repro gives DA:5,0 and DA:6,0 on that build. A file with a static field, a #private field and a static {} block also reports its dead lines as 0. bun upgrade --canary has the fix.

#9008 is still open and is tracked in its issue. Reopen this PR if a case is still wrong.

@robobun robobun closed this Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment