Skip to content

[finding] parseParamNames strips only ONE leading modifier, so private readonly code: string parses as the parameter named readonly #13227

Description

@os-elon

Found by measurement while producing the blast-radius census for #13131.

The shape

parseParamNames in scripts/check-dispatcher-error-vocabulary.mjs cleans a parameter before reading its name:

const cleaned = raw.replace(/^\s*(?:readonly|public|private|protected|\.\.\.)\s+/g, '').trim();
const m = /^([A-Za-z_$][\w$]*)/.exec(cleaned.replace(/^\.\.\./, ''));

The g flag reads as "strip them all", but the pattern is anchored with ^ and carries no m flag, so it matches at position 0 once. TypeScript parameter properties routinely carry two modifiers (private readonly, public readonly, protected readonly), and the second one survives the strip and is then read as the parameter's NAME.

Measured, through the real exported function

"private readonly src: string, private readonly opts: ParseOptions"  ->  ["readonly","readonly"]
"public readonly code: string, msg: string"                          ->  ["readonly","msg"]
"readonly code: string"                                              ->  ["code"]      <- control
"private code: string"                                               ->  ["code"]      <- control
"code: string, msg: string"                                          ->  ["code","msg"]  <- control

The three single-modifier controls parse correctly, so this is the second modifier and nothing else.

Why it matters

parseParamNames is the structural half of the codehelper shape: helperCodesFor does parseParamNames(decl.params).indexOf(ident). A wrong name means indexOf returns -1, the identifier is not recognised as a parameter, and the whole helper is dropped — no site, no unresolved, nothing reported. Same silent-drop failure class as #9223 / #9460 / #10918 / #13131, one layer further in.

End to end, through the real deriveSites, with an unregistered code:

class E {
  constructor(private readonly code: string, readonly message: string) {
    (this as any).code = code;
  }
}
throw new E('PARAM_PROP_CODE', 'x');
sites: []      unresolved: []

A constructor code helper written with a parameter property is invisible, and the new E(...) call-site scan helperCodesFor already implements would otherwise have read the literal straight out of it.

There is also a wrong-index hazard beyond the miss: in ["readonly","msg"] the real code parameter is gone but the arity is preserved, so if a caller's helper genuinely had a parameter named readonly the index would resolve to the wrong argument and report a value from another position. Not observed in this tree; noted because the failure would read as a normal finding rather than as a bug.

Live witness

packages/sdui-parser/src/parse.ts, class Parser's constructor:

constructor(private readonly src: string, private readonly opts: ParseOptions)
   ->  parseParamNames  ->  ["readonly","readonly"]

What a fix would have to decide

Mechanically the strip wants to loop (or the pattern wants (?:(?:readonly|public|private|protected)\s+)*), and ... needs to stay handled where it is. Not decided here:

  • whether --self-test should pin the parameter-property spelling as its own case (the published-shapes discipline in this file's header says a spelling that is reached for must be pinned in the same edit);
  • whether accessor modifiers this repo does not yet use (override, in/out on type params) belong in the same class.

Filed unassigned. Related: #13131 (the object-literal stamp position) and the class-method sibling finding — three distinct mechanisms, one failure class.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions