setupTargets

tcmts.core.extract.setupTargets · #zcyz2hs5 · term · runs anywhere

What a top-level statement writes into, outside the functions in it: the root of what it assigns to (codes[i] = …, Impl.prototype = …, delete x.k), and what it hands to Object's and Reflect's mutators (Object.defineProperty(Proto, …), Object.freeze(types)). Its own variables (for (let i …)) don't count; a write into something else than a name (f().x = 1) is "". A method call isn't a write: server.listen() is a side effect, not setting up.

import { MUTATORS } from "#codebase/tcmts/core/extract";
import { forEachChild } from "#codebase/tcmts/core/ts";
import {
  isArrayLiteralExpression as ts_isArrayLiteralExpression, isBinaryExpression as ts_isBinaryExpression,
  isCallExpression as ts_isCallExpression, isCatchClause as ts_isCatchClause,
  isClassLikeDeclaration as ts_isClassLikeDeclaration, isDeleteExpression as ts_isDeleteExpression,
  isElementAccessExpression as ts_isElementAccessExpression,
  isFunctionLikeDeclaration as ts_isFunctionLikeDeclaration, isIdentifier as ts_isIdentifier,
  isNewExpression as ts_isNewExpression, isNonNullExpression as ts_isNonNullExpression,
  isObjectLiteralExpression as ts_isObjectLiteralExpression,
  isParenthesizedExpression as ts_isParenthesizedExpression, isPostfixUnaryExpression as ts_isPostfixUnaryExpression,
  isPrefixUnaryExpression as ts_isPrefixUnaryExpression, isPropertyAccessExpression as ts_isPropertyAccessExpression,
  isVariableDeclaration as ts_isVariableDeclaration, SyntaxKind as ts_SyntaxKind,
} from "typescript/ast";
import type { CallExpression as ts_CallExpression, Node as ts_Node } from "typescript/ast";

export function setupTargets(st: ts_Node): Set<string> {
  const written = new Set<string>();
  const own = new Set<string>();
  const root = (e: ts_Node): ts_Node => {
    while (
      ts_isPropertyAccessExpression(e) ||
      ts_isElementAccessExpression(e) ||
      ts_isParenthesizedExpression(e) ||
      ts_isNonNullExpression(e)
    )
      e = e.expression;
    return e;
  };
  const write = (target: ts_Node) => {
    const r = root(target);
    written.add(ts_isIdentifier(r) ? r.text : "");
  };
  const mutator = (n: ts_Node): n is ts_CallExpression =>
    ts_isCallExpression(n) &&
    ts_isPropertyAccessExpression(n.expression) &&
    ts_isIdentifier(n.expression.expression) &&
    MUTATORS.has(`${n.expression.expression.text}.${n.expression.name.text}`) &&
    !!n.arguments[0];
  // a value made here (`Object.freeze(Object.create(null))`, `Object.assign({}, …)`) is no one else's
  const fresh = (n: ts_Node): boolean =>
    ts_isObjectLiteralExpression(n) ||
    ts_isArrayLiteralExpression(n) ||
    ts_isNewExpression(n) ||
    (ts_isCallExpression(n) && n.expression.getText() === "Object.create") ||
    (mutator(n) && fresh(n.arguments[0]!));
  const bind = (name: ts_Node) => {
    if (ts_isIdentifier(name)) own.add(name.text);
    else forEachChild(name, bind);
  };
  const visit = (n: ts_Node): void => {
    // a function's body runs when it's called, not as the statement does
    if (ts_isFunctionLikeDeclaration(n) || ts_isClassLikeDeclaration(n)) return;
    if (ts_isVariableDeclaration(n) || ts_isCatchClause(n)) {
      const name = ts_isVariableDeclaration(n) ? n.name : n.variableDeclaration?.name;
      if (name) bind(name);
    }
    if (
      ts_isBinaryExpression(n) &&
      n.operatorToken.kind >= ts_SyntaxKind.FirstAssignment &&
      n.operatorToken.kind <= ts_SyntaxKind.LastAssignment
    )
      write(n.left);
    else if (
      (ts_isPrefixUnaryExpression(n) || ts_isPostfixUnaryExpression(n)) &&
      (n.operator === ts_SyntaxKind.PlusPlusToken || n.operator === ts_SyntaxKind.MinusMinusToken)
    )
      write(n.operand);
    else if (ts_isDeleteExpression(n)) write(n.expression);
    else if (mutator(n) && !fresh(n.arguments[0]!)) write(n.arguments[0]!);
    forEachChild(n, visit);
  };
  visit(st);
  for (const name of own) written.delete(name);
  return written;
}

uses

used by