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