core/analyzers/definition.analyzer.ts
core/analyzers/definition.analyzer.ts is a file in GovLab Docs. 146 lines of code and 35 definitions.
import type { AxisBarrel, PackageJsonLike, TypeDecl, TypeEdgeContext } from "#types/code.types";
import type { TypeEdge, TypeEdgeKind, TypeGraph, TypeNode, TypeNodeKind } from "#types/graph.types";
import { PROGRAM_OPTIONS } from "#configuration/constants/program.constants";
import { hasSymbolFlag } from "#core/predicates/program.predicate";
import { nodeId } from "#core/normalizers/diagram.normalizer";
import { relFromModules } from "#core/resolvers/source.resolver";
import { resolveSourceBarrels } from "#core/resolvers/barrel.resolver";
import ts from "typescript";
const EMPTY: TypeGraph = { edges: [], nodes: [] };
const kindOfDecl = function kindOfDecl(decl: TypeDecl): TypeNodeKind {
if (ts.isClassDeclaration(decl)) {
return "class";
}
if (ts.isEnumDeclaration(decl)) {
return "enum";
}
return ts.isInterfaceDeclaration(decl) ? "interface" : "type-alias";
};
const isTypeDecl = function isTypeDecl(decl: ts.Declaration): decl is TypeDecl {
return (
ts.isInterfaceDeclaration(decl) ||
ts.isClassDeclaration(decl) ||
ts.isTypeAliasDeclaration(decl) ||
ts.isEnumDeclaration(decl)
);
};
const typeDeclOf = function typeDeclOf(symbol: ts.Symbol, checker: ts.TypeChecker): TypeDecl | null {
const target = hasSymbolFlag(symbol.getFlags(), ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
return (target.getDeclarations() ?? []).find(isTypeDecl) ?? null;
};
const declName = function declName(decl: TypeDecl): string {
return decl.name ? decl.name.text : "";
};
const declId = function declId(decl: TypeDecl): string {
return nodeId(`type:${decl.getSourceFile().fileName}:${declName(decl)}`);
};
const memberNames = function memberNames(decl: TypeDecl): string[] {
if (ts.isTypeAliasDeclaration(decl)) {
return [];
}
const members: readonly ts.NamedDeclaration[] = decl.members;
return members.flatMap((member) => (member.name && ts.isIdentifier(member.name) ? [member.name.text] : []));
};
const collectRefs = function collectRefs(node: ts.Node): ts.Identifier[] {
const out: ts.Identifier[] = ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) ? [node.typeName] : [];
ts.forEachChild(node, (child) => {
out.push(...collectRefs(child));
});
return out;
};
const targetIdFor = function targetIdFor(ref: ts.Identifier, context: TypeEdgeContext): string | null {
const symbol = context.checker.getSymbolAtLocation(ref);
const decl = symbol ? typeDeclOf(symbol, context.checker) : null;
return decl === null ? null : (context.ids.get(decl) ?? null);
};
const clauseEdges = function clauseEdges(clause: ts.HeritageClause, context: TypeEdgeContext): TypeEdge[] {
const kind: TypeEdgeKind = clause.token === ts.SyntaxKind.ImplementsKeyword ? "implements" : "extends";
return clause.types.flatMap((type) => {
const to = ts.isIdentifier(type.expression) ? targetIdFor(type.expression, context) : null;
return to !== null && to !== context.fromId ? [{ from: context.fromId, kind, to }] : [];
});
};
const heritageEdges = function heritageEdges(decl: TypeDecl, context: TypeEdgeContext): TypeEdge[] {
if (ts.isTypeAliasDeclaration(decl) || ts.isEnumDeclaration(decl)) {
return [];
}
return (decl.heritageClauses ?? []).flatMap((clause) => clauseEdges(clause, context));
};
const referenceRoots = function referenceRoots(decl: TypeDecl): ts.Node[] {
if (ts.isTypeAliasDeclaration(decl)) {
return [decl.type];
}
if (ts.isEnumDeclaration(decl)) {
return [];
}
const members: readonly ts.Node[] = decl.members;
return members.filter(
(member) => (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) && Boolean(member.type),
);
};
const referenceEdges = function referenceEdges(decl: TypeDecl, context: TypeEdgeContext): TypeEdge[] {
const kind: TypeEdgeKind = ts.isTypeAliasDeclaration(decl) ? "uses" : "has";
const targets = new Set(
referenceRoots(decl)
.flatMap(collectRefs)
.map((ref) => targetIdFor(ref, context))
.filter((to): to is string => to !== null && to !== context.fromId),
);
return [...targets].map((to) => ({ from: context.fromId, kind, to }));
};
const nodeForDecl = function nodeForDecl(decl: TypeDecl, id: string): TypeNode {
const file = decl.getSourceFile();
const position = file.getLineAndCharacterOfPosition(decl.getStart());
return {
id,
kind: kindOfDecl(decl),
label: declName(decl),
members: memberNames(decl),
source: { file: relFromModules(file.fileName), line: position.line + 1 },
};
};
const exportedTypeDecls = function exportedTypeDecls(
program: ts.Program,
checker: ts.TypeChecker,
barrel: string,
): TypeDecl[] {
const file = program.getSourceFile(barrel);
const moduleSymbol = file ? checker.getSymbolAtLocation(file) : undefined;
if (!moduleSymbol) {
return [];
}
return checker.getExportsOfModule(moduleSymbol).flatMap((symbol) => {
const decl = typeDeclOf(symbol, checker);
return decl !== null && declName(decl) !== "" ? [decl] : [];
});
};
const collectTypeDecls = function collectTypeDecls(
program: ts.Program,
barrels: readonly AxisBarrel[],
): Map<TypeDecl, string> {
const checker = program.getTypeChecker();
const decls = new Map<TypeDecl, string>();
for (const { barrel } of barrels) {
for (const decl of exportedTypeDecls(program, checker, barrel)) {
if (!decls.has(decl)) {
decls.set(decl, declId(decl));
}
}
}
return decls;
};
export const deriveTypeGraph = function deriveTypeGraph(moduleDir: string, pkg: PackageJsonLike): TypeGraph {
const barrels = resolveSourceBarrels(moduleDir, pkg);
if (barrels.length === 0) {
return EMPTY;
}
const program = ts.createProgram({ options: PROGRAM_OPTIONS, rootNames: barrels.map((barrel) => barrel.barrel) });
const checker = program.getTypeChecker();
const decls = collectTypeDecls(program, barrels);
const nodes = [...decls].map(([decl, id]) => nodeForDecl(decl, id));
const edges = [...decls].flatMap(([decl, id]) => {
const context: TypeEdgeContext = { checker, fromId: id, ids: decls };
return [...heritageEdges(decl, context), ...referenceEdges(decl, context)];
});
return { edges, nodes };
};