mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 03:15:54 +00:00
fix(javascript): resolve exported callables through lexical bindings (#1351)
* fix(javascript): resolve exported callables through lexical bindings * fix(javascript): preserve export scope and hoisted writes * test(javascript): consolidate export binding assertions * fix(javascript): account for skipped initializers and loop binding writes * fix(javascript): preserve parameters copied into body variables
This commit is contained in:
1 parent
1c0cc8a675
commit
a9f5dad5a8
6 files changed
+929
-96
No files matched your search
@@ -286,6 +286,8 @@ const createState = (program: t.Program): JavaScriptSemanticAnalysisState => {
|
||||
callables: [],
|
||||
callableNodesById: new Map(),
|
||||
moduleLinks: [],
|
||||
moduleLinkBindings: new WeakMap(),
|
||||
conditionalInitializers: new WeakSet(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -297,9 +299,34 @@ const collectDefinitions = (
|
||||
currentSemanticScope(state.scopes),
|
||||
];
|
||||
const openedScopes = new WeakMap<t.Node, number>();
|
||||
const assignments: {
|
||||
readonly node: t.Node;
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
}[] = [];
|
||||
const functionBodies: {
|
||||
readonly body: t.BlockStatement;
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
readonly parameters: t.Function["params"];
|
||||
}[] = [];
|
||||
traverseJavaScriptAst(program, {
|
||||
enter: (node, parent) => {
|
||||
enter: (node, parent, readAncestors) => {
|
||||
let parentScope = currentSemanticScope(stack);
|
||||
if (
|
||||
parent !== null &&
|
||||
(t.isObjectMethod(parent) || t.isClassMethod(parent)) &&
|
||||
parent.computed &&
|
||||
parent.key === node &&
|
||||
parentScope.parentScopeId !== null
|
||||
) {
|
||||
// A computed method key is evaluated before entering the method's
|
||||
// parameter and body environments.
|
||||
const keyScope = state.scopesById.get(parentScope.parentScopeId);
|
||||
if (keyScope !== undefined) {
|
||||
stack.push(keyScope);
|
||||
openedScopes.set(node, 1);
|
||||
parentScope = keyScope;
|
||||
}
|
||||
}
|
||||
if (
|
||||
parent !== null &&
|
||||
t.isWithStatement(parent) &&
|
||||
@@ -324,6 +351,12 @@ const collectDefinitions = (
|
||||
if (nested !== undefined) {
|
||||
stack.push(nested);
|
||||
openedScopes.set(node, (openedScopes.get(node) ?? 0) + 1);
|
||||
if (t.isBlockStatement(node) && t.isFunction(parent))
|
||||
functionBodies.push({
|
||||
body: node,
|
||||
scope: nested,
|
||||
parameters: parent.params,
|
||||
});
|
||||
}
|
||||
const scope = currentSemanticScope(stack);
|
||||
state.scopeByNode.set(node, scope);
|
||||
@@ -335,12 +368,95 @@ const collectDefinitions = (
|
||||
state,
|
||||
});
|
||||
bindInnerDeclaration(node, parent, scope, state);
|
||||
if (
|
||||
t.isVariableDeclarator(node) &&
|
||||
node.init != null &&
|
||||
parent !== null &&
|
||||
t.isVariableDeclaration(parent, { kind: "var" }) &&
|
||||
hasConditionalInitializer(readAncestors())
|
||||
)
|
||||
state.conditionalInitializers.add(node.init);
|
||||
if (
|
||||
t.isAssignmentExpression(node) ||
|
||||
t.isUpdateExpression(node) ||
|
||||
t.isForOfStatement(node) ||
|
||||
t.isForInStatement(node)
|
||||
)
|
||||
assignments.push({ node, scope });
|
||||
},
|
||||
exit: (node) => {
|
||||
for (let count = openedScopes.get(node) ?? 0; count > 0; count--)
|
||||
stack.pop();
|
||||
},
|
||||
});
|
||||
// All declarations must exist before resolving writes, including hoisted
|
||||
// functions and local declarations that shadow an outer binding.
|
||||
for (const { node, scope } of assignments) bindAssignment(node, scope, state);
|
||||
for (const { body, scope, parameters } of functionBodies)
|
||||
for (const parameter of parameters)
|
||||
for (const identifier of assignedPatternIdentifiers(
|
||||
t.isTSParameterProperty(parameter) ? parameter.parameter : parameter,
|
||||
))
|
||||
copyParameterToBody({ identifier, body, scope, state });
|
||||
};
|
||||
|
||||
const copyParameterToBody = (input: {
|
||||
readonly identifier: t.Identifier;
|
||||
readonly body: t.BlockStatement;
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
}): void => {
|
||||
const { identifier, body, scope, state } = input;
|
||||
const binding = scope.bindings.get(identifier.name);
|
||||
if (
|
||||
binding === undefined ||
|
||||
binding.kind !== "variable" ||
|
||||
binding.definitions.some(({ kind }) => kind === "function")
|
||||
)
|
||||
return;
|
||||
// A separate body var environment starts with the parameter's value. The
|
||||
// real parameter identifier retains its scope and source location; entryBody
|
||||
// records when the copy occurs without inventing an expression in the AST.
|
||||
binding.initializers.unshift({
|
||||
node: identifier,
|
||||
projection: [],
|
||||
entryBody: body,
|
||||
});
|
||||
for (let index = binding.initializers.length - 1; index >= 0; index -= 1) {
|
||||
const initializer = binding.initializers[index];
|
||||
if (
|
||||
initializer !== undefined &&
|
||||
initializer.entryBody === undefined &&
|
||||
initializer.projection.length === 0 &&
|
||||
t.isIdentifier(initializer.node) &&
|
||||
resolveSemanticBindingState(
|
||||
state,
|
||||
initializer.node,
|
||||
initializer.node.name,
|
||||
) === binding
|
||||
)
|
||||
// Direct self-initialization preserves the prior value, including any
|
||||
// actual overwrite before it. Keep the source definition/reference facts.
|
||||
binding.initializers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const hasConditionalInitializer = (ancestors: readonly t.Node[]): boolean => {
|
||||
// A hoisted var can remain undefined when control flow skips its initializer.
|
||||
// Conditions outside the owning callable/static block do not govern its body.
|
||||
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
||||
const node = ancestors[index];
|
||||
if (t.isFunction(node) || t.isProgram(node) || t.isStaticBlock(node))
|
||||
return false;
|
||||
if (
|
||||
t.isIfStatement(node) ||
|
||||
t.isLoop(node) ||
|
||||
t.isSwitchCase(node) ||
|
||||
t.isTryStatement(node)
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const bindOuterDeclaration = (
|
||||
@@ -408,7 +524,14 @@ const bindInnerDeclaration = (
|
||||
mutable: true,
|
||||
kind: "catch",
|
||||
});
|
||||
else if (t.isAssignmentExpression(node)) {
|
||||
};
|
||||
|
||||
const bindAssignment = (
|
||||
node: t.Node,
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
if (t.isAssignmentExpression(node)) {
|
||||
if (t.isIdentifier(node.left))
|
||||
addAssignment(
|
||||
node.left,
|
||||
@@ -426,8 +549,11 @@ const bindInnerDeclaration = (
|
||||
} else if (t.isUpdateExpression(node) && t.isIdentifier(node.argument))
|
||||
addAssignment(node.argument, node, scope, state);
|
||||
else if (t.isForOfStatement(node) || t.isForInStatement(node))
|
||||
for (const identifier of assignedPatternIdentifiers(node.left))
|
||||
addAssignment(identifier, node, scope, state);
|
||||
for (const pattern of t.isVariableDeclaration(node.left)
|
||||
? node.left.declarations.map(({ id }) => id)
|
||||
: [node.left])
|
||||
for (const identifier of assignedPatternIdentifiers(pattern))
|
||||
addAssignment(identifier, node, scope, state);
|
||||
};
|
||||
|
||||
const assignedPatternIdentifiers = (
|
||||
@@ -539,12 +665,42 @@ const scopeKind = (
|
||||
if (t.isStaticBlock(node)) return "static-block";
|
||||
if (
|
||||
t.isBlockStatement(node) &&
|
||||
!(parent !== null && t.isFunction(parent) && parent.body === node)
|
||||
)
|
||||
return "block";
|
||||
parent !== null &&
|
||||
t.isFunction(parent) &&
|
||||
parent.body === node
|
||||
) {
|
||||
// Parameter expressions cannot see declarations in the function body.
|
||||
// Retain the outer function scope for parameters and callable ownership;
|
||||
// body declarations belong to a separate variable environment.
|
||||
return functionParametersHaveExpressions(parent) ? "function" : undefined;
|
||||
}
|
||||
if (t.isBlockStatement(node)) return "block";
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const functionParametersHaveExpressions = (node: t.Function): boolean => {
|
||||
const pending: t.Node[] = [...node.params];
|
||||
while (pending.length > 0) {
|
||||
const parameter = pending.pop();
|
||||
if (t.isAssignmentPattern(parameter)) return true;
|
||||
if (t.isTSParameterProperty(parameter)) pending.push(parameter.parameter);
|
||||
else if (t.isRestElement(parameter)) pending.push(parameter.argument);
|
||||
else if (t.isArrayPattern(parameter)) {
|
||||
for (const element of parameter.elements)
|
||||
if (element !== null) pending.push(element);
|
||||
} else if (t.isObjectPattern(parameter)) {
|
||||
for (const property of parameter.properties) {
|
||||
if (t.isRestElement(property)) pending.push(property.argument);
|
||||
else {
|
||||
if (property.computed) return true;
|
||||
pending.push(property.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const bindImports = (
|
||||
node: t.ImportDeclaration,
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
|
||||
Reference in new issue
Block a user