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:
parent
20b802f165
commit
ed24604ee1
|
|
@ -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(`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue