mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
fix(javascript): preserve semantic query identity and source coverage
This commit is contained in:
@@ -25,6 +25,7 @@ import type {
|
||||
JavaScriptArtifactFile,
|
||||
JavaScriptArtifactFileSet,
|
||||
} from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { javaScriptArtifactTextExclusion } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type {
|
||||
AnalyzedJavaScriptArtifactFile,
|
||||
JavaScriptArtifactAnalysisFailure,
|
||||
@@ -159,7 +160,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
|
||||
let analysisStarted = false;
|
||||
try {
|
||||
await setImmediate();
|
||||
if (classifyOutsideAnalysisScope(file, state, projection)) {
|
||||
if (classifyUnavailableSource(file, state, projection)) {
|
||||
if (signal?.aborted === true) {
|
||||
interruption = new AnalysisCancelledError(
|
||||
"analyze_javascript_application",
|
||||
@@ -320,7 +321,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
|
||||
>();
|
||||
for (const file of fileSet.files) {
|
||||
if (analyzedPaths.has(file.path)) continue;
|
||||
if (classifyOutsideAnalysisScope(file, state, projection)) continue;
|
||||
if (classifyUnavailableSource(file, state, projection)) continue;
|
||||
const scope = javascriptArtifactAnalysisScopes[file.kind];
|
||||
let unavailable = scope === null ? undefined : failures.get(scope);
|
||||
if (scope !== null && unavailable === undefined) {
|
||||
@@ -404,7 +405,7 @@ function* analyzeArtifactFileSteps<
|
||||
context: ArtifactAnalysisContext<SemanticIr>,
|
||||
): Generator<void, void> {
|
||||
const { state } = context;
|
||||
if (classifyOutsideAnalysisScope(file, state)) return;
|
||||
if (classifyUnavailableSource(file, state)) return;
|
||||
addStructuredObservations(file, state);
|
||||
if (file.kind === "html" && file.text.included) {
|
||||
const html = parseHtmlScripts(file.path, file.text.value);
|
||||
@@ -460,22 +461,17 @@ function* analyzeArtifactFileSteps<
|
||||
if (analysis.parse_status === "failed") state.parseFailures += 1;
|
||||
}
|
||||
|
||||
const classifyOutsideAnalysisScope = <
|
||||
const classifyUnavailableSource = <
|
||||
SemanticIr extends JavaScriptModuleSemanticIr,
|
||||
>(
|
||||
file: JavaScriptArtifactFile,
|
||||
state: MutableArtifactAnalysis<SemanticIr>,
|
||||
projection?: JavaScriptSemanticGraphProjection,
|
||||
): boolean => {
|
||||
if (file.text.included || file.text.reason !== "outside-analysis-scope")
|
||||
return false;
|
||||
const reason = javaScriptArtifactTextExclusion(file);
|
||||
if (reason === null) return false;
|
||||
state.files.push({ file, javascript: null, semantic: null });
|
||||
projection?.recordUnavailableFile(
|
||||
file,
|
||||
"Source was not analyzed because it is outside the caller-selected source_paths.",
|
||||
null,
|
||||
"incomplete-module",
|
||||
);
|
||||
projection?.recordUnavailableFile(file, reason, null, "incomplete-module");
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -240,6 +240,7 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
|
||||
javascript !== null && javascript.parse_status === "partial",
|
||||
);
|
||||
const unknownGap =
|
||||
context.snapshot.coverage.status !== "complete" ||
|
||||
(context.fileSet.source_selection?.unselected_files ?? 0) > 0 ||
|
||||
context.fileSet.unavailable_relevant_files.length > 0 ||
|
||||
context.analysis.files.some(
|
||||
@@ -331,11 +332,12 @@ const graphLimitations = (
|
||||
);
|
||||
return [
|
||||
...context.analysis.limitations,
|
||||
...(context.fileSet.source_selection === undefined
|
||||
...((context.fileSet.source_selection?.unselected_files ?? 0) === 0
|
||||
? []
|
||||
: [
|
||||
`Caller-selected source analysis leaves ${String(context.fileSet.source_selection.unselected_files)} inventoried files outside analysis scope. Inventory identity covers the complete artifact; relationships through unselected sources remain unknown.`,
|
||||
`Caller-selected source analysis leaves ${String(context.fileSet.source_selection?.unselected_files)} inventoried files outside analysis scope. Inventory identity covers the complete artifact; relationships through unselected sources remain unknown.`,
|
||||
]),
|
||||
...context.snapshot.limitations,
|
||||
...context.snapshot.integrity_contradictions.map(
|
||||
({ logical_path: path }) =>
|
||||
`Artifact integrity metadata contradicts observed bytes at ${path}; the observed bytes are untrusted.`,
|
||||
|
||||
@@ -19,7 +19,10 @@ import type {
|
||||
import { sourceRangesEqual as rangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
|
||||
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
javaScriptArtifactTextExclusion,
|
||||
type JavaScriptArtifactFile,
|
||||
} from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
projectSemanticEvents,
|
||||
projectSemanticTimers,
|
||||
@@ -156,16 +159,16 @@ export const buildJavaScriptSemanticGraph = ({
|
||||
analyzed.semantic.ir,
|
||||
analyzed.javascript?.electron.call_paths,
|
||||
);
|
||||
else if (
|
||||
!analyzed.file.text.included &&
|
||||
analyzed.file.text.reason === "outside-analysis-scope"
|
||||
)
|
||||
projection.recordUnavailableFile(
|
||||
analyzed.file,
|
||||
"Source was not analyzed because it is outside the caller-selected source_paths.",
|
||||
null,
|
||||
"incomplete-module",
|
||||
);
|
||||
else {
|
||||
const reason = javaScriptArtifactTextExclusion(analyzed.file);
|
||||
if (reason !== null)
|
||||
projection.recordUnavailableFile(
|
||||
analyzed.file,
|
||||
reason,
|
||||
null,
|
||||
"incomplete-module",
|
||||
);
|
||||
}
|
||||
}
|
||||
return projection.finish(rootArtifactSha256, applicationGraph);
|
||||
};
|
||||
|
||||
@@ -27,8 +27,12 @@ export function* projectSemanticObjects(
|
||||
);
|
||||
for (const [index, operation] of context.ir.objectOperations.entries()) {
|
||||
if (index % 64 === 0) yield;
|
||||
const occurrence = addObjectOccurrence(context, operation);
|
||||
const target = objectTarget(context, values, operation);
|
||||
const occurrence = addObjectOccurrence(
|
||||
context,
|
||||
operation,
|
||||
target.slotApplicability,
|
||||
);
|
||||
const ends = objectRelationEnds(context, operation, occurrence, target);
|
||||
const resolved =
|
||||
operation.resolution === "complete" &&
|
||||
@@ -57,6 +61,7 @@ interface ObjectTarget {
|
||||
readonly slot: JavaScriptSemanticGraphNode | null;
|
||||
readonly presence: JavaScriptSemanticSlotPresence;
|
||||
readonly receiverResolved: boolean;
|
||||
readonly slotApplicability: "resolved" | "unresolved";
|
||||
}
|
||||
|
||||
const objectTarget = (
|
||||
@@ -82,6 +87,7 @@ const objectTarget = (
|
||||
slot: null,
|
||||
presence: "unknown-coverage",
|
||||
receiverResolved: false,
|
||||
slotApplicability: "unresolved",
|
||||
};
|
||||
const fact = semanticSlotAtPath(value, operation.propertyPath);
|
||||
if (operation.propertyPath.length === 0)
|
||||
@@ -90,6 +96,7 @@ const objectTarget = (
|
||||
slot: null,
|
||||
presence: fact.presence,
|
||||
receiverResolved: semanticContainer(value) !== null,
|
||||
slotApplicability: "unresolved",
|
||||
};
|
||||
const slot = semanticPropertySlot(
|
||||
context,
|
||||
@@ -104,6 +111,7 @@ const objectTarget = (
|
||||
return {
|
||||
objectNode: slot,
|
||||
slot,
|
||||
slotApplicability: "resolved",
|
||||
presence: fact.presence,
|
||||
receiverResolved:
|
||||
receiver.presence === "present" &&
|
||||
@@ -138,6 +146,7 @@ const objectRelationEnds = (
|
||||
const addObjectOccurrence = (
|
||||
context: SemanticFlowProjectionContext,
|
||||
operation: JavaScriptSemanticObjectOperation,
|
||||
slotApplicability: ObjectTarget["slotApplicability"],
|
||||
): JavaScriptSemanticGraphNode | null =>
|
||||
retainSemanticGraphNode(context.state, context.file, {
|
||||
kind: "expression",
|
||||
@@ -156,6 +165,7 @@ const addObjectOccurrence = (
|
||||
operation_kind: operation.kind,
|
||||
property_path:
|
||||
operation.propertyPath === null ? null : [...operation.propertyPath],
|
||||
property_slot_resolution: slotApplicability,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { graphForJavaScript } from "../../../tests/fixtures/javascriptSemanticGraph.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
|
||||
|
||||
it.each([
|
||||
"function read() { return this.speed; }",
|
||||
"class Game { read() { return this.config.speed; } }",
|
||||
])(
|
||||
"matches retained slotless property accesses and their unknowns: %s",
|
||||
(source) => {
|
||||
const graph = graphForJavaScript(source);
|
||||
const accesses = graph.nodes.filter(
|
||||
({ kind, properties }) =>
|
||||
kind === "expression" &&
|
||||
properties.property_slot_resolution === "unresolved" &&
|
||||
Array.isArray(properties.property_path) &&
|
||||
properties.property_path.at(-1) === "speed",
|
||||
);
|
||||
expect(accesses.length).toBeGreaterThan(0);
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "property", name: "speed" },
|
||||
direction: "forward-influence",
|
||||
});
|
||||
expect(trace.seed_node_ids).toEqual(
|
||||
accesses.map(({ node_id }) => node_id).sort(),
|
||||
);
|
||||
const relevant = graph.unknowns.filter(({ node_id }) =>
|
||||
accesses.some((node) => node.node_id === node_id),
|
||||
);
|
||||
expect(relevant.length).toBeGreaterThan(0);
|
||||
expect(trace.unknowns.map(({ unknown_id }) => unknown_id)).toEqual(
|
||||
expect.arrayContaining(relevant.map(({ unknown_id }) => unknown_id)),
|
||||
);
|
||||
expect(
|
||||
queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "property", name: "absent" },
|
||||
direction: "forward-influence",
|
||||
}).seed_node_ids,
|
||||
).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps resolved property seeds on their slot without duplicating access occurrences", () => {
|
||||
const graph = graphForJavaScript(
|
||||
"const state = { speed: 1 }; const result = state.speed;",
|
||||
);
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "property", name: "speed" },
|
||||
direction: "backward-provenance",
|
||||
});
|
||||
expect(trace.seed_node_ids).toHaveLength(1);
|
||||
expect(
|
||||
graph.nodes.find(({ node_id }) => node_id === trace.seed_node_ids[0])?.kind,
|
||||
).toBe("property-slot");
|
||||
});
|
||||
|
||||
it("queries an empty event name as an exact retained event identity", () => {
|
||||
const graph = graphForJavaScript(
|
||||
'const { EventEmitter } = require("node:events"); const bus = new EventEmitter(); bus.on("", function emptyListener() {}); bus.on("data", function dataListener() {});',
|
||||
);
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "event", name: "" },
|
||||
direction: "forward-influence",
|
||||
});
|
||||
expect(trace.seed_node_ids.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
trace.nodes
|
||||
.filter(({ node_id }) => trace.seed_node_ids.includes(node_id))
|
||||
.every(({ properties }) => properties.event_name === ""),
|
||||
).toBe(true);
|
||||
});
|
||||
Reference in New Issue
Block a user