Babel `BatchRenamer`, 100x faster babel `scope.rename()` when you have many identifier to rename

3 min read Original article ↗
// original https://github.com/babel/babel/blob/9c77558234c87b9220604fbc1519089e2d6334e2/packages/babel-traverse/src/scope/lib/renamer.ts#L61 import splitExportDeclaration from '@babel/helper-split-export-declaration' import type { Scope } from '@babel/traverse' import { visitors } from '@babel/traverse' import { traverseNode } from '@babel/traverse/lib/traverse-node' import * as t from '@babel/types' import { NodePath, Visitor } from '@babel/core' import type { Identifier } from '@babel/types' const renameVisitor: Visitor<BatchRenamer> = { ReferencedIdentifier({ node }, state) { for (let [oldName, newName] of state.map) { // console.log(node.name, oldName, newName) if (node.name === oldName) { node.name = newName } } }, ObjectProperty({ node, scope }, state) { const { name } = node.key as Identifier if ( node.shorthand && // In destructuring the identifier is already renamed by the // AssignmentExpression|Declaration|VariableDeclarator visitor, // while in object literals it's renamed later by the // ReferencedIdentifier visitor. // (name === state.oldName || name === state.newName) && (state.map.has(name) || inverseMap(state.map).has(name)) && // Ignore shadowed bindings [...state.map.keys()].some( (oldName) => state.scope.getBindingIdentifier(oldName) === scope.getBindingIdentifier(name), ) ) { node.shorthand = false if (node.extra?.shorthand) node.extra.shorthand = false } }, // @ts-ignore 'AssignmentExpression|Declaration|VariableDeclarator'( path: NodePath< t.AssignmentPattern | t.Declaration | t.VariableDeclarator >, state, ) { if (path.isVariableDeclaration()) return const ids = path.getOuterBindingIdentifiers() for (const name in ids) { for (let [oldName, newName] of state.map) { // console.log(name, oldName, newName) if (name === oldName) ids[name].name = newName } } }, } let cache = new WeakMap() function inverseMap(map: Map<string, string>) { if (cache.has(map)) return cache.get(map) const inverse = new Map() for (let [key, value] of map) { inverse.set(value, key) } cache.set(map, inverse) return inverse } export default class BatchRenamer { constructor(scope: Scope, map: Map<string, string>) { this.map = map this.scope = scope } declare map: Map<string, string> declare scope: Scope maybeConvertFromExportDeclaration(parentDeclar: NodePath) { const maybeExportDeclar = parentDeclar.parentPath if (!maybeExportDeclar?.isExportDeclaration()) { return } if (maybeExportDeclar.isExportDefaultDeclaration()) { const { declaration } = maybeExportDeclar.node if (t.isDeclaration(declaration) && !declaration.id) { return } } if (maybeExportDeclar.isExportAllDeclaration()) { return } splitExportDeclaration( maybeExportDeclar as NodePath< Exclude<t.ExportDeclaration, t.ExportAllDeclaration> >, ) } maybeConvertFromClassFunctionExpression(path: NodePath) { return path } rename(/* Babel 7 - block?: t.Pattern | t.Scopable */) { const { scope, map } = this for (let binding of [...map.keys()].map((name) => scope.getBinding(name), )) { const path = binding!.path const parentDeclar = path.find( (path) => path.isDeclaration() || path.isFunctionExpression() || path.isClassExpression(), ) if (parentDeclar) { const bindingIds = parentDeclar.getOuterBindingIdentifiers() const oldNames = Object.keys(bindingIds) for (let oldName of oldNames) { const binding = scope.getBinding(oldName) if (binding && bindingIds[oldName] === binding.identifier) { // When we are renaming an exported identifier, we need to ensure that // the exported binding keeps the old name. this.maybeConvertFromExportDeclaration(parentDeclar) } } } } traverseNode( scope.block, visitors.explode(renameVisitor), scope, this, scope.path, // When blockToTraverse is a SwitchStatement, the discriminant // is not part of the current scope and thus should be skipped. { discriminant: true }, ) for (let [oldName, newName] of map) { if (oldName === newName) continue if (!arguments[0]) { scope.removeOwnBinding(oldName) const binding = scope.getBinding(oldName) if (binding) { binding.identifier.name = newName scope.bindings[newName] = binding } else { console.log(`binding not found for ${oldName}`) } } } } }