diff --git a/scripts/verify-dep-lockstep.mjs b/scripts/verify-dep-lockstep.mjs index 487a927c..0eb8a9dd 100644 --- a/scripts/verify-dep-lockstep.mjs +++ b/scripts/verify-dep-lockstep.mjs @@ -101,12 +101,25 @@ export function packageNameOf(specifier) { return name; } -// `from "x"` (import and re-export), a side-effect `import "x"`, and a dynamic -// `import("x")`. Only these three forms introduce a dependency's *types*. +// Every form that can introduce a dependency's *types* (Copilot, #1962). +// `import x = require("pkg")` and a bare `require("pkg")` matter because the +// shared trees may hold `.cts` sources, where that is the ordinary import +// syntax; and neither call form requires the closing paren, so an import +// attributes argument (`import("pkg", { with: … })`) can't hide the specifier. +// +// Backticks are accepted ONLY in the call forms, where a static template +// literal is legal. A `from` clause requires a string literal, so allowing +// backticks there would buy nothing while reopening the prose hazard the +// anchored specifier pattern exists to close — inline code in a comment is +// written with backticks throughout this codebase, and "…derived from +// `tools`…" would otherwise enter the candidate set as a package named +// `tools`. Inert today, but a prose word that collides with a real package +// name would silently widen the set. const SPECIFIER_FORMS = [ - /\bfrom\s*["']([^"']+)["']/g, - /\bimport\s+["']([^"']+)["']/g, - /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g, + /\bfrom\s*["']([^"']+)["']/g, // import … from "x" / export … from "x" + /\bimport\s+["']([^"']+)["']/g, // side-effect import "x" + /\bimport\s*\(\s*["'`]([^"'`]+)["'`]/g, // dynamic import("x"[, opts]) + /\brequire\s*\(\s*["'`]([^"'`]+)["'`]/g, // import x = require("x"), require("x") ]; /** diff --git a/scripts/verify-dep-lockstep.test.mjs b/scripts/verify-dep-lockstep.test.mjs index 640291fb..890d90b5 100644 --- a/scripts/verify-dep-lockstep.test.mjs +++ b/scripts/verify-dep-lockstep.test.mjs @@ -79,6 +79,26 @@ test("packageNameOf: non-packages are rejected", () => { assert.equal(packageNameOf(undefined), null); }); +test("importedPackageNames: CommonJS and awkward dynamic-import forms (Copilot, #1962)", () => { + // Under-approximating is the dangerous direction: a package the scan misses + // never enters the candidate set, so its skew passes the guard silently. + // `.cts` sources in the shared trees use `import x = require(…)` as ordinary + // syntax, and a dynamic import may carry import attributes or a static + // template literal — none of which the original three patterns matched. + const source = ` + import express = require("express"); + const yaml = require("yaml"); + const a = await import("undici", { with: { type: "json" } }); + const b = await import(\`jose\`); + `; + assert.deepEqual([...importedPackageNames(source)].sort(), [ + "express", + "jose", + "undici", + "yaml", + ]); +}); + test("importedPackageNames: the three specifier forms that introduce types", () => { const source = ` import { z } from "zod/v4"; @@ -109,6 +129,19 @@ test("importedPackageNames: prose after `from` is not an import", () => { assert.deepEqual([...importedPackageNames(source)], ["zod"]); }); +test("importedPackageNames: a backticked `from` in prose is not an import", () => { + // Backticks are legal in the *call* forms (a static template literal) but + // never after `from`, which requires a string literal. Accepting them there + // would sweep in inline code from comments — this codebase writes it with + // backticks throughout — and a prose word colliding with a real package name + // would silently widen the candidate set. + const source = ` + /** The excluded set derived from \\\`tools\\\`, keyed off \\\`express\\\`-style paths. */ + import { z } from "zod"; + `; + assert.deepEqual([...importedPackageNames(source)], ["zod"]); +}); + test("topLevelLockVersions: nested duplicates are ignored", () => { // A nested `node_modules/a/node_modules/b` is npm resolving a transitive // conflict *inside* one install — routine, and not the cross-install skew