/** * Disallow passing full error objects to logger methods. Full error objects * contain stack traces, provider response bodies, and potentially credentials. * Extract specific fields instead. * * Bad: * catch (error) { logger.error(error); } * logger.warn(err); * * Good: * catch (error) { logger.error('upload failed', { message: error.message }); } * logger.warn({ message: err.message, code: err.code }); * * Detection tiers: * 1. (high confidence) Inside a CatchClause, the caught parameter is passed * directly to a logger call — regardless of the parameter's name. * 2. (medium confidence) Any logger call with an argument matching the * common error identifier pattern /^(e|err|error|ex|exception|cause)$/i. */ import type { Rule } from 'eslint'; import type * as ESTree from 'estree'; import { walkMemberChain, getCatchClauseParam, LOGGER_NAME_BROAD_RE } from '../utils.js'; const LOGGER_METHOD_RE = /^(error|warn|info|debug)$/; const ERROR_IDENTIFIER_RE = /^(e|err|error|ex|exception|cause)$/i; const SCRIPTS_FILE_RE = /\/scripts?\//; function isLoggerCall(node: ESTree.CallExpression): boolean { const callee = node.callee; if (!callee || callee.type !== 'MemberExpression') { return false; } const methodProp = callee.property; if (!methodProp || methodProp.type !== 'Identifier') { return false; } if (!LOGGER_METHOD_RE.test(methodProp.name)) { return false; } const { root, props } = walkMemberChain(callee.object); return [root, ...props].some(p => p !== null && LOGGER_NAME_BROAD_RE.test(p)); } const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { description: 'Disallow passing full error objects to logger methods; extract specific fields like error.message instead', recommended: true, }, schema: [], messages: { fullErrorObjectLogged: "Do not pass the full error object to {{method}}(). Full error objects contain stack traces, provider response bodies, and potentially credentials. Extract specific fields instead: {{method}}('description', { message: {{identifier}}.message })", }, }, create(context) { const filename = context.filename ?? ''; if (SCRIPTS_FILE_RE.test(filename)) { return {}; } return { CallExpression(node) { if (!isLoggerCall(node)) { return; } const callee = node.callee as ESTree.MemberExpression; const methodProp = callee.property as ESTree.Identifier; const method = methodProp.name; const hasIdentifierArg = node.arguments.some(arg => arg.type === 'Identifier'); if (!hasIdentifierArg) { return; } const catchParam = getCatchClauseParam(node as Rule.Node); for (const arg of node.arguments) { if (arg.type !== 'Identifier') { continue; } if ((catchParam && arg.name === catchParam) || ERROR_IDENTIFIER_RE.test(arg.name)) { context.report({ node, messageId: 'fullErrorObjectLogged', data: { method, identifier: arg.name }, }); return; } } }, }; }, }; export default rule;