unitOf

tcmts.cli.npm.unitOf · #vvtfp2vv · term · runs anywhere

What code in node uses, as one unit: the globals it refers to (an identifier no scope inside the module declares), Global.members, the module's top-level names (top) and imported bindings (imported), require() and import() of modules (computed ones are opaque), and eval.

import { blockNames, bound, emptyUnit, hoisted, isFunctionLike, literalText } from "#codebase/tcmts/cli/npm";
import type { Unit } from "#codebase/tcmts/cli/npm";
import { isStructuralName } from "#codebase/tcmts/core/canonicalize";
import { forEachChild } from "#codebase/tcmts/core/ts";
import {
  isBindingElement, isBlock, isCallExpression, isCaseBlock, isCatchClause, isClassDeclaration, isClassExpression,
  isForInStatement, isForOfStatement, isForStatement, isFunctionDeclaration, isFunctionExpression, isIdentifier,
  isNewExpression, isParameterDeclaration, isPropertyAccessExpression, isTypeOfExpression, isVariableDeclaration,
  isVariableDeclarationList, SyntaxKind,
} from "typescript/ast";
import type { Node } from "typescript/ast";

export function unitOf(
  node: Node,
  top: Set<string>,
  imported: Map<string, { spec: string; name: string }>,
): Unit {
  const u = emptyUnit();
  const globals = new Set<string>();
  const members = new Set<string>();
  const locals = new Set<string>();
  const imports = new Map<string, { spec: string; name: string }>();
  const scopes: Set<string>[] = [];
  const declared = (name: string) => scopes.some((s) => s.has(name));
  const use = (spec: string, name: string) => imports.set(`${spec}\n${name}`, { spec, name });
  const visit = (n: Node): void => {
    // a new scope: a function's parameters and what's hoisted in it, a block's declarations, a loop's, a catch's
    let scope: Set<string> | undefined;
    if (isFunctionLike(n)) {
      scope = new Set();
      const f = n as unknown as { name?: Node; parameters: readonly Node[]; body?: Node };
      if (isFunctionExpression(n) && n.name) scope.add(n.name.text);
      for (const p of f.parameters)
        if (isParameterDeclaration(p)) for (const b of bound(p.name)) scope.add(b);
      hoisted(f.body, scope);
      if (f.body && isBlock(f.body)) blockNames(f.body.statements, scope);
    } else if (isBlock(n)) blockNames(n.statements, (scope = new Set()));
    else if (isCaseBlock(n)) {
      scope = new Set();
      for (const c of n.clauses) blockNames(c.statements, scope);
    } else if (
      (isForStatement(n) || isForInStatement(n) || isForOfStatement(n)) &&
      n.initializer &&
      isVariableDeclarationList(n.initializer)
    ) {
      scope = new Set();
      for (const d of n.initializer.declarations) for (const b of bound(d.name)) scope.add(b);
    } else if (isCatchClause(n) && n.variableDeclaration)
      scope = new Set(bound(n.variableDeclaration.name));
    else if (isClassExpression(n) && n.name) scope = new Set([n.name.text]);
    if (scope) scopes.push(scope);
    if (isIdentifier(n)) {
      const p = n.parent;
      const declaring =
        (isVariableDeclaration(p) ||
          isBindingElement(p) ||
          isParameterDeclaration(p) ||
          isFunctionDeclaration(p) ||
          isFunctionExpression(p) ||
          isClassDeclaration(p) ||
          isClassExpression(p)) &&
        (p as unknown as { name?: Node }).name === n;
      // (`typeof window`: asking whether it's there, not using it)
      if (!declaring && !isStructuralName(n) && !isTypeOfExpression(p) && !declared(n.text)) {
        const name = n.text;
        if (imported.has(name)) {
          const i = imported.get(name)!;
          // `ns.member` of a namespace import is that export
          if (i.name === "*" && isPropertyAccessExpression(p) && p.expression === n)
            use(i.spec, p.name.text);
          else use(i.spec, i.name);
        } else if (top.has(name)) locals.add(name);
        else if (name !== "undefined" && name !== "arguments") {
          globals.add(name);
          if (isPropertyAccessExpression(p) && p.expression === n)
            members.add(`${name}.${p.name.text}`);
          if (name === "eval") u.opaque.push("eval");
          if (
            name === "Date" &&
            (isNewExpression(p) || isCallExpression(p)) &&
            (p.arguments?.length ?? 0) === 0
          )
            u.clock = true;
          if (name === "require" && isCallExpression(p) && p.expression === n) {
            const spec = literalText(p.arguments[0]);
            if (spec === undefined) u.opaque.push("a computed require()");
            else use(spec, "*");
          }
        }
      }
    }
    if (isCallExpression(n) && n.expression.kind === SyntaxKind.ImportKeyword) {
      const spec = literalText(n.arguments[0]);
      if (spec === undefined) u.opaque.push("a computed import()");
      else use(spec, "*");
    }
    forEachChild(n, visit);
    if (scope) scopes.pop();
  };
  visit(node);
  u.globals = [...globals].sort();
  u.members = [...members].sort();
  u.locals = [...locals].sort();
  u.imports = [...imports.values()];
  return u;
}

uses

used by