mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(javascript): match unchanged module exports across application versions (#5196)
* fix(javascript): match unchanged module exports across application versions An export binding's identity is keyed within its whole application root (container_sha256), so it changed whenever any sibling file changed, and no match tier paired export nodes: every export of an unchanged file was reported removed and added, and its module fell to unknown. The exact module-source-digest tier now also pairs an export binding by its own file's bytes (identity artifact_sha256) and exported name, as it already pairs a source module by source_sha256. Exports of edited files stay unmatched. * style(javascript): format the version-matching export test --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
26573c3aa4
commit
5d3ae66e63
2 files changed
+109
No files matched your search
@@ -200,6 +200,18 @@ const contentDigestKey = (node: ApplicationNode): string | null =>
|
||||
|
||||
const moduleSourceDigestKey = (node: ApplicationNode): string | null => {
|
||||
if (node.kind !== "javascript-module") return null;
|
||||
// An export binding is identified within its whole application root, so its
|
||||
// id changes with any sibling file. Its own file's bytes and exported name
|
||||
// identify it across versions, as `source_sha256` does a source module.
|
||||
if (
|
||||
node.identity.strategy === "artifact-local-key" &&
|
||||
node.identity.namespace === "module-export"
|
||||
) {
|
||||
const name = uniqueStringProperty(node, "exported_name");
|
||||
return name === null
|
||||
? null
|
||||
: `module-export\0${node.identity.artifact_sha256}\0${name}`;
|
||||
}
|
||||
const value = uniqueStringProperty(node, "source_sha256");
|
||||
return value === null ? null : `module-source\0${node.kind}\0${value}`;
|
||||
};
|
||||
|
||||
@@ -454,3 +454,100 @@ cliTest(
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"matches unchanged module exports after a sibling file changes",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-export-version-");
|
||||
const util = {
|
||||
v1: "module.exports = { greet: () => 1 };\n",
|
||||
v2: "module.exports = { greet: () => 2 };\n",
|
||||
};
|
||||
const analyze = async (
|
||||
name: string,
|
||||
version: string,
|
||||
utilSource: string,
|
||||
) => {
|
||||
const application = join(root, name);
|
||||
await mkdir(join(application, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
join(application, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "export-fixture",
|
||||
version,
|
||||
main: "src/main.js",
|
||||
}),
|
||||
);
|
||||
await writeFile(
|
||||
join(application, "src/main.js"),
|
||||
'const util = require("./util.js");\nmodule.exports = { start: () => util.greet() };\n',
|
||||
);
|
||||
await writeFile(join(application, "src/util.js"), utilSource);
|
||||
const response = await cli.run({
|
||||
arguments: ["analyze-javascript-application", application, "--json"],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
return parseEvidence(response.json);
|
||||
};
|
||||
const compare = async (left: unknown, right: unknown) => {
|
||||
const inputPath = join(root, `comparison-${Math.random()}.json`);
|
||||
await writeFile(inputPath, JSON.stringify({ left, right }));
|
||||
const response = await cli.run({
|
||||
arguments: ["compare-application-versions", inputPath, "--json"],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
return applicationVersionComparisonResultSchema.parse(
|
||||
parseEvidence(response.json).normalized_result,
|
||||
);
|
||||
};
|
||||
const exportsOf = (evidence: { normalized_result: unknown }) =>
|
||||
javascriptApplicationAnalysisResultSchema
|
||||
.parse(evidence.normalized_result)
|
||||
.graph.nodes.filter(({ observations }) =>
|
||||
observations.some(
|
||||
({ properties }) => properties.semantic_role === "export-binding",
|
||||
),
|
||||
);
|
||||
const base = await analyze("base", "1.0.0", util.v1);
|
||||
// Only package.json differs: both JavaScript files are byte-identical.
|
||||
const bumped = await analyze("bumped", "1.0.1", util.v1);
|
||||
const edited = await analyze("edited", "1.0.1", util.v2);
|
||||
const baseExports = exportsOf(base);
|
||||
expect(baseExports.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const unchanged = await compare(base, bumped);
|
||||
for (const node of baseExports)
|
||||
expect(unchanged.items).toContainEqual(
|
||||
expect.objectContaining({
|
||||
left_node_id: node.node_id,
|
||||
status: "unchanged",
|
||||
match: expect.objectContaining({
|
||||
basis: "exact-module-source-digest",
|
||||
confidence: "exact",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
unchanged.items.filter(
|
||||
({ node_kind, status }) =>
|
||||
node_kind === "javascript-module" && status !== "unchanged",
|
||||
),
|
||||
).toEqual([]);
|
||||
|
||||
// An edited file's export is not paired by name alone.
|
||||
const changed = await compare(base, edited);
|
||||
const utilExport = baseExports.find(({ observations }) =>
|
||||
observations.some(
|
||||
({ properties }) => properties.module_path === "src/util.js",
|
||||
),
|
||||
);
|
||||
expect(changed.items).toContainEqual(
|
||||
expect.objectContaining({
|
||||
left_node_id: utilExport?.node_id,
|
||||
status: "removed",
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user