# core/analyzers/surface.analyzer.ts

> 133 lines of code and 35 definitions.

Tree: GovLab Docs
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/docs#file-docs-core-analyzers-surface-analyzer-ts
Source text: https://banes-lab.com/source/docs/core/analyzers/surface.analyzer.ts.txt

Listed in [core/analyzers](https://banes-lab.com/api/source/docs/core/analyzers.md), after [core/analyzers/reference.analyzer.ts](https://banes-lab.com/source/docs/core/analyzers/reference.analyzer.ts.md) and before [core/analyzers/text.analyzer.ts](https://banes-lab.com/source/docs/core/analyzers/text.analyzer.ts.md).

## Definitions

- `readable` (lexical_declaration, line 71)
- `valueKind` (lexical_declaration, line 80)
- `resolveMember` (lexical_declaration, line 104)
- `collapseWhitespace` (lexical_declaration, line 27)
- `collapseBraces` (lexical_declaration, line 55)
- `namedKind` (lexical_declaration, line 75)
- `readPublicSurface` (lexical_declaration, line 129, exported)
- `exportsOf` (lexical_declaration, line 118)
- `SIGNATURE_FLAGS` (lexical_declaration, line 8)
- `TYPE_BODY_MAX` (lexical_declaration, line 14)
- `DEFAULT_EXPORT` (lexical_declaration, line 15)
- `COLLAPSED_BODY` (lexical_declaration, line 16)
- `WHITESPACE` (lexical_declaration, line 17)
- `NAMED_KINDS` (lexical_declaration, line 19)
- `inSpace` (lexical_declaration, line 29)
- `isSpace` (lexical_declaration, line 31)
- `braceEnd` (lexical_declaration, line 43)
- `depth` (lexical_declaration, line 44)
- `out` (lexical_declaration, line 56)
- `at` (lexical_declaration, line 57)
- `end` (lexical_declaration, line 59)
- `hit` (lexical_declaration, line 76)
- `aliasType` (lexical_declaration, line 87)
- `type` (lexical_declaration, line 93)
- `[callSignature]` (lexical_declaration, line 94)
- `name` (lexical_declaration, line 109)
- `target` (lexical_declaration, line 113)
- `targetDecl` (lexical_declaration, line 114)
- `moduleSymbol` (lexical_declaration, line 119)
- `decl` (lexical_declaration, line 124)
- `barrels` (lexical_declaration, line 130, exported)
- `program` (lexical_declaration, line 134, exported)
- `checker` (lexical_declaration, line 135, exported)
- `surface` (lexical_declaration, line 136, exported)
- `file` (lexical_declaration, line 137, exported)

## Contained in

- [core/analyzers](https://banes-lab.com/anatomy/docs/folder-docs-core-analyzers.md)

## Uses

- [core/predicates/program.predicate.ts](https://banes-lab.com/source/docs/core/predicates/program.predicate.ts.md)

## Used by

- [core/factories/document.factory.ts](https://banes-lab.com/source/docs/core/factories/document.factory.ts.md)

## Linked from

- [core/analyzers](https://banes-lab.com/anatomy/docs/folder-docs-core-analyzers.md)
- [core/factories](https://banes-lab.com/anatomy/docs/folder-docs-core-factories.md)
- [core/predicates](https://banes-lab.com/anatomy/docs/folder-docs-core-predicates.md)

## Source

```typescript
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),
        ),
    };
};
```
