core/analyzers/code.analyzer.ts

core/analyzers/code.analyzer.ts is a file in GovLab Patterns. 104 lines of code and 30 definitions.

import type { CallEdge, CodeGraph, CodeInsight, CodeSymbol, ExternalCall, SymbolStat } from "#types/code.types";
import { ROLE_CALL, ROLE_DEFINITION } from "#core/classifiers/syntax.classifier";
import { MODULE_SCOPE } from "#configuration/constants/report.constants";
import { increment } from "#core/counters/base.counter";
import { keyOf } from "#core/formatters/definition.formatter";
import { syntaxDistribution } from "#core/analyzers/syntax.analyzer";

const FLOW_ENTRY = "entry";
const FLOW_RELAY = "relay";
const FLOW_LEAF = "leaf";
const FLOW_ISOLATED = "isolated";

const flowOf = function flowOf(inDegree: number, outDegree: number): string {
    if (inDegree === 0) {
        return outDegree === 0 ? FLOW_ISOLATED : FLOW_ENTRY;
    }
    return outDegree === 0 ? FLOW_LEAF : FLOW_RELAY;
};

const definitionKeys = function definitionKeys(symbols: readonly CodeSymbol[]): Map<string, CodeSymbol> {
    return new Map(
        symbols
            .filter((symbol) => symbol.role === ROLE_DEFINITION)
            .map((symbol) => [keyOf(symbol.file, symbol.name), symbol]),
    );
};

const definitionFiles = function definitionFiles(symbols: readonly CodeSymbol[]): Map<string, Set<string>> {
    const map = new Map<string, Set<string>>();
    for (const symbol of symbols.filter((entry) => entry.role === ROLE_DEFINITION)) {
        map.set(symbol.name, (map.get(symbol.name) ?? new Set<string>()).add(symbol.file));
    }
    return map;
};

const uniqueKeyByName = function uniqueKeyByName(defFiles: ReadonlyMap<string, Set<string>>): Map<string, string> {
    const out = new Map<string, string>();
    for (const [name, files] of defFiles) {
        const [only] = [...files];
        if (files.size === 1 && only !== undefined) {
            out.set(name, keyOf(only, name));
        }
    }
    return out;
};

const externalFrom = function externalFrom(symbol: CodeSymbol): ExternalCall {
    return { caller: symbol.enclosing, file: symbol.file, line: symbol.line, name: symbol.name };
};

export const codeGraph = function codeGraph(symbols: readonly CodeSymbol[]): CodeGraph {
    const defKeys = definitionKeys(symbols);
    const defFiles = definitionFiles(symbols);
    const unique = uniqueKeyByName(defFiles);
    const calls = symbols.filter(
        (symbol) =>
            symbol.role === ROLE_CALL &&
            !symbol.member &&
            defKeys.get(keyOf(symbol.file, symbol.enclosing))?.callable === true,
    );
    const edges = calls.flatMap((call): CallEdge[] => {
        const same = keyOf(call.file, call.name);
        const to = defKeys.has(same) ? same : (unique.get(call.name) ?? "");
        const from = keyOf(call.file, call.enclosing);
        return to === "" || to === from ? [] : [{ file: call.file, from, line: call.line, to }];
    });
    return { edges, external: calls.filter((call) => !defFiles.has(call.name)).map(externalFrom) };
};

const byPosition = function byPosition(a: SymbolStat, b: SymbolStat): number {
    return a.file.localeCompare(b.file) || a.line - b.line || a.name.localeCompare(b.name);
};

const byDegree = function byDegree(a: SymbolStat, b: SymbolStat): number {
    return b.inDegree - a.inDegree || b.outDegree - a.outDegree || byPosition(a, b);
};

const toStat = function toStat(symbol: CodeSymbol, inDegree: number, outDegree: number): SymbolStat {
    return {
        callable: symbol.callable,
        exported: symbol.exported,
        file: symbol.file,
        flow: flowOf(inDegree, outDegree),
        inDegree,
        kind: symbol.kind,
        line: symbol.line,
        local: symbol.enclosing !== MODULE_SCOPE,
        name: symbol.name,
        outDegree,
        role: symbol.role,
    };
};

export const codeInsight = function codeInsight(
    symbols: readonly CodeSymbol[],
    records: readonly Record<string, unknown>[],
): CodeInsight {
    const defKeys = definitionKeys(symbols);
    const { edges } = codeGraph(symbols);
    const inDeg = new Map<string, number>();
    const outDeg = new Map<string, number>();
    for (const edge of edges) {
        increment(outDeg, edge.from);
        increment(inDeg, edge.to);
    }
    const stats = [...defKeys].map(([key, symbol]) => toStat(symbol, inDeg.get(key) ?? 0, outDeg.get(key) ?? 0));
    const distribution = syntaxDistribution(records);
    return {
        definitions: defKeys.size,
        edges: edges.length,
        invariants: distribution.invariants,
        symbols: stats.toSorted(byDegree),
        variants: distribution.variants,
    };
};