rules/eslint/require-snapshot-coverage.eslint.rule.ts

rules/eslint/require-snapshot-coverage.eslint.rule.ts is a file in GovLab Extension Host. 143 lines of code and 30 definitions.

import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { isType, literalString, locOf, nameOf, nodeAt, nodesAt, walk } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { SNAPSHOT_COVERAGE_EXCLUSIONS } from "../../shared/exclusions/snapshot.exclusions.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";

const SNAPSHOT_METHOD = "snapshot";
const RESTORE_METHOD = "restore";

const isMethod = function isMethod(node: AstNode, name: string): boolean {
    if (node.type !== "MethodDefinition") {
        return false;
    }
    const key = nodeAt(node, "key");
    return isType(key, "Identifier") && nameOf(key) === name;
};

const methodReferencesField = function methodReferencesField(method: AstNode, fieldName: string): boolean {
    let found = false;
    walk(method, (n) => {
        if (found || n.type !== "MemberExpression" || !isType(nodeAt(n, "object"), "ThisExpression")) {
            return;
        }
        const property = nodeAt(n, "property");
        if (isType(property, "Identifier") && nameOf(property) === fieldName) {
            found = true;
        }
    });
    return found;
};

const calledMethodNames = function calledMethodNames(method: AstNode): Set<string> {
    const names = new Set<string>();
    walk(method, (n) => {
        if (n.type !== "CallExpression") {
            return;
        }
        const callee = nodeAt(n, "callee");
        if (!isType(callee, "MemberExpression") || !isType(nodeAt(callee, "object"), "ThisExpression")) {
            return;
        }
        const property = nodeAt(callee, "property");
        if (isType(property, "Identifier")) {
            names.add(nameOf(property));
        }
    });
    return names;
};

const referencesField = function referencesField(
    method: AstNode | null,
    fieldName: string,
    members: readonly AstNode[],
    seen: Set<AstNode>,
): boolean {
    if (method === null || seen.has(method)) {
        return false;
    }
    seen.add(method);
    if (methodReferencesField(method, fieldName)) {
        return true;
    }
    for (const name of calledMethodNames(method)) {
        const target = members.find((m) => isMethod(m, name)) ?? null;
        if (referencesField(target, fieldName, members, seen)) {
            return true;
        }
    }
    return false;
};

const fieldNameOf = function fieldNameOf(field: AstNode): string | null {
    const key = nodeAt(field, "key");
    if (key === null) {
        return null;
    }
    return isType(key, "Identifier") ? nameOf(key) : literalString(key);
};

const isExcluded = function isExcluded(className: string, fieldName: string): boolean {
    return className !== "" && SNAPSHOT_COVERAGE_EXCLUSIONS.has(`${className}.${fieldName}`);
};

interface Persisted {
    cls: string;
    members: AstNode[];
    restore: AstNode;
    snapshot: AstNode;
}

const persistedShapeOf = function persistedShapeOf(view: AstNode): Persisted | null {
    const members = nodesAt(nodeAt(view, "body"), "body");
    const snapshot = members.find((m) => isMethod(m, SNAPSHOT_METHOD));
    const restore = members.find((m) => isMethod(m, RESTORE_METHOD));
    if (snapshot === undefined || restore === undefined) {
        return null;
    }
    const id = nodeAt(view, "id");
    const name = id === null ? "" : nameOf(id);
    return { cls: name === "" ? "<anonymous>" : name, members, restore, snapshot };
};

const uncoveredFields = function uncoveredFields(
    shape: Persisted,
    className: string,
): { field: AstNode; which: string }[] {
    const out: { field: AstNode; which: string }[] = [];
    const fields = shape.members.filter((m) => m.type === "PropertyDefinition" && m["static"] !== true);
    for (const field of fields) {
        const fieldName = fieldNameOf(field);
        if (fieldName === null || isExcluded(className, fieldName)) {
            continue;
        }
        if (!referencesField(shape.snapshot, fieldName, shape.members, new Set())) {
            out.push({ field, which: "snapshot()" });
        }
        if (!referencesField(shape.restore, fieldName, shape.members, new Set())) {
            out.push({ field, which: "restore()" });
        }
    }
    return out;
};

export default {
    create(context: RuleContext): RuleListener {
        const checkClass = function checkClass(view: AstNode): void {
            const shape = persistedShapeOf(view);
            if (shape === null) {
                return;
            }
            const id = nodeAt(view, "id");
            const className = id === null ? "" : nameOf(id);
            for (const hit of uncoveredFields(shape, className)) {
                const payload = { cls: shape.cls, field: fieldNameOf(hit.field) ?? "", which: hit.which };
                context.report({ data: payload, loc: locOf(hit.field), messageId: "missing" });
            }
        };
        return listener({ classDeclaration: checkClass, classExpression: checkClass });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:memento-pattern"] }),
            description:
                "Instance fields on classes with both snapshot() and restore() methods must be referenced in both — or class-qualified-listed in the snapshot exclusions registry.",
        },
        messages: {
            missing:
                "Field '{{cls}}.{{field}}' is not referenced in {{which}}. Either include it in {{which}}, or add '{{cls}}.{{field}}' to SNAPSHOT_COVERAGE_EXCLUSIONS in the snapshot exclusions registry if it is intentionally excluded from persistence.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;