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