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:
Iven Kooandgensokyo-dev authored and GitHub committed 2026-10-24 10:04:38 +08:00
1 parent 2968d1e932
commit 9df94a62cd
17 files changed
+702 -35

No files matched your search

@@ -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);
});