core/quality/eslint/no-call-enumeration.eslint.rule.ts

core/quality/eslint/no-call-enumeration.eslint.rule.ts is a file in GovLab Quality. 127 lines of code and 28 definitions.

import type { Rule } from "eslint";
import { govlabMeta } from "#core/factories/eslint.factory";

interface AstNode {
    type: string;
    name?: string;
    computed?: boolean;
    object?: AstNode;
    property?: AstNode;
    expression?: AstNode;
    argument?: AstNode;
    arguments?: AstNode[];
    callee?: AstNode;
    body?: AstNode[];
}

type OnHit = (node: AstNode, callee: string, count: number) => void;

const DEFAULT_THRESHOLD = 3;

const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;

const isAstNode = (value: unknown): value is AstNode => isRecord(value) && "type" in value;

const asNode = (value: unknown): AstNode | null => (isAstNode(value) ? value : null);

const isRuleNode = (value: unknown): value is Rule.Node => isRecord(value) && "type" in value;

const resolveThreshold = (raw: unknown): number => {
    const threshold = isRecord(raw) ? raw["threshold"] : DEFAULT_THRESHOLD;
    return typeof threshold === "number" ? threshold : DEFAULT_THRESHOLD;
};

const memberPath = (object: string | null, property: string | undefined): string | null =>
    typeof object === "string" && typeof property === "string" ? `${object}.${property}` : null;

const serializeCallee = (node: AstNode | null | undefined): string | null => {
    if (!node) {
        return null;
    }
    if (node.type === "Identifier") {
        return node.name ?? null;
    }
    if (node.type === "ThisExpression") {
        return "this";
    }
    if (node.type !== "MemberExpression" || node.computed === true) {
        return null;
    }
    return memberPath(serializeCallee(node.object), node.property?.name);
};

const descriptorCalleeOf = (stmt: AstNode): string | null => {
    if (stmt.type !== "ExpressionStatement" || !stmt.expression) {
        return null;
    }
    const inner = stmt.expression;
    const expr = inner.type === "AwaitExpression" && inner.argument ? inner.argument : inner;
    if (expr.type !== "CallExpression") {
        return null;
    }
    const first = (expr.arguments ?? []).at(0);
    if (first?.type !== "ObjectExpression") {
        return null;
    }
    return serializeCallee(expr.callee);
};

const extendRun = (body: AstNode[], start: number, callee: string): number => {
    let end = start;
    while (end + 1 < body.length) {
        const next = body[end + 1];
        if (!next || descriptorCalleeOf(next) !== callee) {
            break;
        }
        end += 1;
    }
    return end;
};

const scanRun = (body: AstNode[], start: number, ctx: { onHit: OnHit; threshold: number }): number => {
    const head = body[start];
    if (!head) {
        return start + 1;
    }
    const callee = descriptorCalleeOf(head);
    if (callee === null) {
        return start + 1;
    }
    const end = extendRun(body, start, callee);
    if (end - start + 1 >= ctx.threshold) {
        ctx.onHit(head, callee, ctx.threshold);
    }
    return end + 1;
};

const scanBody = (body: AstNode[], threshold: number, onHit: OnHit): void => {
    let start = 0;
    while (start < body.length) {
        start = scanRun(body, start, { onHit, threshold });
    }
};

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        const threshold = resolveThreshold(context.options[0]);
        const onHit: OnHit = (node, callee, count) => {
            if (isRuleNode(node)) {
                context.report({ data: { callee, count: String(count) }, messageId: "callEnumeration", node });
            }
        };
        const visit = (node: Rule.Node): void => {
            const body = asNode(node)?.body;
            if (Array.isArray(body)) {
                scanBody(body, threshold, onHit);
            }
        };
        const handlers: [string, (node: Rule.Node) => void][] = [
            ["BlockStatement", visit],
            ["Program", visit],
            ["StaticBlock", visit],
        ];
        return Object.fromEntries(handlers);
    },
    meta: govlabMeta({
        canonical: ["open-closed"],
        description:
            "Disallow a run of consecutive statement-calls to the same callee — mandate a declarative data collection folded by iteration",
        messages: {
            callEnumeration:
                "{{count}} consecutive statements call '{{callee}}' with a descriptor object literal — a list of similar definitions enumerated as repeated code. Collect the descriptors into one declarative frozen array (const ITEMS = [ {{callee}}({...}), ... ]) and consume that array, so a new entry is a data row rather than another statement.",
        },
        ruleId: "call_enumeration_to_data",
        schema: [
            {
                additionalProperties: false,
                properties: { threshold: { maximum: 20, minimum: 3, type: "integer" } },
                type: "object",
            },
        ],
    }),
} satisfies Rule.RuleModule;