# rules/eslint/require-pool-registration.eslint.rule.ts

> 134 lines of code and 32 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-require-pool-registration-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/require-pool-registration.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/require-component.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/require-component.eslint.rule.ts.md) and before [rules/eslint/require-self-registration.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/require-self-registration.eslint.rule.ts.md).

## Definitions

- `primitiveDirs` (lexical_declaration, line 25)
- `hasRegisterPoolCall` (lexical_declaration, line 104)
- `isAllowedFile` (lexical_declaration, line 68)
- `basenameOf` (lexical_declaration, line 62)
- `poolConstructorName` (lexical_declaration, line 73)
- `create` (method_definition, line 115, exported)
- `program` (method_definition, line 120, exported)
- `derivePrimitives` (lexical_declaration, line 38)
- `topLevelPoolConstructions` (lexical_declaration, line 85)
- `REGISTER_CALL_NAME` (lexical_declaration, line 12)
- `POOLED_CONCERNS` (lexical_declaration, line 13)
- `CLASS_EXPORT` (lexical_declaration, line 14)
- `identifierEnd` (lexical_declaration, line 16)
- `dirs` (lexical_declaration, line 26)
- `abs` (lexical_declaration, line 29)
- `ctors` (lexical_declaration, line 39)
- `basenames` (lexical_declaration, line 40)
- `text` (lexical_declaration, line 47)
- `at` (lexical_declaration, line 48)
- `rest` (lexical_declaration, line 50)
- `end` (lexical_declaration, line 51)
- `{ basenames: ALLOWED_BASENAMES, ctors: POOL_CONSTRUCTORS }` (lexical_declaration, line 60)
- `idx` (lexical_declaration, line 64)
- `norm` (lexical_declaration, line 69)
- `callee` (lexical_declaration, line 77)
- `name` (lexical_declaration, line 81)
- `found` (lexical_declaration, line 88)
- `candidates` (lexical_declaration, line 90)
- `ctor` (lexical_declaration, line 95)
- `expression` (lexical_declaration, line 109)
- `constructions` (lexical_declaration, line 121, exported)
- `payload` (lexical_declaration, line 126, exported)

## Uses

- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/resolvers/anchor.resolver.ts](https://banes-lab.com/source/governance/shared/resolvers/anchor.resolver.ts.md)
- [shared/resolvers/container.resolver.ts](https://banes-lab.com/source/governance/shared/resolvers/container.resolver.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

- [Registry Pattern](https://banes-lab.com/records/architecture/registry-pattern.md)

## Source

```typescript
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { PROJECT_ROOT, normalizePath, projectDirs } from "../../shared/resolvers/anchor.resolver.ts";
import { calleeName, isType, locOf, nameOf, nodeAt, nodesAt } from "../../shared/selectors/syntax.selector.ts";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import type { AstNode } from "../../types/syntax.types.ts";
import { concernFolders } from "../../shared/resolvers/container.resolver.ts";
import { defineCheck } from "@govlab/context/check";
import { isIdentifierChar } from "@govlab/constants";
import { join } from "node:path";
import { listener } from "../../shared/factories/listener.factory.ts";

const REGISTER_CALL_NAME = "registerPool";
const POOLED_CONCERNS = ["cache", "pool"];
const CLASS_EXPORT = "export class ";

const identifierEnd = function identifierEnd(text: string): number {
    for (let at = 0; at < text.length; at += 1) {
        if (!isIdentifierChar(text[at] ?? "")) {
            return at;
        }
    }
    return -1;
};

const primitiveDirs = function primitiveDirs(): string[] {
    const dirs: string[] = [];
    for (const tag of POOLED_CONCERNS) {
        for (const folder of concernFolders(tag, projectDirs())) {
            const abs = join(PROJECT_ROOT, folder);
            if (existsSync(abs) && !dirs.includes(abs)) {
                dirs.push(abs);
            }
        }
    }
    return dirs;
};

const derivePrimitives = function derivePrimitives(): { basenames: Set<string>; ctors: Set<string> } {
    const ctors = new Set<string>();
    const basenames = new Set<string>();
    for (const dir of primitiveDirs()) {
        for (const name of readdirSync(dir)) {
            if (!name.endsWith(".ts")) {
                continue;
            }
            basenames.add(name);
            const text = readFileSync(join(dir, name), "utf8");
            let at = text.indexOf(CLASS_EXPORT);
            while (at !== -1) {
                const rest = text.slice(at + CLASS_EXPORT.length);
                const end = identifierEnd(rest);
                ctors.add(end === -1 ? rest : rest.slice(0, end));
                at = text.indexOf(CLASS_EXPORT, at + 1);
            }
        }
    }
    return { basenames, ctors };
};

const { basenames: ALLOWED_BASENAMES, ctors: POOL_CONSTRUCTORS } = derivePrimitives();

const basenameOf = function basenameOf(path: string): string {
    const norm = normalizePath(path);
    const idx = norm.lastIndexOf("/");
    return idx === -1 ? norm : norm.slice(idx + 1);
};

const isAllowedFile = function isAllowedFile(filename: string): boolean {
    const norm = normalizePath(filename);
    return norm.endsWith(".test.ts") || ALLOWED_BASENAMES.has(basenameOf(norm));
};

const poolConstructorName = function poolConstructorName(node: AstNode | null): string {
    if (!isType(node, "NewExpression")) {
        return "";
    }
    const callee = nodeAt(node, "callee");
    if (!isType(callee, "Identifier")) {
        return "";
    }
    const name = nameOf(callee);
    return POOL_CONSTRUCTORS.has(name) ? name : "";
};

const topLevelPoolConstructions = function topLevelPoolConstructions(
    program: AstNode,
): { ctor: string; node: AstNode }[] {
    const found: { ctor: string; node: AstNode }[] = [];
    for (const stmt of nodesAt(program, "body")) {
        const candidates =
            stmt.type === "VariableDeclaration"
                ? nodesAt(stmt, "declarations").map((decl) => nodeAt(decl, "init"))
                : [stmt.type === "ExpressionStatement" ? nodeAt(stmt, "expression") : null];
        for (const candidate of candidates) {
            const ctor = poolConstructorName(candidate);
            if (ctor !== "" && candidate !== null) {
                found.push({ ctor, node: candidate });
            }
        }
    }
    return found;
};

const hasRegisterPoolCall = function hasRegisterPoolCall(program: AstNode): boolean {
    return nodesAt(program, "body").some((stmt) => {
        if (stmt.type !== "ExpressionStatement") {
            return false;
        }
        const expression = nodeAt(stmt, "expression");
        return isType(expression, "CallExpression") && calleeName(expression) === REGISTER_CALL_NAME;
    });
};

export default {
    create(context: RuleContext): RuleListener {
        if (isAllowedFile(context.filename)) {
            return {};
        }
        return listener({
            program(view) {
                const constructions = topLevelPoolConstructions(view);
                if (constructions.length === 0 || hasRegisterPoolCall(view)) {
                    return;
                }
                for (const hit of constructions) {
                    const payload = { ctor: hit.ctor, required: "{id, kind, handle}" };
                    context.report({ data: payload, loc: locOf(hit.node), messageId: "unregistered" });
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:registry-pattern"] }),
            description:
                "Module-top-level cache/pool constructions must be paired with a registerPool(...) call so the pool is visible in the pool registry.",
        },
        messages: {
            unregistered:
                "Module-top-level 'new {{ctor}}(...)' must be paired with a 'registerPool({{required}})' call in the same module so the pool is discoverable via the pool registry.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
