estree-toolkit
Version:
Traverser, scope tracker, and more tools for working with ESTree AST
1,449 lines • 51.8 kB
JavaScript
import { assertNever } from './helpers.mjs';
import { Traverser } from './traverse.mjs';
import { Binding, GlobalBinding } from './binding.mjs';
import { is } from './is.mjs';
import { builders as b } from './builders.mjs';
const scopedNodeTypes = [
'ArrowFunctionExpression',
'BlockStatement',
'CatchClause',
'ClassDeclaration',
'ClassExpression',
'DoWhileStatement',
'ForInStatement',
'ForOfStatement',
'ForStatement',
'FunctionDeclaration',
'FunctionExpression',
'Program',
'SwitchStatement',
'WhileStatement'
];
const scopedNodesTypesSet = new Set(scopedNodeTypes);
const shouldBlockStatementMakeScope = (parent) => {
/*
Don't create a new scope if `BlockStatement` is placed in these places
- for (let x in f) {} -- ForInStatement -> BlockStatement
- () => {} -- ArrowFunctionExpression -> BlockStatement
- function () {} -- FunctionExpression -> BlockStatement
- while (x) {} -- WhileStatement -> BlockStatement
- ...
But not in these cases
- { let x; { let x; } } -- BlockStatement -> BlockStatement
- { } -- Program -> BlockStatement
*/
if (parent != null &&
parent.type !== 'BlockStatement' &&
parent.type !== 'Program' &&
scopedNodesTypesSet.has(parent.type)) {
return false;
}
return true;
};
const shouldMakeScope = (path) => {
if (path.node == null)
return false;
if (path.node.type === 'BlockStatement' &&
!shouldBlockStatementMakeScope(path.parent)) {
return false;
}
return scopedNodesTypesSet.has(path.node.type);
};
const isIdentifierJSX = (name) => !(/^[a-z]/.test(name));
/*
```
[PARENT_TYPE]: {
key: KEY,
path: PATH,
state: CRAWLER_STATE
}
```
- PARENT_TYPE: Parent type of the identifier
- KEY: The identifier's key in the parent
- PATH: The NodePath of the identifier
- CRAWLER_STATE: The state object of crawler
*/
const identifierCrawlers = {
ArrowFunctionExpression(key, path, state) {
switch (key) {
case 'body':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
AssignmentExpression(key, path, state) {
switch (key) {
/* istanbul ignore next */
case 'left':
throw new Error('This should be handled by `crawlerVisitor.AssignmentExpression`');
case 'right':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
AssignmentPattern(key, path, state) {
switch (key) {
/* istanbul ignore next */
case 'left':
// TODO
// ? IDK what to do
// Appears in
// - const { a = 0 } = x;
// - function fn(a = 0) {}
// - ...
//
// `a = 0` is AssignmentPattern
// I don't think this would ever get called
throw new Error('`identifierCrawlers.AssignmentPattern` is not implemented');
case 'right':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
AwaitExpression(key, path, state) {
switch (key) {
case 'argument':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
/* istanbul ignore next */
FunctionDeclaration(key) {
switch (key) {
case 'id':
// Handled by `crawlerVisitor.ClassDeclaration`
// Do nothing
break;
default: assertNever(key);
}
},
/* istanbul ignore next */
FunctionExpression(key) {
switch (key) {
case 'id':
throw new Error('This should be handled by `scopePathCrawlers.FunctionExpression`');
default: assertNever(key);
}
},
SwitchCase(key, path, state) {
switch (key) {
case 'test':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
/* istanbul ignore next */
CatchClause(key) {
switch (key) {
case 'param':
throw new Error('This should be handled by `scopePathCrawlers.CatchClause`');
default: assertNever(key);
}
},
VariableDeclarator(key, path, state) {
switch (key) {
/* istanbul ignore next */
case 'id':
throw new Error('This should be handled by `scopePathCrawlers.VariableDeclarator`');
case 'init':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ExpressionStatement(key, path, state) {
switch (key) {
case 'expression':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
/* istanbul ignore next */
WithStatement(key, path, state) {
switch (key) {
case 'object':
state.references.push(path);
break;
default: assertNever(key);
}
},
ReturnStatement(key, path, state) {
switch (key) {
case 'argument':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
LabeledStatement() {
// Do nothing as it is handled by
// `scopePathCrawlers.{BlockStatement,ForStatement,ForInStatement,ForOfStatement}`
},
BreakStatement(key, path, state) {
switch (key) {
case 'label':
state.labelReferences.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ContinueStatement(key, path, state) {
switch (key) {
case 'label':
state.labelReferences.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
IfStatement(key, path, state) {
switch (key) {
case 'test':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
SwitchStatement(key, path, state) {
switch (key) {
case 'discriminant':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ThrowStatement(key, path, state) {
switch (key) {
case 'argument':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
WhileStatement(key, path, state) {
switch (key) {
case 'test':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
DoWhileStatement(key, path, state) {
switch (key) {
case 'test':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ForStatement(key, path, state) {
switch (key) {
case 'init':
case 'test':
case 'update':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ForInStatement(key, path, state) {
switch (key) {
/* istanbul ignore next */
case 'left':
throw new Error('This should be handled by `scopePathCrawlers.ForInStatement`');
case 'right':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ForOfStatement(key, path, state) {
switch (key) {
/* istanbul ignore next */
case 'left':
throw new Error('This should be handled by `scopePathCrawlers.ForOfStatement`');
case 'right':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ClassDeclaration(key, path, state) {
switch (key) {
/* istanbul ignore next */
case 'id':
// Handled by `crawlerVisitor.ClassDeclaration`
// Do nothing
break;
case 'superClass':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
YieldExpression(key, path, state) {
switch (key) {
case 'argument':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
UnaryExpression(key, path, state) {
switch (key) {
case 'argument':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
UpdateExpression(key, path, state) {
switch (key) {
case 'argument':
state.constantViolations.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
BinaryExpression(key, path, state) {
switch (key) {
case 'left':
case 'right':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
LogicalExpression(key, path, state) {
switch (key) {
case 'left':
case 'right':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
MemberExpression(key, path, state) {
switch (key) {
case 'object':
state.references.push(path);
break;
case 'property':
if (path.parent.computed) {
state.references.push(path);
}
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ConditionalExpression(key, path, state) {
switch (key) {
case 'test':
case 'consequent':
case 'alternate':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
CallExpression(key, path, state) {
switch (key) {
case 'callee':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
NewExpression(key, path, state) {
switch (key) {
case 'callee':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
TaggedTemplateExpression(key, path, state) {
switch (key) {
case 'tag':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ClassExpression(key, path, state) {
switch (key) {
/* istanbul ignore next */
case 'id':
throw new Error('This should be handled by `scopePathCrawlers.ClassExpression`');
case 'superClass':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
MetaProperty(key) {
switch (key) {
case 'meta':
case 'property': break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ImportExpression(key, path, state) {
switch (key) {
case 'source':
state.references.push(path);
break;
case 'options':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
Property(key, path, state) {
switch (key) {
case 'key':
if (path.parent.computed) {
state.references.push(path);
}
break;
case 'value':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
SpreadElement(key, path, state) {
switch (key) {
case 'argument':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
/* istanbul ignore next */
RestElement(key) {
switch (key) {
case 'argument':
throw new Error('This should be handled by `findVisiblePathsInPattern`');
default: assertNever(key);
}
},
MethodDefinition(key, path, state) {
switch (key) {
case 'key':
if (path.parent.computed) {
state.references.push(path);
}
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ExportDefaultDeclaration(key, path, state) {
switch (key) {
case 'declaration':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ImportSpecifier(key, path, state) {
switch (key) {
case 'imported':
/* istanbul ignore next */
if (path.parent.local == null) {
state.scope.registerBinding('module', path, path.parentPath);
}
// Sometimes parsers set imported and local to the same node
// (ImportSpecifier.imported === ImportSpecifier.local)
// in that case the `local` part would not get traversed
// because the traverser thinks that it has already traversed the `local`
// but it has just traversed the `imported`
if (path.parent.local === path.parent.imported) {
const ctx = path.ctx;
let parentPath = path.parentPath;
const parentNode = parentPath.node;
ctx.newQueue();
parentPath = parentPath.replaceWith(Object.assign({}, parentNode, {
local: Object.assign({}, parentNode.local),
imported: Object.assign({}, parentNode.imported)
}));
ctx.popQueue();
state.scope.registerBinding('module', parentPath.get('local'), parentPath);
}
break;
case 'local':
state.scope.registerBinding('module', path, path.parentPath);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ImportDefaultSpecifier(key, path, state) {
switch (key) {
case 'local':
state.scope.registerBinding('module', path, path.parentPath);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ImportNamespaceSpecifier(key, path, state) {
switch (key) {
case 'local':
state.scope.registerBinding('module', path, path.parentPath);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ExportSpecifier(key, path, state) {
switch (key) {
case 'local':
// Sometimes parsers set exported and local to the same node
// (ExportSpecifier.exported === ExportSpecifier.local)
// It messes up the renaming process, here is a workaround
// so that these two object does not reference each other
if (path.parent.local === path.parent.exported) {
const ctx = path.ctx;
let parentPath = path.parentPath;
const parentNode = parentPath.node;
ctx.newQueue();
parentPath = parentPath.replaceWith(Object.assign({}, parentNode, {
local: Object.assign({}, parentNode.local),
exported: Object.assign({}, parentNode.exported)
}));
ctx.popQueue();
state.references.push(parentPath.get('local'));
}
else {
state.references.push(path);
}
break;
case 'exported': break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ExportAllDeclaration(key) {
switch (key) {
case 'exported': break;
/* istanbul ignore next */
default: assertNever(key);
}
},
PropertyDefinition(key, path, state) {
switch (key) {
case 'key':
if (path.parent.computed) {
state.references.push(path);
}
break;
case 'value':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
ImportAttribute(key) {
switch (key) {
case 'key': break;
/* istanbul ignore next */
default: assertNever(key);
}
},
/// JSX
JSXExpressionContainer(key, path, state) {
switch (key) {
case 'expression':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
JSXSpreadAttribute(key, path, state) {
switch (key) {
case 'argument':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
JSXSpreadChild(key, path, state) {
switch (key) {
case 'expression':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(key);
}
}
};
const jsxIdentifierCrawlers = {
JSXNamespacedName(key, path, state) {
switch (key) {
case 'namespace':
if (isIdentifierJSX(path.node.name)) {
state.references.push(path);
}
break;
case 'name': break;
/* istanbul ignore next */
default: assertNever(key);
}
},
JSXAttribute(key) {
switch (key) {
case 'name': break;
/* istanbul ignore next */
default: assertNever(key);
}
},
JSXClosingElement(key, path, state) {
switch (key) {
case 'name':
if (isIdentifierJSX(path.node.name)) {
state.references.push(path);
}
break;
/* istanbul ignore next */
default: assertNever(key);
}
},
JSXMemberExpression(key, path, state) {
switch (key) {
case 'object':
state.references.push(path);
break;
case 'property': break;
/* istanbul ignore next */
default: assertNever(key);
}
},
JSXOpeningElement(key, path, state) {
switch (key) {
case 'name':
if (isIdentifierJSX(path.node.name)) {
state.references.push(path);
}
break;
/* istanbul ignore next */
default: assertNever(key);
}
}
};
/*
```
[PARENT_TYPE]: {
listKey: LIST_KEY,
path: PATH,
state: CRAWLER_STATE
}
```
- PARENT_TYPE: Parent type of the identifier
- LIST_KEY: The identifier's list key in the parent
- PATH: The NodePath of the identifier
- CRAWLER_STATE: The state object of crawler
*/
const inListIdentifierCrawlers = {
ArrayExpression(listKey, path, state) {
switch (listKey) {
case 'elements':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(listKey);
}
},
CallExpression(listKey, path, state) {
switch (listKey) {
case 'arguments':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(listKey);
}
},
NewExpression(listKey, path, state) {
switch (listKey) {
case 'arguments':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(listKey);
}
},
SequenceExpression(listKey, path, state) {
switch (listKey) {
case 'expressions':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(listKey);
}
},
TemplateLiteral(listKey, path, state) {
switch (listKey) {
case 'expressions':
state.references.push(path);
break;
/* istanbul ignore next */
default: assertNever(listKey);
}
},
/* istanbul ignore next */
ArrayPattern(listKey) {
switch (listKey) {
case 'elements':
// The code should never reach this
throw new Error('`inListIdentifierCrawler.ArrayPattern` is not implemented');
default: assertNever(listKey);
}
},
/* istanbul ignore next */
FunctionDeclaration(listKey) {
switch (listKey) {
case 'params':
throw new Error('This should be handled by `scopePathCrawlers.FunctionDeclaration`');
default: assertNever(listKey);
}
},
/* istanbul ignore next */
FunctionExpression(listKey) {
switch (listKey) {
case 'params':
throw new Error('This should be handled by `scopePathCrawlers.FunctionExpression`');
default: assertNever(listKey);
}
},
/* istanbul ignore next */
ArrowFunctionExpression(listKey) {
switch (listKey) {
case 'params':
throw new Error('This should be handled by `scopePathCrawlers.ArrowFunctionExpression`');
default: assertNever(listKey);
}
}
};
const inListJSXIdentifierCrawlers = {};
// From -
// const { a, b: [c, { d }], e: f = 0, ...g } = x;
// Returns paths to
// - a, c, d, f, g
const findVisiblePathsInPattern = (path, result) => {
switch (path.node.type) {
case 'Identifier':
result.push(path);
// Already crawled, skip it
path.skip();
break;
case 'ObjectPattern': {
const properties = path.get('properties');
for (let i = 0; i < properties.length; i++) {
const property = properties[i];
const propertyNode = property.node;
switch (propertyNode.type) {
case 'RestElement':
findVisiblePathsInPattern(property, result);
break;
case 'Property':
/* istanbul ignore else */
if (propertyNode.value != null) {
let propertyPath = property;
// Sometimes parsers set key and value to the same node
// (Property.key === Property.value)
// It messes up the renaming process, here is a workaround
// so that these two object does not reference each other
if (propertyNode.value === propertyNode.key) {
const ctx = path.ctx;
ctx.newQueue();
propertyPath = propertyPath.replaceWith(Object.assign({}, propertyNode, {
key: Object.assign({}, propertyNode.key),
value: Object.assign({}, propertyNode.value)
}));
ctx.popQueue();
}
findVisiblePathsInPattern(propertyPath.get('value'), result);
}
else /* istanbul ignore if */ if (!propertyNode.computed &&
propertyNode.key.type === 'Identifier') {
const keyPath = property.get('key');
result.push(keyPath);
// Already crawled, skip it
keyPath.skip();
}
break;
}
}
break;
}
case 'ArrayPattern': {
const aPath = path;
const elementPaths = aPath.get('elements');
const elements = aPath.node.elements;
for (let i = 0; i < elementPaths.length; i++) {
if (elements[i] == null)
continue;
findVisiblePathsInPattern(elementPaths[i], result);
}
break;
}
case 'RestElement':
findVisiblePathsInPattern(path.get('argument'), result);
break;
case 'AssignmentPattern':
findVisiblePathsInPattern(path.get('left'), result);
break;
/* istanbul ignore next */
case 'MemberExpression': break;
/* istanbul ignore next */
default: assertNever(path.node);
}
};
const registerBindingFromPattern = (path, scope, kind, bindingPath) => {
const identifierPaths = [];
findVisiblePathsInPattern(path, identifierPaths);
for (let i = 0; i < identifierPaths.length; i++) {
scope.registerBinding(kind, identifierPaths[i], bindingPath);
}
};
const registerConstantViolationFromPattern = (path, state) => {
const identifierPaths = [];
findVisiblePathsInPattern(path, identifierPaths);
for (let i = 0; i < identifierPaths.length; i++) {
state.constantViolations.push(identifierPaths[i]);
}
};
const registerVariableDeclaration = (path, scope) => {
const kind = path.node.kind;
const declarators = path.get('declarations');
for (let i = 0; i < declarators.length; i++) {
const declarator = declarators[i];
registerBindingFromPattern(declarator.get('id'), scope, kind, declarator);
}
};
const crawlerVisitor = {
Identifier: {
enter(path, state) {
var _a;
const parentType = (_a = path.parentPath.node) === null || _a === void 0 ? void 0 : _a.type;
if (path.listKey != null) {
const crawler = inListIdentifierCrawlers[parentType];
if (crawler != null) {
crawler(path.listKey, path, state);
}
}
else {
const crawler = identifierCrawlers[parentType];
if (crawler != null) {
crawler(path.key, path, state);
}
}
}
},
JSXIdentifier: {
enter(path, state) {
var _a;
const parentType = (_a = path.parentPath.node) === null || _a === void 0 ? void 0 : _a.type;
// TODO: Change this if there is any `inListJSXIdentifierCrawlers`
/* istanbul ignore if */
if (path.listKey != null) /* istanbul ignore next */ {
const crawler = inListJSXIdentifierCrawlers[parentType];
if (crawler != null) {
crawler(path.listKey, path, state);
}
}
else {
const crawler = jsxIdentifierCrawlers[parentType];
if (crawler != null) {
crawler(path.key, path, state);
}
}
}
},
AssignmentExpression: {
enter(path, state) {
registerConstantViolationFromPattern(path.get('left'), state);
}
},
VariableDeclaration: {
enter(path, state) {
registerVariableDeclaration(path, state.scope);
}
}
};
{
const cVisitors = crawlerVisitor;
const skipToChildNodeVisitor = {
enter(path, state) {
// Stop crawling whenever a scoped node is found
// children will handle the further crawling
state.childScopedPaths.push(path);
path.skip();
}
};
for (let i = 0; i < scopedNodeTypes.length; i++) {
cVisitors[scopedNodeTypes[i]] = skipToChildNodeVisitor;
}
// `crawlerVisitor` stops whenever it finds `FunctionDeclaration` or `ClassDeclaration`
// so it never gets the chance to register the declaration's binding
// We are making an exception to handle the case
cVisitors.FunctionDeclaration =
cVisitors.ClassDeclaration = {
enter(path, state) {
// ? Register `unknown` binding if `id` is null
if (path.node.id != null) {
const id = path.get('id');
state.scope.registerBinding('hoisted', id, path);
// Skip it as we have already gathered information from it
id.skip();
}
skipToChildNodeVisitor.enter.call({}, path, state);
}
};
// But things are kind of different for `BlockStatement`
// - (see the comments of `shouldBlockStatementMakeScope` function)
// This is the workaround for the case
cVisitors.BlockStatement = {
enter(path, state) {
if (shouldBlockStatementMakeScope(path.parent)) {
skipToChildNodeVisitor.enter.call({}, path, state);
}
}
};
}
const registerFunctionParams = (paths, scope) => {
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
registerBindingFromPattern(path, scope, 'param', path);
}
};
const scopePathCrawlers = {
Program: null,
FunctionDeclaration(path, { scope }) {
registerFunctionParams(path.get('params'), scope);
},
ClassDeclaration: null,
FunctionExpression(path, { scope }) {
if (path.node.id != null) {
const id = path.get('id');
scope.registerBinding('local', id, path);
id.skip();
}
registerFunctionParams(path.get('params'), scope);
},
ClassExpression(path, { scope }) {
if (path.node.id != null) {
const id = path.get('id');
scope.registerBinding('local', id, path);
id.skip();
}
},
ArrowFunctionExpression(path, { scope }) {
registerFunctionParams(path.get('params'), scope);
},
CatchClause(path, { scope }) {
if (path.has('param')) {
registerBindingFromPattern(path.get('param'), scope, 'let', path);
}
},
BlockStatement(path, { scope }) {
if (path.parent != null && path.parent.type === 'LabeledStatement') {
scope.registerLabel(path.parentPath.get('label'));
}
},
SwitchStatement: null,
WhileStatement: null,
DoWhileStatement: null,
ForStatement(path, state) {
if (path.parent != null && path.parent.type === 'LabeledStatement') {
state.scope.registerLabel(path.parentPath.get('label'));
}
if (path.node.init != null && path.node.init.type === 'VariableDeclaration') {
registerVariableDeclaration(path.get('init'), state.scope);
}
},
ForXStatement(path, state) {
if (path.parent != null && path.parent.type === 'LabeledStatement') {
state.scope.registerLabel(path.parentPath.get('label'));
}
if (path.node.left.type === 'VariableDeclaration') {
registerVariableDeclaration(path.get('left'), state.scope);
}
else if (is.pattern(path.node.left)) {
registerConstantViolationFromPattern(path.get('left'), state);
}
},
ForInStatement(path, state) {
scopePathCrawlers.ForXStatement(path, state);
},
ForOfStatement(path, state) {
scopePathCrawlers.ForXStatement(path, state);
},
};
// _|_|_| _| _|_| _|_|_| _|_|_|
// _| _| _| _| _| _|
// _| _| _|_|_|_| _|_| _|_|
// _| _| _| _| _| _|
// _|_|_| _|_|_|_| _| _| _|_|_| _|_|_|
export class Scope {
constructor(path, parentScope) {
this.children = [];
this.initialized = false;
this.bindings = Object.create(null);
this.globalBindings = Object.create(null);
this.labels = Object.create(null);
this.priv = {
prevState: null,
memoizedBindings: Object.create(null),
memoizedLabels: Object.create(null),
idMap: Object.create(null),
declaration: null
};
this.path = path;
this.parent = parentScope;
if (this.parent != null)
this.parent.children.push(this);
}
static for(path, parentScope) {
if (shouldMakeScope(path)) {
if (path.ctx.scopeCache.has(path)) {
return path.ctx.scopeCache.get(path);
}
const scope = new Scope(path, parentScope);
path.ctx.scopeCache.set(path, scope);
return scope;
}
return parentScope;
}
init() {
if (this.initialized)
return;
if (this.path.type !== 'Program') {
this.priv.idMap = this.getProgramScope().priv.idMap;
}
this.crawl();
}
// Temporarily memoize stuffs. Improves performance in deep tree
getMemoBinding(bindingName) {
const { memoizedBindings } = this.priv;
return bindingName in memoizedBindings
? memoizedBindings[bindingName]
: (memoizedBindings[bindingName] = this.getBinding(bindingName));
}
getMemoLabel(labelName) {
const { memoizedLabels } = this.priv;
return labelName in memoizedLabels
? memoizedLabels[labelName]
: (memoizedLabels[labelName] = this.getLabel(labelName));
}
clearMemo() {
this.priv.memoizedBindings = Object.create(null);
this.priv.memoizedLabels = Object.create(null);
}
getProgramScope() {
if (this.path.type === 'Program') {
return this;
}
else {
return this.path.findParent((p) => p.type === 'Program').scope;
}
}
crawl() {
var _a, _b;
/* istanbul ignore next */
if (this.path.node == null)
return;
/* istanbul ignore next */
if (this.path.removed) {
throw Error('This scope is no longer part of the AST, the containing path has been removed');
}
// Rollback previous registrations
// This will be used when re-crawling
Scope.rollbackState(this);
this.bindings = Object.create(null);
this.globalBindings = this.path.type === 'Program' ? Object.create(null) : this.getProgramScope().globalBindings;
this.labels = Object.create(null);
const state = {
references: [],
constantViolations: [],
labelReferences: [],
scope: this,
childScopedPaths: []
};
// Disable making scope for children or it will cause an infinite loop
this.path.ctx.makeScope = false;
// Create a new skip path stack so that it won't affect the user's skip path stack
this.path.ctx.newSkipPathStack();
{
const scopePathCrawler = scopePathCrawlers[this.path.node.type];
if (scopePathCrawler != null) {
scopePathCrawler(this.path, state);
}
}
Traverser.traverseNode({
node: this.path.node,
parentPath: this.path.parentPath,
ctx: this.path.ctx,
state,
visitors: crawlerVisitor,
expand: false,
visitOnlyChildren: true
});
this.path.ctx.makeScope = true;
this.path.ctx.restorePrevSkipPathStack();
this.clearMemo();
{
for (let i = 0; i < state.references.length; i++) {
const path = state.references[i];
const bindingName = path.node.name;
const binding = this.getMemoBinding(bindingName);
if (binding != null) {
binding.addReference(path);
}
else {
((_a = this.globalBindings)[bindingName] || (_a[bindingName] = new GlobalBinding({ name: bindingName }))).addReference(path);
}
}
for (let i = 0; i < state.constantViolations.length; i++) {
const path = state.constantViolations[i];
const bindingName = path.node.name;
const binding = this.getMemoBinding(bindingName);
if (binding != null) {
binding.addConstantViolation(path);
}
else {
((_b = this.globalBindings)[bindingName] || (_b[bindingName] = new GlobalBinding({ name: bindingName }))).addConstantViolation(path);
}
}
for (let i = 0; i < state.labelReferences.length; i++) {
const path = state.labelReferences[i];
const labelName = path.node.name;
const label = this.getMemoLabel(labelName);
if (label != null) {
label.references.push(path);
}
}
}
this.initialized = true;
this.priv.prevState = {
references: state.references,
constantViolations: state.constantViolations,
labelReferences: state.labelReferences
};
this.clearMemo();
for (let i = 0; i < state.childScopedPaths.length; i++) {
// Manually pass the parent scope,
// as `childScopedPaths` parent node's `scope` property may not be set in this phase
state.childScopedPaths[i].init(this);
}
}
/** Rollback all the changes contributed by this scope
* @internal
*/
static rollbackState(scope) {
const { prevState: state } = scope.priv;
if (state == null)
return;
scope.clearMemo();
for (let i = 0; i < state.references.length; i++) {
const path = state.references[i];
const bindingName = path.node.name;
const binding = scope.getMemoBinding(bindingName);
if (binding != null) {
binding.removeReference(path);
}
else {
const globalBinding = scope.globalBindings[bindingName];
if (globalBinding != null) {
globalBinding.removeReference(path);
}
}
}
for (let i = 0; i < state.constantViolations.length; i++) {
const path = state.constantViolations[i];
const bindingName = path.node.name;
const binding = scope.getMemoBinding(bindingName);
if (binding != null) {
binding.removeConstantViolation(path);
}
else {
const globalBinding = scope.globalBindings[bindingName];
if (globalBinding != null) {
globalBinding.removeConstantViolation(path);
}
}
}
for (let i = 0; i < state.labelReferences.length; i++) {
const path = state.labelReferences[i];
const labelName = path.node.name;
const label = scope.getMemoLabel(labelName);
if (label != null) {
const idx = label.references.findIndex((x) => x === path);
if (idx > -1)
label.references.splice(idx, 1);
}
}
const globalNames = Object.keys(scope.globalBindings);
for (let i = 0; i < globalNames.length; i++) {
const name = globalNames[i];
const global = scope.globalBindings[name];
if (global.references.length === 0 && global.constantViolations.length === 0) {
scope.globalBindings[name] = undefined;
delete scope.globalBindings[name];
}
}
}
/** @internal */
static recursiveRollback(scope) {
for (let i = 0; i < scope.children.length; i++) {
Scope.recursiveRollback(scope.children[i]);
}
Scope.rollbackState(scope);
}
/** @internal */
static handleRemoval(scope, path) {
if (path === scope.path) {
Scope.recursiveRollback(scope);
if (scope.parent != null) {
const { children } = scope.parent;
const idx = children.indexOf(scope);
if (idx > -1)
children.splice(idx, 1);
}
}
else {
for (let i = 0; i < scope.children.length; i++) {
const child = scope.children[i];
if (child.path.isDescendantOf(path)) {
Scope.recursiveRollback(child);
const idx = scope.children.indexOf(child);
if (idx > -1)
scope.children.splice(idx, 1);
}
}
}
}
registerBinding(kind, identifierPath, bindingPath) {
const bindingName = identifierPath.node.name;
const binding = this.getOwnBinding(bindingName);
if (binding != null) {
binding.addConstantViolation(identifierPath);
return;
}
this.bindings[bindingName] = new Binding({
kind: kind,
name: bindingName,
scope: this,
identifierPath,
path: bindingPath
});
}
hasOwnBinding(name) {
return name in this.bindings;
}
getOwnBinding(name) {
return this.bindings[name];
}
hasBinding(name) {
return this.getBinding(name) != null;
}
getBinding(name) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let scope = this;
while (scope != null) {
if (scope.hasOwnBinding(name)) {
return scope.getOwnBinding(name);
}
scope = scope.parent;
}
}
getAllBindings(...kind) {
const result = Object.create(null);
const kindLength = kind.length;
const kindSet = new Set(kind);
// eslint-disable-next-line @typescript-eslint/no-this-alias
let scope = this;
while (scope != null) {
for (const name in scope.bindings) {
if (!(name in result)) {
if (kindLength === 0 || (kindLength && kindSet.has(scope.bindings[name].kind))) {
result[name] = scope.bindings[name];
}
}
}
scope = scope.parent;
}
return result;
}
hasGlobalBinding(name) {
return this.getGlobalBinding(name) != null;
}
getGlobalBinding(name) {
return this.getProgramScope().globalBindings[name];
}
/** @internal */
registerLabel(path) {
const labelName = path.node.name;
/* istanbul ignore next */
if (this.hasLabel(labelName)) {
// Label has already been declared
// The parser should already inform the user about this
// there's nothing to do in our side
return;
}
this.labels[labelName] = {
path,
references: []
};
}
hasLabel(name) {
return this.getLabel(name) != null;
}
getLabel(name) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let scope = this;
while (scope != null) {
if (scope.labels[name] != null) {
return scope.labels[name];
}
scope = scope.parent;
}
}
generateUid(name = '_tmp') {
var _a;
const allIDs = Object.keys(this.getAllBindings())
.concat(Object.keys(this.globalBindings))
.concat(Object.keys(this.priv.idMap));
(_a = this.priv.idMap)[name] || (_a[name] = 1);
let fName = name = name.replace(/[^a-zA-Z_]+/g, '');
while (allIDs.includes(fName)) {
fName = name + ++this.priv.idMap[name];
}
return fName;
}
generateUidIdentifier(name) {
return b.identifier(this.generateUid(name));
}
generateDeclaredUidIdentifier(name) {
let declaratorPath;
const { ctx } = this.path;
ctx.newSkipPathStack();
ctx.newQueue();
if (this.priv.declaration == null) {
// Get the closest block statement
let block = null;
switch (this.path.type) {
case 'ArrowFunctionExpression':
{
const path = this.path;
const body = path.get('body');
if (body.type === 'BlockStatement') {
block = body;
}
else {
const bodyNode = Object.assign({}, body.node);
block = body.replaceWith(b.blockStatement([b.returnStatement(bodyNode)]));
}
}
break;
case 'Program':
case 'BlockStatement':
block = this.path;
break;
case 'SwitchStatement':
case 'ClassDeclaration':
case 'ClassExpression':
ctx.restorePrevSkipPathStack();
ctx.popQueue();
return this.parent.generateDeclaredUidIdentifier(name);
case 'DoWhileStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'ForStatement':
case 'WhileStatement':
{
const path = this.path;
const body = path.get('body');
if (body.type === 'BlockStatement') {
block = body;
}
else {
const bodyNode = Object.assign({}, body.node);
block = body.replaceWith(b.blockStatement([bodyNode]));
}
}
break;
case 'CatchClause':
case 'FunctionDeclaration':
case 'FunctionExpression':
block = this.path.get('body');
break;
/* istanbul ignore next */
case null: break;
/* istanbul ignore next */
default: assertNever(this.path.type);
}
const declarationNode = b.variableDeclaration('var', [b.variableDeclarator(this.generateUidIdentifier(name))]);
const [declarationPath] = block
.unshiftContainer('body', [declarationNode]);
this.priv.declaration = declarationPath;
declaratorPath = declarationPath.get('declarations')[0];
}
else {
[declaratorPath] = this.priv.declaration.pushContainer('declarations', [b.variableDeclarator(this.generateUidIdentifier(name))]);
}
const identifier = declaratorPath.get('id');
this.registerBinding('var', identifier, declaratorPath);
ctx.restorePrevSkipPathStack();
ctx.popQueue();
return Object.assign({}, identifier.node);
}
/** @internal */
renameConsideringParent(path, newName) {
var _a, _b, _c, _d, _e, _f;
const parent = path.parent;
if