* 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>
64 lines
2.6 KiB
TypeScript
64 lines
2.6 KiB
TypeScript
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),
|
|
});
|
|
}
|
|
}
|
|
}
|