rules/eslint/no-swallowed-failure.eslint.rule.ts
rules/eslint/no-swallowed-failure.eslint.rule.ts is a file in GovLab Extension Host. 93 lines of code and 14 definitions.
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import {
argumentAt,
calleeName,
isType,
nameOf,
nodeAt,
nodesAt,
walk,
} from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";
const HANDLER_TYPES: ReadonlySet<string> = new Set(["ArrowFunctionExpression", "FunctionExpression"]);
const REJECTION_SLOTS: ReadonlyMap<string, number> = new Map([
["catch", 0],
["then", 1],
]);
const throwsIn = function throwsIn(body: AstNode | null): boolean {
let found = false;
if (body !== null) {
walk(body, (node) => {
found ||= node.type === "ThrowStatement";
});
}
return found;
};
const readsIn = function readsIn(body: AstNode | null, name: string): boolean {
let found = false;
if (body !== null) {
walk(body, (node) => {
found ||= isType(node, "Identifier") && nameOf(node) === name;
});
}
return found;
};
const dropsFailure = function dropsFailure(binding: AstNode | null, body: AstNode | null): boolean {
if (throwsIn(body)) {
return false;
}
if (binding === null) {
return true;
}
return isType(binding, "Identifier") && !readsIn(body, nameOf(binding));
};
const rejectionHandlerOf = function rejectionHandlerOf(call: AstNode): AstNode | null {
const callee = nodeAt(call, "callee");
const slot = REJECTION_SLOTS.get(calleeName(call));
if (!isType(callee, "MemberExpression") || slot === undefined) {
return null;
}
const handler = argumentAt(call, slot);
return handler !== null && HANDLER_TYPES.has(handler.type) ? handler : null;
};
export default {
create(context: RuleContext): RuleListener {
return listener({
callExpression(view, node) {
const handler = rejectionHandlerOf(view);
if (handler === null) {
return;
}
const [binding = null] = nodesAt(handler, "params");
if (dropsFailure(binding, nodeAt(handler, "body"))) {
context.report({ messageId: "droppedRejection", node });
}
},
catchClause(view, node) {
if (dropsFailure(nodeAt(view, "param"), nodeAt(view, "body"))) {
context.report({ messageId: "droppedFailure", node });
}
},
});
},
meta: {
docs: {
checks: defineCheck({
detects: ["architecture:unobservable-failure"],
enforces: ["architecture:error-handling"],
}),
description:
"A caught failure is rethrown or used. A catch clause or rejection handler that neither throws nor reads what it caught turns a defect into silence, and silence cannot be fixed.",
},
messages: {
droppedFailure:
"This catch drops the failure it caught: it neither rethrows nor reads the error. Rethrow it with context, report it through the module's reporter with the error attached, or remove the catch so the failure surfaces.",
droppedRejection:
"This rejection handler drops the failure it received: it neither rethrows nor reads the reason. Take the reason as a parameter and report it with the error attached, rethrow it, or let the rejection propagate.",
},
schema: [],
type: "problem",
},
} satisfies LocalRule;