rules/eslint/closure-register-needs-consumer.eslint.rule.ts

rules/eslint/closure-register-needs-consumer.eslint.rule.ts is a file in GovLab Extension Host. 114 lines of code and 20 definitions.

import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { isType, nameOf, nodeAt } from "../../shared/selectors/syntax.selector.ts";
import type { ClosureGraph } from "../../types/closure.types.ts";
import { INSTANCE_SCOPED_REGISTRARS } from "../../shared/allowlists/registrar.allowlist.ts";
import { defineCheck } from "@govlab/context/check";
import { isUpperAlpha } from "@govlab/constants";
import { listener } from "../../shared/factories/listener.factory.ts";
import { loadClosureGraph } from "../../shared/loaders/graph.loader.ts";
import { suffixAfterVerb } from "../../shared/manifests/verb.manifest.ts";

const REGISTER_VERB = "register";
const CONSUMER_VERBS = ["get", "list", "has", "emit", "dispatch"];

const registrarSuffixes = function registrarSuffixes(closure: ClosureGraph): Map<string, number> {
    const counts = new Map<string, number>();
    for (const r of closure.registers) {
        if (INSTANCE_SCOPED_REGISTRARS.has(r.fn)) {
            continue;
        }
        const suffix = suffixAfterVerb(r.fn, REGISTER_VERB);
        if (suffix !== null) {
            counts.set(suffix, (counts.get(suffix) ?? 0) + 1);
        }
    }
    return counts;
};

const consumerSuffixes = function consumerSuffixes(closure: ClosureGraph): Set<string> {
    const suffixes = new Set<string>();
    for (const c of closure.consumers) {
        for (const verb of CONSUMER_VERBS) {
            const suffix = suffixAfterVerb(c.fn, verb);
            if (suffix === null) {
                continue;
            }
            suffixes.add(suffix);
            if (suffix.endsWith("es")) {
                suffixes.add(suffix.slice(0, -2));
            }
            if (suffix.endsWith("s")) {
                suffixes.add(suffix.slice(0, -1));
            }
        }
    }
    return suffixes;
};

const coversSuffix = function coversSuffix(consumed: ReadonlySet<string>, suffix: string): boolean {
    const lowered = suffix.charAt(0).toLowerCase() + suffix.slice(1);
    if (consumed.has(suffix) || consumed.has(lowered)) {
        return true;
    }
    for (const candidate of consumed) {
        if (candidate.length === 0 || candidate.length >= suffix.length) {
            continue;
        }
        if (suffix.startsWith(candidate) && isUpperAlpha(suffix.charAt(candidate.length))) {
            return true;
        }
    }
    return false;
};

const findOrphanRegistries = function findOrphanRegistries(closure: ClosureGraph): Map<string, number> {
    const consumed = consumerSuffixes(closure);
    const orphans = new Map<string, number>();
    for (const [suffix, count] of registrarSuffixes(closure)) {
        if (!coversSuffix(consumed, suffix)) {
            orphans.set(suffix, count);
        }
    }
    return orphans;
};

const GRAPH = loadClosureGraph();
const ORPHANS = GRAPH === null ? new Map<string, number>() : findOrphanRegistries(GRAPH);

export default {
    create(context: RuleContext): RuleListener {
        if (GRAPH === null) {
            return listener({
                program(_view, node) {
                    context.report({ messageId: "graphMissing", node });
                },
            });
        }
        if (ORPHANS.size === 0) {
            return {};
        }
        return listener({
            callExpression(view, node) {
                const callee = nodeAt(view, "callee");
                if (!isType(callee, "Identifier")) {
                    return;
                }
                const suffix = suffixAfterVerb(nameOf(callee), REGISTER_VERB);
                const count = suffix === null ? undefined : ORPHANS.get(suffix);
                if (suffix === null || count === undefined) {
                    return;
                }
                const payload = { count: String(count), suffix };
                context.report({ data: payload, messageId: "orphanRegistry", node });
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: ["architecture:zombie-code"], enforces: ["architecture:registry-pattern"] }),
            description:
                "Every dispatch-table registry (anything declared via `registerX(...)`) MUST have at least one consumer site (`getX(...)`, `listXs()`, `hasX(...)`). Write-only registries are dead infrastructure — consumers bypass them via direct import OR the registry serves zero purpose. Either delete the registry's consumer API, OR migrate every consumer to dispatch via `getX(id)`.",
        },
        messages: {
            graphMissing:
                "The closure graph is missing, so this rule fails closed rather than passing vacuously. Run the gate, whose auto-fix stage rebuilds the graph before linting reads it.",
            orphanRegistry:
                "Registry `{{ suffix }}` is write-only: {{ count }} register{{ suffix }}() call(s), 0 get{{ suffix }}/list{{ suffix }}s/has{{ suffix }} consumer sites. Either DELETE the consumer API and treat as a roster, OR migrate every consumer to dispatch via the registry. Cannot have both halves living simultaneously.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;