core/fragment/eslint/require-fragment-metadata.eslint.rule.ts

core/fragment/eslint/require-fragment-metadata.eslint.rule.ts is a file in GovLab Quality. 95 lines of code and 18 definitions.

import {
    fragmentObject,
    identifierName,
    property,
    propertyNames,
    stringArrayValues,
} from "#core/selectors/context.fragment.selector";
import { isArray, isDefineFragmentCall, isRuleNode } from "#core/predicates/context.fragment.predicate";
import type { AstNode } from "#types/context.types";
import type { Rule } from "eslint";
import { walk } from "#core/visitors/context.fragment.visitor";

const REQUIRED = ["id", "concern", "applies", "stable", "order", "params", "body"] as const;

const firstParamName = function firstParamName(body: AstNode | null): string | null {
    if (body === null || (body.type !== "ArrowFunctionExpression" && body.type !== "FunctionExpression")) {
        return null;
    }
    const { params } = body;
    const first = isArray(params) ? params[0] : null;
    return identifierName(first);
};

const accessedParamNames = function accessedParamNames(body: AstNode, paramName: string): Set<string> {
    const names = new Set<string>();
    walk(body, (node) => {
        if (node.type !== "MemberExpression" || node["computed"] === true) {
            return;
        }
        if (identifierName(node["object"]) === paramName) {
            const propName = identifierName(node["property"]);
            if (propName !== null) {
                names.add(propName);
            }
        }
    });
    return names;
};

interface ParamCheck {
    object: AstNode;
    body: AstNode;
    paramName: string;
}

const checkRequired = function checkRequired(context: Rule.RuleContext, object: AstNode, node: Rule.Node): void {
    const present = propertyNames(object);
    const missing = REQUIRED.filter((name) => !present.has(name));
    if (missing.length > 0) {
        context.report({ data: { names: missing.join(", ") }, messageId: "missing", node });
    }
};

const checkParams = function checkParams(context: Rule.RuleContext, pc: ParamCheck): void {
    const declared = new Set(stringArrayValues(property(pc.object, "params")));
    for (const accessed of accessedParamNames(pc.body, pc.paramName)) {
        if (!declared.has(accessed) && isRuleNode(pc.body)) {
            context.report({ data: { param: accessed }, messageId: "undeclared", node: pc.body });
        }
    }
};

const checkFragment = function checkFragment(context: Rule.RuleContext, node: Rule.Node): void {
    if (!isDefineFragmentCall(node)) {
        return;
    }
    const object = fragmentObject(node);
    if (object === null) {
        return;
    }
    checkRequired(context, object, node);
    const body = property(object, "body");
    const paramName = firstParamName(body);
    if (body !== null && paramName !== null) {
        checkParams(context, { body, object, paramName });
    }
};

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        return Object.fromEntries([
            [
                "CallExpression",
                (node: Rule.Node): void => {
                    checkFragment(context, node);
                },
            ],
        ]);
    },

    meta: {
        docs: {
            description:
                "Every defineContextFragment must declare id/concern/applies/stable/order/params/body, and its body may reference only params it declared.",
        },
        messages: {
            missing: "defineContextFragment is missing required metadata: {{names}}. [require_fragment_metadata]",
            undeclared:
                "Fragment body reads param '{{param}}' that is not in its declared params array — composeContext only passes declared params, so this renders empty. Add it to params, or remove the read. [require_fragment_metadata]",
        },
        schema: [],
        type: "problem",
    },
} satisfies Rule.RuleModule;