rules/eslint/closure-register-verb-reserved.eslint.rule.ts
rules/eslint/closure-register-verb-reserved.eslint.rule.ts is a file in GovLab Extension Host. 57 lines of code and 13 definitions.
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { isType, nameOf, nodeAt } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { basenameOf } from "../../shared/resolvers/anchor.resolver.ts";
import { concernSuffix } from "../../shared/manifests/taxonomy.manifest.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";
import { suffixAfterVerb } from "../../shared/manifests/verb.manifest.ts";
const REGISTER_VERB = "register";
const REGISTRY_FILE_SUFFIXES = [concernSuffix("registry")];
const EXEMPT_BASENAME_SUFFIXES = [".test.ts", ".spec.ts"];
const EXPORT_PARENTS = new Set(["ExportNamedDeclaration", "ExportDefaultDeclaration"]);
const isSkippedFile = function isSkippedFile(filename: string): boolean {
const basename = basenameOf(filename);
if (REGISTRY_FILE_SUFFIXES.some((suffix) => basename.endsWith(suffix))) {
return true;
}
return EXEMPT_BASENAME_SUFFIXES.some((suffix) => basename.endsWith(suffix));
};
const isExportedFunction = function isExportedFunction(node: AstNode): boolean {
const parent = nodeAt(node, "parent");
return parent !== null && EXPORT_PARENTS.has(parent.type);
};
export default {
create(context: RuleContext): RuleListener {
if (isSkippedFile(context.filename)) {
return {};
}
return listener({
functionDeclaration(view, node) {
const id = nodeAt(view, "id");
if (!isExportedFunction(view) || !isType(id, "Identifier")) {
return;
}
const name = nameOf(id);
const suffix = suffixAfterVerb(name, REGISTER_VERB);
if (suffix === null) {
return;
}
context.report({ data: { name, suffix }, messageId: "reservedVerb", node });
},
});
},
meta: {
docs: {
checks: defineCheck({ detects: [], enforces: ["architecture:intent-revealing-interface"] }),
description:
"The `register*` verb is reserved for module-scope dispatch-table operations — functions that mutate a shared registry. A function that attaches a callback to one specific instance is doing something else, and naming it `register*` makes the two indistinguishable to every tool that finds registrations by that verb. Instance-scoped attachment uses `subscribe*` / `add*` / `track*` / `attach*` instead.",
},
messages: {
reservedVerb:
"Function `{{ name }}` uses the `register*` prefix but is declared outside a *.registry.ts file. The `register*` verb is reserved for dispatch-table registrations. Rename to `subscribe{{ suffix }}` / `track{{ suffix }}` / `attach{{ suffix }}` depending on the operation's semantics.",
},
schema: [],
type: "problem",
},
} satisfies LocalRule;