Files
rea/tests/boundary/cli/javascriptBundlerSelfReferences.test.ts
Kaoru0822andClaude Opus 5.5 b620e2a2bd fix(javascript): omit self-referential static references with disclosure (#1157)
* fix(javascript): omit self-referential static references with disclosure

A bundled module that requires its own key (`r(50)` inside webpack
module 50) resolved, through the static-reference composer, to its own
module node. The resulting self-referential imports edge failed result
validation and lost the entire analysis ("Result schema rejected 1
issue ... (custom)"). This is the same invariant as #1146; #1152 fixed
the bundler async-chunk composer, and this is the remaining adjacent
composer.

Skip a reference whose resolved target is its own source, count it, and
disclose the omission the way #1152 discloses async-chunk references.
Sibling requires are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(javascript): share the self-reference omission disclosure

The self-import (#1130), async-chunk (#1152) and static-reference
disclosures repeated one sentence template in three composers, and the
new test repeated the analyze-and-check sequence of its neighbour. That
put jscpd at its 2.5% threshold. Share the template through
selfReferenceOmissions with identical wording, and share one test helper
for the graph-wide self-reference invariants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:01:59 +08:00

131 lines
4.2 KiB
TypeScript

import { expect } from "vitest";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
import {
findChunkNode,
findGraphEdge,
writeFixtureFiles,
} from "../../support/javascriptApplicationFixture.js";
/** Analyze one fixture and check the graph-wide self-reference invariants. */
const analyzeBundle = async (
cli: TestCli,
prefix: string,
files: Readonly<Record<string, string>>,
) => {
const root = await createTestTempDirectory(prefix);
await writeFixtureFiles(root, files);
const output = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
});
expect(output.exitCode).toBe(0);
const result = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(output.json).normalized_result,
);
expect(
result.graph.edges.filter(
(edge) => edge.source_node_id === edge.target_node_id,
),
).toEqual([]);
expect(result.limitations).toEqual(
expect.arrayContaining(result.graph.limitations),
);
return result.graph;
};
for (const scenario of [
{ runtime: "webpackChunkdemo", keys: [1], omitted: 1 },
{ runtime: "rspackChunkdemo", keys: [1, 2, 3], omitted: 3 },
] as const) {
cliTest(
`preserves ${scenario.runtime} evidence when a chunk requests its own IDs`,
async ({ cli }) => {
const graph = await analyzeBundle(cli, "rea-bundler-self-", {
"runtime.js": `
(self.${scenario.runtime} = self.${scenario.runtime} || []).push([
${JSON.stringify(scenario.keys)},
{10(e,t,r){
${scenario.keys.map((key) => `r.e(${String(key)});`).join("\n")}
r.e(4); r.e(99);
}}
]);
(self.${scenario.runtime} = self.${scenario.runtime} || []).push([
[4], {40(e,t,r){r.r(t)}}
]);
`,
});
expect(graph.limitations).toContainEqual(
expect.stringMatching(
new RegExp(
`^${String(scenario.omitted)} bundler async-chunk references? .*omitted;`,
"u",
),
),
);
const source = findChunkNode(graph, scenario.runtime, "1");
expect(source?.observations[0]?.properties.async_chunk_keys).toEqual(
expect.arrayContaining([...scenario.keys.map(String), "4", "99"]),
);
expect(source?.observations[0]?.evidence.location).toMatchObject({
available: true,
value: { kind: "source-range", source: "runtime.js" },
});
expect(
findGraphEdge(
graph,
source,
findChunkNode(graph, scenario.runtime, "4"),
"imports",
)?.properties,
).toMatchObject({
kind: "bundler-async-chunk",
chunk_key: "4",
resolution_status: "resolved",
});
expect(
graph.edges.find(
(edge) =>
edge.source_node_id === source?.node_id &&
edge.properties.chunk_key === "99",
)?.properties,
).toMatchObject({
kind: "bundler-async-chunk",
resolution_status: "not-found",
});
},
);
}
cliTest(
"preserves bundle evidence when a module requires its own key",
async ({ cli }) => {
const graph = await analyzeBundle(cli, "rea-bundler-self-require-", {
"runtime.js": `
(self.webpackChunkdemo = self.webpackChunkdemo || []).push([
[5], {50(e,t,r){r(50); r(51)}, 51(e,t,r){r.r(t)}}
]);
`,
});
expect(graph.limitations).toContainEqual(
expect.stringMatching(
/^1 static reference resolved back to the referencing module itself and was omitted;/u,
),
);
expect(
graph.edges.filter(
({ relation, properties }) =>
relation === "imports" &&
properties.kind === "require" &&
["50", "51"].includes(String(properties.specifier)),
),
).toEqual([
expect.objectContaining({
properties: expect.objectContaining({ specifier: "51" }),
}),
]);
},
);