fix(javascript)!: preserve property paths and uncertain slot presence

Use one explicit object/array slot model and JSON-pointer property identity across mutation analysis, return projection, and semantic graphs. Require observed presence for discriminants and resolve writes only through retained object or array receivers.

BREAKING CHANGE: projected return fields require explicit presence and semantic object operations carry full property paths.
This commit is contained in:
morluto
2026-10-09 20:03:19 +08:00
parent 63bf91405c
commit 475a0f84da
23 changed files with 831 additions and 560 deletions
@@ -1,3 +1,12 @@
import {
semanticSlotAtPath,
semanticContainer,
semanticPropertyPointer,
} from "../../domain/javascript/javascriptSemanticSlots.js";
import type {
JavaScriptSemanticSlotPresence,
JavaScriptSemanticValue,
} from "../../domain/javascript/javascriptSemanticValueTypes.js";
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticObjectOperation } from "../../domain/javascript/javascriptSemanticIr.js";
@@ -14,56 +23,115 @@ import { semanticPropertySlot } from "./JavaScriptSemanticGraphValueProjection.j
export const projectSemanticObjects = (
context: SemanticFlowProjectionContext,
): void => {
const values = new Map(
context.ir.bindings.map((binding) => [binding.bindingId, binding.value]),
);
for (const operation of context.ir.objectOperations) {
const occurrence = addObjectOccurrence(context, operation);
const slot =
operation.objectBindingId === null || operation.propertyName === null
? null
: semanticPropertySlot(
context,
operation.objectBindingId,
operation.propertyName,
);
if (operation.kind === "read")
addSemanticGraphRelation(context.state, {
source: slot,
target: occurrence,
relation: "reads-property",
resolution:
operation.resolution === "complete" ? "resolved" : "candidate",
});
else if (operation.kind === "write")
addSemanticGraphRelation(context.state, {
source: occurrence,
target: slot,
relation: "writes-property",
resolution:
operation.resolution === "complete" ? "resolved" : "candidate",
});
else if (operation.kind === "destructure")
addSemanticGraphRelation(context.state, {
source: slot,
const target = objectTarget(context, values, operation);
const ends = objectRelationEnds(context, operation, occurrence, target);
const resolved =
operation.resolution === "complete" &&
(operation.kind === "write"
? target.receiverResolved
: target.presence === "present");
addSemanticGraphRelation(context.state, {
...ends,
relation: OBJECT_RELATIONS[operation.kind],
resolution: resolved ? "resolved" : "candidate",
});
if (!resolved)
addObjectUnknown(context, operation, target.slot ?? occurrence, target);
}
};
const OBJECT_RELATIONS = {
read: "reads-property",
write: "writes-property",
destructure: "destructures",
spread: "spreads",
} as const;
interface ObjectTarget {
readonly objectNode: JavaScriptSemanticGraphNode | null | undefined;
readonly slot: JavaScriptSemanticGraphNode | null;
readonly presence: JavaScriptSemanticSlotPresence;
readonly receiverResolved: boolean;
}
const objectTarget = (
context: SemanticFlowProjectionContext,
values: ReadonlyMap<string, JavaScriptSemanticValue>,
operation: JavaScriptSemanticObjectOperation,
): ObjectTarget => {
const binding =
operation.objectBindingId === null
? undefined
: context.bindingNodes.get(operation.objectBindingId);
const value =
operation.objectBindingId === null
? undefined
: values.get(operation.objectBindingId);
if (
operation.objectBindingId === null ||
operation.propertyPath === null ||
value === undefined
)
return {
objectNode: binding,
slot: null,
presence: "unknown-coverage",
receiverResolved: false,
};
const fact = semanticSlotAtPath(value, operation.propertyPath);
if (operation.propertyPath.length === 0)
return {
objectNode: binding,
slot: null,
presence: fact.presence,
receiverResolved: semanticContainer(value) !== null,
};
const slot = semanticPropertySlot(
context,
operation.objectBindingId,
operation.propertyPath,
fact,
);
const receiver = semanticSlotAtPath(
value,
operation.propertyPath.slice(0, -1),
);
return {
objectNode: slot,
slot,
presence: fact.presence,
receiverResolved:
receiver.presence === "present" &&
semanticContainer(receiver.value) !== null,
};
};
const objectRelationEnds = (
context: SemanticFlowProjectionContext,
operation: JavaScriptSemanticObjectOperation,
occurrence: JavaScriptSemanticGraphNode | null,
target: ObjectTarget,
) => {
switch (operation.kind) {
case "read":
return { source: target.slot, target: occurrence };
case "write":
return { source: occurrence, target: target.slot };
case "destructure":
return {
source: target.slot,
target:
operation.targetBindingId === null
? undefined
: context.bindingNodes.get(operation.targetBindingId),
relation: "destructures",
resolution:
operation.resolution === "complete" ? "resolved" : "candidate",
});
else
addSemanticGraphRelation(context.state, {
source:
operation.objectBindingId === null
? undefined
: context.bindingNodes.get(operation.objectBindingId),
target: occurrence,
relation: "spreads",
resolution:
operation.resolution === "complete" ? "resolved" : "candidate",
});
if (operation.resolution !== "complete")
addObjectUnknown(context, operation, slot ?? occurrence);
};
case "spread":
return { source: target.objectNode, target: occurrence };
}
};
@@ -76,9 +144,9 @@ const addObjectOccurrence = (
roleKey: operation.objectOperationId,
location: operation.location,
label:
operation.propertyName === null
operation.propertyPath === null
? operation.kind
: `${operation.kind}:${operation.propertyName}`,
: `${operation.kind}:${semanticPropertyPointer(operation.propertyPath)}`,
functionNodeId:
operation.ownerCallableId === null
? null
@@ -86,7 +154,8 @@ const addObjectOccurrence = (
null),
properties: {
operation_kind: operation.kind,
property_name: operation.propertyName,
property_path:
operation.propertyPath === null ? null : [...operation.propertyPath],
},
});
@@ -94,15 +163,9 @@ const addObjectUnknown = (
context: SemanticFlowProjectionContext,
operation: JavaScriptSemanticObjectOperation,
node: JavaScriptSemanticGraphNode | null,
target: Pick<ObjectTarget, "presence" | "receiverResolved">,
): void => {
const relation =
operation.kind === "read"
? "reads-property"
: operation.kind === "write"
? "writes-property"
: operation.kind === "spread"
? "spreads"
: "destructures";
const relation = OBJECT_RELATIONS[operation.kind];
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown({
@@ -110,7 +173,7 @@ const addObjectUnknown = (
family: "object-flow",
relation_kinds: [relation],
reason: "ambiguous-target",
detail: `Static ${operation.kind} object identity is partial.`,
detail: `Static ${operation.kind} property ${operation.propertyPath === null ? "path is unresolved" : semanticPropertyPointer(operation.propertyPath)} has ${target.presence} presence; its receiver is ${target.receiverResolved ? "a retained container" : "unresolved or not a retained container"}; object identity is ${operation.resolution}.`,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, operation.location),
}),