Skip to content

finding: check-cross-package-test-inputs counts a template-literal argument as ONE descent, so it is LESS safe than an argument the scan cannot fold #11487

Description

@os-steve

Observed while implementing #11093 (PR #11486); measured on origin/main at 7f30b6be, not reasoned.

The claim the file makes

scripts/check-cross-package-test-inputs.mjs states, twice, that an argument it cannot read costs the NAME and keeps the DEPTH, because the escape verdict is a lower bound:

An argument this scan cannot read leaves the DEPTH walk where it was — deliberately, since the escape verdict is a lower bound and has always been computed this way — but the NAME is gone

and lists a template-literal path among the shapes that yield "NO name — never a wrong one".

What is measured

PATH_LITERAL is /^(['"])([^'"]*)\1$/. A BACKTICK-delimited argument whose content holds no quote characters matches it — which includes an interpolating template such as `${someVar}`. walkLiteral() then treats ${someVar} as an ordinary segment and walks it as one descent.

That is not a lower bound. It biases the walk upward, and it can hide an escape that the documented trade catches. Same climb, same file, only the unreadable middle argument differs:

FLAGGED unreadable argument (documented trade: depth unchanged) -> [{"name":"P","depth":-1}]
NOT SEEN template literal argument (counted as ONE descent) -> []

produced by

constHERE=dirname(fileURLToPath(import.meta.url));constP=join(HERE,someVar,'../../other-pkg/src/y.ts');// flagged, depth -1constP=join(HERE,`${someVar}`,'../../other-pkg/src/y.ts');// NOT flagged

both at hereDepth = 1, through the exported escapingBindings().

The NAME half is currently harmless in practice but for a reason one level away from the resolver: pathExpression() does hand back segments containing the literal text ${someVar}, and it is findEscapingPackages()'s statSync(...).isFile() filter that drops the resulting roster entry. So "never a wrong name" holds today by a downstream filesystem check, not by the resolver the sentence is written against.

Why this is a finding rather than a defect card

No test in the tree spells a path this way today, so nothing is currently mis-verdicted — this is a property of the detector, not an outage. It is filed because the shape is the exact inverse of the trade the file relies on everywhere else: an unreadable argument is safe, and a template literal reads as readable while being less safe than unreadable.

Two dispositions, for triage rather than a recommendation:

  1. Reject an interpolating template in PATH_LITERAL (a backtick literal containing ${), so it falls into the existing unreadable-argument branch — depth held, name dropped. Smallest change; makes the stated lower bound true.
  2. Resolve a template whose interpolations are all statically knowable bindings, and reject the rest. Larger, and widens the recognised set, which check-cross-package-test-inputs: a resolve() call whose literal sits on a following line yields no roster name — multi-line new URL() is recognised, multi-line resolve() is not #11093's own scope note argues against doing without a pull.

Either way it wants a --self-test case per the header's own rule.

Refs: #9763 (the reconstruction that added the RESOLVED half and wrote the lower-bound sentence) · #11093 / PR #11486 (the line-spanning spelling, same resolver).


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions