import { RuleTester } from 'eslint'; import test from 'node:test'; import rule from '../../src/eslint/rules/no-throw-new-error.js'; const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 2022, sourceType: 'module', }, }); const HANDLER_FILE = '/repo/integrations/foo/src/handlers/item.ts'; test('no-throw-new-error', () => { ruleTester.run('no-throw-new-error', rule, { valid: [ { name: 'throw typed HttpErrors', filename: HANDLER_FILE, code: `function f() { throw new HttpErrors.NotFoundError('not found'); }`, }, { name: 'throw user-defined typed error', filename: HANDLER_FILE, code: `function f() { throw new ProviderRateLimitError('slow down'); }`, }, { name: 'rethrow caught error', filename: HANDLER_FILE, code: `function f() { try { g(); } catch (e) { throw e; } }`, }, { name: 'allowlist: cache.ts may throw raw Error', filename: '/repo/integrations/foo/src/cache.ts', code: `function hash(v) { if (v === undefined) { throw new Error('Cannot hash undefined value.'); } }`, }, { name: 'allowlist: scripts/ may throw raw Error', filename: '/repo/integrations/foo/src/scripts/createBatch.ts', code: `async function main() { throw new Error('Failed to fetch credentials'); }`, }, { name: 'allowlist: script/ (singular) may throw raw Error', filename: '/repo/integrations/foo/src/script/credentialsHelper.ts', code: `async function main() { throw new Error('No access token found in credential payload'); }`, }, { name: 'allowlist: errors.ts (typed error definitions) may throw raw', filename: '/repo/integrations/foo/src/errors.ts', code: `class FooError extends Error { constructor(m) { super(m); throw new Error('init'); } }`, }, { name: 'allowlist: helpers/stringHelper.ts may throw raw Error', filename: '/repo/integrations/foo/src/helpers/stringHelper.ts', code: `function decode(s) { if (s === undefined) { throw new Error('String to encode is undefined'); } }`, }, { name: 'allowlist message: "only supported in tests"', filename: HANDLER_FILE, code: `async function deleteItem() { throw new Error('Deleting issues is only supported in tests'); }`, }, { name: 'allowlist message: assertNever', filename: HANDLER_FILE, code: `function check(x) { throw new Error('assertNever: ' + x); }`, }, { name: 'throw a literal value (not an Error constructor) — out of scope', filename: HANDLER_FILE, code: `function f() { throw 'plain string'; }`, }, { name: 'throw custom subclass of Error', filename: HANDLER_FILE, code: `class FooError extends Error {}\nfunction f() { throw new FooError('boom'); }`, }, { name: 'MemberExpression callee like globalThis.Error is out of scope (edge case)', filename: HANDLER_FILE, code: `function f() { throw new globalThis.Error('x'); }`, }, ], invalid: [ { name: 'throw new Error in handler', filename: HANDLER_FILE, code: `function f() { throw new Error('Item not found'); }`, errors: [{ messageId: 'rawThrow', data: { name: 'Error' } }], }, { name: 'throw Error without `new` (still an Error instance)', filename: HANDLER_FILE, code: `function f() { throw Error('Item not found'); }`, errors: [{ messageId: 'rawThrow', data: { name: 'Error' } }], }, { name: 'throw new TypeError in handler', filename: HANDLER_FILE, code: `function f() { throw new TypeError('Invalid input'); }`, errors: [{ messageId: 'rawThrow', data: { name: 'TypeError' } }], }, { name: 'throw new RangeError in handler', filename: HANDLER_FILE, code: `function f() { throw new RangeError('out of range'); }`, errors: [{ messageId: 'rawThrow', data: { name: 'RangeError' } }], }, { name: 'throw new Error w/ template literal does not match allowlist', filename: HANDLER_FILE, code: 'function f(t) { throw new Error(`Unsupported item type "${t}"`); }', errors: [{ messageId: 'rawThrow', data: { name: 'Error' } }], }, { name: 'throw new Error w/ template literal — partial allowlist match should NOT exempt', filename: HANDLER_FILE, code: 'function f(t) { throw new Error(`Unsupported item type "${t}" - only supported in production`); }', errors: [{ messageId: 'rawThrow', data: { name: 'Error' } }], }, ], }); });