Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06444ad67 |
@@ -12,7 +12,11 @@ import {
|
|||||||
addJavaScriptHtmlRoles,
|
addJavaScriptHtmlRoles,
|
||||||
addJavaScriptSourceMapOriginals,
|
addJavaScriptSourceMapOriginals,
|
||||||
} from "./JavaScriptArtifactGraphDocuments.js";
|
} from "./JavaScriptArtifactGraphDocuments.js";
|
||||||
import { addJavaScriptStaticFindings } from "./JavaScriptArtifactGraphFindings.js";
|
import {
|
||||||
|
addJavaScriptStaticFindings,
|
||||||
|
type JavaScriptStaticFindingOmissions,
|
||||||
|
} from "./JavaScriptArtifactGraphFindings.js";
|
||||||
|
import type { JavaScriptBundlerOmissions } from "./JavaScriptArtifactGraphBundlers.js";
|
||||||
import {
|
import {
|
||||||
addJavaScriptModuleRelationships,
|
addJavaScriptModuleRelationships,
|
||||||
type JavaScriptModuleRelationshipOmissions,
|
type JavaScriptModuleRelationshipOmissions,
|
||||||
@@ -88,9 +92,9 @@ const buildJavaScriptArtifactGraphInput = (
|
|||||||
addJavaScriptArtifactFiles(context);
|
addJavaScriptArtifactFiles(context);
|
||||||
const packageRoots = addJavaScriptPackageNodes(context);
|
const packageRoots = addJavaScriptPackageNodes(context);
|
||||||
addJavaScriptSourceModules(context);
|
addJavaScriptSourceModules(context);
|
||||||
addJavaScriptBundlerNodes(context);
|
const bundlerOmissions = addJavaScriptBundlerNodes(context);
|
||||||
const relationshipOmissions = addJavaScriptModuleRelationships(context);
|
const relationshipOmissions = addJavaScriptModuleRelationships(context);
|
||||||
addJavaScriptStaticFindings(context);
|
const findingOmissions = addJavaScriptStaticFindings(context);
|
||||||
addElectronBoundaries(context);
|
addElectronBoundaries(context);
|
||||||
addJavaScriptHtmlRoles(context);
|
addJavaScriptHtmlRoles(context);
|
||||||
addJavaScriptSourceMapOriginals(context);
|
addJavaScriptSourceMapOriginals(context);
|
||||||
@@ -104,11 +108,11 @@ const buildJavaScriptArtifactGraphInput = (
|
|||||||
nodes: accumulator.nodes(),
|
nodes: accumulator.nodes(),
|
||||||
edges: accumulator.edges(),
|
edges: accumulator.edges(),
|
||||||
coverage,
|
coverage,
|
||||||
limitations: graphLimitations(
|
limitations: graphLimitations(context, coverage.status, {
|
||||||
context,
|
...relationshipOmissions,
|
||||||
coverage.status,
|
...bundlerOmissions,
|
||||||
relationshipOmissions,
|
...findingOmissions,
|
||||||
),
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,10 +177,47 @@ const applicationGraphResourceLimit = (
|
|||||||
: ("items" as const),
|
: ("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 = (
|
const graphLimitations = (
|
||||||
context: JavaScriptArtifactGraphContext,
|
context: JavaScriptArtifactGraphContext,
|
||||||
coverage: "complete" | "partial" | "unknown" | "unavailable",
|
coverage: "complete" | "partial" | "unknown" | "unavailable",
|
||||||
relationshipOmissions: JavaScriptModuleRelationshipOmissions,
|
omissions: SelfReferenceOmissions,
|
||||||
): string[] => {
|
): string[] => {
|
||||||
const ipc = collectElectronIpcRecords(context.analysis);
|
const ipc = collectElectronIpcRecords(context.analysis);
|
||||||
const pairings = classifyElectronIpcPairings(ipc);
|
const pairings = classifyElectronIpcPairings(ipc);
|
||||||
@@ -195,11 +236,7 @@ const graphLimitations = (
|
|||||||
);
|
);
|
||||||
return [
|
return [
|
||||||
...context.analysis.limitations,
|
...context.analysis.limitations,
|
||||||
...(relationshipOmissions.selfImports > 0
|
...selfReferenceLimitations(omissions),
|
||||||
? [
|
|
||||||
`${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.`,
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
"CommonJS and ESM binding relationships were recovered from inert syntax and resolved only within the inventoried artifact container.",
|
"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.",
|
"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.",
|
"Static imports, entrypoints, workers, endpoints, and storage relationships do not prove runtime execution.",
|
||||||
|
|||||||
@@ -15,10 +15,17 @@ import {
|
|||||||
staticInferenceEvidence,
|
staticInferenceEvidence,
|
||||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
} 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. */
|
/** Project Webpack/Rspack chunk and factory literals recovered from AST. */
|
||||||
export const addJavaScriptBundlerNodes = (
|
export const addJavaScriptBundlerNodes = (
|
||||||
context: JavaScriptArtifactGraphContext,
|
context: JavaScriptArtifactGraphContext,
|
||||||
): void => {
|
): JavaScriptBundlerOmissions => {
|
||||||
|
const omissions = { selfAsyncChunks: 0 };
|
||||||
for (const analyzed of context.analysis.files) {
|
for (const analyzed of context.analysis.files) {
|
||||||
const { file, javascript } = analyzed;
|
const { file, javascript } = analyzed;
|
||||||
const asset = context.assetNodes.get(file.path);
|
const asset = context.assetNodes.get(file.path);
|
||||||
@@ -49,8 +56,10 @@ export const addJavaScriptBundlerNodes = (
|
|||||||
records.push({ file, registration, chunk, coverage });
|
records.push({ file, registration, chunk, coverage });
|
||||||
addBundlerModuleNodes(context, { ...input, chunk });
|
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 {
|
interface BundlerProjectionInput {
|
||||||
@@ -177,6 +186,7 @@ interface BundlerGraphRecord {
|
|||||||
const addBundlerRuntimeEdges = (
|
const addBundlerRuntimeEdges = (
|
||||||
context: JavaScriptArtifactGraphContext,
|
context: JavaScriptArtifactGraphContext,
|
||||||
record: BundlerGraphRecord,
|
record: BundlerGraphRecord,
|
||||||
|
omissions: { selfAsyncChunks: number },
|
||||||
): void => {
|
): void => {
|
||||||
const { file, registration, chunk, coverage } = record;
|
const { file, registration, chunk, coverage } = record;
|
||||||
for (const moduleKey of registration.entry_module_keys) {
|
for (const moduleKey of registration.entry_module_keys) {
|
||||||
@@ -209,6 +219,13 @@ const addBundlerRuntimeEdges = (
|
|||||||
for (const chunkKey of registration.async_chunk_keys) {
|
for (const chunkKey of registration.async_chunk_keys) {
|
||||||
const lookup = chunkLookupKey(file.path, registration.runtime, chunkKey);
|
const lookup = chunkLookupKey(file.path, registration.runtime, chunkKey);
|
||||||
const resolved = context.chunkNodes.get(lookup);
|
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 =
|
const target =
|
||||||
resolved ?? unresolvedBundlerChunkNode(context, record, chunkKey);
|
resolved ?? unresolvedBundlerChunkNode(context, record, chunkKey);
|
||||||
context.accumulator.addEdge({
|
context.accumulator.addEdge({
|
||||||
|
|||||||
@@ -39,17 +39,24 @@ interface ResolvedReference {
|
|||||||
readonly node: ApplicationNode;
|
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. */
|
/** Project imports, workers, roles, endpoints, storage, and source-map links. */
|
||||||
export const addJavaScriptStaticFindings = (
|
export const addJavaScriptStaticFindings = (
|
||||||
context: JavaScriptArtifactGraphContext,
|
context: JavaScriptArtifactGraphContext,
|
||||||
): void => {
|
): JavaScriptStaticFindingOmissions => {
|
||||||
|
const omissions = { selfReferences: 0 };
|
||||||
for (const analyzed of context.analysis.files) {
|
for (const analyzed of context.analysis.files) {
|
||||||
const { file, javascript } = analyzed;
|
const { file, javascript } = analyzed;
|
||||||
const asset = context.assetNodes.get(file.path);
|
const asset = context.assetNodes.get(file.path);
|
||||||
if (javascript === null || asset === undefined) continue;
|
if (javascript === null || asset === undefined) continue;
|
||||||
const coverage = javascriptAnalysisCoverage(javascript);
|
const coverage = javascriptAnalysisCoverage(javascript);
|
||||||
for (const value of javascript.references)
|
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)
|
for (const value of javascript.endpoints)
|
||||||
addEndpoint(context, { file, asset, value, coverage });
|
addEndpoint(context, { file, asset, value, coverage });
|
||||||
for (const value of javascript.storage)
|
for (const value of javascript.storage)
|
||||||
@@ -59,11 +66,13 @@ export const addJavaScriptStaticFindings = (
|
|||||||
for (const value of javascript.source_map_urls)
|
for (const value of javascript.source_map_urls)
|
||||||
addSourceMapEdge(context, { file, asset, value, coverage });
|
addSourceMapEdge(context, { file, asset, value, coverage });
|
||||||
}
|
}
|
||||||
|
return omissions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addReference = (
|
const addReference = (
|
||||||
context: JavaScriptArtifactGraphContext,
|
context: JavaScriptArtifactGraphContext,
|
||||||
input: FindingInput<StaticReference>,
|
input: FindingInput<StaticReference>,
|
||||||
|
omissions: { selfReferences: number },
|
||||||
): void => {
|
): void => {
|
||||||
const source =
|
const source =
|
||||||
sourceNodeFor(context, input.file.path, input.value.module_key) ??
|
sourceNodeFor(context, input.file.path, input.value.module_key) ??
|
||||||
@@ -119,6 +128,13 @@ const addReference = (
|
|||||||
});
|
});
|
||||||
return;
|
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 =
|
const target =
|
||||||
resolved?.node ??
|
resolved?.node ??
|
||||||
unresolvedReferenceNode(context, input.file, input.value, input.coverage);
|
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