From 16e64d58ffbd831219205c4fcc00832023ef7984 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:18:04 +0800 Subject: [PATCH] fix(reference-imports): retain exact dynamic import targets Use the shared exact-literal reader for dynamic imports, preserving expression-free templates and transparent TypeScript wrappers while leaving computed targets unknown. Fixes #6654 --- src/domain/referenceSourceImportParsing.ts | 12 +++++++++++- .../filesystem/referenceSourceDynamicImports.test.ts | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/domain/referenceSourceImportParsing.ts b/src/domain/referenceSourceImportParsing.ts index f2c7f2692a..ac7d90c622 100644 --- a/src/domain/referenceSourceImportParsing.ts +++ b/src/domain/referenceSourceImportParsing.ts @@ -72,6 +72,16 @@ const safeModuleName = (node: Node | null | undefined): string | undefined => { return undefined; }; +const exactStringModuleName = ( + node: Node | null | undefined, +): string | undefined => { + if (node === null || node === undefined) return undefined; + const literal = readExactJavaScriptLiteral(node); + return literal.found && typeof literal.value === "string" + ? literal.value + : undefined; +}; + const resolveSpecifier = ( specifier: string, ): ReferenceSourceImportResolution => { @@ -248,7 +258,7 @@ const extractRequireAndDynamicImports = ( for (const call of expressions) { if (isImportExpression(call)) { - const specifier = safeModuleName(call.source); + const specifier = exactStringModuleName(call.source); appendRelationship(relationships, limitations, { fromPath: from_path, to: specifier ?? "", diff --git a/tests/boundary/filesystem/referenceSourceDynamicImports.test.ts b/tests/boundary/filesystem/referenceSourceDynamicImports.test.ts index b826e1dbae..4ed88bb911 100644 --- a/tests/boundary/filesystem/referenceSourceDynamicImports.test.ts +++ b/tests/boundary/filesystem/referenceSourceDynamicImports.test.ts @@ -15,6 +15,8 @@ it("resolves dynamic source imports while retaining computed targets as unknown" 'import "./static";', 'require("./common");', 'const lazy = import("./lazy");', + "const templated = import(`./template-lazy.mjs`);", + 'const wrapped = import("./wrapped-lazy.mjs" as const);', 'async function load() { return await import("./data.json", { with: { type: "json" } }); }', 'const name = "./not-guessed.js"; import(name);', "import(`./${name}.js`);", @@ -24,6 +26,9 @@ it("resolves dynamic source imports while retaining computed targets as unknown" ...["static.ts", "common.js", "lazy.ts"].map((path) => writeFile(join(root, path), "export {};\n"), ), + ...["template-lazy.mjs", "wrapped-lazy.mjs"].map((path) => + writeFile(join(root, path), "export {};\n"), + ), writeFile(join(root, "data.json"), '{"value":1}\n'), ]); @@ -34,11 +39,13 @@ it("resolves dynamic source imports while retaining computed targets as unknown" }); if (!result.ok) throw result.error; expect(result.value.parse_failures).toEqual([]); - expect(result.value.relationships).toHaveLength(6); + expect(result.value.relationships).toHaveLength(8); for (const [to, kind] of [ ["static.ts", "imports"], ["common.js", "requires"], ["lazy.ts", "imports"], + ["template-lazy.mjs", "imports"], + ["wrapped-lazy.mjs", "imports"], ["data.json", "imports"], ]) expect(result.value.relationships).toContainEqual({