rules/eslint/require-signal-guard.eslint.rule.ts

rules/eslint/require-signal-guard.eslint.rule.ts is a file in GovLab Extension Host. 79 lines of code and 17 definitions.

import type { Rule } from "eslint";
import { defineCheck } from "@govlab/context/check";

defineCheck({ detects: ["architecture:unobservable-failure"], enforces: ["architecture:error-handling"] });

interface AstNode {
    type: string;
    name?: string;
    callee?: AstNode;
    id?: AstNode;
}

const SPAWN_CALLEE = "spawnTool";
const GUARD_NAMES = new Set(["signalKilled", "signalDeathResult", "terminalOf", "passOf"]);

const MESSAGE =
    "A spawned tool that dies by a signal reports no exit status, and a caller that gates on that status reads the killed run as a clean one. Check the spawn result for signal death before any exit status gates it, by classifying the result for a terminal outcome or by testing for signal death and returning the signal-death result. [require_signal_guard]";

const isNode = function isNode(value: unknown): value is AstNode {
    return value !== null && typeof value === "object" && "type" in value;
};

const asNode = function asNode(value: unknown): AstNode | null {
    return isNode(value) ? value : null;
};

interface GuardState {
    declaresSpawn: boolean;
    sawGuard: boolean;
    spawnNode: Rule.Node | null;
}

const makeNoteIdentifier = function makeNoteIdentifier(state: GuardState): (node: Rule.Node) => void {
    return function noteIdentifier(node: Rule.Node): void {
        const name = asNode(node)?.name;
        if (name !== undefined && GUARD_NAMES.has(name)) {
            state.sawGuard = true;
        }
    };
};

const makeOnDeclarator = function makeOnDeclarator(state: GuardState): (node: Rule.Node) => void {
    return function onDeclarator(node: Rule.Node): void {
        if (asNode(node)?.id?.name === SPAWN_CALLEE) {
            state.declaresSpawn = true;
        }
    };
};

const makeOnCall = function makeOnCall(state: GuardState): (node: Rule.Node) => void {
    return function onCall(node: Rule.Node): void {
        const call = asNode(node);
        if (state.spawnNode === null && call?.callee?.type === "Identifier" && call.callee.name === SPAWN_CALLEE) {
            state.spawnNode = node;
        }
    };
};

const makeOnProgramExit = function makeOnProgramExit(context: Rule.RuleContext, state: GuardState): () => void {
    return function onProgramExit(): void {
        if (state.spawnNode !== null && !state.sawGuard && !state.declaresSpawn) {
            context.report({ messageId: "guard", node: state.spawnNode });
        }
    };
};

const requireSignalGuard: Rule.RuleModule = {
    create(context: Rule.RuleContext): Rule.RuleListener {
        const state: GuardState = { declaresSpawn: false, sawGuard: false, spawnNode: null };
        const handlers: [string, (node: Rule.Node) => void][] = [
            ["CallExpression", makeOnCall(state)],
            ["Identifier", makeNoteIdentifier(state)],
            ["VariableDeclarator", makeOnDeclarator(state)],
            ["Program:exit", makeOnProgramExit(context, state)],
        ];
        return Object.fromEntries(handlers);
    },
    meta: {
        docs: {
            description:
                "A spawned child that dies by signal reports no exit status, so coercing that absent status to success folds a killed process into the clean path and parses its missing output as zero findings — a false all-clear, the worst outcome a gate can produce. Check for signal death immediately after the spawn's error branch, before any status coercion.",
        },
        messages: { guard: MESSAGE },
        schema: [],
        type: "problem",
    },
};

export default {
    plugins: { "govlab-quality": { rules: { "require-signal-guard": requireSignalGuard } } },
    tool: "eslint",
};