import type {NodePath} from '@babel/traverse';
import type * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {getOrInsertDefault} from '../Utils/utils';
import {GeneratedSource} from './HIR';
type IdentifierInfo = {
reassigned: boolean;
reassignedByInnerFn: boolean;
referencedByInnerFn: boolean;
};
const DEFAULT_IDENTIFIER_INFO: IdentifierInfo = {
reassigned: false,
reassignedByInnerFn: false,
referencedByInnerFn: false,
};
type BabelFunction =
| NodePath<t.FunctionDeclaration>
| NodePath<t.FunctionExpression>
| NodePath<t.ArrowFunctionExpression>
| NodePath<t.ObjectMethod>;
type FindContextIdentifierState = {
currentFn: Array<BabelFunction>;
identifiers: Map<t.Identifier, IdentifierInfo>;
};
const withFunctionScope = {
enter: function (
path: BabelFunction,
state: FindContextIdentifierState,
): void {
state.currentFn.push(path);
},
exit: function (_: BabelFunction, state: FindContextIdentifierState): void {
state.currentFn.pop();
},
};
export function findContextIdentifiers(
func: NodePath<t.Function>,
): Set<t.Identifier> {
const state: FindContextIdentifierState = {
currentFn: [],
identifiers: new Map(),
};
func.traverse<FindContextIdentifierState>(
{
FunctionDeclaration: withFunctionScope,
FunctionExpression: withFunctionScope,
ArrowFunctionExpression: withFunctionScope,
ObjectMethod: withFunctionScope,
AssignmentExpression(
path: NodePath<t.AssignmentExpression>,
state: FindContextIdentifierState,
): void {
const left = path.get('left');
const currentFn = state.currentFn.at(-1) ?? null;
handleAssignment(currentFn, state.identifiers, left);
},
UpdateExpression(
path: NodePath<t.UpdateExpression>,
state: FindContextIdentifierState,
): void {
const argument = path.get('argument');
const currentFn = state.currentFn.at(-1) ?? null;
if (argument.isLVal()) {
handleAssignment(currentFn, state.identifiers, argument);
}
},
Identifier(
path: NodePath<t.Identifier>,
state: FindContextIdentifierState,
): void {
const currentFn = state.currentFn.at(-1) ?? null;
if (path.isReferencedIdentifier()) {
handleIdentifier(currentFn, state.identifiers, path);
}
},
},
state,
);
const result = new Set<t.Identifier>();
for (const [id, info] of state.identifiers.entries()) {
if (info.reassignedByInnerFn) {
result.add(id);
} else if (info.reassigned && info.referencedByInnerFn) {
result.add(id);
}
}
return result;
}
function handleIdentifier(
currentFn: BabelFunction | null,
identifiers: Map<t.Identifier, IdentifierInfo>,
path: NodePath<t.Identifier>,
): void {
const name = path.node.name;
const binding = path.scope.getBinding(name);
if (binding == null) {
return;
}
const identifier = getOrInsertDefault(identifiers, binding.identifier, {
...DEFAULT_IDENTIFIER_INFO,
});
if (currentFn != null) {
const bindingAboveLambdaScope = currentFn.scope.parent.getBinding(name);
if (binding === bindingAboveLambdaScope) {
identifier.referencedByInnerFn = true;
}
}
}
function handleAssignment(
currentFn: BabelFunction | null,
identifiers: Map<t.Identifier, IdentifierInfo>,
lvalPath: NodePath<t.LVal>,
): void {
const lvalNode = lvalPath.node;
switch (lvalNode.type) {
case 'Identifier': {
const path = lvalPath as NodePath<t.Identifier>;
const name = path.node.name;
const binding = path.scope.getBinding(name);
if (binding == null) {
break;
}
const state = getOrInsertDefault(identifiers, binding.identifier, {
...DEFAULT_IDENTIFIER_INFO,
});
state.reassigned = true;
if (currentFn != null) {
const bindingAboveLambdaScope = currentFn.scope.parent.getBinding(name);
if (binding === bindingAboveLambdaScope) {
state.reassignedByInnerFn = true;
}
}
break;
}
case 'ArrayPattern': {
const path = lvalPath as NodePath<t.ArrayPattern>;
for (const element of path.get('elements')) {
if (nonNull(element)) {
handleAssignment(currentFn, identifiers, element);
}
}
break;
}
case 'ObjectPattern': {
const path = lvalPath as NodePath<t.ObjectPattern>;
for (const property of path.get('properties')) {
if (property.isObjectProperty()) {
const valuePath = property.get('value');
CompilerError.invariant(valuePath.isLVal(), {
reason: `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
description: null,
loc: valuePath.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(currentFn, identifiers, valuePath);
} else {
CompilerError.invariant(property.isRestElement(), {
reason: `[FindContextIdentifiers] Invalid assumptions for babel types.`,
description: null,
loc: property.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(currentFn, identifiers, property);
}
}
break;
}
case 'AssignmentPattern': {
const path = lvalPath as NodePath<t.AssignmentPattern>;
const left = path.get('left');
handleAssignment(currentFn, identifiers, left);
break;
}
case 'RestElement': {
const path = lvalPath as NodePath<t.RestElement>;
handleAssignment(currentFn, identifiers, path.get('argument'));
break;
}
case 'MemberExpression': {
break;
}
default: {
CompilerError.throwTodo({
reason: `[FindContextIdentifiers] Cannot handle Object destructuring assignment target ${lvalNode.type}`,
description: null,
loc: lvalNode.loc ?? GeneratedSource,
suggestions: null,
});
}
}
}
function nonNull<T extends NonNullable<t.Node>>(
t: NodePath<T | null>,
): t is NodePath<T> {
return t.node != null;
}