mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
feat(javascript): link assignment right-hand sides to their writes (#6206)
* feat(javascript): link assignment right-hand sides to their writes A declaration carries its value forward, but a later assignment to a variable, a property or through a compound operator stopped the forward trace at the right-hand-side read. The semantic IR now records one syntactic assignment flow per simple assignment target (ranges, operator, owning callable), and the graph projection connects each retained read of the right-hand side to the write occurrence through a defines relation. Compound assignments also consume the previous value of the target; the written value itself stays unknown, keeping the boundary of #4342 intact. Fixes #6193 * fix(javascript): preserve computed key assignment ownership * test(javascript): retain nested callback assignment ownership * docs(javascript): record assignment influence verification * test(mcp): isolate mocks and acknowledge cancellation boundaries * test(javascript): retain CLI diagnostics and current-main verification --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
2968d1e932
commit
9df94a62cd
17 files changed
+702
-35
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
import { createSemanticNodeRangeLookup } from "./JavaScriptSemanticGraphProjection.js";
|
||||
import { sourceRangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
addSemanticGraphRelation,
|
||||
addSemanticGraphUnknown,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
|
||||
/** Connect retained reads to writes without claiming assignment values or order. */
|
||||
export function* projectSemanticAssignments(
|
||||
context: SemanticFlowProjectionContext,
|
||||
): Generator<void, void> {
|
||||
const readsWithin = createSemanticNodeRangeLookup(context.readNodes);
|
||||
const writesWithin = createSemanticNodeRangeLookup(context.writeNodes);
|
||||
for (const [index, flow] of context.ir.assignmentFlows.entries()) {
|
||||
if (index % 64 === 0) yield;
|
||||
const owner =
|
||||
flow.ownerCallableId === null
|
||||
? null
|
||||
: context.callableNodes.get(flow.ownerCallableId)?.node_id;
|
||||
const writes = writesWithin(flow.target).filter((node) =>
|
||||
sourceRangesEqual(node.identity.source_range, flow.target),
|
||||
);
|
||||
const reads = readsWithin(flow.source).filter(
|
||||
(node) => node.function_node_id === owner,
|
||||
);
|
||||
// Compound assignments consume the previous value as well as the RHS.
|
||||
if (flow.operator !== "=")
|
||||
reads.push(
|
||||
...readsWithin(flow.target).filter((node) =>
|
||||
sourceRangesEqual(node.identity.source_range, flow.target),
|
||||
),
|
||||
);
|
||||
for (const write of writes) {
|
||||
for (const read of reads)
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: read,
|
||||
target: write,
|
||||
relation: "defines",
|
||||
resolution:
|
||||
sourceRangesEqual(read.identity.source_range, flow.target) ||
|
||||
(flow.sourceIsReference &&
|
||||
sourceRangesEqual(read.identity.source_range, flow.source) &&
|
||||
!["&&=", "||=", "??="].includes(flow.operator))
|
||||
? "resolved"
|
||||
: "candidate",
|
||||
properties: { assignment_operator: flow.operator },
|
||||
});
|
||||
if (reads.length > 0)
|
||||
addSemanticGraphUnknown(context.state, {
|
||||
node_id: write.node_id,
|
||||
family: "data-flow",
|
||||
relation_kinds: ["defines"],
|
||||
reason: "unknown-value",
|
||||
detail:
|
||||
"Assignment inputs may influence this write; execution order, branch selection and the resulting value are not established.",
|
||||
candidate_node_ids: [],
|
||||
evidence: unknownSemanticEvidence(context.file, flow.target),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { graphForJavaScript } from "../../../tests/fixtures/javascriptSemanticGraph.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
|
||||
|
||||
it.each([
|
||||
["binding", "speed = accel; return speed;", "speed"],
|
||||
["property", "state.speed = accel; return state.speed;", "read:/speed"],
|
||||
["compound", "state.speed += accel; return state.speed;", "read:/speed"],
|
||||
["logical", "state.speed ||= accel; return state.speed;", "read:/speed"],
|
||||
["nested", "speed = (state.speed = accel); return speed;", "speed"],
|
||||
["property input", "speed = input.accel; return speed;", "speed"],
|
||||
])("traces assignment influence through %s", (_name, body, reached) => {
|
||||
const graph = graphForJavaScript(`
|
||||
const accel = 0.001;
|
||||
const input = { accel: 0.001 };
|
||||
let speed = 6;
|
||||
const state = { speed: 6 };
|
||||
export function update() { ${body} }
|
||||
`);
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "literal", value: 0.001 },
|
||||
direction: "forward-influence",
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
});
|
||||
expect(trace.nodes.map((node) => node.label)).toContain(reached);
|
||||
expect(trace.nodes.map((node) => node.label)).toContain("return");
|
||||
expect(
|
||||
trace.relations.some(
|
||||
(edge) => edge.properties.assignment_operator !== undefined,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not attribute deferred callback reads to an enclosing assignment", () => {
|
||||
const graph = graphForJavaScript(`
|
||||
const accel = 0.001;
|
||||
let callback;
|
||||
callback = () => accel;
|
||||
`);
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "literal", value: 0.001 },
|
||||
direction: "forward-influence",
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
});
|
||||
expect(
|
||||
graph.relations.filter(
|
||||
(edge) => edge.properties.assignment_operator !== undefined,
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
trace.nodes
|
||||
.filter((node) => node.kind === "binding")
|
||||
.map((node) => node.label),
|
||||
).not.toContain("callback");
|
||||
});
|
||||
|
||||
it("retains both compound operands without assigning the RHS value to the destination", () => {
|
||||
const graph = graphForJavaScript(`
|
||||
const accel = 0.001;
|
||||
let speed = 6;
|
||||
speed += accel;
|
||||
`);
|
||||
const edges = graph.relations.filter(
|
||||
(edge) => edge.properties.assignment_operator === "+=",
|
||||
);
|
||||
expect(edges).toHaveLength(2);
|
||||
expect(
|
||||
edges
|
||||
.map(
|
||||
(edge) =>
|
||||
graph.nodes.find((node) => node.node_id === edge.source_node_id)
|
||||
?.label,
|
||||
)
|
||||
.sort(),
|
||||
).toEqual(["accel", "speed"]);
|
||||
const speed = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === "speed",
|
||||
);
|
||||
expect(
|
||||
graph.unknowns.some(
|
||||
(item) =>
|
||||
item.node_id === speed?.node_id && item.reason === "unknown-value",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("reaches a property write by default and keeps uncertain later reads opt-in", () => {
|
||||
const graph = graphForJavaScript(
|
||||
"const accel = 0.001; const state = {speed: 6}; function update() { state.speed += accel; return state.speed; }",
|
||||
);
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "literal", value: 0.001 },
|
||||
direction: "forward-influence",
|
||||
});
|
||||
expect(
|
||||
trace.nodes.some(
|
||||
(node) => node.kind === "property-slot" && node.label === "speed",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(trace.nodes.map((node) => node.label)).not.toContain("read:/speed");
|
||||
expect(trace.unknowns.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"object",
|
||||
"const obj = { [speed = accel]() {} }; export function current() { return speed; }",
|
||||
],
|
||||
[
|
||||
"class",
|
||||
"class Widget { [speed = accel]() {} } export function current() { return speed; }",
|
||||
],
|
||||
[
|
||||
"enclosing function",
|
||||
"export function update() { const obj = { [speed = accel]() {} }; return speed; }",
|
||||
],
|
||||
])(
|
||||
"retains assignments in a computed method key in its %s owner",
|
||||
(_kind, body) => {
|
||||
const graph = graphForJavaScript(
|
||||
`const accel = 0.001; let speed = 6; ${body}`,
|
||||
);
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
||||
const assignment = graph.relations.find(
|
||||
(edge) =>
|
||||
edge.properties.assignment_operator === "=" &&
|
||||
nodes.get(edge.source_node_id)?.label === "accel" &&
|
||||
nodes.get(edge.target_node_id)?.label === "speed",
|
||||
);
|
||||
expect(assignment).toBeDefined();
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "literal", value: 0.001 },
|
||||
direction: "forward-influence",
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
});
|
||||
expect(trace.nodes.map((node) => node.label)).toContain("return");
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps assignments in computed methods' parameter defaults in the method", () => {
|
||||
const graph = graphForJavaScript(`
|
||||
const accel = 0.001; let speed = 6;
|
||||
const obj = { ["tick"](value = (speed = accel)) { return speed; } };
|
||||
`);
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
||||
const assignment = graph.relations.find(
|
||||
(edge) => edge.properties.assignment_operator === "=",
|
||||
);
|
||||
expect(assignment).toBeDefined();
|
||||
const source = nodes.get(assignment!.source_node_id);
|
||||
const target = nodes.get(assignment!.target_node_id);
|
||||
expect(source?.function_node_id).toBeTypeOf("string");
|
||||
expect(source?.function_node_id).toBe(target?.function_node_id);
|
||||
});
|
||||
|
||||
it("does not treat a callback body nested in a computed key as a key-time assignment input", () => {
|
||||
const graph = graphForJavaScript(`
|
||||
const accel = 0.001; let callback;
|
||||
const obj = { [(callback = () => accel)]() {} };
|
||||
`);
|
||||
expect(
|
||||
graph.relations.filter(
|
||||
(edge) => edge.properties.assignment_operator !== undefined,
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("retains an assignment owned by a callback inside a computed key", () => {
|
||||
const graph = graphForJavaScript(`
|
||||
const accel = 0.001; let speed = 6;
|
||||
const obj = { [(() => { speed = accel; return "tick"; })()]() {} };
|
||||
`);
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
||||
const assignment = graph.relations.find(
|
||||
(edge) => edge.properties.assignment_operator === "=",
|
||||
);
|
||||
expect(assignment).toBeDefined();
|
||||
const source = nodes.get(assignment!.source_node_id);
|
||||
const target = nodes.get(assignment!.target_node_id);
|
||||
expect(source?.label).toBe("accel");
|
||||
expect(target?.label).toBe("speed");
|
||||
expect(source?.function_node_id).toBeTypeOf("string");
|
||||
expect(source?.function_node_id).toBe(target?.function_node_id);
|
||||
});
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
projectSemanticRequests,
|
||||
} from "./JavaScriptSemanticGraphDataProjection.js";
|
||||
import { projectSemanticResources } from "./JavaScriptSemanticGraphResourceProjection.js";
|
||||
import { projectSemanticAssignments } from "./JavaScriptSemanticGraphAssignmentProjection.js";
|
||||
import { projectSemanticObjects } from "./JavaScriptSemanticGraphObjectProjection.js";
|
||||
import { projectSemanticValues } from "./JavaScriptSemanticGraphValueProjection.js";
|
||||
import {
|
||||
@@ -712,6 +713,8 @@ function* projectFileSteps(
|
||||
returnSiteNodes,
|
||||
argumentNodes: new Map(),
|
||||
referenceNodes: [],
|
||||
readNodes: [],
|
||||
writeNodes: [],
|
||||
callResolutions: new Map(
|
||||
ir.callSites.map((call) => [
|
||||
call.callSiteId,
|
||||
@@ -858,7 +861,11 @@ function* projectDefinitionsAndReferences(
|
||||
reference.bindingId === null
|
||||
? undefined
|
||||
: context.bindingNodes.get(reference.bindingId);
|
||||
if (expression !== null) context.referenceNodes.push(expression);
|
||||
if (expression !== null) {
|
||||
context.referenceNodes.push(expression);
|
||||
if (reference.role === "read") context.readNodes.push(expression);
|
||||
if (reference.role === "write") context.writeNodes.push(expression);
|
||||
}
|
||||
if (binding === undefined || expression === null) continue;
|
||||
if (reference.role === "read")
|
||||
addRelation(context.state, {
|
||||
@@ -977,6 +984,7 @@ const FILE_PROJECTION_PASSES: readonly ((
|
||||
) => Iterator<void, void> | void)[] = [
|
||||
projectSemanticValues,
|
||||
projectSemanticObjects,
|
||||
projectSemanticAssignments,
|
||||
projectCalls,
|
||||
projectSemanticPromises,
|
||||
projectSemanticEvents,
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface SemanticFlowProjectionContext {
|
||||
readonly callableNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
|
||||
readonly callSiteNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
|
||||
readonly returnSiteNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
|
||||
readonly readNodes: JavaScriptSemanticGraphNode[];
|
||||
readonly writeNodes: JavaScriptSemanticGraphNode[];
|
||||
readonly referenceNodes: readonly JavaScriptSemanticGraphNode[];
|
||||
readonly referenceNodesWithin: (
|
||||
range: JavaScriptSourceRange,
|
||||
|
||||
@@ -33,6 +33,10 @@ export function* projectSemanticObjects(
|
||||
operation,
|
||||
target.slotApplicability,
|
||||
);
|
||||
if (occurrence !== null) {
|
||||
if (operation.kind === "read") context.readNodes.push(occurrence);
|
||||
if (operation.kind === "write") context.writeNodes.push(occurrence);
|
||||
}
|
||||
const ends = objectRelationEnds(context, operation, occurrence, target);
|
||||
const resolved =
|
||||
operation.resolution === "complete" &&
|
||||
|
||||
@@ -17,6 +17,7 @@ interface SourceInterval {
|
||||
interface CallableInterval extends SourceInterval {
|
||||
readonly callableId: string;
|
||||
readonly priority: string;
|
||||
readonly computedKeyRange: JavaScriptSourceRange | null;
|
||||
}
|
||||
|
||||
interface SourceIntervalTree<Interval extends SourceInterval> {
|
||||
@@ -40,6 +41,7 @@ export const createSemanticCallableOwnerLookup = (
|
||||
.map((callable, order): CallableInterval => ({
|
||||
callableId: callable.callableId,
|
||||
range: callable.location,
|
||||
computedKeyRange: callable.computedKeyLocation,
|
||||
order,
|
||||
priority: positionKey(callable.location),
|
||||
}))
|
||||
@@ -65,7 +67,11 @@ export const createSemanticCallableOwnerLookup = (
|
||||
)
|
||||
continue;
|
||||
const candidate = current.entry;
|
||||
if (javascriptSourceRangeContains(candidate.range, location)) {
|
||||
if (
|
||||
javascriptSourceRangeContains(candidate.range, location) &&
|
||||
(candidate.computedKeyRange === null ||
|
||||
!javascriptSourceRangeContains(candidate.computedKeyRange, location))
|
||||
) {
|
||||
const node = nodes.get(candidate.callableId);
|
||||
const rank =
|
||||
best === undefined
|
||||
|
||||
Reference in new issue
Block a user