Documentation content

Coverage Reports

scriptc coverage tells you how much of a program compiles before you build it. Use it when you bring an existing Node.js program to scriptc, add a dependency, or want CI to catch unsupported code early. It analyzes the program without producing an executable, counts the statements that compile, and groups anything unsupported by diagnostic code with a location and a hint.

Run a report

Pass the program's entry file:

$ cat hello.ts
const who: string = process.argv.length > 2 ? process.argv[2] : "world";
console.log(`hello, ${who}`);

$ scriptc coverage hello.ts

  statements analyzed   2
  compile statically    2  (100%)

  fully static — all analyzed statements compile to native code.

Every statement in this program compiles, so scriptc build will succeed. The counts describe this program only, not JavaScript or Node.js support in general.

Reading the report

When a program has unsupported operations, the report adds a group for each root cause. A group shows the diagnostic code, the reason, the first few sites in source order, and a hint. A generic declaration that fails for several type arguments appears once.

FieldMeaning
statements analyzedExecutable statements in application code, excluding dependency implementation code.
compile staticallyStatements that compile to native instructions.
in dynamic tierOf those, the statements in functions that operate on values typed only at runtime, such as any values and untyped JavaScript. Shown when there are any. See Dynamic tier.
blockersUnsupported operations with counts, diagnostic codes, reasons, locations, and hints.
runs at startupDeferred operations in a JavaScript module's top-level code, which runs when the module loads: at startup for a module the program imports.
deferred to runtimeOther deferred operations in JavaScript code. Each throws only if it runs.
in unreached codeBlockers in code the entry never reaches. A build never compiles that code, so these cannot fail it.
differs from NodeSites that compile but can behave differently from Node.js. See Node divergences.
npm package compilationNative package compilation outcomes and refusal reasons.

Blockers in unreached code and deferred operations in JavaScript packages are listed separately because they don't fail a build. A deferred operation throws a runtime error if it runs, so a successful build doesn't mean every API of a package works; see unsupported code inside a package.

If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. The report still lists the diagnostics of those statements; a use of a parameter whose type can't compile reports SC2004 back to the signature.

Dynamic tier

Code that operates on values whose types are known only at runtime, such as any values, the result of an untyped JSON.parse, or untyped JavaScript, compiles into dynamic-tier functions. They are native code too, but they check types and look up properties at runtime, so they are slower than code with static types. The report's in dynamic tier line counts their statements.

--explain-dynamic lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as --explain-dynamic=Point, to show only the matching units. With --print=diagnostics, the document gains a tiers object with the same information.

--tier=static-only keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today auto, the default, behaves the same way.

Untyped code that likely runs often

When functions of the program's own files use untyped values and likely run often, scriptc build and coverage report them as an SC6101 warning. For the Benchmarks Game n-body program in plain JavaScript:

$ scriptc build nbody.js -o nbody
warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:
  advance (nbody.js:22): untyped JavaScript file, in a loop
  energy (nbody.js:40): untyped JavaScript file, in a loop
  offsetMomentum (nbody.js:12): untyped JavaScript file, in a loop
  hint: add types to compile these statically; see scriptc coverage --explain-dynamic

The program compiled. Adding types to these functions lets scriptc compile them to faster code.

Without a profile, a dynamic-tier function counts as likely to run often when a dynamic operation runs on every iteration of one of its loops (a for loop's initializer and a for...of loop's iterable run once), when a loop calls it, or when it calls itself. A function is named only when its source says why its values are untyped: a value typed any (for a for...of variable, the value it iterates), an untyped JSON.parse result, or an untyped JavaScript file. Dynamic code that runs once, such as setup code that reads an any configuration, is not reported, and neither are functions that are dynamic for an operation with no static lowering, which more types would not change.

The warning names at most five functions, most likely to run often first, and gives the total. A function expression is named by its own name, else by the variable, property or class field it is assigned to, else as anonymous function. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. --no-dynamic-tier-warning leaves it out, and --fail-on=dynamic fails a coverage run on it.

Dynamic slots

A declared type that has no native representation does not fail the build. The field, parameter, return value, variable or expression that holds it becomes a checked dynamic slot: the parts of the type that compile keep their native layout, and the failing parts hold checked-dynamic values. Coverage lists the slots under the statement counts, and scriptc build prints one SC6104 warning per slot.

$ cat plugins.ts
type Hook = (() => void) | AbortSignal | Map<string, (x: number) => string>;

interface Plugin {
  name: string;
  hook: Hook;
}

const plugins: Plugin[] = [{ name: "log", hook: () => console.log("hook ran") }];
for (const p of plugins) {
  const hook = p.hook;
  if (typeof hook === "function") hook();
  console.log(p.name);
}

$ scriptc coverage plugins.ts

  statements analyzed   6
  compile statically    6  (100%)
    in dynamic tier     1  (operate on values typed only at runtime; --explain-dynamic lists them)
    dynamic slots       1  (declared types with no static lowering, held as checked-dynamic values; SC6104)
      module global 'plugins' (declared 'Plugin[]') (dynamic pieces)  plugins.ts:8:7

  fully static — all analyzed statements compile to native code.

Here Plugin keeps its native name field, and only hook is checked-dynamic. Reading a dynamic value into typed code checks it against the type that reads it, so data that contradicts the declaration still throws a TypeError. Slots with the most dynamic operations are listed first. --fail-on=dynamic-slots fails a coverage run when any slot exists, and --tier=static-only reports the original errors instead.

Diagnostic codes

Coverage and scriptc build report the same diagnostic codes. Common ones include:

  • SC2020: a declared standard-library or Node.js API has no supported compiler implementation.
  • SC2012: this API form cannot compile natively.
  • SC2013: a package cannot compile natively; inspect the package-specific reason and the dependency guide.
  • SC1100: a value whose type is only known at runtime, such as unknown, is used where scriptc needs a specific type. Narrow or cast the value as the diagnostic suggests.

