core/analyzers/surface.analyzer.ts

core/analyzers/surface.analyzer.ts is a file in GovLab Docs. 133 lines of code and 35 definitions.

import type { PublicMember, ResolvedMember, SurfaceResult } from "#types/readme.types";
import { PROGRAM_OPTIONS } from "#configuration/constants/program.constants";
import type { PackageJsonLike } from "#types/code.types";
import { hasSymbolFlag } from "#core/predicates/program.predicate";
import { resolveSourceBarrels } from "#core/resolvers/barrel.resolver";
import ts from "typescript";

const SIGNATURE_FLAGS =
    ts.TypeFormatFlags.NoTruncation |
    ts.TypeFormatFlags.UseFullyQualifiedType |
    ts.TypeFormatFlags.WriteTypeArgumentsOfSignature |
    ts.TypeFormatFlags.MultilineObjectLiterals;

const TYPE_BODY_MAX = 96;
const DEFAULT_EXPORT = "default";
const COLLAPSED_BODY = "{ … }";
const WHITESPACE: ReadonlySet<string> = new Set([" ", "\t", "\n", "\r"]);

const NAMED_KINDS: readonly (readonly [number, string, string])[] = [
    [ts.SymbolFlags.Class, "class", "class"],
    [ts.SymbolFlags.Interface, "type", "interface"],
    [ts.SymbolFlags.Enum, "enum", "enum"],
    [ts.SymbolFlags.EnumMember, "enum", "enum"],
    [ts.SymbolFlags.Module, "namespace", "namespace"],
];

const collapseWhitespace = function collapseWhitespace(text: string): string {
    let out = "";
    let inSpace = false;
    for (const char of text) {
        const isSpace = WHITESPACE.has(char);
        if (!isSpace) {
            out += char;
        }
        if (isSpace && !inSpace) {
            out += " ";
        }
        inSpace = isSpace;
    }
    return out.trim();
};

const braceEnd = function braceEnd(text: string, start: number): number {
    let depth = 0;
    for (let at = start; at < text.length; at += 1) {
        depth += text.charAt(at) === "{" ? 1 : 0;
        depth -= text.charAt(at) === "}" ? 1 : 0;
        if (depth === 0 && text.charAt(at) === "}") {
            return at;
        }
    }
    return text.length;
};

const collapseBraces = function collapseBraces(text: string, threshold: number): string {
    let out = "";
    let at = 0;
    while (at < text.length) {
        const end = text.charAt(at) === "{" ? braceEnd(text, at) : -1;
        if (end !== -1 && end - at - 1 > threshold) {
            out += COLLAPSED_BODY;
            at = end + 1;
        } else {
            out += text.charAt(at);
            at += 1;
        }
    }
    return out;
};

const readable = function readable(text: string): string {
    return collapseBraces(collapseWhitespace(text), TYPE_BODY_MAX);
};

const namedKind = function namedKind(name: string, flags: number): ResolvedMember | null {
    const hit = NAMED_KINDS.find(([mask]) => hasSymbolFlag(flags, mask));
    return hit === undefined ? null : { kind: hit[1], signature: `${hit[2]} ${name}` };
};

const valueKind = function valueKind(
    name: string,
    target: ts.Symbol,
    checker: ts.TypeChecker,
    decl: ts.Declaration,
): ResolvedMember {
    if (hasSymbolFlag(target.getFlags(), ts.SymbolFlags.TypeAlias)) {
        const aliasType = checker.getDeclaredTypeOfSymbol(target);
        return {
            kind: "type",
            signature: readable(`type ${name} = ${checker.typeToString(aliasType, decl, SIGNATURE_FLAGS)}`),
        };
    }
    const type = checker.getTypeOfSymbolAtLocation(target, decl);
    const [callSignature] = type.getCallSignatures();
    if (callSignature) {
        return { kind: "fn", signature: readable(`function ${name}${checker.signatureToString(callSignature, decl)}`) };
    }
    return {
        kind: "const",
        signature: readable(`const ${name}: ${checker.typeToString(type, decl, SIGNATURE_FLAGS)}`),
    };
};

const resolveMember = function resolveMember(
    symbol: ts.Symbol,
    decl: ts.Declaration,
    checker: ts.TypeChecker,
): ResolvedMember {
    const name = symbol.getName();
    if (name === DEFAULT_EXPORT) {
        return { kind: DEFAULT_EXPORT, signature: "default export" };
    }
    const target = hasSymbolFlag(symbol.getFlags(), ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
    const targetDecl = target.getDeclarations()?.[0] ?? decl;
    return namedKind(name, target.getFlags()) ?? valueKind(name, target, checker, targetDecl);
};

const exportsOf = function exportsOf(file: ts.SourceFile, checker: ts.TypeChecker, axis: string): PublicMember[] {
    const moduleSymbol = checker.getSymbolAtLocation(file);
    if (!moduleSymbol) {
        return [];
    }
    return checker.getExportsOfModule(moduleSymbol).flatMap((symbol): PublicMember[] => {
        const decl = symbol.getDeclarations()?.[0];
        return decl ? [{ axis, name: symbol.getName(), ...resolveMember(symbol, decl, checker) }] : [];
    });
};

export const readPublicSurface = function readPublicSurface(moduleDir: string, pkg: PackageJsonLike): SurfaceResult {
    const barrels = resolveSourceBarrels(moduleDir, pkg);
    if (barrels.length === 0) {
        return { surface: [] };
    }
    const program = ts.createProgram({ options: PROGRAM_OPTIONS, rootNames: barrels.map((barrel) => barrel.barrel) });
    const checker = program.getTypeChecker();
    const surface = barrels.flatMap(({ axis, barrel }) => {
        const file = program.getSourceFile(barrel);
        return file ? exportsOf(file, checker, axis) : [];
    });
    return {
        surface: surface.toSorted(
            (left, right) => left.axis.localeCompare(right.axis) || left.name.localeCompare(right.name),
        ),
    };
};