@unito/integration-sdk
Version:
Integration SDK
120 lines (119 loc) • 3.87 kB
JavaScript
const FALSY_LITERAL_VALUES = new Set([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 = {
ObjectExpression: '{}',
ArrayExpression: '[]',
};
function isFalsyLiteral(node) {
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) {
const props = [];
let current = 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) {
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);
return [root, ...props].some(p => p !== null && LOGGER_NAME_REGEX.test(p));
}
function isLoggerCall(node) {
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) {
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 = {
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) {
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) },
});
},
};
},
};
export default rule;