Skip to content

js-comment-mask.d.mts under-declares scanSource: the interpolation flag it returns, and one gate already reads, is missing from SourceFlags #15398

Description

@os-litant

What was measured

scripts/js-comment-mask.mjs is this tree's one code/prose separator. Its scanSource
returns three per-character flag arrays — its own JSDoc says so, and so does the code:

scripts/js-comment-mask.mjs
   * @returns {{ comment: Uint8Array, literal: Uint8Array, interpolation: Uint8Array }}
  return { comment, literal, interpolation };

The hand-written declaration beside it, scripts/js-comment-mask.d.mts, declares two:

export interface SourceFlags {
  comment: Uint8Array;
  literal: Uint8Array;
}
export function scanSource(source: string): SourceFlags;

interpolation is not a vestigial field. It is already load-bearing for a real consumer:

scripts/pm/dispatch-gates.mjs:3005   * interpolations, which are code — `scan.interpolation` exists for exactly
scripts/pm/dispatch-gates.mjs:3027       if (scan.literal[i] && !scan.interpolation[i]) continue;

Why this is worth a card

That consumer is a .mjs file, so no tsc program reads it and the gap costs nothing today.
It is the NEXT consumer that pays, and the shape of the payment is the specific failure the
whole module exists to prevent.

js-comment-mask.d.mts's own header states why it exists: a gate under
packages/spec/scripts/ imports the scanner, that directory is inside a tsc program, and an
untyped .mjs import is TS7016 — the scanner silently becomes any. So the declaration is
the surface every TYPE-CHECKED consumer sees. A TypeScript gate that needs to tell an
interpolation's code bytes from its template's literal bytes reads .interpolation, gets
TS2339 against SourceFlags, and reasonably concludes the shared scanner cannot answer its
question.

The next step after that conclusion is a private scanner — which is exactly what
check:comment-mask-adoption exists to red, and exactly the drift whose measured cost is
recorded in the module's own header (16 files disagreeing with a real parser, 15 of them in
the fabricating direction).

The same header says the declaration is maintained by hand: "Keep this file in step
with the module by hand, and keep it small enough that doing so stays trivial."
This is one
step out of step. Nothing mechanically holds the two in agreement, which is presumably how
it drifted and is the reason it can drift again.

Not proposed here, left to triage

Whether the fix is (a) adding the field to SourceFlags, (b) adding it and a check that
holds the declaration equal to the module's returned shape, or (c) declaring the omission
deliberate in the header (an intentionally narrowed public surface, with the .mjs
consumer named as the exception) is a real choice with different costs, and this card takes
no position. ⚠️ Option (c) is not obviously wrong: the header does say the two flag arrays
are "the load-bearing part of the surface, so they are typed precisely", which can be read
as a deliberate narrowing rather than an oversight. If it was deliberate, the header should
say so where the next reader will look, because today the JSDoc and the declaration simply
disagree.

Context — surfaced while implementing #13874, which imports scanSource from this
module for its literal flag. That card needed only literal and is unaffected; nothing in
it is blocked by this. ⛔ Deliberately not fixed there: it is a different file, a different
question, and #13874's ruling is strictly additive.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions