From 269c461e30fa9637be18ffbb194f9596416eb0b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 09:30:30 +0000 Subject: [PATCH] fix(devx): reject an interpolating template literal in check-cross-package-test-inputs' NEW_URL_LITERAL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- scripts/check-cross-package-test-inputs.mjs | 57 ++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/scripts/check-cross-package-test-inputs.mjs b/scripts/check-cross-package-test-inputs.mjs index ba9436c288..b7db1c4cad 100644 --- a/scripts/check-cross-package-test-inputs.mjs +++ b/scripts/check-cross-package-test-inputs.mjs @@ -578,6 +578,27 @@ function readablePathLiteral(arg) { return lit; } +/** + * `NEW_URL_LITERAL` has the identical character-class shape as `PATH_LITERAL` + * above, and the identical 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) `` would read `${someVar}` as the + * literal segment text and walk it as one ordinary descent (#12085). Unlike + * `readablePathLiteral()`'s call site, this one has no "cannot read, keep + * depth" fallback to route into — `pathExpression()` just returns `undefined` + * for the whole `new URL(...)` seed when this returns `null`, the same outcome + * as any other unrecognised seed shape. A single- or double-quoted literal is + * unaffected — `${` inside one of those is ordinary text, never interpolation. + * This is the ONE call site `NEW_URL_LITERAL` has in this file, so narrowing it + * here does not move any other consumer's verdict. + */ +function readableNewUrlLiteral(expr) { + const lit = expr.match(NEW_URL_LITERAL); + if (!lit) return null; + if (lit[1] === '`' && lit[2].includes('${')) return null; + return lit; +} + /** * A formatter's TRAILING COMMA, dropped from an argument list before it is read. * @@ -659,7 +680,7 @@ function pathExpression(expr, hereDepth, known, fileSegs = null) { // seeds above. This is the ASCENT-RELATIVE spelling of #9763: one string, but // it starts at `..`, so the flat literal regex below never saw it while the // walk here has always resolved it — the name was thrown away, not the path. - const url = expr.match(NEW_URL_LITERAL); + const url = readableNewUrlLiteral(expr); if (url) return walkLiteral(hereDepth, url[2], dirSegs); if (/^[A-Za-z_$][\w$]*$/.test(expr)) return known.get(expr); @@ -1991,6 +2012,40 @@ function selfTest() { return at(src, 1) && named(src, 1, CO).includes('packages/other-pkg/src/y.ts'); })(), ); + // ── the INTERPOLATING TEMPLATE argument, `NEW_URL_LITERAL` sibling (#12085) ─ + // + // `NEW_URL_LITERAL` has the identical character-class shape as `PATH_LITERAL` + // above and the identical blind spot — `` new URL(`${someVar}`, import.meta.url) `` + // reads `${someVar}` as the literal segment text and walks it as one ordinary + // descent. But this call site has no "cannot read, keep depth" fallback to + // fall into: `readableNewUrlLiteral()` rejecting the argument makes + // `pathExpression()` return `undefined` for the WHOLE `new URL(...)` seed — + // the same outcome as any other unrecognised seed shape, NOT the depth-kept + // outcome `PATH_LITERAL`'s pair above pins. So this case must assert + // "does not flag, no name" rather than "flags at the unreadable depth". + const URL_TEMPLATE_INTERP = 'const P = new URL(`../../other-pkg/${someVar}`, import.meta.url);'; + ok( + "(control) the same climb spelled with a real segment instead of interpolation still flags and is named — proves the case above isn't vacuous", + (() => { + const src = 'const P = new URL(`../../other-pkg/src/y.ts`, import.meta.url);'; + return at(src, 1) && named(src, 1, CO).includes('packages/other-pkg/src/y.ts'); + })(), + ); + ok( + 'an interpolating new URL() template does NOT flag — the whole seed is unrecognised, not depth-kept (#12085)', + !at(URL_TEMPLATE_INTERP, 1), + ); + ok( + 'and — like any unrecognised seed — yields no name at all (never a fabricated NAME)', + named(URL_TEMPLATE_INTERP, 1, CO).length === 0, + ); + ok( + 'a quoted (non-backtick) new URL() literal containing literal `${` text is unaffected — `${` outside a backtick is ordinary text, never interpolation', + (() => { + const src = "const P = new URL('../../other-pkg/${literalText}', import.meta.url);"; + return at(src, 1) && named(src, 1, CO).includes('packages/other-pkg/${literalText}'); + })(), + ); ok( 'a climb ABOVE the repo root yields no name (there is no repo-relative one)', named("const OUT = resolve(__dirname, '../../../../../../elsewhere/x.ts');", 1, CO).length === 0,