Last active
May 6, 2024 21:32
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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}`) | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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