import { RuleTester } from 'eslint'; import test from 'node:test'; import rule from '../../src/eslint/rules/no-full-error-object-logging.js'; const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 2022, sourceType: 'module', }, }); const wrap = (body: string): string => `async function f() { ${body} }`; test('no-full-error-object-logging', () => { ruleTester.run('no-full-error-object-logging', rule, { valid: [ { name: 'structured field extraction', code: wrap('try { foo(); } catch (error) { logger.error({ message: error.message }); }'), }, { name: 'string description + structured metadata', code: wrap("try { foo(); } catch (error) { logger.error('upload failed', { message: error.message }); }"), }, { name: 'just the message string', code: wrap('try { foo(); } catch (error) { logger.error(error.message); }'), }, { name: 'static string — no error reference', code: wrap("logger.error('static string');"), }, { name: 'non-error identifier — result does not match error pattern', code: wrap('logger.error(result);'), }, { name: 'not a logger call — someFunction(error) is ignored', code: wrap('try { foo(); } catch (error) { someFunction(error); }'), }, { name: 'error.code extraction is fine', code: wrap('try { foo(); } catch (error) { logger.error({ code: error.code }); }'), }, { name: 'nested function inside catch — function boundary stops scope walk', code: wrap(` try { foo(); } catch (oops) { const handler = () => { logger.error(oops); }; handler(); } `), }, { name: 'catch with destructured param — not an Identifier, tier 1 skips', code: wrap('try { foo(); } catch ({ message }) { logger.error(message); }'), }, { name: 'scripts/ files are excluded — CLI tooling needs full error output', filename: '/repo/integrations/foo/src/scripts/createIssueInBatch.ts', code: wrap('try { foo(); } catch (err) { logger.error(err); }'), }, ], invalid: [ { name: 'tier 1: catch parameter passed to logger.error', code: wrap('try { foo(); } catch (error) { logger.error(error); }'), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'error', identifier: 'error' } }], }, { name: 'tier 1: renamed catch parameter', code: wrap('try { foo(); } catch (oops) { logger.error(oops); }'), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'error', identifier: 'oops' } }], }, { name: 'tier 1: chained logger (context.logger.warn)', code: wrap('try { foo(); } catch (e) { context.logger.warn(e); }'), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'warn', identifier: 'e' } }], }, { name: 'tier 1: catch param as second argument', code: wrap("try { foo(); } catch (error) { logger.error('failed', error); }"), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'error', identifier: 'error' } }], }, { name: 'tier 2: error-named identifier outside catch', code: wrap('logger.error(err);'), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'error', identifier: 'err' } }], }, { name: 'tier 2: logger.warn(error)', code: wrap('logger.warn(error);'), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'warn', identifier: 'error' } }], }, { name: 'tier 2: logger.error(exception)', code: wrap('logger.error(exception);'), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'error', identifier: 'exception' } }], }, { name: 'tier 2: logger.info(cause)', code: wrap('logger.info(cause);'), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'info', identifier: 'cause' } }], }, { name: 'tier 2: console.error(error)', code: wrap('console.error(error);'), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'error', identifier: 'error' } }], }, { name: 'tier 2: error-named var inside nested function in catch — tier 2 still fires', code: wrap(` try { foo(); } catch (err) { const handler = () => { logger.error(err); }; handler(); } `), errors: [{ messageId: 'fullErrorObjectLogged', data: { method: 'error', identifier: 'err' } }], }, ], }); });