core/quality/eslint/require-resource-disposal.eslint.rule.ts

core/quality/eslint/require-resource-disposal.eslint.rule.ts is a file in GovLab Quality. 104 lines of code and 22 definitions.

import type { Rule } from "eslint";
import { govlabMeta } from "#core/factories/eslint.factory";

const DISPOSE_METHOD = "delete";
const DISPOSABLE_MODULES = new Set(["web-tree-sitter"]);

interface Frame {
    acquisitions: Rule.Node[];
    disposed: boolean;
}

const newCalleeName = function newCalleeName(node: Rule.Node): string | null {
    if (node.type !== "NewExpression") {
        return null;
    }
    const { callee } = node;
    return callee.type === "Identifier" ? callee.name : null;
};

const isDisposeCall = function isDisposeCall(node: Rule.Node): boolean {
    if (node.type !== "CallExpression") {
        return false;
    }
    const { callee } = node;
    if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") {
        return false;
    }
    return callee.property.name === DISPOSE_METHOD;
};

const importsDisposable = function importsDisposable(node: Rule.Node): boolean {
    return (
        node.type === "ImportDeclaration" &&
        typeof node.source.value === "string" &&
        DISPOSABLE_MODULES.has(node.source.value)
    );
};

class DisposalVisitor {
    private readonly disposableCtors = new Set<string>();
    private readonly frames: Frame[] = [];
    private readonly context: Rule.RuleContext;

    public constructor(context: Rule.RuleContext) {
        this.context = context;
    }

    public listeners(): Rule.RuleListener {
        const enter = this.enter.bind(this);
        const exit = this.exit.bind(this);
        const handlers: [string, (node: Rule.Node) => void][] = [
            ["ArrowFunctionExpression", enter],
            ["ArrowFunctionExpression:exit", exit],
            ["CallExpression", this.onCall.bind(this)],
            ["FunctionDeclaration", enter],
            ["FunctionDeclaration:exit", exit],
            ["FunctionExpression", enter],
            ["FunctionExpression:exit", exit],
            ["ImportDeclaration", this.onImport.bind(this)],
            ["NewExpression", this.onNew.bind(this)],
        ];
        return Object.fromEntries(handlers);
    }

    private onImport(node: Rule.Node): void {
        if (importsDisposable(node) && node.type === "ImportDeclaration") {
            for (const spec of node.specifiers) {
                this.disposableCtors.add(spec.local.name);
            }
        }
    }

    private enter(): void {
        this.frames.push({ acquisitions: [], disposed: false });
    }

    private exit(): void {
        const frame = this.frames.pop();
        if (!frame || frame.disposed) {
            return;
        }
        for (const acquisition of frame.acquisitions) {
            this.context.report({ messageId: "undisposed", node: acquisition });
        }
    }

    private onNew(node: Rule.Node): void {
        const name = newCalleeName(node);
        const frame = this.frames.at(-1);
        if (frame && name !== null && this.disposableCtors.has(name)) {
            frame.acquisitions.push(node);
        }
    }

    private onCall(node: Rule.Node): void {
        const frame = this.frames.at(-1);
        if (frame && isDisposeCall(node)) {
            frame.disposed = true;
        }
    }
}

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        return new DisposalVisitor(context).listeners();
    },
    meta: govlabMeta({
        canonical: ["resource-leak"],
        description:
            "Require a .delete() disposal for every WASM or native resource acquired in a function. The disposable set is declared in DISPOSABLE_MODULES.",
        messages: {
            undisposed:
                "This acquires a disposable WASM or native resource, declared in DISPOSABLE_MODULES, but the enclosing function never calls .delete(). Dispose every acquired handle, in a finally block.",
        },
        ruleId: "require_resource_disposal",
        type: "problem",
    }),
} satisfies Rule.RuleModule;