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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user