diff --git a/scripts/lib/guard-inventory-utils.mjs b/scripts/lib/guard-inventory-utils.mjs index d11f3dbaaa0c..11e03b9a9874 100644 --- a/scripts/lib/guard-inventory-utils.mjs +++ b/scripts/lib/guard-inventory-utils.mjs @@ -103,18 +103,14 @@ export function writeLine(stream, text) { } function hasSupplementalModuleReference(ts, source, acceptSpecifier) { - const checkUrl = source.includes("new") && source.includes("import") && source.includes("meta"); - const interpolationStart = source.indexOf("${"); - const checkRequire = - interpolationStart >= 0 && - (source.includes("require") || source.indexOf("\\u", interpolationStart + 2) >= 0); - const interpolatedSlash = - interpolationStart < 0 ? -1 : source.indexOf("/", interpolationStart + 2); - const checkTemplateImport = - interpolatedSlash >= 0 && source.indexOf("import", interpolatedSlash + 1) >= 0; - const checkNamespaceExport = - source.includes("export") && source.includes("*") && source.includes("as"); - if (!checkUrl && !checkRequire && !checkTemplateImport && !checkNamespaceExport) { + // Escaped names and regexp ambiguity must reach the scanner before rejecting the file. + if ( + !source.includes("new") && + !source.includes("${") && + !source.includes("export") && + !source.includes("/") && + !source.includes("\\u") + ) { return false; } @@ -136,7 +132,6 @@ function hasSupplementalModuleReference(ts, source, acceptSpecifier) { for (let token = scanner.scan(); token !== kinds.EndOfFileToken; token = scanner.scan()) { if ( - checkUrl && token === kinds.NewKeyword && scanner.lookAhead( () => @@ -149,7 +144,6 @@ function hasSupplementalModuleReference(ts, source, acceptSpecifier) { return true; } if ( - checkRequire && (token === kinds.RequireKeyword || (token === kinds.Identifier && scanner.getTokenValue() === "require")) && scanner.lookAhead(() => scanner.scan() === kinds.OpenParenToken && scanAcceptedSpecifier()) @@ -157,7 +151,6 @@ function hasSupplementalModuleReference(ts, source, acceptSpecifier) { return true; } if ( - checkNamespaceExport && token === kinds.ExportKeyword && scanner.lookAhead(() => { let next = scanner.scan(); @@ -170,11 +163,9 @@ function hasSupplementalModuleReference(ts, source, acceptSpecifier) { return true; } - // A context-free slash may start a regexp whose `}` would corrupt interpolation tracking. - if ( - templateBraceDepths.length > 0 && - (token === kinds.SlashToken || token === kinds.SlashEqualsToken) - ) { + // Only the parser distinguishes division from regexps, whose quotes or braces + // can hide later module references from a context-free scanner. + if (token === kinds.SlashToken || token === kinds.SlashEqualsToken) { return true; } diff --git a/test/architecture-smells.test.ts b/test/architecture-smells.test.ts index 46c8ed2c60a4..60c061f75713 100644 --- a/test/architecture-smells.test.ts +++ b/test/architecture-smells.test.ts @@ -173,6 +173,8 @@ describe("architecture boundary module reference scanner", () => { }); it.each([ + 'const pattern = /"/; import "../../src/allowed.js";', + 'new URL("../../src/allowed.js", import.m\\u0065ta.url)', 'const message = `${(24 / 2) ? require("../../src/allowed.js") : null}`', 'const message = `${/}/.test("safe") ? require("../../src/allowed.js") : null}`', 'const message = `${\\u0072equire("../../src/allowed.js")}`', @@ -193,6 +195,16 @@ describe("architecture boundary module reference scanner", () => { ).toEqual([{ kind: "import", line: 3, specifier: forbiddenPath }]); }); + it("sorts references on the same line by kind and specifier", () => { + expect( + collectModuleReferencesFromSource('import "./z.js"; require("./z.js"); import "./a.js";'), + ).toEqual([ + { kind: "commonjs-require", line: 1, specifier: "./z.js" }, + { kind: "import", line: 1, specifier: "./a.js" }, + { kind: "import", line: 1, specifier: "./z.js" }, + ]); + }); + it.each([ { name: "import.meta URL", diff --git a/test/scripts/extension-import-boundary-checker.test.ts b/test/scripts/extension-import-boundary-checker.test.ts index 8b78a8152c97..935975721699 100644 --- a/test/scripts/extension-import-boundary-checker.test.ts +++ b/test/scripts/extension-import-boundary-checker.test.ts @@ -95,12 +95,30 @@ describe("extension import boundary checker", () => { createSource: (specifier: string) => "require(" + JSON.stringify(specifier) + ")", kind: "commonjs-require", }, + { + name: "import after a quote-containing regexp", + createSource: (specifier: string) => + 'const pattern = /"/; import ' + JSON.stringify(specifier), + kind: "import", + }, + { + name: "CommonJS require after a quote-containing regexp", + createSource: (specifier: string) => + 'const pattern = /"/; require(' + JSON.stringify(specifier) + ")", + kind: "commonjs-require", + }, { name: "import.meta URL", createSource: (specifier: string) => "new URL(" + JSON.stringify(specifier) + ", import.meta.url)", kind: "import-meta-url", }, + { + name: "import.meta URL with an escaped meta property", + createSource: (specifier: string) => + "new URL(" + JSON.stringify(specifier) + ", import.m\\u0065ta.url)", + kind: "import-meta-url", + }, { name: "comment-obscured namespace export", createSource: (specifier: string) =>