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

@@ -1,4 +1,4 @@
import type * as t from "@babel/types";
import * as t from "@babel/types";
import { collectJavaScriptSemanticAsyncEffects } from "./javascriptSemanticAsyncEffects.js";
import { collectJavaScriptSemanticCalls } from "./javascriptSemanticCalls.js";
@@ -15,8 +15,14 @@ import { collectJavaScriptSemanticObjects } from "./javascriptSemanticObjects.js
import { collectJavaScriptSemanticResources } from "./javascriptSemanticResources.js";
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
import { createSemanticCallableIdStack } from "./javascriptSemanticCallableStack.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
type DerivedSemanticAnalysis = Pick<
JavaScriptSemanticIr,
| "assignmentFlows"
| "argumentFlows"
| "callResultFlows"
| "callReturnFlows"
@@ -86,6 +92,10 @@ export function* collectJavaScriptDerivedSemanticsSteps(
callables,
);
yield;
const assignmentFlows = yield* collectAssignmentFlowsSteps(
program,
callables,
);
const functionFingerprints =
yield* collectJavaScriptSemanticFingerprintsSteps({
state,
@@ -101,6 +111,7 @@ export function* collectJavaScriptDerivedSemanticsSteps(
parserPartial,
});
return {
assignmentFlows,
...calls,
promiseOperations,
...asyncEffects,
@@ -111,3 +122,34 @@ export function* collectJavaScriptDerivedSemanticsSteps(
functionFingerprints,
};
}
/** Keep assignment syntax in the IR so graph consumers never reparse source. */
function* collectAssignmentFlowsSteps(
program: t.Program,
callables: readonly JavaScriptSemanticCallable[],
): Generator<void, JavaScriptSemanticIr["assignmentFlows"]> {
const flows: JavaScriptSemanticIr["assignmentFlows"][number][] = [];
const stack = createSemanticCallableIdStack(
new Set(callables.map((item) => item.callableId)),
);
yield* traverseJavaScriptAstSteps(program, {
enter: (node, parent) => {
stack.enter(node, parent);
if (!t.isAssignmentExpression(node)) return;
const target = unwrapJavaScriptExpression(node.left).node;
// Destructuring has its own property-selection owner.
if (!t.isIdentifier(target) && !t.isMemberExpression(target)) return;
const source = unwrapJavaScriptExpression(node.right).node;
flows.push({
target: range(target),
source: range(source),
sourceIsReference:
t.isIdentifier(source) || t.isMemberExpression(source),
operator: node.operator,
ownerCallableId: stack.callables.at(-1) ?? null,
});
},
exit: (node) => stack.exit(node),
});
return flows;
}