UNPKG

estree-toolkit

Version:

Traverser, scope tracker, and more tools for working with ESTree AST

1,449 lines 51.8 kB
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