mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
b620e2a2bd
* 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>
318 lines
10 KiB
TypeScript
318 lines
10 KiB
TypeScript
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
|
import type { JavaScriptBundlerRegistration } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
|
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
|
import {
|
|
addAstContainsEdge,
|
|
artifactLocalIdentity,
|
|
chunkLookupKey,
|
|
javascriptAnalysisCoverage,
|
|
moduleLookupKey,
|
|
selfReferenceOmissions,
|
|
type JavaScriptArtifactGraphContext,
|
|
type JavaScriptArtifactGraphCoverage,
|
|
} from "./JavaScriptArtifactGraphContext.js";
|
|
import {
|
|
astObservationEvidence,
|
|
staticInferenceEvidence,
|
|
} from "./JavaScriptArtifactGraphEvidence.js";
|
|
|
|
/** Project Webpack/Rspack AST literals and disclose unrepresentable relationships. */
|
|
export const addJavaScriptBundlerNodes = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
): readonly string[] => {
|
|
let selfAsyncChunks = 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);
|
|
const records: BundlerGraphRecord[] = [];
|
|
for (const registration of javascript.bundler_registrations) {
|
|
const input = {
|
|
file,
|
|
registration,
|
|
coverage,
|
|
limitations: javascript.limitations,
|
|
};
|
|
const chunk = createBundlerChunkNode(context, input);
|
|
addAstContainsEdge(context, {
|
|
source: asset,
|
|
target: chunk,
|
|
file,
|
|
range: registration.location,
|
|
coverage,
|
|
properties: { bundler: registration.bundler },
|
|
});
|
|
for (const key of registration.chunk_keys)
|
|
context.chunkNodes.set(
|
|
chunkLookupKey(file.path, registration.runtime, key),
|
|
chunk,
|
|
);
|
|
records.push({ file, registration, chunk, coverage });
|
|
addBundlerModuleNodes(context, { ...input, chunk });
|
|
}
|
|
for (const record of records)
|
|
selfAsyncChunks += addBundlerRuntimeEdges(context, record);
|
|
}
|
|
return selfReferenceOmissions(
|
|
selfAsyncChunks,
|
|
["bundler async-chunk reference", "bundler async-chunk references"],
|
|
"the requesting chunk",
|
|
);
|
|
};
|
|
|
|
interface BundlerProjectionInput {
|
|
readonly file: JavaScriptArtifactFile;
|
|
readonly registration: JavaScriptBundlerRegistration;
|
|
readonly coverage: JavaScriptArtifactGraphCoverage;
|
|
readonly limitations: readonly string[];
|
|
}
|
|
|
|
interface BundlerModuleProjectionInput extends BundlerProjectionInput {
|
|
readonly chunk: ApplicationNode;
|
|
}
|
|
|
|
const createBundlerChunkNode = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
input: BundlerProjectionInput,
|
|
): ApplicationNode => {
|
|
const { file, registration, coverage, limitations } = input;
|
|
const chunkKey = `${registration.runtime}:${registration.chunk_keys.join(",")}`;
|
|
return context.accumulator.addNode({
|
|
kind: "javascript-chunk",
|
|
identity: artifactLocalIdentity(file.sha256, "bundler-chunk", chunkKey),
|
|
observations: [
|
|
{
|
|
label: chunkKey,
|
|
properties: {
|
|
path: file.path,
|
|
bundler: registration.bundler,
|
|
runtime: registration.runtime,
|
|
chunk_keys: registration.chunk_keys,
|
|
unknown_chunk_keys: registration.unknown_chunk_keys,
|
|
runtime_require_name: registration.runtime_require_name,
|
|
runtime_module_cache_status: registration.runtime_module_cache_status,
|
|
entry_module_keys: registration.entry_module_keys,
|
|
unknown_entry_module_keys: registration.unknown_entry_module_keys,
|
|
async_chunk_keys: registration.async_chunk_keys,
|
|
unknown_async_chunk_keys: registration.unknown_async_chunk_keys,
|
|
module_count: registration.modules.length,
|
|
},
|
|
evidence: astObservationEvidence({
|
|
sha256: file.sha256,
|
|
path: file.path,
|
|
range: registration.location,
|
|
operation: "extract-bundler-registration",
|
|
coverage,
|
|
limitations,
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const addBundlerModuleNodes = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
input: BundlerModuleProjectionInput,
|
|
): void => {
|
|
const { file, registration, chunk, coverage, limitations } = input;
|
|
for (const moduleValue of registration.modules) {
|
|
const module = context.accumulator.addNode({
|
|
kind: "javascript-module",
|
|
identity: artifactLocalIdentity(
|
|
file.sha256,
|
|
`${registration.runtime}:module`,
|
|
moduleValue.module_key,
|
|
),
|
|
observations: [
|
|
{
|
|
label: moduleValue.module_key,
|
|
properties: {
|
|
path: file.path,
|
|
bundler: registration.bundler,
|
|
runtime: registration.runtime,
|
|
chunk_keys: registration.chunk_keys,
|
|
module_key: moduleValue.module_key,
|
|
factory_require_name: moduleValue.factory_require_name,
|
|
runtime_entry: registration.entry_module_keys.includes(
|
|
moduleValue.module_key,
|
|
),
|
|
source_sha256: moduleValue.source_sha256,
|
|
structural_fingerprint_sha256:
|
|
moduleValue.structural_fingerprint_sha256,
|
|
structural_fingerprint_algorithm:
|
|
moduleValue.structural_fingerprint_algorithm,
|
|
exports: moduleValue.exports,
|
|
},
|
|
evidence: astObservationEvidence({
|
|
sha256: file.sha256,
|
|
path: file.path,
|
|
range: moduleValue.location,
|
|
operation: "extract-bundler-module",
|
|
coverage,
|
|
limitations,
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
context.moduleNodes.set(
|
|
moduleLookupKey(file.path, moduleValue.module_key),
|
|
module,
|
|
);
|
|
addAstContainsEdge(context, {
|
|
source: chunk,
|
|
target: module,
|
|
file,
|
|
range: moduleValue.location,
|
|
coverage,
|
|
properties: {
|
|
bundler: registration.bundler,
|
|
runtime: registration.runtime,
|
|
chunk_keys: registration.chunk_keys,
|
|
module_key: moduleValue.module_key,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
interface BundlerGraphRecord {
|
|
readonly file: JavaScriptArtifactFile;
|
|
readonly registration: JavaScriptBundlerRegistration;
|
|
readonly chunk: ApplicationNode;
|
|
readonly coverage: JavaScriptArtifactGraphCoverage;
|
|
}
|
|
|
|
const addBundlerRuntimeEdges = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
record: BundlerGraphRecord,
|
|
): number => {
|
|
let selfAsyncChunks = 0;
|
|
const { file, registration, chunk, coverage } = record;
|
|
for (const moduleKey of registration.entry_module_keys) {
|
|
const resolved = context.moduleNodes.get(
|
|
moduleLookupKey(file.path, moduleKey),
|
|
);
|
|
const target =
|
|
resolved ?? unresolvedBundlerModuleNode(context, record, moduleKey);
|
|
context.accumulator.addEdge({
|
|
source_node_id: chunk.node_id,
|
|
target_node_id: target.node_id,
|
|
relation: "loads",
|
|
properties: {
|
|
kind: "bundler-entry-module",
|
|
bundler: registration.bundler,
|
|
runtime: registration.runtime,
|
|
module_key: moduleKey,
|
|
resolution_status: resolved === undefined ? "not-found" : "resolved",
|
|
},
|
|
evidence: staticInferenceEvidence({
|
|
sha256: file.sha256,
|
|
path: file.path,
|
|
range: registration.location,
|
|
operation: "resolve-bundler-entry-module",
|
|
coverage,
|
|
confidence: resolved === undefined ? "low" : "high",
|
|
}),
|
|
});
|
|
}
|
|
for (const chunkKey of registration.async_chunk_keys) {
|
|
const lookup = chunkLookupKey(file.path, registration.runtime, chunkKey);
|
|
const resolved = context.chunkNodes.get(lookup);
|
|
if (resolved?.node_id === chunk.node_id) {
|
|
selfAsyncChunks++;
|
|
continue;
|
|
}
|
|
const target =
|
|
resolved ?? unresolvedBundlerChunkNode(context, record, chunkKey);
|
|
context.accumulator.addEdge({
|
|
source_node_id: chunk.node_id,
|
|
target_node_id: target.node_id,
|
|
relation: "imports",
|
|
properties: {
|
|
kind: "bundler-async-chunk",
|
|
bundler: registration.bundler,
|
|
runtime: registration.runtime,
|
|
chunk_key: chunkKey,
|
|
resolution_status: resolved === undefined ? "not-found" : "resolved",
|
|
},
|
|
evidence: staticInferenceEvidence({
|
|
sha256: file.sha256,
|
|
path: file.path,
|
|
range: registration.location,
|
|
operation: "resolve-bundler-async-chunk",
|
|
coverage,
|
|
confidence: resolved === undefined ? "low" : "high",
|
|
}),
|
|
});
|
|
}
|
|
return selfAsyncChunks;
|
|
};
|
|
|
|
const unresolvedBundlerModuleNode = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
record: BundlerGraphRecord,
|
|
moduleKey: string,
|
|
): ApplicationNode =>
|
|
context.accumulator.addNode({
|
|
kind: "javascript-module",
|
|
identity: artifactLocalIdentity(
|
|
record.file.sha256,
|
|
`${record.registration.runtime}:unresolved-entry-module`,
|
|
moduleKey,
|
|
),
|
|
observations: [
|
|
{
|
|
label: moduleKey,
|
|
properties: {
|
|
semantic_role: "bundler-module-reference",
|
|
bundler: record.registration.bundler,
|
|
runtime: record.registration.runtime,
|
|
module_key: moduleKey,
|
|
resolution_status: "not-found",
|
|
},
|
|
evidence: staticInferenceEvidence({
|
|
sha256: record.file.sha256,
|
|
path: record.file.path,
|
|
range: record.registration.location,
|
|
operation: "retain-unresolved-bundler-entry-module",
|
|
coverage: record.coverage,
|
|
confidence: "low",
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
const unresolvedBundlerChunkNode = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
record: BundlerGraphRecord,
|
|
chunkKey: string,
|
|
): ApplicationNode =>
|
|
context.accumulator.addNode({
|
|
kind: "javascript-chunk",
|
|
identity: artifactLocalIdentity(
|
|
record.file.sha256,
|
|
`${record.registration.runtime}:unresolved-async-chunk`,
|
|
chunkKey,
|
|
),
|
|
observations: [
|
|
{
|
|
label: chunkKey,
|
|
properties: {
|
|
semantic_role: "bundler-chunk-reference",
|
|
bundler: record.registration.bundler,
|
|
runtime: record.registration.runtime,
|
|
chunk_key: chunkKey,
|
|
resolution_status: "not-found",
|
|
},
|
|
evidence: staticInferenceEvidence({
|
|
sha256: record.file.sha256,
|
|
path: record.file.path,
|
|
range: record.registration.location,
|
|
operation: "retain-unresolved-bundler-async-chunk",
|
|
coverage: record.coverage,
|
|
confidence: "low",
|
|
}),
|
|
},
|
|
],
|
|
});
|