renamed
uses node:path
A definition's code, as its view writes it (without its imports), with the names names maps given theirs where
they're what the view's imports and declarations bind, as the checker resolves them: not a word in a comment, a
string, a regular expression or JSX text, a property's name (.x, { x: … }, <a x={…} />), or a local that
only shadows one; a shorthand ({ x }) is written out ({ x: x_1 }). How a definition's edited lines are written
in a file holding others too, where some of its names are others'.
import { compilerOptions, createProgram } from "#codebase/tcmts/core/program"; import { parse } from "#codebase/tcmts/core/ts"; import os from "node:os"; import path from "node:path"; import { SyntaxKind as ts_SyntaxKind } from "typescript/ast"; import type { BindingElement as ts_BindingElement, Identifier as ts_Identifier, Node as ts_Node, ShorthandPropertyAssignment as ts_ShorthandPropertyAssignment, } from "typescript/ast"; import type { Symbol as ts_Symbol } from "typescript/sync"; export function renamed(code: string, names: ReadonlyMap<string, string>, jsx = false): string { if (!names.size) return code; const file = path.join(os.tmpdir(), `tcm-renamed-${process.pid}.${jsx ? "tsx" : "ts"}`); // the names a top-level statement declares (a destructuring's bindings: not its properties' names or defaults) const declares = (st: ts_Node): ts_Identifier[] => { const bound = (n: ts_Node | undefined): ts_Identifier[] => !n ? [] : n.kind === ts_SyntaxKind.Identifier ? [n as ts_Identifier] : n.kind === ts_SyntaxKind.ObjectBindingPattern || n.kind === ts_SyntaxKind.ArrayBindingPattern ? (n as unknown as { elements: { name?: ts_Node }[] }).elements.flatMap((e) => bound(e.name), ) : []; if (st.kind === ts_SyntaxKind.VariableStatement) return ( st as unknown as { declarationList: { declarations: { name: ts_Node }[] } } ).declarationList.declarations.flatMap((d) => bound(d.name)); if (st.kind === ts_SyntaxKind.ImportDeclaration) { const ids: ts_Identifier[] = []; const walk = (n: ts_Node) => { if (n.kind === ts_SyntaxKind.Identifier) ids.push(n as ts_Identifier); else n.forEachChild(walk, (arr) => void arr.forEach(walk)); }; walk(st); return ids; } return bound((st as { name?: ts_Node }).name); }; // what it doesn't declare it imports: so a reference to one resolves to the import, whichever meaning it has // (value, type or namespace) const declared = new Set( parse(file, code) .statements.flatMap(declares) .map((id) => id.text), ); const free = [...names.keys()].filter((n) => !declared.has(n)); const head = free.length ? `import { ${free.join(", ")} } from "tcm:renamed";\n` : ""; const { program, checker } = createProgram( [file], { ...compilerOptions(), noLib: true, types: [], noResolve: true }, new Map([[file, head + code]]), ); const sf = program.getSourceFile(file)!; // what those names bind: the imports, and what the code declares at the top const ours = new Set<ts_Symbol>(); for (const id of sf.statements.flatMap(declares)) { const s = names.has(id.text) ? checker.getSymbolAtLocation(id) : undefined; if (s) ours.add(s); } const ids: ts_Identifier[] = []; const walk = (n: ts_Node) => { if (n.kind === ts_SyntaxKind.Identifier && names.has((n as ts_Identifier).text)) ids.push(n as ts_Identifier); n.forEachChild(walk, (arr) => void arr.forEach(walk)); }; walk(sf); const symbolOf = (id: ts_Identifier) => id.parent.kind === ts_SyntaxKind.ShorthandPropertyAssignment ? checker.getShorthandAssignmentValueSymbol(id.parent) : checker.getSymbolAtLocation(id); let out = head + code; for (const id of ids.reverse()) { if (id.pos < head.length) continue; const s = symbolOf(id); if (!s || !ours.has(s)) continue; const was = id.text; const to = names.get(was)!; // (a shorthand's name is the property's too, which keeps it) const p = id.parent; const short = (p.kind === ts_SyntaxKind.ShorthandPropertyAssignment && (p as ts_ShorthandPropertyAssignment).name === id) || (p.kind === ts_SyntaxKind.BindingElement && !(p as ts_BindingElement).propertyName && (p as ts_BindingElement).name === id); const start = id.getStart(sf); out = out.slice(0, start) + (short ? `${was}: ${to}` : to) + out.slice(id.end); } return out.slice(head.length); }