fix(tooling): detect imports following regular expressions (#137804)

This commit is contained in:
Peter Steinberger 2026-09-04 04:28:06 -07:00 • committed by GitHub
parent 06c7cec68a
commit daa5cba0cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 41 additions and 20 deletions

View file

@ -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;
}

View file

@ -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",

View file

@ -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) =>