mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
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:
@@ -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),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user