rules/eslint/markup-through-builder.eslint.rule.ts

rules/eslint/markup-through-builder.eslint.rule.ts is a file in GovLab Extension Host. 53 lines of code and 7 definitions.

import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { declaredAtTop, nodesAt, recordAt, stringIn } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { MARKUP_BUILDERS } from "../../shared/manifests/invariant.manifest.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";

const COPY_SUFFIX = concernSuffix("strings");

const buildersSpelled = function buildersSpelled(view: AstNode): string[] {
    if (nodesAt(view, "expressions").length === 0) {
        return [];
    }
    const text = nodesAt(view, "quasis")
        .map((quasi) => stringIn(recordAt(quasi, "value"), "cooked"))
        .join("");
    return [...MARKUP_BUILDERS].filter(([opening]) => text.includes(opening)).map(([, builder]) => builder);
};

export default {
    create(context: RuleContext): RuleListener {
        if (basenameOf(context.filename).endsWith(COPY_SUFFIX)) {
            return {};
        }
        let declared: ReadonlySet<string> = new Set();
        return listener({
            program(view) {
                declared = declaredAtTop(view);
            },
            templateLiteral(view, node) {
                for (const builder of buildersSpelled(view)) {
                    if (!declared.has(builder)) {
                        context.report({ data: { builder }, messageId: "inlineMarkup", node });
                    }
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({
                detects: ["architecture:duplicate-code"],
                enforces: ["architecture:single-source-of-truth"],
            }),
            description:
                "Code that builds a markup element from computed values calls the one builder declared for that element. The elements and their builders are data in the markup builders registry. A template that interpolates values into the element by hand is a second copy of the builder. Copy in a strings module is authored text and is not checked.",
        },
        messages: {
            inlineMarkup: "This template builds markup that '{{builder}}' builds. Call '{{builder}}' with the values instead.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;