codemods/analyzers/field.reference.analyzer.ts
codemods/analyzers/field.reference.analyzer.ts is a file in GovLab Extension Host. 155 lines of code and 30 definitions.
import { declaredTarget, innerTypes, keyOf } from "../selectors/field.selector.ts";
import ts from "typescript";
type Marker = (checker: ts.TypeChecker, node: ts.Node, read: Set<string>) => boolean;
const GENERIC_READERS: ReadonlySet<string> = new Set(["entries", "keys", "values", "stringify"]);
const markSymbol = function markSymbol(
checker: ts.TypeChecker,
symbol: ts.Symbol | undefined,
read: Set<string>,
): void {
const roots = symbol === undefined ? [] : checker.getRootSymbols(symbol);
const keys = roots.flatMap((root) => (root.declarations ?? []).map(keyOf));
for (const key of keys) {
if (key !== null) {
read.add(key);
}
}
};
const markWhole = function markWhole(
checker: ts.TypeChecker,
node: ts.Node,
read: Set<string>,
kept: ReadonlySet<string> | null = null,
): void {
const properties = innerTypes(checker, checker.getTypeAtLocation(node)).flatMap((type) =>
checker.getPropertiesOfType(type),
);
for (const property of properties) {
if (kept === null || kept.has(property.name)) {
markSymbol(checker, property, read);
}
}
};
const keptBySpread = function keptBySpread(
checker: ts.TypeChecker,
node: ts.SpreadAssignment,
): ReadonlySet<string> | null {
const contextual = declaredTarget(checker, node.parent);
if (contextual === undefined) {
return null;
}
const properties = innerTypes(checker, contextual).flatMap((type) => checker.getPropertiesOfType(type));
return new Set(properties.map((property) => property.name));
};
const markAccess: Marker = (checker, node, read) => {
if (!ts.isPropertyAccessExpression(node)) {
return false;
}
markSymbol(checker, checker.getSymbolAtLocation(node.name), read);
return true;
};
const markKeys: Marker = (checker, node, read) => {
if (!ts.isElementAccessExpression(node)) {
return false;
}
const target = checker.getTypeAtLocation(node.expression);
for (const key of innerTypes(checker, checker.getTypeAtLocation(node.argumentExpression))) {
if (key.isStringLiteral()) {
markSymbol(checker, target.getProperty(key.value), read);
}
}
return true;
};
const markBinding: Marker = (checker, node, read) => {
if (!ts.isBindingElement(node) || !ts.isObjectBindingPattern(node.parent)) {
return false;
}
const name = node.propertyName ?? node.name;
const owner = checker.getTypeAtLocation(node.parent);
markSymbol(checker, ts.isIdentifier(name) ? owner.getProperty(name.text) : undefined, read);
return true;
};
const markSpreadAssignment: Marker = (checker, node, read) => {
if (!ts.isSpreadAssignment(node)) {
return false;
}
markWhole(checker, node.expression, read, keptBySpread(checker, node));
return true;
};
const markSpreadElement: Marker = (checker, node, read) => {
if (!ts.isSpreadElement(node)) {
return false;
}
markWhole(checker, node.expression, read);
return true;
};
const isGenericRead = function isGenericRead(node: ts.Node): node is ts.CallExpression {
const callee = ts.isCallExpression(node) ? node.expression : null;
return callee !== null && ts.isPropertyAccessExpression(callee) && GENERIC_READERS.has(callee.name.text);
};
const markGenericCall: Marker = (checker, node, read) => {
if (!isGenericRead(node)) {
return false;
}
for (const argument of node.arguments) {
markWhole(checker, argument, read);
}
return true;
};
const MARKERS: readonly Marker[] = [
markAccess,
markKeys,
markBinding,
markSpreadAssignment,
markSpreadElement,
markGenericCall,
];
const isArgument = function isArgument(parent: ts.Node, node: ts.Node): boolean {
return (
ts.isExpression(node) &&
(ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
parent.arguments?.includes(node) === true
);
};
const isInitializer = function isInitializer(parent: ts.Node, node: ts.Node): boolean {
return (
ts.isExpression(node) &&
(ts.isVariableDeclaration(parent) || ts.isPropertyAssignment(parent)) &&
parent.initializer === node
);
};
const HANDING_PARENTS: readonly ((parent: ts.Node, node: ts.Node) => boolean)[] = [
isArgument,
isInitializer,
(parent) => ts.isReturnStatement(parent) || ts.isArrayLiteralExpression(parent),
(parent, node) => ts.isArrowFunction(parent) && parent.body === node,
];
const isHandedOn = function isHandedOn(node: ts.Node): node is ts.Expression {
const literal = ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node);
return ts.isExpression(node) && !literal && HANDING_PARENTS.some((test) => test(node.parent, node));
};
const markConverted = function markConverted(checker: ts.TypeChecker, node: ts.Expression, read: Set<string>): void {
const contextual = checker.getContextualType(node);
if (contextual === undefined) {
return;
}
const wanted = innerTypes(checker, contextual).flatMap((type) => checker.getPropertiesOfType(type));
const properties = innerTypes(checker, checker.getTypeAtLocation(node)).flatMap((type) =>
checker.getPropertiesOfType(type),
);
for (const property of properties) {
const counterpart = wanted.find((candidate) => candidate.name === property.name);
if (counterpart !== undefined && counterpart !== property) {
markSymbol(checker, property, read);
}
}
};
export const visitRead = function visitRead(checker: ts.TypeChecker, node: ts.Node, read: Set<string>): void {
MARKERS.some((mark) => mark(checker, node, read));
if (isHandedOn(node)) {
markConverted(checker, node, read);
}
ts.forEachChild(node, (child) => {
visitRead(checker, child, read);
});
};