Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06444ad67 |
@@ -12,7 +12,11 @@ import {
|
||||
addJavaScriptHtmlRoles,
|
||||
addJavaScriptSourceMapOriginals,
|
||||
} from "./JavaScriptArtifactGraphDocuments.js";
|
||||
import { addJavaScriptStaticFindings } from "./JavaScriptArtifactGraphFindings.js";
|
||||
import {
|
||||
addJavaScriptStaticFindings,
|
||||
type JavaScriptStaticFindingOmissions,
|
||||
} from "./JavaScriptArtifactGraphFindings.js";
|
||||
import type { JavaScriptBundlerOmissions } from "./JavaScriptArtifactGraphBundlers.js";
|
||||
import {
|
||||
addJavaScriptModuleRelationships,
|
||||
type JavaScriptModuleRelationshipOmissions,
|
||||
@@ -88,9 +92,9 @@ const buildJavaScriptArtifactGraphInput = (
|
||||
addJavaScriptArtifactFiles(context);
|
||||
const packageRoots = addJavaScriptPackageNodes(context);
|
||||
addJavaScriptSourceModules(context);
|
||||
addJavaScriptBundlerNodes(context);
|
||||
const bundlerOmissions = addJavaScriptBundlerNodes(context);
|
||||
const relationshipOmissions = addJavaScriptModuleRelationships(context);
|
||||
addJavaScriptStaticFindings(context);
|
||||
const findingOmissions = addJavaScriptStaticFindings(context);
|
||||
addElectronBoundaries(context);
|
||||
addJavaScriptHtmlRoles(context);
|
||||
addJavaScriptSourceMapOriginals(context);
|
||||
@@ -104,11 +108,11 @@ const buildJavaScriptArtifactGraphInput = (
|
||||
nodes: accumulator.nodes(),
|
||||
edges: accumulator.edges(),
|
||||
coverage,
|
||||
limitations: graphLimitations(
|
||||
context,
|
||||
coverage.status,
|
||||
relationshipOmissions,
|
||||
),
|
||||
limitations: graphLimitations(context, coverage.status, {
|
||||
...relationshipOmissions,
|
||||
...bundlerOmissions,
|
||||
...findingOmissions,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -173,10 +177,47 @@ const applicationGraphResourceLimit = (
|
||||
: ("items" as const),
|
||||
});
|
||||
|
||||
type SelfReferenceOmissions = JavaScriptModuleRelationshipOmissions &
|
||||
JavaScriptBundlerOmissions &
|
||||
JavaScriptStaticFindingOmissions;
|
||||
|
||||
/** Disclose each relationship omitted because it would reference itself. */
|
||||
const selfReferenceLimitations = (
|
||||
omissions: SelfReferenceOmissions,
|
||||
): string[] =>
|
||||
(
|
||||
[
|
||||
[
|
||||
omissions.selfImports,
|
||||
"import specifier",
|
||||
"import specifiers",
|
||||
"the importing module",
|
||||
],
|
||||
[
|
||||
omissions.selfAsyncChunks,
|
||||
"bundler async-chunk request",
|
||||
"bundler async-chunk requests",
|
||||
"the requesting chunk",
|
||||
],
|
||||
[
|
||||
omissions.selfReferences,
|
||||
"static reference",
|
||||
"static references",
|
||||
"the referencing module",
|
||||
],
|
||||
] as const
|
||||
).flatMap(([count, singular, plural, owner]) =>
|
||||
count === 0
|
||||
? []
|
||||
: [
|
||||
`${String(count)} ${count === 1 ? singular : plural} resolved back to ${owner} itself and ${count === 1 ? "was" : "were"} omitted; application graph edges cannot be self-referential.`,
|
||||
],
|
||||
);
|
||||
|
||||
const graphLimitations = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
coverage: "complete" | "partial" | "unknown" | "unavailable",
|
||||
relationshipOmissions: JavaScriptModuleRelationshipOmissions,
|
||||
omissions: SelfReferenceOmissions,
|
||||
): string[] => {
|
||||
const ipc = collectElectronIpcRecords(context.analysis);
|
||||
const pairings = classifyElectronIpcPairings(ipc);
|
||||
@@ -195,11 +236,7 @@ const graphLimitations = (
|
||||
);
|
||||
return [
|
||||
...context.analysis.limitations,
|
||||
...(relationshipOmissions.selfImports > 0
|
||||
? [
|
||||
`${String(relationshipOmissions.selfImports)} import ${relationshipOmissions.selfImports === 1 ? "specifier" : "specifiers"} resolved back to the importing module itself and ${relationshipOmissions.selfImports === 1 ? "was" : "were"} omitted; application graph edges cannot be self-referential.`,
|
||||
]
|
||||
: []),
|
||||
...selfReferenceLimitations(omissions),
|
||||
"CommonJS and ESM binding relationships were recovered from inert syntax and resolved only within the inventoried artifact container.",
|
||||
"Webpack/Rspack factories were recovered from AST literals; REA did not invoke push handlers or bundle bootstrap code.",
|
||||
"Static imports, entrypoints, workers, endpoints, and storage relationships do not prove runtime execution.",
|
||||
|
||||
@@ -15,10 +15,17 @@ import {
|
||||
staticInferenceEvidence,
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
|
||||
/** Bundler compositions the application graph cannot represent, for disclosure. */
|
||||
export interface JavaScriptBundlerOmissions {
|
||||
/** Async chunk requests that resolved back to the requesting chunk itself. */
|
||||
readonly selfAsyncChunks: number;
|
||||
}
|
||||
|
||||
/** Project Webpack/Rspack chunk and factory literals recovered from AST. */
|
||||
export const addJavaScriptBundlerNodes = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
): void => {
|
||||
): JavaScriptBundlerOmissions => {
|
||||
const omissions = { selfAsyncChunks: 0 };
|
||||
for (const analyzed of context.analysis.files) {
|
||||
const { file, javascript } = analyzed;
|
||||
const asset = context.assetNodes.get(file.path);
|
||||
@@ -49,8 +56,10 @@ export const addJavaScriptBundlerNodes = (
|
||||
records.push({ file, registration, chunk, coverage });
|
||||
addBundlerModuleNodes(context, { ...input, chunk });
|
||||
}
|
||||
for (const record of records) addBundlerRuntimeEdges(context, record);
|
||||
for (const record of records)
|
||||
addBundlerRuntimeEdges(context, record, omissions);
|
||||
}
|
||||
return omissions;
|
||||
};
|
||||
|
||||
interface BundlerProjectionInput {
|
||||
@@ -177,6 +186,7 @@ interface BundlerGraphRecord {
|
||||
const addBundlerRuntimeEdges = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
record: BundlerGraphRecord,
|
||||
omissions: { selfAsyncChunks: number },
|
||||
): void => {
|
||||
const { file, registration, chunk, coverage } = record;
|
||||
for (const moduleKey of registration.entry_module_keys) {
|
||||
@@ -209,6 +219,13 @@ const addBundlerRuntimeEdges = (
|
||||
for (const chunkKey of registration.async_chunk_keys) {
|
||||
const lookup = chunkLookupKey(file.path, registration.runtime, chunkKey);
|
||||
const resolved = context.chunkNodes.get(lookup);
|
||||
// A chunk file that requests one of its own registered chunk ids carries
|
||||
// no chunk-graph relationship, and the application graph forbids
|
||||
// self-referential edges, so one such request would fail the analysis.
|
||||
if (resolved?.node_id === chunk.node_id) {
|
||||
omissions.selfAsyncChunks += 1;
|
||||
continue;
|
||||
}
|
||||
const target =
|
||||
resolved ?? unresolvedBundlerChunkNode(context, record, chunkKey);
|
||||
context.accumulator.addEdge({
|
||||
|
||||
@@ -39,17 +39,24 @@ interface ResolvedReference {
|
||||
readonly node: ApplicationNode;
|
||||
}
|
||||
|
||||
/** Static finding compositions the application graph cannot represent. */
|
||||
export interface JavaScriptStaticFindingOmissions {
|
||||
/** References, such as a bundled module requiring its own key, that resolve to their source. */
|
||||
readonly selfReferences: number;
|
||||
}
|
||||
|
||||
/** Project imports, workers, roles, endpoints, storage, and source-map links. */
|
||||
export const addJavaScriptStaticFindings = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
): void => {
|
||||
): JavaScriptStaticFindingOmissions => {
|
||||
const omissions = { selfReferences: 0 };
|
||||
for (const analyzed of context.analysis.files) {
|
||||
const { file, javascript } = analyzed;
|
||||
const asset = context.assetNodes.get(file.path);
|
||||
if (javascript === null || asset === undefined) continue;
|
||||
const coverage = javascriptAnalysisCoverage(javascript);
|
||||
for (const value of javascript.references)
|
||||
addReference(context, { file, asset, value, coverage });
|
||||
addReference(context, { file, asset, value, coverage }, omissions);
|
||||
for (const value of javascript.endpoints)
|
||||
addEndpoint(context, { file, asset, value, coverage });
|
||||
for (const value of javascript.storage)
|
||||
@@ -59,11 +66,13 @@ export const addJavaScriptStaticFindings = (
|
||||
for (const value of javascript.source_map_urls)
|
||||
addSourceMapEdge(context, { file, asset, value, coverage });
|
||||
}
|
||||
return omissions;
|
||||
};
|
||||
|
||||
const addReference = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
input: FindingInput<StaticReference>,
|
||||
omissions: { selfReferences: number },
|
||||
): void => {
|
||||
const source =
|
||||
sourceNodeFor(context, input.file.path, input.value.module_key) ??
|
||||
@@ -119,6 +128,13 @@ const addReference = (
|
||||
});
|
||||
return;
|
||||
}
|
||||
// A reference that resolves to its own source, such as a bundled module
|
||||
// requiring its own key, cannot become an edge: the application graph
|
||||
// forbids self-referential edges and one would fail the whole analysis.
|
||||
if (resolved?.node.node_id === source.node_id) {
|
||||
omissions.selfReferences += 1;
|
||||
return;
|
||||
}
|
||||
const target =
|
||||
resolved?.node ??
|
||||
unresolvedReferenceNode(context, input.file, input.value, input.coverage);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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 { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
const chunk = (ids: readonly number[], modules: string): string =>
|
||||
`"use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[${ids.join(",")}],{${modules}}]);\n`;
|
||||
|
||||
const analyze = async (
|
||||
cli: TestCli,
|
||||
files: Readonly<Record<string, string>>,
|
||||
) => {
|
||||
const root = await createTestTempDirectory("rea-self-bundler-");
|
||||
await writeFixtureFiles(root, files);
|
||||
const output = await cli.run({
|
||||
arguments: ["analyze-javascript-application", root, "--json"],
|
||||
});
|
||||
expect(output.exitCode, JSON.stringify(output.json)).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;
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"omits a chunk's request for its own id while keeping sibling chunk requests",
|
||||
async ({ cli }) => {
|
||||
const graph = await analyze(cli, {
|
||||
"1.js": chunk(
|
||||
[1],
|
||||
"10(e,t,r){r.e(1).then(r.bind(r,11));r.e(2)},11(e,t,r){r.r(t)}",
|
||||
),
|
||||
"2.js": chunk([2], "20(e,t,r){r.r(t)}"),
|
||||
"3.js": chunk(
|
||||
[3, 4],
|
||||
"30(e,t,r){r.e(4).then(r.bind(r,31))},31(e,t,r){r.r(t)}",
|
||||
),
|
||||
});
|
||||
expect(graph.limitations).toContainEqual(
|
||||
expect.stringMatching(
|
||||
/^2 bundler async-chunk requests resolved back to the requesting chunk itself and were omitted;/u,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
graph.edges
|
||||
.filter(({ properties }) => properties.kind === "bundler-async-chunk")
|
||||
.map(({ properties }) => properties.chunk_key),
|
||||
).toEqual(["2"]);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"omits a bundled module's require of its own key while keeping sibling requires",
|
||||
async ({ cli }) => {
|
||||
const graph = await analyze(cli, {
|
||||
"5.js": chunk([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.some(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" && properties.specifier === "51",
|
||||
),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user