Node divergences

Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as SC6xxx warnings, and scriptc build prints the same warnings after a successful build. They never fail a build, but --fail-on=divergences can fail a CI job on them.

  • SC6001: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity.
  • SC6002: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through JSON.stringify, Object.keys, Object.values, Object.entries, for...in, or console output.
  • SC6003: localeCompare compares a string literal outside the built-in Latin-script collation.
  • SC6004: a call passes a class instance to a function that casts it to an unrelated class.

Gating builds and CI

By default, scriptc coverage exits with status 0 whenever the program type-checks, so you can use the report to track progress. To fail a CI job instead, add --fail-on:

$ scriptc coverage src/main.ts --fail-on=blockers

--fail-on=blockers exits with status 1 when the program has blockers that would fail scriptc build. Blockers in unreached code and deferred operations don't count. --fail-on=divergences also fails on Node divergences, --fail-on=dynamic also fails on the SC6101 warning for untyped code that likely runs often, and --fail-on=dynamic-slots also fails on dynamic slots. A program that doesn't type-check always exits with status 1.

Coverage caches its result. Running it again on an unchanged program, or after edits that only touch comments, reuses the previous verdict. Set SCRIPTC_NO_CACHE=1 to turn the cache off.

Machine-readable diagnostics

For editors, CI annotations, and other tools, scriptc coverage --print=diagnostics and scriptc build --print=diagnostics write one JSON document to stdout instead of the human-readable report. The exit status is unchanged. The document looks like this:

{
  "schema": "scriptc-diagnostics",
  "schemaVersion": 1,
  "compilerVersion": "0.3.0",
  "command": "coverage",
  "entry": "/work/app/src/main.ts",
  "success": false,
  "phase": "compile",
  "stats": {
    "statementsTotal": 13,
    "statementsStatic": 12,
    "statementsDynamic": 0,
    "statementsFailed": 1,
    "functionsSkipped": 0,
    "percentStatic": 92
  },
  "groups": [
    {
      "id": "SC1031-c2cc8425",
      "code": "SC1031",
      "category": "unsupported",
      "severity": "error",
      "scope": "reached",
      "message": "object destructuring of non-record values (the source is array-typed)",
      "hint": "...",
      "count": 1,
      "primary": { "file": "/work/app/src/main.ts", "line": 13, "column": 7 },
      "sites": [{ "file": "/work/app/src/main.ts", "line": 13, "column": 7 }]
    }
  ],
  "diagnostics": [
    {
      "code": "SC1031",
      "category": "unsupported",
      "severity": "error",
      "scope": "reached",
      "file": "/work/app/src/main.ts",
      "line": 13,
      "column": 7,
      "endLine": 13,
      "endColumn": 28,
      "start": 352,
      "end": 373,
      "message": "object destructuring of non-record values (the source is array-typed) are not supported yet",
      "hint": "...",
      "group": "SC1031-c2cc8425"
    }
  ]
}
FieldMeaning
schemaVersionIncremented when a field is renamed or removed or its meaning changes. New optional fields, categories, and codes keep the version.
successCoverage: no blockers on the entry path (and, with --fail-on=divergences, no divergences). Build: an artifact was produced, named by artifact.
phasepreflight when type checking, configuration, or the module graph stopped the command; compile when blockers remain; native when native code generation or linking failed; otherwise complete.
statsCoverage only, absent after a preflight failure: the report's statement counts. statementsStatic counts every statement that compiles to native code; statementsDynamic is the part of those in dynamic-tier functions.
categoryinvalid-source (the program has a TypeScript or JSON error), unsupported (valid code scriptc cannot compile yet), divergence (compiles, but can behave differently from Node.js), performance (compiles and behaves as under Node.js, but runs slower than typed code would), environment (project configuration, scriptc's type world, toolchain, or target), or internal (a compiler bug).
severityerror, or warning for divergences.
scopepreflight, reached (fails a build), unreached (code the entry never reaches), or deferred (a JavaScript statement that compiles to a throw of its diagnostic, which happens only if it executes).
line, column1-based line and UTF-16 column; start and end are 0-based UTF-16 offsets.
groupThe id of the root-cause group the diagnostic belongs to. A group's primary site is its first in source order.
startuptrue on a deferred diagnostic in a module's top-level code, which runs when the module loads. Absent otherwise.

Type errors

Coverage needs a program that type-checks. If type checking fails, the report lists the type errors instead of statement counts; fix them first.

scriptc type-checks with its own settings: strict null checks, the ES2025 library, and Node.js types (its bundled declarations when the project has no @types/node). An error those settings cause, rather than the project's own tsc configuration, carries a hint naming the cause, such as a library member newer than ES2025 or a global of another runtime, and machine-readable output reports it in the environment category.

External host type declarations

When your code will run inside a host application that provides its own modules, those modules aren't in node_modules. Use --external-types to give coverage their declarations so it can analyze the rest of your code:

$ scriptc coverage src/core.ts --external-types @native-sdk/core=types/native-sdk-core.d.ts

The option is repeatable. Relative declaration paths resolve from the current working directory. Supported declaration extensions are .d.ts, .d.mts, and .d.cts. Relative declaration imports and re-exports are included, so the mapped file can be an index.d.ts barrel. Project-owned code using these types participates in coverage; type-only imports add no runtime boundary.

The mapping supplies types for analysis, not a runtime implementation. Value imports and uses remain SC1010 external-host blockers while analysis counts the rest of the application. This option is available only for coverage; builds require a module implementation or runtime integration.