rules/eslint/closure-registry-consumer-naming.eslint.rule.ts
rules/eslint/closure-registry-consumer-naming.eslint.rule.ts is a file in GovLab Extension Host. 100 lines of code and 21 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 { listener } from "../../shared/factories/listener.factory.ts";
import { loadClosureGraph } from "../../shared/loaders/graph.loader.ts";
import { normalizePath } from "../../shared/resolvers/anchor.resolver.ts";
import { suffixAfterVerb } from "../../shared/manifests/verb.manifest.ts";
const REGISTER_VERB = "register";
const STANDARD_PREFIXES = ["get", "list", "has"];
interface Offender {
files: string[];
fn: string;
suffix: string;
}
const registrarFiles = function registrarFiles(closure: ClosureGraph): Map<string, Set<string>> {
const byFn = new Map<string, Set<string>>();
for (const r of closure.registers) {
if (INSTANCE_SCOPED_REGISTRARS.has(r.fn)) {
continue;
}
const files = byFn.get(r.fn) ?? new Set<string>();
files.add(r.file);
byFn.set(r.fn, files);
}
return byFn;
};
const matchesSuffix = function matchesSuffix(consumerSuffix: string, suffix: string): boolean {
return consumerSuffix === suffix || consumerSuffix === `${suffix}s` || `${consumerSuffix}s` === suffix;
};
const hasStandardConsumer = function hasStandardConsumer(closure: ClosureGraph, suffix: string): boolean {
return closure.consumers.some((c) =>
STANDARD_PREFIXES.some((prefix) => {
const consumerSuffix = suffixAfterVerb(c.fn, prefix);
return consumerSuffix !== null && matchesSuffix(consumerSuffix, suffix);
}),
);
};
const findNonStandardRegistries = function findNonStandardRegistries(closure: ClosureGraph): Offender[] {
const offenders: Offender[] = [];
for (const [fn, files] of registrarFiles(closure)) {
const suffix = suffixAfterVerb(fn, REGISTER_VERB);
if (suffix !== null && !hasStandardConsumer(closure, suffix)) {
offenders.push({ files: [...files], fn, suffix });
}
}
return offenders;
};
const GRAPH = loadClosureGraph();
const OFFENDERS = GRAPH === null ? [] : findNonStandardRegistries(GRAPH);
export default {
create(context: RuleContext): RuleListener {
if (GRAPH === null) {
return listener({
program(_view, node) {
context.report({ messageId: "graphMissing", node });
},
});
}
if (OFFENDERS.length === 0) {
return {};
}
const filename = normalizePath(context.filename);
const here = OFFENDERS.filter((off) => off.files.some((f) => filename.endsWith(f)));
if (here.length === 0) {
return {};
}
return listener({
functionDeclaration(view, node) {
const id = nodeAt(view, "id");
if (!isType(id, "Identifier")) {
return;
}
const name = nameOf(id);
for (const off of here.filter((entry) => entry.fn === name)) {
context.report({ data: { suffix: off.suffix }, messageId: "nonStandard", node });
}
},
});
},
meta: {
docs: {
checks: defineCheck({
detects: [],
enforces: ["architecture:intent-revealing-interface", "architecture:registry-pattern"],
}),
description:
"Every registry must expose at least one consumer named to the standard shape — `getX(id)`, `listXs()` or `hasX(id)`. A bespoke selector name defeats every cross-cutting tool that finds registries by that convention, this workspace's own rules included, so the registry becomes invisible to the checks meant to govern it. A bespoke selector is added ALONGSIDE the standard trio, never in place of it. A registrar that attaches to one instance rather than a shared table is classified in the instance-registrar allowlist, which is data the mechanism reads rather than a name written into this rule.",
},
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.",
nonStandard:
"Registry `{{ suffix }}` has no standard consumer (get{{ suffix }} / list{{ suffix }}s / has{{ suffix }}). Bespoke verb-suffix consumers defeat audit tooling. Add a standard accessor; the bespoke one can stay alongside.",
},
schema: [],
type: "problem",
},
} satisfies LocalRule;