renamed

tcmts.cli.main.renamed · #jcmtrgcg · term · runs on the server

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);
}

uses

used by