rules/eslint/validated-construction.eslint.rule.ts

rules/eslint/validated-construction.eslint.rule.ts is a file in GovLab Extension Host. 63 lines of code and 7 definitions.

import type { LocalRule, RuleContext, RuleListener, RuleNode } from "../../types/rule.types.ts";
import { asNode, isType, nameOf, nodeAt } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { VALIDATING_CLASSES } from "../../shared/manifests/invariant.manifest.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";

const isMethodCallOn = function isMethodCallOn(member: AstNode | null, object: AstNode, method: string): boolean {
    return (
        isType(member, "MemberExpression") &&
        nodeAt(member, "object") === object &&
        nameOf(nodeAt(member, "property")) === method &&
        isType(nodeAt(member, "parent"), "CallExpression")
    );
};

const bindingIsValidated = function bindingIsValidated(
    context: RuleContext,
    view: AstNode,
    raw: RuleNode,
    method: string,
): boolean {
    const declarator = nodeAt(view, "parent");
    if (!isType(declarator, "VariableDeclarator") || nodeAt(declarator, "init") !== view || raw.parent === null) {
        return false;
    }
    return context.sourceCode.getDeclaredVariables(raw.parent).some((variable) =>
        variable.references.some((reference) => {
            const identifier = asNode(reference.identifier);
            return identifier !== null && isMethodCallOn(nodeAt(identifier, "parent"), identifier, method);
        }),
    );
};

export default {
    create(context: RuleContext): RuleListener {
        return listener({
            newExpression(view, raw) {
                const method = VALIDATING_CLASSES.get(nameOf(nodeAt(view, "callee")));
                if (method === undefined) {
                    return;
                }
                if (
                    isMethodCallOn(nodeAt(view, "parent"), view, method) ||
                    bindingIsValidated(context, view, raw, method)
                ) {
                    return;
                }
                context.report({ data: { method }, messageId: "unvalidated", node: raw });
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:invariant"] }),
            description:
                "An instance of a class that checks its invariants in a separate method is validated before it is used. Construction alone runs none of the checks, so an unvalidated instance is indistinguishable from a valid one until something downstream misreads it.",
        },
        messages: {
            unvalidated:
                "This instance is constructed without its validation method '{{method}}' running on it. Call '{{method}}()' on the new instance, chained or on the binding it is assigned to, before the instance leaves the construction site.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;