fix(javascript): retain assignment and nested-default destructuring

Share source identity and one iterative pattern walker across declarations and assignments. Traverse nested default targets without losing dependency paths, preserving existing scalar-default occurrence identities and conservative unresolved slots.

Validation: 405 focused tests across five files, including public CLI dependency Evidence. Refs #4609, #4604
This commit is contained in:
morluto
2026-10-17 08:32:26 +08:00
parent c3e42385e7
commit bfb7ed0bd4
4 changed files with 187 additions and 27 deletions
@@ -65,7 +65,24 @@ export const collectJavaScriptSemanticObjects = (
t.isVariableDeclarator(node) &&
(t.isObjectPattern(node.id) || t.isArrayPattern(node.id))
)
collectDestructuring(node, ownerCallableId, state, output);
collectDestructuring(
node.id,
node.init,
ownerCallableId,
state,
output,
);
else if (
t.isAssignmentExpression(node, { operator: "=" }) &&
(t.isObjectPattern(node.left) || t.isArrayPattern(node.left))
)
collectDestructuring(
node.left,
node.right,
ownerCallableId,
state,
output,
);
},
exit: (node) => callableStack.exit(node),
});
@@ -126,46 +143,62 @@ const collectMember = (
};
const collectDestructuring = (
node: t.VariableDeclarator,
root: t.Node,
initializer: t.Node | null | undefined,
ownerCallableId: string | null,
state: JavaScriptSemanticAnalysisState,
output: JavaScriptSemanticObjectOperation[],
): void => {
const source = expressionObjectIdentity(node.init, state);
const visit = (pattern: t.Node, path: readonly string[]): void => {
const source = expressionObjectIdentity(initializer, state);
const pending: {
readonly pattern: t.Node;
readonly path: readonly string[];
}[] = [{ pattern: root, path: [] }];
while (pending.length > 0) {
const next = pending.pop();
if (next === undefined) break;
const { path } = next;
const occurrence = unwrapJavaScriptExpression(next.pattern).node;
const pattern = t.isAssignmentPattern(occurrence)
? unwrapJavaScriptExpression(occurrence.left).node
: occurrence;
if (t.isObjectPattern(pattern)) {
for (const property of pattern.properties) {
for (const property of pattern.properties.toReversed()) {
if (!t.isObjectProperty(property)) continue;
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name !== null) visit(property.value, [...path, name]);
if (name !== null)
pending.push({ pattern: property.value, path: [...path, name] });
}
return;
continue;
}
if (t.isArrayPattern(pattern)) {
pattern.elements.forEach((element, index) => {
if (element !== null && !t.isRestElement(element))
visit(element, [...path, String(index)]);
});
return;
for (let index = pattern.elements.length - 1; index >= 0; index -= 1) {
const element = pattern.elements[index];
if (
element !== null &&
element !== undefined &&
!t.isRestElement(element)
)
pending.push({ pattern: element, path: [...path, String(index)] });
}
continue;
}
const target = bindingIdentifier(pattern);
if (target === null) return;
if (!t.isIdentifier(pattern)) continue;
addObjectOperation(
{
node: pattern,
node: occurrence,
kind: "destructure",
ownerCallableId,
objectBindingId: source.bindingId,
targetBindingId:
resolveSemanticBindingState(state, target, target.name)?.bindingId ??
null,
resolveSemanticBindingState(state, pattern, pattern.name)
?.bindingId ?? null,
propertyPath: source.path === null ? null : [...source.path, ...path],
},
state,
output,
);
};
visit(node.id, []);
}
};
interface AddObjectOperationInput {
@@ -240,10 +273,3 @@ const expressionObjectIdentity = (
path: staticPath ? path.reverse() : null,
};
};
const bindingIdentifier = (node: t.Node): t.Identifier | null => {
if (t.isIdentifier(node)) return node;
if (t.isAssignmentPattern(node) && t.isIdentifier(node.left))
return node.left;
return null;
};