Skip to content

Instantly share code, notes, and snippets.

@remorses
Last active May 6, 2024 21:32
Show Gist options
  • Save remorses/9a11d96f9f00d3af1388a197be2a7878 to your computer and use it in GitHub Desktop.
Save remorses/9a11d96f9f00d3af1388a197be2a7878 to your computer and use it in GitHub Desktop.
Babel `BatchRenamer`, 100x faster babel `scope.rename()` when you have many identifier to rename
// 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}`)
}
}
}
}
}
const renamer = new BatchRenamer(
path.scope, // for example body.scope to rename all variables in an AST
new Map('oldName', 'newName'),
)
renamer.rename()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment