Created
June 21, 2025 04:04
-
-
Save mary-ext/c8190a647993a8af91f935eb4435f055 to your computer and use it in GitHub Desktop.
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
import type { Identifier, LocalIdentifier } from './estree.ts'; | |
const isLocalIdentifier = (ident: Identifier | LocalIdentifier): ident is LocalIdentifier => { | |
return 'preferredName' in ident; | |
}; | |
interface RenameContext { | |
usedNames: Set<string>; | |
} | |
export class Scope { | |
readonly parent: Scope | undefined; | |
readonly children: Scope[] = []; | |
readonly declarations = new Set<Identifier>(); | |
readonly references = new Set<Identifier>(); | |
constructor(parent?: Scope) { | |
if (parent) { | |
this.parent = parent; | |
parent.children.push(this); | |
} | |
} | |
begin(): Scope { | |
return new Scope(this); | |
} | |
end(): void { | |
if (!this.parent) { | |
const context: RenameContext = { | |
usedNames: new Set(), | |
}; | |
this.#rename(context); | |
} | |
} | |
declare(identifier: Identifier): void { | |
this.declarations.add(identifier); | |
} | |
reference(identifier: Identifier): void { | |
this.references.add(identifier); | |
} | |
#rename(context: RenameContext): void { | |
// Step 1: Reserve all global names | |
this.#reserveGlobalNames(context); | |
// Step 2: Group local identifiers by preferred name | |
const groups = this.#groupLocalIdentifiers(); | |
// Step 3: Assign names to each group | |
for (const [preferredName, identifiers] of groups) { | |
this.#assignNames(context, preferredName, identifiers); | |
} | |
} | |
#reserveGlobalNames(context: RenameContext): void { | |
this.#walk((scope) => { | |
// Check declarations | |
for (const ident of scope.declarations) { | |
if (!isLocalIdentifier(ident)) { | |
context.usedNames.add(ident.name); | |
} | |
} | |
// Check references | |
for (const ident of scope.references) { | |
if (!isLocalIdentifier(ident)) { | |
context.usedNames.add(ident.name); | |
} | |
} | |
}); | |
} | |
#groupLocalIdentifiers(): Map<string, LocalIdentifier[]> { | |
const groups = new Map<string, LocalIdentifier[]>(); | |
const seenIds = new Set<LocalIdentifier>(); | |
this.#walk((scope) => { | |
// Check declarations | |
for (const ident of scope.declarations) { | |
if (isLocalIdentifier(ident) && !seenIds.has(ident)) { | |
const key = ident.preferredName; | |
const array = groups.get(key); | |
if (!array) { | |
groups.set(key, [ident]); | |
} else { | |
array.push(ident); | |
} | |
seenIds.add(ident); | |
} | |
} | |
// Check references | |
for (const ident of scope.references) { | |
if (isLocalIdentifier(ident) && !seenIds.has(ident)) { | |
const key = ident.preferredName; | |
const array = groups.get(key); | |
if (!array) { | |
groups.set(key, [ident]); | |
} else { | |
array.push(ident); | |
} | |
seenIds.add(ident); | |
} | |
} | |
}); | |
return groups; | |
} | |
#assignNames(context: RenameContext, preferredName: string, identifiers: LocalIdentifier[]): void { | |
const hasGlobalConflict = context.usedNames.has(preferredName); | |
const conflicts = this.#findConflicts(identifiers); | |
if (conflicts.length === 0 && !hasGlobalConflict) { | |
// No conflicts - all can use preferred name | |
for (let idx = 0, len = identifiers.length; idx < len; idx++) { | |
identifiers[idx].name = preferredName; | |
} | |
context.usedNames.add(preferredName); | |
} else { | |
// Has conflicts - prioritize declarations over references | |
const declarations = identifiers.filter((id) => this.#isDeclaration(id)); | |
const references = identifiers.filter((id) => !this.#isDeclaration(id)); | |
let suffix = hasGlobalConflict ? 1 : 0; | |
// Assign names to declarations first | |
for (let idx = 0, len = declarations.length; idx < len; idx++) { | |
const ident = declarations[idx]; | |
const name = suffix === 0 ? preferredName : `${preferredName}$${suffix}`; | |
ident.name = name; | |
context.usedNames.add(name); | |
suffix++; | |
} | |
// Then assign names to references | |
for (let idx = 0, len = references.length; idx < len; idx++) { | |
const ident = references[idx]; | |
const name = suffix === 0 ? preferredName : `${preferredName}$${suffix}`; | |
ident.name = name; | |
context.usedNames.add(name); | |
suffix++; | |
} | |
} | |
} | |
#isDeclaration(identifier: LocalIdentifier): boolean { | |
return this.#walk((scope) => scope.declarations.has(identifier)); | |
} | |
#findConflicts(identifiers: LocalIdentifier[]): [LocalIdentifier, LocalIdentifier][] { | |
const conflicts: [LocalIdentifier, LocalIdentifier][] = []; | |
// Build a map of scope -> set of identifiers present in that scope | |
const scopeToIdentifiers = new Map<Scope, Set<LocalIdentifier>>(); | |
this.#walk((scope) => { | |
const presentIds = new Set<LocalIdentifier>(); | |
for (let idx = 0, len = identifiers.length; idx < len; idx++) { | |
const ident = identifiers[idx]; | |
if (scope.declarations.has(ident) || scope.references.has(ident)) { | |
presentIds.add(ident); | |
} | |
} | |
if (presentIds.size > 1) { | |
scopeToIdentifiers.set(scope, presentIds); | |
} | |
}); | |
// Find conflicts from scopes that have multiple identifiers | |
for (const presentIdents of scopeToIdentifiers.values()) { | |
const idents = Array.from(presentIdents); | |
for (let i = 0, l = idents.length; i < l; i++) { | |
for (let j = i + 1; j < l; j++) { | |
conflicts.push([idents[i], idents[j]]); | |
} | |
} | |
} | |
return conflicts; | |
} | |
#walk(fn: (scope: Scope) => boolean | void): boolean { | |
if (fn(this)) { | |
return true; | |
} | |
const children = this.children; | |
for (let idx = 0, len = children.length; idx < len; idx++) { | |
if (children[idx].#walk(fn)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment