Skip to content

Commit 9799ffe

Browse files
yinlianghuiclaude
andauthored
fix(devx): reject an interpolating template literal in check-cross-package-test-inputs' NEW_URL_LITERAL (#12113)
`NEW_URL_LITERAL`'s character class is byte-identical to `PATH_LITERAL`'s (#11487/#12087) and shares the same blind spot: a backtick-delimited argument holding no quotes matches it even when it is an interpolating template, so `` new URL(`${someVar}`, import.meta.url) `` reads `${someVar}` as the literal segment text and `walkLiteral()` counts it as one ordinary descent — biasing the depth walk upward and, when the climb lands outside the package, adding a fabricated NAME to the roster. Unlike `PATH_LITERAL`'s call site, this one has no "cannot read, keep depth" fallback to route into: `NEW_URL_LITERAL` has exactly one call site, directly inside `pathExpression()`, with no enclosing loop. So the fix (a `readableNewUrlLiteral()` wrapper, mirroring `readablePathLiteral()`'s shape but scoped to this call site rather than sharing it) makes an interpolating match return `null`, which flows straight into `pathExpression()`'s existing "no call matched" path and returns `undefined` for the WHOLE `new URL(...)` seed -- the same outcome as any other unrecognised seed shape, not a depth-kept one. The self-test pins that outcome explicitly (does not flag, no name), plus a control proving a non-interpolating backtick `new URL()` literal is unaffected, and a control proving `${` inside a quoted (non-backtick) literal is ordinary text, never interpolation. Measured (Zone 2.3 of #12085): before this fix, an escaping interpolating `new URL()` seed CAN push a fabricated NAME onto the roster (confirmed via a temporary export of `scanPathExpressions()` and a fixture that climbs out of its package), but `findEscapingPackages()`'s downstream `statSync(...).isFile()` filter throws ENOENT on the fabricated literal and drops it -- the same safety net #11487's Zone 2.3 found for `PATH_LITERAL`. Today's blast radius was therefore smaller than the card's open question implied; this fix closes the gap at the source regardless. Both directions ablated: reverting the call-site wrapper alone (tests intact) turns exactly the two new discriminating self-test cases red and leaves the other 115 green, then the wrapper was restored and reverified at 117/117. Fixes #12085 Co-authored-by: Claude <noreply@anthropic.com>
1 parent d2cacbc commit 9799ffe

1 file changed

Lines changed: 56 additions & 1 deletion

File tree

scripts/check-cross-package-test-inputs.mjs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,27 @@ function readablePathLiteral(arg) {
578578
return lit;
579579
}
580580

581+
/**
582+
* `NEW_URL_LITERAL` has the identical character-class shape as `PATH_LITERAL`
583+
* above, and the identical blind spot: a BACKTICK-delimited argument holding no
584+
* quotes matches it even when it is an interpolating template, so
585+
* `` new URL(`${someVar}`, import.meta.url) `` would read `${someVar}` as the
586+
* literal segment text and walk it as one ordinary descent (#12085). Unlike
587+
* `readablePathLiteral()`'s call site, this one has no "cannot read, keep
588+
* depth" fallback to route into — `pathExpression()` just returns `undefined`
589+
* for the whole `new URL(...)` seed when this returns `null`, the same outcome
590+
* as any other unrecognised seed shape. A single- or double-quoted literal is
591+
* unaffected — `${` inside one of those is ordinary text, never interpolation.
592+
* This is the ONE call site `NEW_URL_LITERAL` has in this file, so narrowing it
593+
* here does not move any other consumer's verdict.
594+
*/
595+
function readableNewUrlLiteral(expr) {
596+
const lit = expr.match(NEW_URL_LITERAL);
597+
if (!lit) return null;
598+
if (lit[1] === '`' && lit[2].includes('${')) return null;
599+
return lit;
600+
}
601+
581602
/**
582603
* A formatter's TRAILING COMMA, dropped from an argument list before it is read.
583604
*
@@ -659,7 +680,7 @@ function pathExpression(expr, hereDepth, known, fileSegs = null) {
659680
// seeds above. This is the ASCENT-RELATIVE spelling of #9763: one string, but
660681
// it starts at `..`, so the flat literal regex below never saw it while the
661682
// walk here has always resolved it — the name was thrown away, not the path.
662-
const url = expr.match(NEW_URL_LITERAL);
683+
const url = readableNewUrlLiteral(expr);
663684
if (url) return walkLiteral(hereDepth, url[2], dirSegs);
664685

665686
if (/^[A-Za-z_$][\w$]*$/.test(expr)) return known.get(expr);
@@ -1991,6 +2012,40 @@ function selfTest() {
19912012
return at(src, 1) && named(src, 1, CO).includes('packages/other-pkg/src/y.ts');
19922013
})(),
19932014
);
2015+
// ── the INTERPOLATING TEMPLATE argument, `NEW_URL_LITERAL` sibling (#12085) ─
2016+
//
2017+
// `NEW_URL_LITERAL` has the identical character-class shape as `PATH_LITERAL`
2018+
// above and the identical blind spot — `` new URL(`${someVar}`, import.meta.url) ``
2019+
// reads `${someVar}` as the literal segment text and walks it as one ordinary
2020+
// descent. But this call site has no "cannot read, keep depth" fallback to
2021+
// fall into: `readableNewUrlLiteral()` rejecting the argument makes
2022+
// `pathExpression()` return `undefined` for the WHOLE `new URL(...)` seed —
2023+
// the same outcome as any other unrecognised seed shape, NOT the depth-kept
2024+
// outcome `PATH_LITERAL`'s pair above pins. So this case must assert
2025+
// "does not flag, no name" rather than "flags at the unreadable depth".
2026+
const URL_TEMPLATE_INTERP = 'const P = new URL(`../../other-pkg/${someVar}`, import.meta.url);';
2027+
ok(
2028+
"(control) the same climb spelled with a real segment instead of interpolation still flags and is named — proves the case above isn't vacuous",
2029+
(() => {
2030+
const src = 'const P = new URL(`../../other-pkg/src/y.ts`, import.meta.url);';
2031+
return at(src, 1) && named(src, 1, CO).includes('packages/other-pkg/src/y.ts');
2032+
})(),
2033+
);
2034+
ok(
2035+
'an interpolating new URL() template does NOT flag — the whole seed is unrecognised, not depth-kept (#12085)',
2036+
!at(URL_TEMPLATE_INTERP, 1),
2037+
);
2038+
ok(
2039+
'and — like any unrecognised seed — yields no name at all (never a fabricated NAME)',
2040+
named(URL_TEMPLATE_INTERP, 1, CO).length === 0,
2041+
);
2042+
ok(
2043+
'a quoted (non-backtick) new URL() literal containing literal `${` text is unaffected — `${` outside a backtick is ordinary text, never interpolation',
2044+
(() => {
2045+
const src = "const P = new URL('../../other-pkg/${literalText}', import.meta.url);";
2046+
return at(src, 1) && named(src, 1, CO).includes('packages/other-pkg/${literalText}');
2047+
})(),
2048+
);
19942049
ok(
19952050
'a climb ABOVE the repo root yields no name (there is no repo-relative one)',
19962051
named("const OUT = resolve(__dirname, '../../../../../../elsewhere/x.ts');", 1, CO).length === 0,

0 commit comments

Comments
 (0)