oxc_coverage_instrument
Istanbul-compatible JavaScript/TypeScript coverage instrumentation, built on the Oxc parser. 5-33x faster than existing tools.
Why
swc-coverage-instrument fills this role for SWC. There is no equivalent for the Oxc ecosystem. Any tool built on oxc_parser that needs coverage instrumentation currently has to pull in SWC or Babel.
This crate fills that gap. AST-level instrumentation via oxc_traverse + oxc_codegen produces correct Istanbul-compatible output, verified against the canonical istanbul-lib-instrument on 27 shared fixtures.
Install
Rust
[]
= "0.3"
Node.js
CLI
Usage
Rust
use ;
let source = "function add(a, b) { return a + b; }";
let result = instrument.unwrap;
// Istanbul-compatible coverage map
assert_eq!;
// Instrumented source with counters injected
println!;
Node.js
import from 'oxc-coverage-instrument';
const result = ;
result.; // instrumented source
const coverageMap = JSON.; // Istanbul format
result.; // source map JSON (if enabled)
CLI
# Print instrumented code to stdout
# Write to file
# Print coverage map only
# With source map
Vitest integration
import { defineConfig } from 'vitest/config'
import { createOxcInstrumenter } from 'oxc-coverage-instrument/vitest'
export default defineConfig({
test: {
coverage: {
provider: 'istanbul',
instrumenter: () => createOxcInstrumenter(),
}
}
})
Note: Requires Vitest with custom instrumenter support (see vitest#10119).
Vite plugin example
import from 'oxc-coverage-instrument';
export
Reading existing coverage data
use parse_coverage_map;
// Parse a coverage-final.json file
let json = read_to_string.unwrap;
let map = parse_coverage_map.unwrap;
for in &map
What it tracks
| Dimension | What gets a counter |
|---|---|
| Statements | Every executable statement |
| Functions | Declarations, expressions, arrows, class methods |
| Branches | if/else, ternary, switch, &&/||/??, ??=/||=/&&=, default-arg |
| Pragmas | istanbul/v8/c8 ignore next/if/else/file/start/stop |
Istanbul conformance
Verified against istanbul-lib-instrument on 27 shared fixtures covering all branch types, function forms, Unicode columns, pragma boundaries, and edge cases. 189 automated conformance checks validate:
- Function counts match exactly
- Branch counts, types, and location counts match exactly
- Statement counts match exactly
- JSON structure matches Istanbul's field set
- Instrumented output re-parses as valid JS
CI also runs a blocking byte-for-byte Istanbul diff over the shared fixture corpus after filtering documented intentional divergences. This catches span-level and counter-shape drift that count-only tests can miss.
Real-world verification: 1,061 TS/TSX/JS files from a production React monorepo produce byte-for-byte identical statement, function, and branch counts to istanbul-lib-instrument (when both instrumenters receive the same Babel-transpiled input).
Independently validated against the Vitest test suite: from v0.3.5 onward, coverage-final.json for the Vitest math.ts fixture is byte-for-byte identical to @vitest/coverage-istanbul's output — including statementMap, fnMap spans, branchMap, and all counter arrays.
Column conventions: all start.column / end.column values in statementMap, fnMap, branchMap, and unhandledPragmas are reported as UTF-16 code units (JavaScript string indices), matching Babel and istanbul-lib-instrument. Sources containing non-ASCII characters — π, accented identifiers, emoji — produce the same column numbers as the reference tool. Verified by the 26-non-ascii-identifiers.js conformance fixture (tests/conformance/fixtures/).
Differences from istanbul-lib-instrument
Intentional divergences from istanbul-lib-instrument:
1. ES2021 logical-assignment operators are instrumented as branches
x ??= y, x ||= y, and x &&= y each contain a genuine short-circuit conditional: the right-hand side is evaluated (and the assignment happens) only when the left operand matches the operator's polarity. oxc-coverage-instrument emits one binary-expr branch entry per logical-assignment with two locations (left = always reached, right = conditional). istanbul-lib-instrument has no AssignmentExpression visitor entry and emits zero branches for these operators.
Pinned by tests/conformance_test.rs::logical_assignment_is_intentional_branch_superset.
2. Inferred function names over (anonymous_N)
For anonymous function expressions assigned to a variable or declared as a class method, oxc-coverage-instrument uses the name the JavaScript runtime actually assigns to Function.prototype.name:
| Source | oxc fnMap[].name |
istanbul fnMap[].name |
|---|---|---|
const f = function() {} |
f |
(anonymous_0) |
const g = () => 1 |
g |
(anonymous_0) |
class C { bar() {} } |
bar |
(anonymous_0) |
(function() {})() (IIFE) |
(anonymous_0) |
(anonymous_0) |
Coverage reports and stack traces benefit from real names. Pinned by tests/conformance_test.rs::fn_name_inference_is_intentional_superset.
3. Full method-key spans in fnMap[*].decl
For class and object methods, oxc-coverage-instrument records the whole method key as the declaration span. istanbul-lib-instrument truncates method declarations to the key's first character.
| Source | oxc decl |
istanbul decl |
|---|---|---|
class C { bar() {} } |
bar |
b |
The byte-diff check still pins the method declaration start, line, body loc, and all non-method function declaration spans.
Migration from @vitest/coverage-istanbul: a codebase that uses ??=/||=/&&= heavily will see a higher branch-coverage denominator (and so a slightly lower branch %) after switching providers. To rebaseline CI thresholds after the swap:
This is additional coverage signal, not a regression. Every extra branch represents a real runtime decision path.
Performance
Benchmarked on real-world JavaScript libraries, all running in the same Node.js process for a fair comparison. Reproduce with ./scripts/benchmark-comparison.sh.
| File | Size | oxc (napi) | babel-plugin-istanbul | swc-plugin (wasm) | istanbul-lib |
|---|---|---|---|---|---|
| react.development.js | 107 KB | 1.7 ms | 18.1 ms | 25.5 ms | 51.1 ms |
| lodash.js | 531 KB | 6.7 ms | 56.4 ms | 83.5 ms | 164.7 ms |
| vue.global.js | 462 KB | 12.3 ms | 98.2 ms | 187.6 ms | 365.8 ms |
| d3.js | 573 KB | 22.4 ms | 173.5 ms | 271.1 ms | 576.1 ms |
| three.js | 1.2 MB | 29.1 ms | 270.2 ms | 378.2 ms | 875.7 ms |
8-11x faster than babel-plugin-istanbul, 13-15x faster than swc-plugin-coverage-instrument (Rust/WASM), 25-30x faster than istanbul-lib-instrument.
Note: swc-plugin-coverage-instrument is written in Rust but runs as a WASM module inside SWC's sandbox, adding serialisation overhead at every AST boundary. The comparison measures end-to-end instrumentation time as users experience it.
Architecture
source code (JS/TS)
|
v
oxc_parser -- parse to AST
|
v
SemanticBuilder -- build scope tree
|
v
CoverageTransform -- traverse AST, inject ++cov().s[N] counters
|
v
oxc_codegen -- emit instrumented code + source map
|
v
instrumented code + coverage map
Related projects
| Project | AST | Notes |
|---|---|---|
istanbul-lib-instrument |
Babel | The canonical Istanbul instrumenter |
babel-plugin-istanbul |
Babel | Babel plugin wrapper around istanbul-lib-instrument |
swc-plugin-coverage-instrument |
SWC | SWC WASM plugin |
| this crate | Oxc | Native Rust, 5-33x faster |
Compatibility
- Rust: 1.92+ (2024 edition)
- Oxc: 0.126.x
- Istanbul:
coverage-final.jsonv3+ format - Node.js: 18+ (via napi-rs)
License
MIT