core/visitors/code.typescript.visitor.ts
core/visitors/code.typescript.visitor.ts is a file in GovLab Docs. 135 lines of code and 30 definitions.
import { BUILTIN_METHODS, LIB_OBJECTS } from "#configuration/constants/code.typescript.constants";
import type { CallContext, CollaboratorSpec, Recognizer, WalkerContext } from "#types/code.types";
import { communicationObjName, declName, firstDeclaration, functionOf } from "#core/selectors/code.typescript.selector";
import { declId, mkNodeId, sourceOf } from "#core/factories/code.typescript.factory";
import { inPackage, isFactoryName } from "#core/predicates/code.typescript.predicate";
import { GRAPH_LABELS } from "#configuration/strings/graph.strings";
import type { GraphStore } from "#core/stores/graph.store";
import { nodeId } from "#core/normalizers/diagram.normalizer";
import ts from "typescript";
export class CallVisitor {
readonly #checker: ts.TypeChecker;
readonly #program: ts.Program;
readonly #dirPosix: string;
readonly #store: GraphStore;
readonly #recognizers: readonly Recognizer[];
public constructor(context: WalkerContext) {
this.#checker = context.checker;
this.#program = context.program;
this.#dirPosix = context.dirPosix;
this.#store = context.store;
this.#recognizers = context.recognizers;
}
public walk(body: ts.Node, currentId: string): void {
const visit = (child: ts.Node): void => {
this.#runRecognizers(child, currentId);
if (ts.isCallExpression(child)) {
this.#classifyCall(child, currentId);
}
if (ts.isNewExpression(child)) {
this.#classifyNew(child, currentId);
}
ts.forEachChild(child, visit);
};
ts.forEachChild(body, visit);
}
#declarationOf(expression: ts.Expression): ts.Declaration | null {
const symbol = this.#checker.getSymbolAtLocation(expression);
return symbol ? firstDeclaration(symbol, this.#checker) : null;
}
#classifyNew(child: ts.NewExpression, currentId: string): void {
if (!ts.isIdentifier(child.expression)) {
return;
}
const decl = this.#declarationOf(child.expression);
if (decl === null || !ts.isClassDeclaration(decl) || !inPackage(decl, this.#dirPosix)) {
return;
}
const cid = declId(decl);
this.#store.addNode({ id: cid, kind: "collaborator", label: declName(decl), source: sourceOf(decl) });
this.#store.pushEdge({ from: currentId, kind: "dependency", label: GRAPH_LABELS.constructs, to: cid });
}
#runRecognizers(child: ts.Node, currentId: string): void {
const context = { currentId, mkNodeId, sourceOf };
for (const recognizer of this.#recognizers) {
const result = recognizer.classify(child, context);
for (const node of result?.nodes ?? []) {
this.#store.addNode(node);
}
for (const edge of result?.edges ?? []) {
this.#store.pushEdge(edge);
}
}
}
#classifyCall(child: ts.CallExpression, currentId: string): void {
const sym = this.#checker.getSymbolAtLocation(child.expression);
const decl = sym ? firstDeclaration(sym, this.#checker) : null;
const context: CallContext = { child, currentId, decl, fn: functionOf(decl), sym };
if (decl !== null && context.fn !== null && inPackage(decl, this.#dirPosix)) {
this.#handleInPackageCall(decl, context.fn, currentId);
return;
}
if (ts.isIdentifier(child.expression)) {
this.#handleIdentifierCall(context, child.expression);
return;
}
if (ts.isPropertyAccessExpression(child.expression) && ts.isIdentifier(child.expression.name)) {
this.#handlePropertyCall(context, child.expression);
return;
}
this.#bumpUnresolved(sym);
}
#bumpUnresolved(sym: ts.Symbol | undefined): void {
if (!sym) {
this.#store.bumpUnresolved();
}
}
#collaborator(spec: CollaboratorSpec): void {
this.#store.addNode({ id: spec.cid, kind: "collaborator", label: spec.label, source: spec.source });
this.#store.pushEdge({
from: spec.currentId,
kind: "call",
...(typeof spec.edgeLabel === "string" ? { label: spec.edgeLabel } : {}),
to: spec.cid,
});
}
#handleInPackageCall(decl: ts.Declaration, fn: ts.FunctionLikeDeclaration, currentId: string): void {
const cid = declId(decl);
this.#store.pushEdge({ from: currentId, kind: "call", to: cid });
if (this.#store.hasVisited(cid)) {
this.#store.pushEdge({ from: currentId, kind: "loop", to: cid });
return;
}
this.#store.markVisited(cid);
this.#store.addNode({ id: cid, kind: "method", label: declName(decl), source: sourceOf(decl) });
if (fn.body) {
this.walk(fn.body, cid);
}
}
#handleIdentifierCall(context: CallContext, callee: ts.Identifier): void {
const { child, currentId, decl, sym } = context;
const isLibGlobal = decl !== null && this.#program.isSourceFileDefaultLibrary(decl.getSourceFile());
const isCollaborator = !isLibGlobal && (decl !== null || isFactoryName(callee.text));
if (!isCollaborator) {
this.#bumpUnresolved(sym);
return;
}
this.#collaborator({ cid: mkNodeId(child), currentId, label: callee.text, source: sourceOf(child) });
}
#handlePropertyCall(context: CallContext, callee: ts.PropertyAccessExpression): void {
const { child, currentId, sym } = context;
const obj = communicationObjName(callee.expression);
const method = callee.name.text;
if (obj === null || LIB_OBJECTS.has(obj) || BUILTIN_METHODS.has(method)) {
this.#bumpUnresolved(sym);
return;
}
this.#collaborator({
cid: nodeId(`collab:${obj}`),
currentId,
edgeLabel: method,
label: obj,
source: sourceOf(child),
});
}
}