fix(javascript): preserve empty ESM import names in provenance (#2797)

Co-authored-by: LouisChen <3361008+chqcjq@users.noreply.github.com>
This commit is contained in:
LouisChen 2026-10-14 03:12:04 +08:00 committed by GitHub
parent 20b802f165
commit ed24604ee1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 110 additions and 2 deletions

View File

@ -10,6 +10,45 @@ import {
origin,
} from "./javascriptSemanticAnalysis.fixture.js";
describe("JavaScript semantic analysis: ESM import origins", () => {
it.each(["", "a", "0"])(
"preserves exact ESM import name %j in binding origins",
(name) => {
const ir = analyzeJavaScriptSemantics(
"import { " +
JSON.stringify(name) +
' as selected } from "./module.mjs"; const alias = selected; export { alias };',
);
for (const binding of ["selected", "alias"])
expect(origin(topLevelBinding(ir, binding))).toEqual({
specifier: "./module.mjs",
importedPath: [name],
});
expect(ir.moduleLinks).toContainEqual(
expect.objectContaining({
kind: "import",
importedName: name,
localName: "selected",
}),
);
},
);
it("keeps default and namespace import origins distinct from an empty named import", () => {
const ir = analyzeJavaScriptSemantics(
'import value from "./module.mjs"; import * as namespace from "./module.mjs";',
);
expect(origin(topLevelBinding(ir, "value"))).toEqual({
specifier: "./module.mjs",
importedPath: ["default"],
});
expect(origin(topLevelBinding(ir, "namespace"))).toEqual({
specifier: "./module.mjs",
importedPath: [],
});
});
});
describe("JavaScript semantic analysis: structure 1", () => {
it("resolves imports, require destructuring, aliases, assignments, and shadowing", () => {
const ir = analyzeJavaScriptSemantics(`

View File

@ -57,7 +57,6 @@ import {
ELECTRON_MODULE,
} from "./javascriptElectronMemberWrites.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { propertyName } from "./javascriptAstValues.js";
import { semanticCoverage } from "./javascriptSemanticCoverage.js";
import { semanticResourceLimitsIn } from "./javascriptSemanticResourceLimits.js";
import {
@ -1109,7 +1108,7 @@ const bindImports = (
? ["default"]
: t.isImportNamespaceSpecifier(specifier)
? []
: [propertyName(specifier.imported) || "[dynamic]"];
: [semanticStaticPropertyKey(specifier.imported, false) ?? "[dynamic]"];
addBinding({
state,
scope,

View File

@ -0,0 +1,70 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"preserves empty ESM import names on local re-export origins through %s",
async (surface, { cli, processes }) => {
const root = await createTestTempDirectory("rea-empty-import-name-");
const modulePath = join(root, "module.mjs");
const appPath = join(root, "app.mjs");
await writeFile(
modulePath,
'const value = 1; export { value as "", value as "a", value as "0" };',
);
await writeFile(
appPath,
[
'import { "" as empty, "a" as letter, "0" as zero } from "./module.mjs";',
"export { empty, letter, zero };",
'export { "" as direct } from "./module.mjs";',
].join("\n"),
);
for (const path of [modulePath, appPath]) {
const checked = await processes.run(process.execPath, ["--check", path]);
expect(checked.exitCode, checked.stderr).toBe(0);
}
const result = await analyzeJavaScriptDirectory(surface, cli, root);
expect(result.statistics.parse_failures).toBe(0);
const imports = result.graph.edges.filter(
({ relation, properties }) =>
relation === "imports" && properties.specifier === "./module.mjs",
);
for (const [name, local] of [
["", "empty"],
["a", "letter"],
["0", "zero"],
]) {
for (const kind of ["import", "export"]) {
expect(imports).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
module_link_kind: kind,
...(kind === "export"
? { exported_name: local }
: { local_name: local }),
imported_name: name,
imported_path: [name],
resolved_path: "module.mjs",
resolution_status: "resolved",
}),
}),
);
}
}
expect(imports).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
exported_name: "direct",
imported_name: "",
imported_path: [""],
resolution_status: "resolved",
}),
}),
);
},
);