unitOf
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
- tcmts.cli.npm.Unit
- tcmts.cli.npm.blockNames
- tcmts.cli.npm.bound
- tcmts.cli.npm.emptyUnit
- tcmts.cli.npm.hoisted
- tcmts.cli.npm.isFunctionLike
- tcmts.cli.npm.literalText
- tcmts.core.canonicalize.isStructuralName
- tcmts.core.ts.forEachChild