@unito/integration-sdk
Version:
Integration SDK
148 lines (134 loc) • 4.29 kB
text/typescript
/**
* Flags catch blocks whose only action is to return a falsy/empty value
* with no logging. Hides bugs and produces silent data loss.
*
* Example violation:
* try { ... } catch (e) { return null; }
*
* Acceptable:
* try { ... } catch (e) { logger.warn('msg', { error: e }); return null; }
* try { ... } catch (e) { throw new HttpErrors.X('...'); }
*/
import type { Rule } from 'eslint';
import type * as ESTree from 'estree';
const FALSY_LITERAL_VALUES = new Set<unknown>([null, '']);
const LOGGER_METHOD_REGEX = /^(log|warn|error|info|debug)$/i;
const LOGGER_NAME_REGEX = /\b(logger|console|log)\b/i;
const RETURNED_VALUE_LABELS: Record<string, string> = {
ObjectExpression: '{}',
ArrayExpression: '[]',
};
function isFalsyLiteral(node: ESTree.Expression | null | undefined): boolean {
if (!node) {
return true;
}
if (node.type === 'Literal' && FALSY_LITERAL_VALUES.has(node.value)) {
return true;
}
if (node.type === 'Identifier' && node.name === 'undefined') {
return true;
}
if (node.type === 'ObjectExpression' && node.properties.length === 0) {
return true;
}
if (node.type === 'ArrayExpression' && node.elements.length === 0) {
return true;
}
return false;
}
function walkMemberChain(node: ESTree.Expression): { root: string | null; props: string[] } {
const props: string[] = [];
let current: ESTree.Expression | ESTree.Super = node;
while (current.type === 'MemberExpression') {
if (current.property.type === 'Identifier') {
props.push(current.property.name);
}
current = current.object;
}
const root = current.type === 'Identifier' ? current.name : null;
return { root, props };
}
function isLoggerCallExpression(callExpr: ESTree.CallExpression): boolean {
const callee = callExpr.callee;
if (callee.type === 'Identifier') {
return LOGGER_METHOD_REGEX.test(callee.name);
}
if (callee.type !== 'MemberExpression') {
return false;
}
const { root, props } = walkMemberChain(callee as ESTree.Expression);
return [root, ...props].some(p => p !== null && LOGGER_NAME_REGEX.test(p));
}
function isLoggerCall(node: ESTree.Statement): boolean {
if (node.type !== 'ExpressionStatement') {
return false;
}
const expr = node.expression;
if (expr.type === 'AwaitExpression' && expr.argument && expr.argument.type === 'CallExpression') {
return isLoggerCallExpression(expr.argument);
}
if (expr.type !== 'CallExpression') {
return false;
}
return isLoggerCallExpression(expr);
}
function describeReturnedValue(node: ESTree.Expression | null | undefined): string {
if (!node) {
return 'undefined';
}
const label = RETURNED_VALUE_LABELS[node.type];
if (label) {
return label;
}
if (node.type === 'Literal') {
return String(node.value);
}
if (node.type === 'Identifier') {
return node.name;
}
return 'value';
}
const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
description:
'Disallow silent catch blocks that return falsy/empty values without logging. ' +
'Detected: null, undefined, bare return, empty string, empty array, empty object. ' +
'Boolean false and numeric 0 are NOT flagged — those are usually intentional semantic results.',
recommended: true,
},
schema: [],
messages: {
silentReturn:
'Catch block silently returns {{value}} without logging. Add a logger call (e.g. `logger.warn(...)`) or rethrow.',
},
},
create(context) {
return {
CatchClause(node: ESTree.CatchClause) {
const body = node.body.body;
if (body.length === 0) {
return;
}
const lastStatement = body[body.length - 1];
if (!lastStatement || lastStatement.type !== 'ReturnStatement') {
return;
}
if (!isFalsyLiteral(lastStatement.argument)) {
return;
}
const earlierStatements = body.slice(0, -1);
if (earlierStatements.some(isLoggerCall)) {
return;
}
context.report({
node: lastStatement,
messageId: 'silentReturn',
data: { value: describeReturnedValue(lastStatement.argument) },
});
},
} as Rule.RuleListener;
},
};
export default rule;