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

rules/eslint/require-pool-registration.eslint.rule.ts is a file in GovLab Extension Host. 134 lines of code and 32 definitions.

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;