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
@@ -38,20 +38,24 @@ export const collectJavaScriptSemanticObjects = (
t.isSpreadElement(node) &&
parent !== null &&
t.isObjectExpression(parent)
)
) {
const identity = expressionObjectIdentity(node.argument, state);
addObjectOperation(
{
node,
kind: "spread",
ownerCallableId,
objectBindingId: expressionBindingId(node.argument, state),
objectBindingId: identity.bindingId,
targetBindingId: null,
propertyName: null,
propertyPath: identity.path,
},
state,
output,
);
else if (t.isVariableDeclarator(node) && t.isObjectPattern(node.id))
} else if (
t.isVariableDeclarator(node) &&
(t.isObjectPattern(node.id) || t.isArrayPattern(node.id))
)
collectDestructuring(node, ownerCallableId, state, output);
},
exit: (node) => {
@@ -75,8 +79,14 @@ const collectMember = (
context: ObjectCollectionContext,
): void => {
const { state, output } = context;
const name = semanticStaticPropertyKey(node.property, node.computed);
if (name === null) return;
// A chain is one property access; retaining every prefix duplicates work and
// would make full paths quadratic in deeply nested producer expressions.
if (
(t.isMemberExpression(parent) || t.isOptionalMemberExpression(parent)) &&
parent.object === node
)
return;
const identity = expressionObjectIdentity(node, state);
const write =
(t.isAssignmentExpression(parent) && parent.left === node) ||
(t.isUpdateExpression(parent) && parent.argument === node);
@@ -87,9 +97,9 @@ const collectMember = (
node,
kind: "read",
ownerCallableId,
objectBindingId: expressionBindingId(node.object, state),
objectBindingId: identity.bindingId,
targetBindingId: null,
propertyName: name,
propertyPath: identity.path,
},
state,
output,
@@ -99,9 +109,9 @@ const collectMember = (
node,
kind: write ? "write" : "read",
ownerCallableId,
objectBindingId: expressionBindingId(node.object, state),
objectBindingId: identity.bindingId,
targetBindingId: null,
propertyName: name,
propertyPath: identity.path,
},
state,
output,
@@ -114,30 +124,41 @@ const collectDestructuring = (
state: JavaScriptSemanticAnalysisState,
output: JavaScriptSemanticObjectOperation[],
): void => {
if (!t.isObjectPattern(node.id)) return;
const objectBindingId = expressionBindingId(node.init, state);
for (const property of node.id.properties) {
if (!t.isObjectProperty(property)) continue;
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name === null) continue;
const target = bindingIdentifier(property.value);
const source = expressionObjectIdentity(node.init, state);
const visit = (pattern: t.Node, path: readonly string[]): void => {
if (t.isObjectPattern(pattern)) {
for (const property of pattern.properties) {
if (!t.isObjectProperty(property)) continue;
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name !== null) visit(property.value, [...path, name]);
}
return;
}
if (t.isArrayPattern(pattern)) {
pattern.elements.forEach((element, index) => {
if (element !== null && !t.isRestElement(element))
visit(element, [...path, String(index)]);
});
return;
}
const target = bindingIdentifier(pattern);
if (target === null) return;
addObjectOperation(
{
node: property,
node: pattern,
kind: "destructure",
ownerCallableId,
objectBindingId,
objectBindingId: source.bindingId,
targetBindingId:
target === null
? null
: (resolveSemanticBindingState(state, target, target.name)
?.bindingId ?? null),
propertyName: name,
resolveSemanticBindingState(state, target, target.name)?.bindingId ??
null,
propertyPath: source.path === null ? null : [...source.path, ...path],
},
state,
output,
);
}
};
visit(node.id, []);
};
interface AddObjectOperationInput {
@@ -146,7 +167,7 @@ interface AddObjectOperationInput {
readonly ownerCallableId: string | null;
readonly objectBindingId: string | null;
readonly targetBindingId: string | null;
readonly propertyName: string | null;
readonly propertyPath: readonly string[] | null;
}
const addObjectOperation = (
@@ -161,27 +182,57 @@ const addObjectOperation = (
ownerCallableId: input.ownerCallableId,
objectBindingId: input.objectBindingId,
targetBindingId: input.targetBindingId,
propertyName: input.propertyName,
propertyPath: input.propertyPath,
resolution:
input.kind === "spread"
? input.objectBindingId === null
? "partial"
: "complete"
: input.objectBindingId === null ||
input.propertyName === null ||
(input.kind === "destructure" && input.targetBindingId === null)
? "partial"
: "complete",
input.objectBindingId === null ||
input.propertyPath === null ||
(input.kind === "destructure" && input.targetBindingId === null)
? "partial"
: "complete",
});
};
const expressionBindingId = (
interface ObjectIdentity {
readonly bindingId: string | null;
readonly path: readonly string[] | null;
}
const expressionObjectIdentity = (
node: t.Node | null | undefined,
state: JavaScriptSemanticAnalysisState,
): string | null =>
t.isIdentifier(node)
? (resolveSemanticBindingState(state, node, node.name)?.bindingId ?? null)
: null;
): ObjectIdentity => {
const path: string[] = [];
let current = node;
let staticPath = true;
while (current !== null && current !== undefined) {
if (
t.isMemberExpression(current) ||
t.isOptionalMemberExpression(current)
) {
const name = semanticStaticPropertyKey(
current.property,
current.computed,
);
if (name === null) staticPath = false;
else path.push(name);
current = current.object;
} else if (
t.isTSAsExpression(current) ||
t.isTSTypeAssertion(current) ||
t.isTSNonNullExpression(current) ||
t.isTSSatisfiesExpression(current)
)
current = current.expression;
else break;
}
return {
bindingId: t.isIdentifier(current)
? (resolveSemanticBindingState(state, current, current.name)?.bindingId ??
null)
: null,
path: staticPath ? path.reverse() : null,
};
};
const bindingIdentifier = (node: t.Node): t.Identifier | null => {
if (t.isIdentifier(node)) return node;