core/factories/graph.typescript.factory.ts
core/factories/graph.typescript.factory.ts is a file in GovLab Docs. 178 lines of code and 42 definitions.
import type { ApiEntry, EntrySeed, ProgramAnalysis } from "#types/code.types";
import { declId, mkNodeId, sourceOf } from "#core/factories/code.typescript.factory";
import { firstDeclaration, functionOf, memberName } from "#core/selectors/code.typescript.selector";
import { inPackage, isFactoryName, isFunctionLike } from "#core/predicates/code.typescript.predicate";
import type { CallVisitor } from "#core/visitors/code.typescript.visitor";
import type { CodeGraph } from "#types/graph.types";
import type { GraphStore } from "#core/stores/graph.store";
import { MODULE_ID } from "#configuration/constants/graph.constants";
import { collectReturnedApi } from "#core/selectors/surface.selector";
import { nodeId } from "#core/normalizers/diagram.normalizer";
import ts from "typescript";
type SeedableMember = ts.ConstructorDeclaration | ts.MethodDeclaration;
const isPrivate = function isPrivate(member: ts.ClassElement): boolean {
const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined;
return (modifiers ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword);
};
const seedableMember = function seedableMember(member: ts.ClassElement): member is SeedableMember {
const callable = ts.isMethodDeclaration(member) || ts.isConstructorDeclaration(member);
return callable && member.body !== undefined && !isPrivate(member) && memberName(member) !== null;
};
export class TsGraphBuilder {
readonly #checker: ts.TypeChecker;
readonly #dirPosix: string;
readonly #program: ts.Program;
readonly #store: GraphStore;
readonly #visitor: CallVisitor;
public constructor(analysis: ProgramAnalysis, store: GraphStore, visitor: CallVisitor) {
this.#checker = analysis.checker;
this.#program = analysis.program;
this.#dirPosix = analysis.dirPosix;
this.#store = store;
this.#visitor = visitor;
}
public get unresolved(): number {
return this.#store.unresolved;
}
public result(): CodeGraph {
return this.#store.result();
}
public seedAll(seeds: readonly EntrySeed[]): void {
for (const seed of seeds) {
this.#seedOne(seed);
}
}
#visitOnce(id: string, onFirst: () => void): void {
if (this.#store.hasVisited(id)) {
return;
}
this.#store.markVisited(id);
onFirst();
}
#seedMember(member: ts.ClassElement, parentId: string): void {
if (!seedableMember(member) || !member.body) {
return;
}
const { body } = member;
const mid = mkNodeId(member);
this.#store.pushEdge({ from: parentId, kind: "call", to: mid });
this.#visitOnce(mid, () => {
this.#store.addNode({ id: mid, kind: "method", label: memberName(member) ?? "", source: sourceOf(member) });
this.#visitor.walk(body, mid);
});
}
#seedClassMethods(classDecl: ts.ClassDeclaration, parentId: string): void {
for (const member of classDecl.members) {
this.#seedMember(member, parentId);
}
}
#seedClass(classDecl: ts.ClassDeclaration, name: string): void {
const id = declId(classDecl);
this.#visitOnce(id, () => {
this.#store.addNode({ id, kind: "entry", label: name, source: sourceOf(classDecl) });
this.#seedClassMethods(classDecl, id);
});
}
#newClassIn(expr: ts.Expression, seen: Set<number>): ts.ClassDeclaration[] {
const inner = ts.isParenthesizedExpression(expr) ? expr.expression : expr;
if (!ts.isNewExpression(inner) || !ts.isIdentifier(inner.expression)) {
return [];
}
const symbol = this.#checker.getSymbolAtLocation(inner.expression);
const decl = symbol ? firstDeclaration(symbol, this.#checker) : null;
if (
decl === null ||
!ts.isClassDeclaration(decl) ||
!inPackage(decl, this.#dirPosix) ||
seen.has(decl.getStart())
) {
return [];
}
seen.add(decl.getStart());
return [decl];
}
#returnedClasses(fn: ts.FunctionLikeDeclaration): ts.ClassDeclaration[] {
const seen = new Set<number>();
if (!fn.body) {
return [];
}
return ts.isBlock(fn.body) ? this.#returnedClassesInBlock(fn.body, seen) : this.#newClassIn(fn.body, seen);
}
#returnedClassesInBlock(body: ts.Block, seen: Set<number>): ts.ClassDeclaration[] {
const out: ts.ClassDeclaration[] = [];
const visit = (node: ts.Node): void => {
if (node !== body && isFunctionLike(node)) {
return;
}
if (ts.isReturnStatement(node) && node.expression) {
out.push(...this.#newClassIn(node.expression, seen));
}
ts.forEachChild(node, visit);
};
ts.forEachChild(body, visit);
return out;
}
#seedOne(seed: EntrySeed): void {
const file = this.#program.getSourceFile(seed.barrel);
if (!file) {
return;
}
if (seed.name === MODULE_ID) {
this.#seedModule(seed, file);
return;
}
const decl = this.#exportedDecl(file, seed.name);
const fn = decl === null ? null : functionOf(decl);
if (decl !== null && fn !== null) {
this.#seedFunction(seed, decl, fn);
return;
}
if (decl !== null && ts.isClassDeclaration(decl)) {
this.#seedClass(decl, seed.label);
}
}
#seedModule(seed: EntrySeed, file: ts.SourceFile): void {
const id = nodeId(`module:${seed.barrel}`);
this.#visitOnce(id, () => {
this.#store.addNode({ id, kind: "entry", label: seed.label, source: sourceOf(file) });
this.#visitor.walk(file, id);
});
}
#exportedDecl(file: ts.SourceFile, name: string): ts.Declaration | null {
const moduleSymbol = this.#checker.getSymbolAtLocation(file);
const exported = moduleSymbol
? this.#checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.name === name)
: undefined;
return exported ? firstDeclaration(exported, this.#checker) : null;
}
#seedFunction(seed: EntrySeed, decl: ts.Declaration, fn: ts.FunctionLikeDeclaration): void {
const id = declId(decl);
this.#visitOnce(id, () => {
const kind = isFactoryName(seed.label) ? "factory" : "entry";
this.#store.addNode({ id, kind, label: seed.label, source: sourceOf(decl) });
this.#walkFunctionBody(id, fn);
});
}
#walkFunctionBody(id: string, fn: ts.FunctionLikeDeclaration): void {
if (!fn.body) {
return;
}
this.#visitor.walk(fn.body, id);
for (const api of collectReturnedApi(this.#checker, fn)) {
this.#seedApi(id, api);
}
for (const cls of this.#returnedClasses(fn)) {
this.#seedClassMethods(cls, id);
}
}
#seedApi(id: string, api: ApiEntry): void {
const mid = mkNodeId(api.fn);
this.#store.pushEdge({ from: id, kind: "call", to: mid });
this.#visitOnce(mid, () => {
this.#store.addNode({ id: mid, kind: "method", label: api.name, source: sourceOf(api.fn) });
if (api.fn.body && ts.isBlock(api.fn.body)) {
this.#visitor.walk(api.fn.body, mid);
}
});
}
}