From a4f7195571f6efafc3062a8ff5475b95330ff6bb Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 1 Dec 2023 11:04:18 +0000 Subject: [PATCH 01/31] feat(eslint-plugin): [prefer-promise-reject-errors] new rule! --- packages/eslint-plugin/src/configs/all.ts | 2 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-throw-literal.ts | 38 +--- .../src/rules/prefer-promise-reject-errors.ts | 156 +++++++++++++++++ .../prefer-promise-reject-errors.test.ts | 164 ++++++++++++++++++ .../prefer-promise-reject-errors.shot | 28 +++ packages/type-utils/src/index.ts | 1 + packages/type-utils/src/isErrorLike.ts | 37 ++++ 8 files changed, 392 insertions(+), 36 deletions(-) create mode 100644 packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts create mode 100644 packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts create mode 100644 packages/eslint-plugin/tests/schema-snapshots/prefer-promise-reject-errors.shot create mode 100644 packages/type-utils/src/isErrorLike.ts diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 7717b386cc9f..d3589df450f6 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -153,6 +153,8 @@ export = { '@typescript-eslint/prefer-namespace-keyword': 'error', '@typescript-eslint/prefer-nullish-coalescing': 'error', '@typescript-eslint/prefer-optional-chain': 'error', + 'prefer-promise-reject-errors': 'off', + '@typescript-eslint/prefer-promise-reject-errors': 'error', '@typescript-eslint/prefer-readonly': 'error', '@typescript-eslint/prefer-readonly-parameter-types': 'error', '@typescript-eslint/prefer-reduce-type-parameter': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 5423a7b82075..54530c6ad3be 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -108,6 +108,7 @@ import preferLiteralEnumMember from './prefer-literal-enum-member'; import preferNamespaceKeyword from './prefer-namespace-keyword'; import preferNullishCoalescing from './prefer-nullish-coalescing'; import preferOptionalChain from './prefer-optional-chain'; +import preferPromiseRejectErrors from './prefer-promise-reject-errors'; import preferReadonly from './prefer-readonly'; import preferReadonlyParameterTypes from './prefer-readonly-parameter-types'; import preferReduceTypeParameter from './prefer-reduce-type-parameter'; @@ -246,6 +247,7 @@ export default { 'prefer-namespace-keyword': preferNamespaceKeyword, 'prefer-nullish-coalescing': preferNullishCoalescing, 'prefer-optional-chain': preferOptionalChain, + 'prefer-promise-reject-errors': preferPromiseRejectErrors, 'prefer-readonly': preferReadonly, 'prefer-readonly-parameter-types': preferReadonlyParameterTypes, 'prefer-reduce-type-parameter': preferReduceTypeParameter, diff --git a/packages/eslint-plugin/src/rules/no-throw-literal.ts b/packages/eslint-plugin/src/rules/no-throw-literal.ts index f1129c252036..f3f5937c7379 100644 --- a/packages/eslint-plugin/src/rules/no-throw-literal.ts +++ b/packages/eslint-plugin/src/rules/no-throw-literal.ts @@ -5,6 +5,7 @@ import * as ts from 'typescript'; import { createRule, getParserServices, + isErrorLike, isTypeAnyType, isTypeUnknownType, } from '../util'; @@ -55,41 +56,6 @@ export default createRule({ ], create(context, [options]) { const services = getParserServices(context); - const checker = services.program.getTypeChecker(); - - function isErrorLike(type: ts.Type): boolean { - if (type.isIntersection()) { - return type.types.some(isErrorLike); - } - if (type.isUnion()) { - return type.types.every(isErrorLike); - } - - const symbol = type.getSymbol(); - if (!symbol) { - return false; - } - - if (symbol.getName() === 'Error') { - const declarations = symbol.getDeclarations() ?? []; - for (const declaration of declarations) { - const sourceFile = declaration.getSourceFile(); - if (services.program.isSourceFileDefaultLibrary(sourceFile)) { - return true; - } - } - } - - if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { - for (const baseType of checker.getBaseTypes(type as ts.InterfaceType)) { - if (isErrorLike(baseType)) { - return true; - } - } - } - - return false; - } function checkThrowArgument(node: TSESTree.Node): void { if ( @@ -114,7 +80,7 @@ export default createRule({ return; } - if (isErrorLike(type)) { + if (isErrorLike(services.program, type)) { return; } diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts new file mode 100644 index 000000000000..20ee790748c3 --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -0,0 +1,156 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; +import { + createRule, + getParserServices, + isErrorLike, + isFunction, + isIdentifier, +} from '../util'; + +import { getDeclaredVariables } from '@typescript-eslint/utils/eslint-utils'; + +export type MessageIds = 'rejectAnError'; + +export type Options = [ + { + allowEmptyReject?: boolean; + }, +]; + +export default createRule({ + name: 'prefer-promise-reject-errors', + meta: { + type: 'suggestion', + docs: { + description: 'Require using Error objects as Promise rejection reasons', + extendsBaseRule: true, + requiresTypeChecking: true, + }, + schema: [ + { + type: 'object', + properties: { + allowEmptyReject: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + messages: { + rejectAnError: 'Expected the Promise rejection reason to be an Error.', + }, + }, + defaultOptions: [ + { + allowEmptyReject: false, + }, + ], + create(context, [options]) { + const services = getParserServices(context); + const program = services.program; + + function checkRejectCall(callExpression: TSESTree.CallExpression) { + const argument = callExpression.arguments.at(0); + if (!argument && options.allowEmptyReject) { + return; + } + + if ( + !argument || + !isErrorLike(program, services.getTypeAtLocation(argument)) + ) { + context.report({ + node: callExpression, + messageId: 'rejectAnError', + }); + } + } + + function isPromiseConstructorLike(type: ts.Type): boolean { + const symbol = type.getSymbol(); + if (!symbol) { + return false; + } + + if (symbol.getName() === 'PromiseConstructor') { + const declarations = symbol.getDeclarations() ?? []; + for (const declaration of declarations) { + const sourceFile = declaration.getSourceFile(); + if (program.isSourceFileDefaultLibrary(sourceFile)) { + return true; + } + } + } + + return false; + } + + function skipChainExpression(node: T): T & TSESTree.ChainElement { + // @ts-expect-error https://github.com/typescript-eslint/typescript-eslint/issues/8008 + return node.type === AST_NODE_TYPES.ChainExpression + ? // @ts-expect-error ^ + node.expression + : node; + } + + return { + CallExpression(node) { + const callee = skipChainExpression(node.callee); + if ( + callee.type !== AST_NODE_TYPES.MemberExpression || + callee.computed + ) { + return; + } + + if ( + callee.property.name !== 'reject' || + !isPromiseConstructorLike(services.getTypeAtLocation(callee.object)) + ) { + return; + } + + checkRejectCall(node); + }, + NewExpression(node) { + const callee = skipChainExpression(node.callee); + if (!isPromiseConstructorLike(services.getTypeAtLocation(callee))) { + return; + } + + const executor = node.arguments.at(0); + if (!executor || !isFunction(executor)) { + return; + } + const rejectParamNode = executor.params.at(1); + if (!rejectParamNode || !isIdentifier(rejectParamNode)) { + return; + } + + const rejectVariable = getDeclaredVariables(context, executor).find( + variable => variable.identifiers.includes(rejectParamNode), + ); + if (!rejectVariable) { + return; + } + + const references = rejectVariable.references.filter( + ref => + ref.isRead() && + ref.identifier.parent.type === 'CallExpression' && + ref.identifier === ref.identifier.parent.callee, + ); + + references.forEach(ref => { + if (ref.identifier.parent.type !== AST_NODE_TYPES.CallExpression) { + return; + } + + checkRejectCall(ref.identifier.parent); + }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts new file mode 100644 index 000000000000..78fb4fcf4cb1 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -0,0 +1,164 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule, { + MessageIds, + Options, +} from '../../src/rules/prefer-promise-reject-errors'; +import { getFixturesRootDir } from '../RuleTester'; +import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('prefer-promise-reject-errors', rule, { + valid: [ + 'Promise.resolve(5)', + { + code: 'Promise.reject()', + options: [ + { + allowEmptyReject: true, + }, + ], + }, + 'Promise.reject(new Error())', + 'Promise.reject(new TypeError)', + "Promise.reject(new Error('foo'))", + ` + class CustomError extends Error {} + Promise.reject(new CustomError()) + `, + ` + declare const foo: () => { err: SyntaxError } + Promise.reject(foo().err) + `, + ` + declare const foo: () => Promise + Promise.reject(await foo()) + `, + 'Promise.reject(foo = new Error())', + + 'new Promise(function(resolve, reject) { resolve(5) })', + 'new Promise(function(resolve, reject) { reject(new Error()) })', + 'new Promise((resolve, reject) => { reject(new Error()) })', + 'new Promise((resolve, reject) => reject(new Error()))', + { + code: 'new Promise(function(resolve, reject) { reject() })', + options: [ + { + allowEmptyReject: true, + }, + ], + }, + ` + class CustomError extends Error {} + new Promise(function(resolve, reject) { reject(new CustomError()) }) + `, + ` + declare const foo: () => { err: SyntaxError } + new Promise(function(resolve, reject) { reject(foo().err) }) + `, + 'new Promise((resolve, reject) => reject(foo = new Error()))', + ` + class Foo { + constructor(executor: (resolve: () => void, reject: (reason?: any) => void) => void): Promise {} + } + new Foo((resolve, reject) => reject(5)) + `, + 'new Promise((resolve, reject) => { return function(reject) { reject(5) } })', + 'new Promise((resolve, reject) => resolve(5, reject))', + 'class C { #error: Error; foo() { Promise.reject(this.#error); } }', + ], + invalid: [ + 'Promise.reject(5)', + "Promise.reject('foo')", + 'Promise.reject(`foo`)', + "Promise.reject('foo', somethingElse)", + 'Promise.reject(false)', + 'Promise.reject(void `foo`)', + 'Promise.reject()', + 'Promise.reject(undefined)', + { + code: 'Promise.reject(undefined)', + options: [{ allowEmptyReject: true }] as const, + }, + 'Promise.reject(null)', + 'Promise.reject({ foo: 1 })', + 'Promise.reject([1, 2, 3])', + ` + declare const foo: Error | undefined + Promise.reject(foo) + `, + ` + declare const foo: () => Promise + Promise.reject(await foo()) + `, + ` + declare const foo: boolean + Promise.reject(foo && new Error()) + `, + + 'Promise.reject?.(5)', + 'Promise?.reject(5)', + 'Promise?.reject?.(5)', + '(Promise?.reject)(5)', + '(Promise?.reject)?.(5)', + + // Assignments with mathematical operators will either evaluate to a primitive value or throw a TypeError + 'Promise.reject(foo += new Error())', + 'Promise.reject(foo -= new Error())', + 'Promise.reject(foo **= new Error())', + 'Promise.reject(foo <<= new Error())', + 'Promise.reject(foo |= new Error())', + 'Promise.reject(foo &= new Error())', + + 'new Promise(function(resolve, reject) { reject() })', + 'new Promise(function(resolve, reject) { reject(5) })', + 'new Promise((resolve, reject) => { reject() })', + 'new Promise((resolve, reject) => reject(5))', + ` + new Promise((resolve, reject) => { + fs.readFile('foo.txt', (err, file) => { + if (err) reject('File not found') + else resolve(file) + }) + }) + `, + 'new Promise((yes, no) => no(5))', + 'new Promise(({foo, bar, baz}, reject) => reject(5))', + 'new Promise(function(reject, reject) { reject(5) })', + 'new Promise(function(foo, arguments) { arguments(5) })', + 'new Promise((foo, arguments) => arguments(5))', + 'new Promise(function({}, reject) { reject(5) })', + 'new Promise(({}, reject) => reject(5))', + 'new Promise((resolve, reject, somethingElse = reject(5)) => {})', + ` + declare const foo: { + bar: PromiseConstructor + } + new foo.bar((resolve, reject) => reject(5)) + `, + ` + declare const foo: { + bar: PromiseConstructor + } + new (foo?.bar)((resolve, reject) => reject(5)) + `, + ].map>(invalidCase => { + return { + errors: [ + { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression }, + ], + ...(typeof invalidCase === 'string' + ? { code: invalidCase } + : invalidCase), + }; + }), +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/prefer-promise-reject-errors.shot b/packages/eslint-plugin/tests/schema-snapshots/prefer-promise-reject-errors.shot new file mode 100644 index 000000000000..fc04d11fd3f1 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/prefer-promise-reject-errors.shot @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes prefer-promise-reject-errors 1`] = ` +" +# SCHEMA: + +[ + { + "additionalProperties": false, + "properties": { + "allowEmptyReject": { + "type": "boolean" + } + }, + "type": "object" + } +] + + +# TYPES: + +type Options = [ + { + allowEmptyReject?: boolean; + }, +]; +" +`; diff --git a/packages/type-utils/src/index.ts b/packages/type-utils/src/index.ts index 9fc499aa8f31..16b43d90d34a 100644 --- a/packages/type-utils/src/index.ts +++ b/packages/type-utils/src/index.ts @@ -6,6 +6,7 @@ export * from './getSourceFileOfNode'; export * from './getTokenAtPosition'; export * from './getTypeArguments'; export * from './getTypeName'; +export * from './isErrorLike'; export * from './isTypeReadonly'; export * from './isUnsafeAssignment'; export * from './predicates'; diff --git a/packages/type-utils/src/isErrorLike.ts b/packages/type-utils/src/isErrorLike.ts new file mode 100644 index 000000000000..9c489123dd41 --- /dev/null +++ b/packages/type-utils/src/isErrorLike.ts @@ -0,0 +1,37 @@ +import * as ts from 'typescript'; + +export function isErrorLike(program: ts.Program, type: ts.Type): boolean { + if (type.isIntersection()) { + return type.types.some(t => isErrorLike(program, t)); + } + if (type.isUnion()) { + return type.types.every(t => isErrorLike(program, t)); + } + + const symbol = type.getSymbol(); + if (!symbol) { + return false; + } + + if (symbol.getName() === 'Error') { + const declarations = symbol.getDeclarations() ?? []; + for (const declaration of declarations) { + const sourceFile = declaration.getSourceFile(); + if (program.isSourceFileDefaultLibrary(sourceFile)) { + return true; + } + } + } + + if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { + const checker = program.getTypeChecker(); + + for (const baseType of checker.getBaseTypes(type as ts.InterfaceType)) { + if (isErrorLike(program, baseType)) { + return true; + } + } + } + + return false; +} From 538323f8d11fcaebef449704fc32183f3aa06bd0 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 1 Dec 2023 12:18:04 +0000 Subject: [PATCH 02/31] test: ~100% coverage --- .../src/rules/prefer-promise-reject-errors.ts | 22 +++++++------------ .../prefer-promise-reject-errors.test.ts | 8 +++++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 20ee790748c3..22a685b52558 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -70,11 +70,8 @@ export default createRule({ function isPromiseConstructorLike(type: ts.Type): boolean { const symbol = type.getSymbol(); - if (!symbol) { - return false; - } - if (symbol.getName() === 'PromiseConstructor') { + if (symbol?.getName() === 'PromiseConstructor') { const declarations = symbol.getDeclarations() ?? []; for (const declaration of declarations) { const sourceFile = declaration.getSourceFile(); @@ -132,19 +129,16 @@ export default createRule({ const rejectVariable = getDeclaredVariables(context, executor).find( variable => variable.identifiers.includes(rejectParamNode), ); - if (!rejectVariable) { + /* istanbul ignore if */ if (!rejectVariable) { return; } - const references = rejectVariable.references.filter( - ref => - ref.isRead() && - ref.identifier.parent.type === 'CallExpression' && - ref.identifier === ref.identifier.parent.callee, - ); - - references.forEach(ref => { - if (ref.identifier.parent.type !== AST_NODE_TYPES.CallExpression) { + rejectVariable.references.forEach(ref => { + if ( + !ref.isRead() || + ref.identifier.parent.type !== AST_NODE_TYPES.CallExpression || + ref.identifier !== ref.identifier.parent.callee + ) { return; } diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 78fb4fcf4cb1..1633394f1a75 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -57,6 +57,11 @@ ruleTester.run('prefer-promise-reject-errors', rule, { }, ], }, + 'new Promise()', + 'new Promise(5)', + 'new Promise((resolve, {apply}) => {})', + 'new Promise((resolve, reject) => {})', + 'new Promise((resolve, reject) => reject)', ` class CustomError extends Error {} new Promise(function(resolve, reject) { reject(new CustomError()) }) @@ -66,6 +71,9 @@ ruleTester.run('prefer-promise-reject-errors', rule, { new Promise(function(resolve, reject) { reject(foo().err) }) `, 'new Promise((resolve, reject) => reject(foo = new Error()))', + ` + new Foo((resolve, reject) => reject(5)) + `, ` class Foo { constructor(executor: (resolve: () => void, reject: (reason?: any) => void) => void): Promise {} From f779d937abf21507c4ed3b5309e3934f4b82bae7 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 1 Dec 2023 12:31:13 +0000 Subject: [PATCH 03/31] docs: add rule docs --- .../rules/prefer-promise-reject-errors.md | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md diff --git a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md new file mode 100644 index 000000000000..864fc176ab01 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md @@ -0,0 +1,165 @@ +--- +description: 'Require using Error objects as Promise rejection reasons.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/prefer-promise-reject-errors** for documentation. + +It is considered good practice to only pass instances of the built-in `Error` object to the `reject()` function for user-defined errors in Promises. `Error` objects automatically store a stack trace, which can be used to debug an error by determining where it came from. If a Promise is rejected with a non-`Error` value, it can be difficult to determine where the rejection occurred. + +This rule restricts what can be used as an Promise rejection reason. + +## Examples + + + +### ❌ Incorrect + +```ts +// Promise.reject + +Promise.reject('error'); + +Promise.reject(0); + +Promise.reject(undefined); + +Promise.reject(null); + +const err = new Error(); +Promise.reject('an ' + err); + +const err = new Error(); +Promise.reject(`${err}`); + +const err = ''; +Promise.reject(err); + +function err() { + return ''; +} +Promise.reject(err()); + +const foo = { + bar: '', +}; +Promise.reject(foo.bar); + +// new Promise + +new Promise((resolve, reject) => reject('error')); + +new Promise((resolve, reject) => reject(0)); + +new Promise((resolve, reject) => reject(undefined)); + +new Promise((resolve, reject) => reject(null)); + +new Promise((resolve, reject) => { + const err = new Error(); + reject('an ' + err); +}); + +new Promise((resolve, reject) => { + const err = new Error(); + reject(`${err}`); +}); + +const err = ''; +new Promise((resolve, reject) => reject(err)); + +new Promise((resolve, reject) => { + function err() { + return ''; + } + return reject(err()); +}); + +new Promise((resolve, reject) => { + const foo = { + bar: '', + }; + return reject(foo.bar); +}); +``` + +### ✅ Correct + +```ts +// Promise.reject + +Promise.reject(new Error()); + +Promise.reject(new Error('error')); + +const e = new Error('error'); +Promise.reject(e); + +try { + Promise.reject(new Error('error')); +} catch (e) { + Promise.reject(e); +} + +const err = new Error(); +Promise.reject(err); + +function err() { + return new Error(); +} +Promise.reject(err()); + +const foo = { + bar: new Error(), +}; +Promise.reject(foo.bar); + +class CustomError extends Error { + // ... +} +Promise.reject(new CustomError()); + +// new Promise + +new Promise((resolve, reject) => reject(new Error())); + +new Promise((resolve, reject) => reject(new Error('error'))); + +new Promise((resolve, reject) => { + const e = new Error('error'); + return reject(e); +}); + +try { + new Promise((resolve, reject) => reject(new Error('error'))); +} catch (e) { + new Promise((resolve, reject) => reject(e)); +} + +new Promise((resolve, reject) => { + const err = new Error(); + return reject(err); +}); + +new Promise((resolve, reject) => { + function err() { + return new Error(); + } + return reject(err()); +}); + +new Promise((resolve, reject) => { + const foo = { + bar: new Error(), + }; + return reject(foo.bar); +}); + +new Promise((resolve, reject) => { + class CustomError extends Error { + // ... + } + return reject(new CustomError()); +}); +``` From a27930a5b996e1f8111f4175fb896893635f21fc Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 1 Dec 2023 14:51:58 +0000 Subject: [PATCH 04/31] test: add some cases --- .../rules/prefer-promise-reject-errors.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 1633394f1a75..4e91e5e75bd9 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -44,6 +44,10 @@ ruleTester.run('prefer-promise-reject-errors', rule, { Promise.reject(await foo()) `, 'Promise.reject(foo = new Error())', + ` + const foo = Promise + foo.reject(new Error()) + `, 'new Promise(function(resolve, reject) { resolve(5) })', 'new Promise(function(resolve, reject) { reject(new Error()) })', @@ -83,6 +87,10 @@ ruleTester.run('prefer-promise-reject-errors', rule, { 'new Promise((resolve, reject) => { return function(reject) { reject(5) } })', 'new Promise((resolve, reject) => resolve(5, reject))', 'class C { #error: Error; foo() { Promise.reject(this.#error); } }', + ` + const foo = Promise + new foo((resolve, reject) => reject(new Error())) + `, ], invalid: [ 'Promise.reject(5)', @@ -112,6 +120,10 @@ ruleTester.run('prefer-promise-reject-errors', rule, { declare const foo: boolean Promise.reject(foo && new Error()) `, + ` + const foo = Promise + foo.reject() + `, 'Promise.reject?.(5)', 'Promise?.reject(5)', @@ -159,6 +171,10 @@ ruleTester.run('prefer-promise-reject-errors', rule, { } new (foo?.bar)((resolve, reject) => reject(5)) `, + ` + const foo = Promise + new foo((resolve, reject) => reject(5)) + `, ].map>(invalidCase => { return { errors: [ From 2b83ea4cc72774722babc7e83fa96ae914867cf5 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 1 Dec 2023 15:37:04 +0000 Subject: [PATCH 05/31] chore: lint --fix --- .../src/rules/prefer-promise-reject-errors.ts | 17 +-- .../prefer-promise-reject-errors.test.ts | 111 ++++++++++++------ 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 22a685b52558..3c6738e78404 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -1,5 +1,8 @@ -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { getDeclaredVariables } from '@typescript-eslint/utils/eslint-utils'; import type * as ts from 'typescript'; + import { createRule, getParserServices, @@ -8,8 +11,6 @@ import { isIdentifier, } from '../util'; -import { getDeclaredVariables } from '@typescript-eslint/utils/eslint-utils'; - export type MessageIds = 'rejectAnError'; export type Options = [ @@ -51,7 +52,7 @@ export default createRule({ const services = getParserServices(context); const program = services.program; - function checkRejectCall(callExpression: TSESTree.CallExpression) { + function checkRejectCall(callExpression: TSESTree.CallExpression): void { const argument = callExpression.arguments.at(0); if (!argument && options.allowEmptyReject) { return; @@ -87,13 +88,13 @@ export default createRule({ function skipChainExpression(node: T): T & TSESTree.ChainElement { // @ts-expect-error https://github.com/typescript-eslint/typescript-eslint/issues/8008 return node.type === AST_NODE_TYPES.ChainExpression - ? // @ts-expect-error ^ - node.expression + ? // @ts-expect-error check the issue above ^ + (node.expression as TSESTree.ChainExpression['expression']) : node; } return { - CallExpression(node) { + CallExpression(node): void { const callee = skipChainExpression(node.callee); if ( callee.type !== AST_NODE_TYPES.MemberExpression || @@ -111,7 +112,7 @@ export default createRule({ checkRejectCall(node); }, - NewExpression(node) { + NewExpression(node): void { const callee = skipChainExpression(node.callee); if (!isPromiseConstructorLike(services.getTypeAtLocation(callee))) { return; diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 4e91e5e75bd9..845d7ea69436 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -1,11 +1,13 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; +import type { TSESLint } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import rule, { +import type { MessageIds, Options, } from '../../src/rules/prefer-promise-reject-errors'; +import rule from '../../src/rules/prefer-promise-reject-errors'; import { getFixturesRootDir } from '../RuleTester'; -import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils'; const rootDir = getFixturesRootDir(); const ruleTester = new RuleTester({ @@ -19,77 +21,112 @@ const ruleTester = new RuleTester({ ruleTester.run('prefer-promise-reject-errors', rule, { valid: [ - 'Promise.resolve(5)', + 'Promise.resolve(5);', { - code: 'Promise.reject()', + code: 'Promise.reject();', options: [ { allowEmptyReject: true, }, ], }, - 'Promise.reject(new Error())', - 'Promise.reject(new TypeError)', - "Promise.reject(new Error('foo'))", + 'Promise.reject(new Error());', + 'Promise.reject(new TypeError());', + "Promise.reject(new Error('foo'));", ` class CustomError extends Error {} - Promise.reject(new CustomError()) + Promise.reject(new CustomError()); `, ` - declare const foo: () => { err: SyntaxError } - Promise.reject(foo().err) + declare const foo: () => { err: SyntaxError }; + Promise.reject(foo().err); `, ` - declare const foo: () => Promise - Promise.reject(await foo()) + declare const foo: () => Promise; + Promise.reject(await foo()); `, - 'Promise.reject(foo = new Error())', + 'Promise.reject((foo = new Error()));', ` - const foo = Promise - foo.reject(new Error()) + const foo = Promise; + foo.reject(new Error()); `, - 'new Promise(function(resolve, reject) { resolve(5) })', - 'new Promise(function(resolve, reject) { reject(new Error()) })', - 'new Promise((resolve, reject) => { reject(new Error()) })', - 'new Promise((resolve, reject) => reject(new Error()))', + ` +new Promise(function (resolve, reject) { + resolve(5); +}); + `, + ` +new Promise(function (resolve, reject) { + reject(new Error()); +}); + `, + ` +new Promise((resolve, reject) => { + reject(new Error()); +}); + `, + 'new Promise((resolve, reject) => reject(new Error()));', { - code: 'new Promise(function(resolve, reject) { reject() })', + code: ` +new Promise(function (resolve, reject) { + reject(); +}); + `, options: [ { allowEmptyReject: true, }, ], }, - 'new Promise()', - 'new Promise(5)', - 'new Promise((resolve, {apply}) => {})', - 'new Promise((resolve, reject) => {})', - 'new Promise((resolve, reject) => reject)', + 'new Promise();', + 'new Promise(5);', + 'new Promise((resolve, { apply }) => {});', + 'new Promise((resolve, reject) => {});', + 'new Promise((resolve, reject) => reject);', ` class CustomError extends Error {} - new Promise(function(resolve, reject) { reject(new CustomError()) }) + new Promise(function (resolve, reject) { + reject(new CustomError()); + }); `, ` - declare const foo: () => { err: SyntaxError } - new Promise(function(resolve, reject) { reject(foo().err) }) + declare const foo: () => { err: SyntaxError }; + new Promise(function (resolve, reject) { + reject(foo().err); + }); `, - 'new Promise((resolve, reject) => reject(foo = new Error()))', + 'new Promise((resolve, reject) => reject((foo = new Error())));', ` - new Foo((resolve, reject) => reject(5)) + new Foo((resolve, reject) => reject(5)); `, ` class Foo { - constructor(executor: (resolve: () => void, reject: (reason?: any) => void) => void): Promise {} + constructor( + executor: (resolve: () => void, reject: (reason?: any) => void) => void, + ): Promise {} } - new Foo((resolve, reject) => reject(5)) + new Foo((resolve, reject) => reject(5)); `, - 'new Promise((resolve, reject) => { return function(reject) { reject(5) } })', - 'new Promise((resolve, reject) => resolve(5, reject))', - 'class C { #error: Error; foo() { Promise.reject(this.#error); } }', ` - const foo = Promise - new foo((resolve, reject) => reject(new Error())) +new Promise((resolve, reject) => { + return function (reject) { + reject(5); + }; +}); + `, + 'new Promise((resolve, reject) => resolve(5, reject));', + ` +class C { + #error: Error; + foo() { + Promise.reject(this.#error); + } +} + `, + ` + const foo = Promise; + new foo((resolve, reject) => reject(new Error())); `, ], invalid: [ From d86f5c02857ba48184e3f6ee7b0e12d8b9170352 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 1 Dec 2023 15:49:29 +0000 Subject: [PATCH 06/31] chore: reformat tests --- .../prefer-promise-reject-errors.test.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 845d7ea69436..10d3cabc88a1 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -52,26 +52,26 @@ ruleTester.run('prefer-promise-reject-errors', rule, { `, ` -new Promise(function (resolve, reject) { - resolve(5); -}); + new Promise(function (resolve, reject) { + resolve(5); + }); `, ` -new Promise(function (resolve, reject) { - reject(new Error()); -}); + new Promise(function (resolve, reject) { + reject(new Error()); + }); `, ` -new Promise((resolve, reject) => { - reject(new Error()); -}); + new Promise((resolve, reject) => { + reject(new Error()); + }); `, 'new Promise((resolve, reject) => reject(new Error()));', { code: ` -new Promise(function (resolve, reject) { - reject(); -}); + new Promise(function (resolve, reject) { + reject(); + }); `, options: [ { @@ -109,20 +109,20 @@ new Promise(function (resolve, reject) { new Foo((resolve, reject) => reject(5)); `, ` -new Promise((resolve, reject) => { - return function (reject) { - reject(5); - }; -}); + new Promise((resolve, reject) => { + return function (reject) { + reject(5); + }; + }); `, 'new Promise((resolve, reject) => resolve(5, reject));', ` -class C { - #error: Error; - foo() { - Promise.reject(this.#error); - } -} + class C { + #error: Error; + foo() { + Promise.reject(this.#error); + } + } `, ` const foo = Promise; From dab6503325cf0062bfd0fe411aa5617f58d5349a Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 1 Dec 2023 15:50:40 +0000 Subject: [PATCH 07/31] feat: add support for literal computed reject name --- .../src/rules/prefer-promise-reject-errors.ts | 11 ++++------- .../tests/rules/prefer-promise-reject-errors.test.ts | 2 ++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 3c6738e78404..f9a5d7bfeefe 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -98,13 +98,10 @@ export default createRule({ const callee = skipChainExpression(node.callee); if ( callee.type !== AST_NODE_TYPES.MemberExpression || - callee.computed - ) { - return; - } - - if ( - callee.property.name !== 'reject' || + (callee.computed + ? callee.property.type === AST_NODE_TYPES.Literal && + callee.property.value !== 'reject' + : callee.property.name !== 'reject') || !isPromiseConstructorLike(services.getTypeAtLocation(callee.object)) ) { return; diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 10d3cabc88a1..c1ecba7014c5 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -50,6 +50,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { const foo = Promise; foo.reject(new Error()); `, + `Promise['reject'](new Error())`, ` new Promise(function (resolve, reject) { @@ -167,6 +168,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { 'Promise?.reject?.(5)', '(Promise?.reject)(5)', '(Promise?.reject)?.(5)', + `Promise['reject'](5)`, // Assignments with mathematical operators will either evaluate to a primitive value or throw a TypeError 'Promise.reject(foo += new Error())', From 8d8bbc32fb8a67097e71a25be2e202fb53f295c9 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 1 Dec 2023 16:08:39 +0000 Subject: [PATCH 08/31] chore: lint --fix --- .../tests/rules/prefer-promise-reject-errors.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index c1ecba7014c5..14ee409525a7 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -50,7 +50,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { const foo = Promise; foo.reject(new Error()); `, - `Promise['reject'](new Error())`, + "Promise['reject'](new Error());", ` new Promise(function (resolve, reject) { From ff307be48db215ffebfdc2aa519d7c41fd3bd733 Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 3 Dec 2023 12:00:46 +0000 Subject: [PATCH 09/31] refactor: get rid of one @ts-expect-error --- .../eslint-plugin/src/rules/prefer-promise-reject-errors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index f9a5d7bfeefe..f3fd42249b64 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -88,8 +88,7 @@ export default createRule({ function skipChainExpression(node: T): T & TSESTree.ChainElement { // @ts-expect-error https://github.com/typescript-eslint/typescript-eslint/issues/8008 return node.type === AST_NODE_TYPES.ChainExpression - ? // @ts-expect-error check the issue above ^ - (node.expression as TSESTree.ChainExpression['expression']) + ? (node as TSESTree.ChainExpression).expression : node; } From ccde4a6933083f906f4978c56253ceb322cc0dfe Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 3 Dec 2023 19:37:31 +0000 Subject: [PATCH 10/31] docs: refer to the original rule description --- .../eslint-plugin/docs/rules/prefer-promise-reject-errors.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md index 864fc176ab01..dc3143968ebb 100644 --- a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md +++ b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md @@ -6,9 +6,8 @@ description: 'Require using Error objects as Promise rejection reasons.' > > See **https://typescript-eslint.io/rules/prefer-promise-reject-errors** for documentation. -It is considered good practice to only pass instances of the built-in `Error` object to the `reject()` function for user-defined errors in Promises. `Error` objects automatically store a stack trace, which can be used to debug an error by determining where it came from. If a Promise is rejected with a non-`Error` value, it can be difficult to determine where the rejection occurred. - -This rule restricts what can be used as an Promise rejection reason. +This rule extends the base [`eslint/prefer-promise-reject-errors`](https://eslint.org/docs/rules/prefer-promise-reject-errors) rule. +It uses type information to guarantee that `Promise` can only be rejected with `Error` objects. ## Examples From d153d13729ef212b2853207da4ad1592a231d00c Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 3 Dec 2023 19:43:15 +0000 Subject: [PATCH 11/31] test: add few cases --- .../tests/rules/prefer-promise-reject-errors.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 14ee409525a7..7c0a104457c4 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -51,6 +51,11 @@ ruleTester.run('prefer-promise-reject-errors', rule, { foo.reject(new Error()); `, "Promise['reject'](new Error());", + 'Promise.reject(true && new Error());', + ` + const foo = false; + Promise.reject(false || new Error()); + `, ` new Promise(function (resolve, reject) { @@ -80,6 +85,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { }, ], }, + 'new Promise((yes, no) => no(new Error()));', 'new Promise();', 'new Promise(5);', 'new Promise((resolve, { apply }) => {});', From 5263fa80ed318f9ae7cf8abbb71c6b3dc91b529f Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 15:17:43 +0000 Subject: [PATCH 12/31] docs: remove some examples --- .../rules/prefer-promise-reject-errors.md | 74 +------------------ 1 file changed, 2 insertions(+), 72 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md index dc3143968ebb..27eaee761569 100644 --- a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md +++ b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md @@ -20,26 +20,9 @@ It uses type information to guarantee that `Promise` can only be rejected with ` Promise.reject('error'); -Promise.reject(0); - -Promise.reject(undefined); - -Promise.reject(null); - const err = new Error(); Promise.reject('an ' + err); -const err = new Error(); -Promise.reject(`${err}`); - -const err = ''; -Promise.reject(err); - -function err() { - return ''; -} -Promise.reject(err()); - const foo = { bar: '', }; @@ -49,32 +32,11 @@ Promise.reject(foo.bar); new Promise((resolve, reject) => reject('error')); -new Promise((resolve, reject) => reject(0)); - -new Promise((resolve, reject) => reject(undefined)); - -new Promise((resolve, reject) => reject(null)); - new Promise((resolve, reject) => { const err = new Error(); reject('an ' + err); }); -new Promise((resolve, reject) => { - const err = new Error(); - reject(`${err}`); -}); - -const err = ''; -new Promise((resolve, reject) => reject(err)); - -new Promise((resolve, reject) => { - function err() { - return ''; - } - return reject(err()); -}); - new Promise((resolve, reject) => { const foo = { bar: '', @@ -90,25 +52,12 @@ new Promise((resolve, reject) => { Promise.reject(new Error()); -Promise.reject(new Error('error')); - -const e = new Error('error'); -Promise.reject(e); - try { - Promise.reject(new Error('error')); + // ... } catch (e) { Promise.reject(e); } -const err = new Error(); -Promise.reject(err); - -function err() { - return new Error(); -} -Promise.reject(err()); - const foo = { bar: new Error(), }; @@ -123,31 +72,12 @@ Promise.reject(new CustomError()); new Promise((resolve, reject) => reject(new Error())); -new Promise((resolve, reject) => reject(new Error('error'))); - -new Promise((resolve, reject) => { - const e = new Error('error'); - return reject(e); -}); - try { - new Promise((resolve, reject) => reject(new Error('error'))); + // ... } catch (e) { new Promise((resolve, reject) => reject(e)); } -new Promise((resolve, reject) => { - const err = new Error(); - return reject(err); -}); - -new Promise((resolve, reject) => { - function err() { - return new Error(); - } - return reject(err()); -}); - new Promise((resolve, reject) => { const foo = { bar: new Error(), From 6981eb5d7b97ed1442031e3e38a766ec97581933 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 16:05:03 +0000 Subject: [PATCH 13/31] refactor: move check if symbol is from default lib or not to new fn --- .../src/rules/prefer-promise-reject-errors.ts | 14 ++------- packages/type-utils/src/index.ts | 1 + packages/type-utils/src/isErrorLike.ts | 12 +++----- .../src/isSymbolFromDefaultLibrary.ts | 29 +++++++++++++++++++ 4 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 packages/type-utils/src/isSymbolFromDefaultLibrary.ts diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index f3fd42249b64..93acfa01f09d 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -9,6 +9,7 @@ import { isErrorLike, isFunction, isIdentifier, + isSymbolFromDefaultLibrary, } from '../util'; export type MessageIds = 'rejectAnError'; @@ -71,18 +72,7 @@ export default createRule({ function isPromiseConstructorLike(type: ts.Type): boolean { const symbol = type.getSymbol(); - - if (symbol?.getName() === 'PromiseConstructor') { - const declarations = symbol.getDeclarations() ?? []; - for (const declaration of declarations) { - const sourceFile = declaration.getSourceFile(); - if (program.isSourceFileDefaultLibrary(sourceFile)) { - return true; - } - } - } - - return false; + return isSymbolFromDefaultLibrary(program, symbol, 'PromiseConstructor'); } function skipChainExpression(node: T): T & TSESTree.ChainElement { diff --git a/packages/type-utils/src/index.ts b/packages/type-utils/src/index.ts index 16b43d90d34a..51ad912495eb 100644 --- a/packages/type-utils/src/index.ts +++ b/packages/type-utils/src/index.ts @@ -7,6 +7,7 @@ export * from './getTokenAtPosition'; export * from './getTypeArguments'; export * from './getTypeName'; export * from './isErrorLike'; +export * from './isSymbolFromDefaultLibrary'; export * from './isTypeReadonly'; export * from './isUnsafeAssignment'; export * from './predicates'; diff --git a/packages/type-utils/src/isErrorLike.ts b/packages/type-utils/src/isErrorLike.ts index 9c489123dd41..57760cc0c1a1 100644 --- a/packages/type-utils/src/isErrorLike.ts +++ b/packages/type-utils/src/isErrorLike.ts @@ -1,5 +1,7 @@ import * as ts from 'typescript'; +import { isSymbolFromDefaultLibrary } from './isSymbolFromDefaultLibrary'; + export function isErrorLike(program: ts.Program, type: ts.Type): boolean { if (type.isIntersection()) { return type.types.some(t => isErrorLike(program, t)); @@ -13,14 +15,8 @@ export function isErrorLike(program: ts.Program, type: ts.Type): boolean { return false; } - if (symbol.getName() === 'Error') { - const declarations = symbol.getDeclarations() ?? []; - for (const declaration of declarations) { - const sourceFile = declaration.getSourceFile(); - if (program.isSourceFileDefaultLibrary(sourceFile)) { - return true; - } - } + if (isSymbolFromDefaultLibrary(program, symbol, 'Error')) { + return true; } if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { diff --git a/packages/type-utils/src/isSymbolFromDefaultLibrary.ts b/packages/type-utils/src/isSymbolFromDefaultLibrary.ts new file mode 100644 index 000000000000..6314959f9a83 --- /dev/null +++ b/packages/type-utils/src/isSymbolFromDefaultLibrary.ts @@ -0,0 +1,29 @@ +import type * as ts from 'typescript'; + +export function isSymbolFromDefaultLibrary( + program: ts.Program, + symbol: ts.Symbol | undefined, + nameOrPredicate?: string | ((symbol: ts.Symbol) => boolean), +): boolean { + if (!symbol) { + return false; + } + + if ( + typeof nameOrPredicate === 'string' + ? symbol.getName() === nameOrPredicate + : nameOrPredicate + ? nameOrPredicate(symbol) + : true + ) { + const declarations = symbol.getDeclarations() ?? []; + for (const declaration of declarations) { + const sourceFile = declaration.getSourceFile(); + if (program.isSourceFileDefaultLibrary(sourceFile)) { + return true; + } + } + } + + return false; +} From a680b8df2b2c5472e7fe7c7a639d6849b81188ce Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 16:12:21 +0000 Subject: [PATCH 14/31] refactor: assert that rejectVariable is non-nullable --- .../eslint-plugin/src/rules/prefer-promise-reject-errors.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 93acfa01f09d..9b1ee5ef593b 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -113,12 +113,10 @@ export default createRule({ return; } + // reject param is always present in variables declared by executor const rejectVariable = getDeclaredVariables(context, executor).find( variable => variable.identifiers.includes(rejectParamNode), - ); - /* istanbul ignore if */ if (!rejectVariable) { - return; - } + )!; rejectVariable.references.forEach(ref => { if ( From 1e2e7711a40c1229a570ceaba9a1c4664ecd2283 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 16:13:04 +0000 Subject: [PATCH 15/31] chore: remove assertion in skipChainExpression --- .../eslint-plugin/src/rules/prefer-promise-reject-errors.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 9b1ee5ef593b..06c6c54ec3d5 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -75,10 +75,12 @@ export default createRule({ return isSymbolFromDefaultLibrary(program, symbol, 'PromiseConstructor'); } - function skipChainExpression(node: T): T & TSESTree.ChainElement { + function skipChainExpression( + node: T, + ): T & TSESTree.ChainElement { // @ts-expect-error https://github.com/typescript-eslint/typescript-eslint/issues/8008 return node.type === AST_NODE_TYPES.ChainExpression - ? (node as TSESTree.ChainExpression).expression + ? node.expression : node; } From e1db9888cf3b8cd38eab2127e2a147ed1134a094 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 16:36:29 +0000 Subject: [PATCH 16/31] test: specify error ranges for invalid test cases --- .../prefer-promise-reject-errors.test.ts | 685 +++++++++++++++--- 1 file changed, 601 insertions(+), 84 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 7c0a104457c4..ebb4f44dd2bf 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -137,97 +137,614 @@ ruleTester.run('prefer-promise-reject-errors', rule, { `, ], invalid: [ - 'Promise.reject(5)', - "Promise.reject('foo')", - 'Promise.reject(`foo`)', - "Promise.reject('foo', somethingElse)", - 'Promise.reject(false)', - 'Promise.reject(void `foo`)', - 'Promise.reject()', - 'Promise.reject(undefined)', + { + code: 'Promise.reject(5)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 18, + }, + ], + }, + { + code: "Promise.reject('foo')", + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 22, + }, + ], + }, + { + code: 'Promise.reject(`foo`)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 22, + }, + ], + }, + { + code: "Promise.reject('foo', somethingElse)", + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 37, + }, + ], + }, + { + code: 'Promise.reject(false)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 22, + }, + ], + }, + { + code: 'Promise.reject(void `foo`)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 27, + }, + ], + }, + { + code: 'Promise.reject()', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 17, + }, + ], + }, { code: 'Promise.reject(undefined)', - options: [{ allowEmptyReject: true }] as const, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 26, + }, + ], + }, + { + code: 'Promise.reject(undefined)', + options: [{ allowEmptyReject: true }], + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 26, + }, + ], + }, + { + code: 'Promise.reject(null)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 21, + }, + ], + }, + { + code: 'Promise.reject({ foo: 1 })', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 27, + }, + ], + }, + { + code: 'Promise.reject([1, 2, 3])', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 26, + }, + ], + }, + { + code: ` +declare const foo: Error | undefined +Promise.reject(foo) + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: () => Promise +Promise.reject(await foo()) + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 28, + }, + ], + }, + { + code: ` +declare const foo: boolean +Promise.reject(foo && new Error()) + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 35, + }, + ], + }, + { + code: ` +const foo = Promise +foo.reject() + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 13, + }, + ], }, - 'Promise.reject(null)', - 'Promise.reject({ foo: 1 })', - 'Promise.reject([1, 2, 3])', - ` - declare const foo: Error | undefined - Promise.reject(foo) - `, - ` - declare const foo: () => Promise - Promise.reject(await foo()) - `, - ` - declare const foo: boolean - Promise.reject(foo && new Error()) - `, - ` - const foo = Promise - foo.reject() - `, - 'Promise.reject?.(5)', - 'Promise?.reject(5)', - 'Promise?.reject?.(5)', - '(Promise?.reject)(5)', - '(Promise?.reject)?.(5)', - `Promise['reject'](5)`, + { + code: 'Promise.reject?.(5)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: 'Promise?.reject(5)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 19, + }, + ], + }, + { + code: 'Promise?.reject?.(5)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 21, + }, + ], + }, + { + code: '(Promise?.reject)(5)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 21, + }, + ], + }, + { + code: '(Promise?.reject)?.(5)', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 23, + }, + ], + }, + { + code: `Promise['reject'](5)`, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 21, + }, + ], + }, // Assignments with mathematical operators will either evaluate to a primitive value or throw a TypeError - 'Promise.reject(foo += new Error())', - 'Promise.reject(foo -= new Error())', - 'Promise.reject(foo **= new Error())', - 'Promise.reject(foo <<= new Error())', - 'Promise.reject(foo |= new Error())', - 'Promise.reject(foo &= new Error())', + { + code: 'Promise.reject(foo += new Error())', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 35, + }, + ], + }, + { + code: 'Promise.reject(foo -= new Error())', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 35, + }, + ], + }, + { + code: 'Promise.reject(foo **= new Error())', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 36, + }, + ], + }, + { + code: 'Promise.reject(foo <<= new Error())', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 36, + }, + ], + }, + { + code: 'Promise.reject(foo |= new Error())', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 35, + }, + ], + }, + { + code: 'Promise.reject(foo &= new Error())', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 1, + endColumn: 35, + }, + ], + }, - 'new Promise(function(resolve, reject) { reject() })', - 'new Promise(function(resolve, reject) { reject(5) })', - 'new Promise((resolve, reject) => { reject() })', - 'new Promise((resolve, reject) => reject(5))', - ` - new Promise((resolve, reject) => { - fs.readFile('foo.txt', (err, file) => { - if (err) reject('File not found') - else resolve(file) - }) - }) - `, - 'new Promise((yes, no) => no(5))', - 'new Promise(({foo, bar, baz}, reject) => reject(5))', - 'new Promise(function(reject, reject) { reject(5) })', - 'new Promise(function(foo, arguments) { arguments(5) })', - 'new Promise((foo, arguments) => arguments(5))', - 'new Promise(function({}, reject) { reject(5) })', - 'new Promise(({}, reject) => reject(5))', - 'new Promise((resolve, reject, somethingElse = reject(5)) => {})', - ` - declare const foo: { - bar: PromiseConstructor - } - new foo.bar((resolve, reject) => reject(5)) - `, - ` - declare const foo: { - bar: PromiseConstructor - } - new (foo?.bar)((resolve, reject) => reject(5)) - `, - ` - const foo = Promise - new foo((resolve, reject) => reject(5)) - `, - ].map>(invalidCase => { - return { + { + code: 'new Promise(function(resolve, reject) { reject() })', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 41, + endColumn: 49, + }, + ], + }, + { + code: 'new Promise(function(resolve, reject) { reject(5) })', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 41, + endColumn: 50, + }, + ], + }, + { + code: 'new Promise((resolve, reject) => { reject() })', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 36, + endColumn: 44, + }, + ], + }, + { + code: 'new Promise((resolve, reject) => reject(5))', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 34, + endColumn: 43, + }, + ], + }, + { + code: ` +new Promise((resolve, reject) => { + fs.readFile('foo.txt', (err, file) => { + if (err) reject('File not found') + else resolve(file) + }) +}) + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 4, + endLine: 4, + column: 14, + endColumn: 38, + }, + ], + }, + { + code: 'new Promise((yes, no) => no(5))', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 26, + endColumn: 31, + }, + ], + }, + { + code: 'new Promise(({foo, bar, baz}, reject) => reject(5))', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 42, + endColumn: 51, + }, + ], + }, + { + code: 'new Promise(function(reject, reject) { reject(5) })', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 40, + endColumn: 49, + }, + ], + }, + { + code: 'new Promise(function(foo, arguments) { arguments(5) })', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 40, + endColumn: 52, + }, + ], + }, + { + code: 'new Promise((foo, arguments) => arguments(5))', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 33, + endColumn: 45, + }, + ], + }, + { + code: 'new Promise(function({}, reject) { reject(5) })', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 36, + endColumn: 45, + }, + ], + }, + { + code: 'new Promise(({}, reject) => reject(5))', + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 29, + endColumn: 38, + }, + ], + }, + { + code: 'new Promise((resolve, reject, somethingElse = reject(5)) => {})', errors: [ - { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression }, + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 1, + endLine: 1, + column: 47, + endColumn: 56, + }, + ], + }, + { + code: ` +declare const foo: { + bar: PromiseConstructor +} +new foo.bar((resolve, reject) => reject(5)) + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 5, + endLine: 5, + column: 34, + endColumn: 43, + }, ], - ...(typeof invalidCase === 'string' - ? { code: invalidCase } - : invalidCase), - }; - }), + }, + { + code: ` +declare const foo: { + bar: PromiseConstructor +} +new (foo?.bar)((resolve, reject) => reject(5)) + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 5, + endLine: 5, + column: 37, + endColumn: 46, + }, + ], + }, + { + code: ` +const foo = Promise +new foo((resolve, reject) => reject(5)) + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 30, + endColumn: 39, + }, + ], + }, + ], }); From 42e5c1f27844a9ef6cfcb50992667b1a3a0945a4 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 16:57:00 +0000 Subject: [PATCH 17/31] chore: format tests --- .../prefer-promise-reject-errors.test.ts | 207 ++++++++++-------- 1 file changed, 113 insertions(+), 94 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index ebb4f44dd2bf..170163f9e101 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -1,11 +1,6 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; -import type { TSESLint } from '@typescript-eslint/utils'; +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import type { - MessageIds, - Options, -} from '../../src/rules/prefer-promise-reject-errors'; import rule from '../../src/rules/prefer-promise-reject-errors'; import { getFixturesRootDir } from '../RuleTester'; @@ -138,7 +133,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], invalid: [ { - code: 'Promise.reject(5)', + code: 'Promise.reject(5);', errors: [ { messageId: 'rejectAnError', @@ -151,7 +146,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: "Promise.reject('foo')", + code: "Promise.reject('foo');", errors: [ { messageId: 'rejectAnError', @@ -164,7 +159,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject(`foo`)', + code: 'Promise.reject(`foo`);', errors: [ { messageId: 'rejectAnError', @@ -177,7 +172,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: "Promise.reject('foo', somethingElse)", + code: "Promise.reject('foo', somethingElse);", errors: [ { messageId: 'rejectAnError', @@ -190,7 +185,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject(false)', + code: 'Promise.reject(false);', errors: [ { messageId: 'rejectAnError', @@ -203,7 +198,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject(void `foo`)', + code: 'Promise.reject(void `foo`);', errors: [ { messageId: 'rejectAnError', @@ -216,7 +211,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject()', + code: 'Promise.reject();', errors: [ { messageId: 'rejectAnError', @@ -229,7 +224,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject(undefined)', + code: 'Promise.reject(undefined);', errors: [ { messageId: 'rejectAnError', @@ -242,7 +237,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject(undefined)', + code: 'Promise.reject(undefined);', options: [{ allowEmptyReject: true }], errors: [ { @@ -256,7 +251,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject(null)', + code: 'Promise.reject(null);', errors: [ { messageId: 'rejectAnError', @@ -269,7 +264,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject({ foo: 1 })', + code: 'Promise.reject({ foo: 1 });', errors: [ { messageId: 'rejectAnError', @@ -282,7 +277,7 @@ ruleTester.run('prefer-promise-reject-errors', rule, { ], }, { - code: 'Promise.reject([1, 2, 3])', + code: 'Promise.reject([1, 2, 3]);', errors: [ { messageId: 'rejectAnError', @@ -296,8 +291,8 @@ ruleTester.run('prefer-promise-reject-errors', rule, { }, { code: ` -declare const foo: Error | undefined -Promise.reject(foo) +declare const foo: Error | undefined; +Promise.reject(foo); `, errors: [ { @@ -312,8 +307,8 @@ Promise.reject(foo) }, { code: ` -declare const foo: () => Promise -Promise.reject(await foo()) +declare const foo: () => Promise; +Promise.reject(await foo()); `, errors: [ { @@ -328,8 +323,8 @@ Promise.reject(await foo()) }, { code: ` -declare const foo: boolean -Promise.reject(foo && new Error()) +declare const foo: boolean; +Promise.reject(foo && new Error()); `, errors: [ { @@ -344,8 +339,8 @@ Promise.reject(foo && new Error()) }, { code: ` -const foo = Promise -foo.reject() +const foo = Promise; +foo.reject(); `, errors: [ { @@ -360,7 +355,7 @@ foo.reject() }, { - code: 'Promise.reject?.(5)', + code: 'Promise.reject?.(5);', errors: [ { messageId: 'rejectAnError', @@ -373,7 +368,7 @@ foo.reject() ], }, { - code: 'Promise?.reject(5)', + code: 'Promise?.reject(5);', errors: [ { messageId: 'rejectAnError', @@ -386,7 +381,7 @@ foo.reject() ], }, { - code: 'Promise?.reject?.(5)', + code: 'Promise?.reject?.(5);', errors: [ { messageId: 'rejectAnError', @@ -399,7 +394,7 @@ foo.reject() ], }, { - code: '(Promise?.reject)(5)', + code: '(Promise?.reject)(5);', errors: [ { messageId: 'rejectAnError', @@ -412,7 +407,7 @@ foo.reject() ], }, { - code: '(Promise?.reject)?.(5)', + code: noFormat`(Promise?.reject)?.(5);`, errors: [ { messageId: 'rejectAnError', @@ -425,7 +420,7 @@ foo.reject() ], }, { - code: `Promise['reject'](5)`, + code: "Promise['reject'](5);", errors: [ { messageId: 'rejectAnError', @@ -440,7 +435,7 @@ foo.reject() // Assignments with mathematical operators will either evaluate to a primitive value or throw a TypeError { - code: 'Promise.reject(foo += new Error())', + code: 'Promise.reject((foo += new Error()));', errors: [ { messageId: 'rejectAnError', @@ -448,12 +443,12 @@ foo.reject() line: 1, endLine: 1, column: 1, - endColumn: 35, + endColumn: 37, }, ], }, { - code: 'Promise.reject(foo -= new Error())', + code: 'Promise.reject((foo -= new Error()));', errors: [ { messageId: 'rejectAnError', @@ -461,12 +456,12 @@ foo.reject() line: 1, endLine: 1, column: 1, - endColumn: 35, + endColumn: 37, }, ], }, { - code: 'Promise.reject(foo **= new Error())', + code: 'Promise.reject((foo **= new Error()));', errors: [ { messageId: 'rejectAnError', @@ -474,12 +469,12 @@ foo.reject() line: 1, endLine: 1, column: 1, - endColumn: 36, + endColumn: 38, }, ], }, { - code: 'Promise.reject(foo <<= new Error())', + code: 'Promise.reject((foo <<= new Error()));', errors: [ { messageId: 'rejectAnError', @@ -487,12 +482,12 @@ foo.reject() line: 1, endLine: 1, column: 1, - endColumn: 36, + endColumn: 38, }, ], }, { - code: 'Promise.reject(foo |= new Error())', + code: 'Promise.reject((foo |= new Error()));', errors: [ { messageId: 'rejectAnError', @@ -500,12 +495,12 @@ foo.reject() line: 1, endLine: 1, column: 1, - endColumn: 35, + endColumn: 37, }, ], }, { - code: 'Promise.reject(foo &= new Error())', + code: 'Promise.reject((foo &= new Error()));', errors: [ { messageId: 'rejectAnError', @@ -513,52 +508,64 @@ foo.reject() line: 1, endLine: 1, column: 1, - endColumn: 35, + endColumn: 37, }, ], }, { - code: 'new Promise(function(resolve, reject) { reject() })', + code: ` +new Promise(function (resolve, reject) { + reject(); +}); + `, errors: [ { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression, - line: 1, - endLine: 1, - column: 41, - endColumn: 49, + line: 3, + endLine: 3, + column: 3, + endColumn: 11, }, ], }, { - code: 'new Promise(function(resolve, reject) { reject(5) })', + code: ` +new Promise(function (resolve, reject) { + reject(5); +}); + `, errors: [ { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression, - line: 1, - endLine: 1, - column: 41, - endColumn: 50, + line: 3, + endLine: 3, + column: 3, + endColumn: 12, }, ], }, { - code: 'new Promise((resolve, reject) => { reject() })', + code: ` +new Promise((resolve, reject) => { + reject(); +}); + `, errors: [ { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression, - line: 1, - endLine: 1, - column: 36, - endColumn: 44, + line: 3, + endLine: 3, + column: 3, + endColumn: 11, }, ], }, { - code: 'new Promise((resolve, reject) => reject(5))', + code: 'new Promise((resolve, reject) => reject(5));', errors: [ { messageId: 'rejectAnError', @@ -574,10 +581,10 @@ foo.reject() code: ` new Promise((resolve, reject) => { fs.readFile('foo.txt', (err, file) => { - if (err) reject('File not found') - else resolve(file) - }) -}) + if (err) reject('File not found'); + else resolve(file); + }); +}); `, errors: [ { @@ -591,7 +598,7 @@ new Promise((resolve, reject) => { ], }, { - code: 'new Promise((yes, no) => no(5))', + code: 'new Promise((yes, no) => no(5));', errors: [ { messageId: 'rejectAnError', @@ -604,46 +611,54 @@ new Promise((resolve, reject) => { ], }, { - code: 'new Promise(({foo, bar, baz}, reject) => reject(5))', + code: 'new Promise(({ foo, bar, baz }, reject) => reject(5));', errors: [ { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression, line: 1, endLine: 1, - column: 42, - endColumn: 51, + column: 44, + endColumn: 53, }, ], }, { - code: 'new Promise(function(reject, reject) { reject(5) })', + code: ` +new Promise(function (reject, reject) { + reject(5); +}); + `, errors: [ { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression, - line: 1, - endLine: 1, - column: 40, - endColumn: 49, + line: 3, + endLine: 3, + column: 3, + endColumn: 12, }, ], }, { - code: 'new Promise(function(foo, arguments) { arguments(5) })', + code: ` +new Promise(function (foo, arguments) { + arguments(5); +}); + `, errors: [ { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression, - line: 1, - endLine: 1, - column: 40, - endColumn: 52, + line: 3, + endLine: 3, + column: 3, + endColumn: 15, }, ], }, { - code: 'new Promise((foo, arguments) => arguments(5))', + code: 'new Promise((foo, arguments) => arguments(5));', errors: [ { messageId: 'rejectAnError', @@ -656,20 +671,24 @@ new Promise((resolve, reject) => { ], }, { - code: 'new Promise(function({}, reject) { reject(5) })', + code: ` +new Promise(function ({}, reject) { + reject(5); +}); + `, errors: [ { messageId: 'rejectAnError', type: AST_NODE_TYPES.CallExpression, - line: 1, - endLine: 1, - column: 36, - endColumn: 45, + line: 3, + endLine: 3, + column: 3, + endColumn: 12, }, ], }, { - code: 'new Promise(({}, reject) => reject(5))', + code: 'new Promise(({}, reject) => reject(5));', errors: [ { messageId: 'rejectAnError', @@ -682,7 +701,7 @@ new Promise((resolve, reject) => { ], }, { - code: 'new Promise((resolve, reject, somethingElse = reject(5)) => {})', + code: 'new Promise((resolve, reject, somethingElse = reject(5)) => {});', errors: [ { messageId: 'rejectAnError', @@ -697,9 +716,9 @@ new Promise((resolve, reject) => { { code: ` declare const foo: { - bar: PromiseConstructor -} -new foo.bar((resolve, reject) => reject(5)) + bar: PromiseConstructor; +}; +new foo.bar((resolve, reject) => reject(5)); `, errors: [ { @@ -715,9 +734,9 @@ new foo.bar((resolve, reject) => reject(5)) { code: ` declare const foo: { - bar: PromiseConstructor -} -new (foo?.bar)((resolve, reject) => reject(5)) + bar: PromiseConstructor; +}; +new (foo?.bar)((resolve, reject) => reject(5)); `, errors: [ { @@ -732,8 +751,8 @@ new (foo?.bar)((resolve, reject) => reject(5)) }, { code: ` -const foo = Promise -new foo((resolve, reject) => reject(5)) +const foo = Promise; +new foo((resolve, reject) => reject(5)); `, errors: [ { From 47eb018f0a463663fc1fcfc06e7b7b5e90521f9e Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 16:59:44 +0000 Subject: [PATCH 18/31] chore: remove unused check if variable reference is read or not --- packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 06c6c54ec3d5..ef171ecf1ca5 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -122,7 +122,6 @@ export default createRule({ rejectVariable.references.forEach(ref => { if ( - !ref.isRead() || ref.identifier.parent.type !== AST_NODE_TYPES.CallExpression || ref.identifier !== ref.identifier.parent.callee ) { From f2ee5d967496beda88be1e8c25feb317551a5097 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 17:13:46 +0000 Subject: [PATCH 19/31] chore: include rule to `strict-type-checked` config --- packages/eslint-plugin/src/configs/disable-type-checked.ts | 1 + packages/eslint-plugin/src/configs/strict-type-checked.ts | 2 ++ .../eslint-plugin/src/rules/prefer-promise-reject-errors.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index 38a7ffd079d8..65b32cad78c1 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -38,6 +38,7 @@ export = { '@typescript-eslint/prefer-includes': 'off', '@typescript-eslint/prefer-nullish-coalescing': 'off', '@typescript-eslint/prefer-optional-chain': 'off', + '@typescript-eslint/prefer-promise-reject-errors': 'off', '@typescript-eslint/prefer-readonly': 'off', '@typescript-eslint/prefer-readonly-parameter-types': 'off', '@typescript-eslint/prefer-reduce-type-parameter': 'off', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index dfba0b81c7fa..8848ed19f652 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -60,6 +60,8 @@ export = { '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-includes': 'error', '@typescript-eslint/prefer-literal-enum-member': 'error', + 'prefer-promise-reject-errors': 'off', + '@typescript-eslint/prefer-promise-reject-errors': 'error', '@typescript-eslint/prefer-reduce-type-parameter': 'error', '@typescript-eslint/prefer-return-this-type': 'error', '@typescript-eslint/prefer-ts-expect-error': 'error', diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index ef171ecf1ca5..9ccb6d67e8f8 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -26,6 +26,7 @@ export default createRule({ type: 'suggestion', docs: { description: 'Require using Error objects as Promise rejection reasons', + recommended: 'strict', extendsBaseRule: true, requiresTypeChecking: true, }, From 0229ae71e1fb919434ae0f12b376715d700c21ba Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 17:27:06 +0000 Subject: [PATCH 20/31] refactor: simplify isSymbolFromDefaultLibrary --- .../src/rules/prefer-promise-reject-errors.ts | 5 ++++- packages/type-utils/src/isErrorLike.ts | 5 ++++- .../src/isSymbolFromDefaultLibrary.ts | 19 +++++-------------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 9ccb6d67e8f8..f1413d8ef18a 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -73,7 +73,10 @@ export default createRule({ function isPromiseConstructorLike(type: ts.Type): boolean { const symbol = type.getSymbol(); - return isSymbolFromDefaultLibrary(program, symbol, 'PromiseConstructor'); + return ( + symbol?.getName() === 'PromiseConstructor' && + isSymbolFromDefaultLibrary(program, symbol) + ); } function skipChainExpression( diff --git a/packages/type-utils/src/isErrorLike.ts b/packages/type-utils/src/isErrorLike.ts index 57760cc0c1a1..0b324bc81032 100644 --- a/packages/type-utils/src/isErrorLike.ts +++ b/packages/type-utils/src/isErrorLike.ts @@ -15,7 +15,10 @@ export function isErrorLike(program: ts.Program, type: ts.Type): boolean { return false; } - if (isSymbolFromDefaultLibrary(program, symbol, 'Error')) { + if ( + symbol.getName() === 'Error' && + isSymbolFromDefaultLibrary(program, symbol) + ) { return true; } diff --git a/packages/type-utils/src/isSymbolFromDefaultLibrary.ts b/packages/type-utils/src/isSymbolFromDefaultLibrary.ts index 6314959f9a83..786ef849a2c4 100644 --- a/packages/type-utils/src/isSymbolFromDefaultLibrary.ts +++ b/packages/type-utils/src/isSymbolFromDefaultLibrary.ts @@ -3,25 +3,16 @@ import type * as ts from 'typescript'; export function isSymbolFromDefaultLibrary( program: ts.Program, symbol: ts.Symbol | undefined, - nameOrPredicate?: string | ((symbol: ts.Symbol) => boolean), ): boolean { if (!symbol) { return false; } - if ( - typeof nameOrPredicate === 'string' - ? symbol.getName() === nameOrPredicate - : nameOrPredicate - ? nameOrPredicate(symbol) - : true - ) { - const declarations = symbol.getDeclarations() ?? []; - for (const declaration of declarations) { - const sourceFile = declaration.getSourceFile(); - if (program.isSourceFileDefaultLibrary(sourceFile)) { - return true; - } + const declarations = symbol.getDeclarations() ?? []; + for (const declaration of declarations) { + const sourceFile = declaration.getSourceFile(); + if (program.isSourceFileDefaultLibrary(sourceFile)) { + return true; } } From 9db6d84a6d28da48817a3263d6f1bc6c7af081ce Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Dec 2023 17:41:39 +0000 Subject: [PATCH 21/31] chore: remove ts-expect-error comment --- .../eslint-plugin/src/rules/prefer-promise-reject-errors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index f1413d8ef18a..43a5139bb381 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -81,8 +81,7 @@ export default createRule({ function skipChainExpression( node: T, - ): T & TSESTree.ChainElement { - // @ts-expect-error https://github.com/typescript-eslint/typescript-eslint/issues/8008 + ): T | TSESTree.ChainElement { return node.type === AST_NODE_TYPES.ChainExpression ? node.expression : node; From a0699cfa5c5143a38c38f8d746a28688d736fd74 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 20 Dec 2023 19:26:26 +0000 Subject: [PATCH 22/31] feat: add checks for Promise child classes and unions/intersections --- .../src/rules/prefer-promise-reject-errors.ts | 28 ++++---- .../prefer-promise-reject-errors.test.ts | 67 ++++++++++++++++++ packages/type-utils/src/index.ts | 2 +- .../type-utils/src/isBuiltinSymbolLike.ts | 70 +++++++++++++++++++ packages/type-utils/src/isErrorLike.ts | 36 ---------- 5 files changed, 153 insertions(+), 50 deletions(-) create mode 100644 packages/type-utils/src/isBuiltinSymbolLike.ts delete mode 100644 packages/type-utils/src/isErrorLike.ts diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 43a5139bb381..f33264fa4a6b 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -1,7 +1,6 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { getDeclaredVariables } from '@typescript-eslint/utils/eslint-utils'; -import type * as ts from 'typescript'; import { createRule, @@ -9,7 +8,8 @@ import { isErrorLike, isFunction, isIdentifier, - isSymbolFromDefaultLibrary, + isPromiseConstructorLike, + isPromiseLike, } from '../util'; export type MessageIds = 'rejectAnError'; @@ -71,14 +71,6 @@ export default createRule({ } } - function isPromiseConstructorLike(type: ts.Type): boolean { - const symbol = type.getSymbol(); - return ( - symbol?.getName() === 'PromiseConstructor' && - isSymbolFromDefaultLibrary(program, symbol) - ); - } - function skipChainExpression( node: T, ): T | TSESTree.ChainElement { @@ -90,13 +82,21 @@ export default createRule({ return { CallExpression(node): void { const callee = skipChainExpression(node.callee); + + if (callee.type !== AST_NODE_TYPES.MemberExpression) { + return; + } + + const calleeObjectType = services.getTypeAtLocation(callee.object); if ( - callee.type !== AST_NODE_TYPES.MemberExpression || (callee.computed ? callee.property.type === AST_NODE_TYPES.Literal && callee.property.value !== 'reject' : callee.property.name !== 'reject') || - !isPromiseConstructorLike(services.getTypeAtLocation(callee.object)) + !( + isPromiseConstructorLike(program, calleeObjectType) || + isPromiseLike(program, calleeObjectType) + ) ) { return; } @@ -105,7 +105,9 @@ export default createRule({ }, NewExpression(node): void { const callee = skipChainExpression(node.callee); - if (!isPromiseConstructorLike(services.getTypeAtLocation(callee))) { + if ( + !isPromiseConstructorLike(program, services.getTypeAtLocation(callee)) + ) { return; } diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 170163f9e101..a8f73e8235e5 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -130,6 +130,24 @@ ruleTester.run('prefer-promise-reject-errors', rule, { const foo = Promise; new foo((resolve, reject) => reject(new Error())); `, + ` + class Foo extends Promise {} + Foo.reject(new Error()); + `, + ` + class Foo extends Promise {} + new Foo((resolve, reject) => reject(new Error())); + `, + ` + declare const someRandomCall: { + reject(arg: any): void; + }; + someRandomCall.reject(5); + `, + ` + declare const foo: PromiseConstructor; + foo.reject(new Error()); + `, ], invalid: [ { @@ -765,5 +783,54 @@ new foo((resolve, reject) => reject(5)); }, ], }, + { + code: ` +class Foo extends Promise {} +Foo.reject(5); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 14, + }, + ], + }, + { + code: ` +declare const foo: PromiseConstructor & string; +foo.reject(5); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 14, + }, + ], + }, + { + code: ` +class Foo extends Promise {} +class Bar extends Foo {} +Bar.reject(5); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 4, + endLine: 4, + column: 1, + endColumn: 14, + }, + ], + }, ], }); diff --git a/packages/type-utils/src/index.ts b/packages/type-utils/src/index.ts index 51ad912495eb..8cc1ea6efc96 100644 --- a/packages/type-utils/src/index.ts +++ b/packages/type-utils/src/index.ts @@ -6,7 +6,7 @@ export * from './getSourceFileOfNode'; export * from './getTokenAtPosition'; export * from './getTypeArguments'; export * from './getTypeName'; -export * from './isErrorLike'; +export * from './isBuiltinSymbolLike'; export * from './isSymbolFromDefaultLibrary'; export * from './isTypeReadonly'; export * from './isUnsafeAssignment'; diff --git a/packages/type-utils/src/isBuiltinSymbolLike.ts b/packages/type-utils/src/isBuiltinSymbolLike.ts new file mode 100644 index 000000000000..5f5b2fd76b86 --- /dev/null +++ b/packages/type-utils/src/isBuiltinSymbolLike.ts @@ -0,0 +1,70 @@ +import * as ts from 'typescript'; + +import { isSymbolFromDefaultLibrary } from './isSymbolFromDefaultLibrary'; + +/** + * class Foo extends Promise {} + * Foo.reject + * ^ PromiseLike + */ +export function isPromiseLike(program: ts.Program, type: ts.Type): boolean { + return isBuiltinSymbolLike(program, type, 'Promise'); +} + +/** + * const foo = Promise + * foo.reject + * ^ PromiseConstructorLike + */ +export function isPromiseConstructorLike( + program: ts.Program, + type: ts.Type, +): boolean { + return isBuiltinSymbolLike(program, type, 'PromiseConstructor'); +} + +/** + * class Foo extends Error {} + * new Foo() + * ^ ErrorLike + */ +export function isErrorLike(program: ts.Program, type: ts.Type): boolean { + return isBuiltinSymbolLike(program, type, 'Error'); +} + +export function isBuiltinSymbolLike( + program: ts.Program, + type: ts.Type, + symbolName: string, +): boolean { + if (type.isIntersection()) { + return type.types.some(t => isBuiltinSymbolLike(program, t, symbolName)); + } + if (type.isUnion()) { + return type.types.every(t => isBuiltinSymbolLike(program, t, symbolName)); + } + + const symbol = type.getSymbol(); + if (!symbol) { + return false; + } + + if ( + symbol.getName() === symbolName && + isSymbolFromDefaultLibrary(program, symbol) + ) { + return true; + } + + if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { + const checker = program.getTypeChecker(); + + for (const baseType of checker.getBaseTypes(type as ts.InterfaceType)) { + if (isBuiltinSymbolLike(program, baseType, symbolName)) { + return true; + } + } + } + + return false; +} diff --git a/packages/type-utils/src/isErrorLike.ts b/packages/type-utils/src/isErrorLike.ts deleted file mode 100644 index 0b324bc81032..000000000000 --- a/packages/type-utils/src/isErrorLike.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as ts from 'typescript'; - -import { isSymbolFromDefaultLibrary } from './isSymbolFromDefaultLibrary'; - -export function isErrorLike(program: ts.Program, type: ts.Type): boolean { - if (type.isIntersection()) { - return type.types.some(t => isErrorLike(program, t)); - } - if (type.isUnion()) { - return type.types.every(t => isErrorLike(program, t)); - } - - const symbol = type.getSymbol(); - if (!symbol) { - return false; - } - - if ( - symbol.getName() === 'Error' && - isSymbolFromDefaultLibrary(program, symbol) - ) { - return true; - } - - if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { - const checker = program.getTypeChecker(); - - for (const baseType of checker.getBaseTypes(type as ts.InterfaceType)) { - if (isErrorLike(program, baseType)) { - return true; - } - } - } - - return false; -} From 1c97219ce7c0299d5045426d978a633669ec4979 Mon Sep 17 00:00:00 2001 From: auvred <61150013+auvred@users.noreply.github.com> Date: Fri, 29 Dec 2023 22:42:00 +0300 Subject: [PATCH 23/31] Update packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- .../eslint-plugin/docs/rules/prefer-promise-reject-errors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md index 27eaee761569..0311b70237d1 100644 --- a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md +++ b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md @@ -7,7 +7,7 @@ description: 'Require using Error objects as Promise rejection reasons.' > See **https://typescript-eslint.io/rules/prefer-promise-reject-errors** for documentation. This rule extends the base [`eslint/prefer-promise-reject-errors`](https://eslint.org/docs/rules/prefer-promise-reject-errors) rule. -It uses type information to guarantee that `Promise` can only be rejected with `Error` objects. +It uses type information to enforce that `Promise`s are only rejected with `Error` objects. ## Examples From 42c5737ea5398e397529a7e806e3857f5baa21dd Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 29 Dec 2023 19:11:57 +0000 Subject: [PATCH 24/31] refactor: `program` -> `services.program` --- .../src/rules/prefer-promise-reject-errors.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index f33264fa4a6b..919aef6f8879 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -52,7 +52,6 @@ export default createRule({ ], create(context, [options]) { const services = getParserServices(context); - const program = services.program; function checkRejectCall(callExpression: TSESTree.CallExpression): void { const argument = callExpression.arguments.at(0); @@ -62,7 +61,7 @@ export default createRule({ if ( !argument || - !isErrorLike(program, services.getTypeAtLocation(argument)) + !isErrorLike(services.program, services.getTypeAtLocation(argument)) ) { context.report({ node: callExpression, @@ -94,8 +93,8 @@ export default createRule({ callee.property.value !== 'reject' : callee.property.name !== 'reject') || !( - isPromiseConstructorLike(program, calleeObjectType) || - isPromiseLike(program, calleeObjectType) + isPromiseConstructorLike(services.program, calleeObjectType) || + isPromiseLike(services.program, calleeObjectType) ) ) { return; @@ -106,7 +105,10 @@ export default createRule({ NewExpression(node): void { const callee = skipChainExpression(node.callee); if ( - !isPromiseConstructorLike(program, services.getTypeAtLocation(callee)) + !isPromiseConstructorLike( + services.program, + services.getTypeAtLocation(callee), + ) ) { return; } From 184de0fa31df3deb539b8e058a25f9dfa58f48d2 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 29 Dec 2023 19:17:31 +0000 Subject: [PATCH 25/31] refactor: split unreadable if condition --- .../src/rules/prefer-promise-reject-errors.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 919aef6f8879..834b55da573e 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -87,16 +87,16 @@ export default createRule({ } const calleeObjectType = services.getTypeAtLocation(callee.object); - if ( - (callee.computed - ? callee.property.type === AST_NODE_TYPES.Literal && - callee.property.value !== 'reject' - : callee.property.name !== 'reject') || - !( - isPromiseConstructorLike(services.program, calleeObjectType) || - isPromiseLike(services.program, calleeObjectType) - ) - ) { + + const rejectMethodCalled = callee.computed + ? callee.property.type === AST_NODE_TYPES.Literal && + callee.property.value === 'reject' + : callee.property.name === 'reject'; + const calleeIsLikePromise = + isPromiseConstructorLike(services.program, calleeObjectType) || + isPromiseLike(services.program, calleeObjectType); + + if (!rejectMethodCalled || !calleeIsLikePromise) { return; } From eaab9eb552b9bc2b854625f286f7d0418062b27d Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 29 Dec 2023 19:38:02 +0000 Subject: [PATCH 26/31] docs: simplify examples --- .../rules/prefer-promise-reject-errors.md | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md index 0311b70237d1..28e465cf00f1 100644 --- a/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md +++ b/packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md @@ -16,75 +16,31 @@ It uses type information to enforce that `Promise`s are only rejected with `Erro ### ❌ Incorrect ```ts -// Promise.reject - Promise.reject('error'); const err = new Error(); Promise.reject('an ' + err); -const foo = { - bar: '', -}; -Promise.reject(foo.bar); - -// new Promise - new Promise((resolve, reject) => reject('error')); new Promise((resolve, reject) => { const err = new Error(); reject('an ' + err); }); - -new Promise((resolve, reject) => { - const foo = { - bar: '', - }; - return reject(foo.bar); -}); ``` ### ✅ Correct ```ts -// Promise.reject - Promise.reject(new Error()); -try { - // ... -} catch (e) { - Promise.reject(e); -} - -const foo = { - bar: new Error(), -}; -Promise.reject(foo.bar); - class CustomError extends Error { // ... } Promise.reject(new CustomError()); -// new Promise - new Promise((resolve, reject) => reject(new Error())); -try { - // ... -} catch (e) { - new Promise((resolve, reject) => reject(e)); -} - -new Promise((resolve, reject) => { - const foo = { - bar: new Error(), - }; - return reject(foo.bar); -}); - new Promise((resolve, reject) => { class CustomError extends Error { // ... From 895f1b542522e40023b4c43312af1458860f616f Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 29 Dec 2023 19:40:32 +0000 Subject: [PATCH 27/31] refactor: rename `isBuiltinSymbolLike.ts` -> `builtinSymbolLikes.ts` --- .../src/{isBuiltinSymbolLike.ts => builtinSymbolLikes.ts} | 0 packages/type-utils/src/index.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/type-utils/src/{isBuiltinSymbolLike.ts => builtinSymbolLikes.ts} (100%) diff --git a/packages/type-utils/src/isBuiltinSymbolLike.ts b/packages/type-utils/src/builtinSymbolLikes.ts similarity index 100% rename from packages/type-utils/src/isBuiltinSymbolLike.ts rename to packages/type-utils/src/builtinSymbolLikes.ts diff --git a/packages/type-utils/src/index.ts b/packages/type-utils/src/index.ts index 8cc1ea6efc96..14d5b652099f 100644 --- a/packages/type-utils/src/index.ts +++ b/packages/type-utils/src/index.ts @@ -1,3 +1,4 @@ +export * from './builtinSymbolLikes'; export * from './containsAllTypesByName'; export * from './getConstrainedTypeAtLocation'; export * from './getContextualType'; @@ -6,7 +7,6 @@ export * from './getSourceFileOfNode'; export * from './getTokenAtPosition'; export * from './getTypeArguments'; export * from './getTypeName'; -export * from './isBuiltinSymbolLike'; export * from './isSymbolFromDefaultLibrary'; export * from './isTypeReadonly'; export * from './isUnsafeAssignment'; From f186084686347b0f3850bf8cbb1ad07818e267c2 Mon Sep 17 00:00:00 2001 From: auvred Date: Thu, 4 Jan 2024 16:56:57 +0000 Subject: [PATCH 28/31] perf: get type of `reject` callee lazily --- .../src/rules/prefer-promise-reject-errors.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 834b55da573e..1552496b9a69 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -78,6 +78,14 @@ export default createRule({ : node; } + function typeAtLocationIsLikePromise(node: TSESTree.Node): boolean { + const type = services.getTypeAtLocation(node); + return ( + isPromiseConstructorLike(services.program, type) || + isPromiseLike(services.program, type) + ); + } + return { CallExpression(node): void { const callee = skipChainExpression(node.callee); @@ -86,17 +94,15 @@ export default createRule({ return; } - const calleeObjectType = services.getTypeAtLocation(callee.object); - const rejectMethodCalled = callee.computed ? callee.property.type === AST_NODE_TYPES.Literal && callee.property.value === 'reject' : callee.property.name === 'reject'; - const calleeIsLikePromise = - isPromiseConstructorLike(services.program, calleeObjectType) || - isPromiseLike(services.program, calleeObjectType); - if (!rejectMethodCalled || !calleeIsLikePromise) { + if ( + !rejectMethodCalled || + !typeAtLocationIsLikePromise(callee.object) + ) { return; } From e4af6eda213478034efc29276ae4b62537c1b16f Mon Sep 17 00:00:00 2001 From: auvred Date: Thu, 4 Jan 2024 17:00:13 +0000 Subject: [PATCH 29/31] test: add cases with arrays,never,unknown --- .../prefer-promise-reject-errors.test.ts | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index a8f73e8235e5..8b6de5dc3a5c 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -51,6 +51,18 @@ ruleTester.run('prefer-promise-reject-errors', rule, { const foo = false; Promise.reject(false || new Error()); `, + ` + declare const foo: Error[]; + Promise.reject(foo[5]); + `, + ` + declare const foo: ReadonlyArray; + Promise.reject(foo[5]); + `, + ` + declare const foo: [Error]; + Promise.reject(foo[0]); + `, ` new Promise(function (resolve, reject) { @@ -130,6 +142,18 @@ ruleTester.run('prefer-promise-reject-errors', rule, { const foo = Promise; new foo((resolve, reject) => reject(new Error())); `, + ` + declare const foo: Error[]; + new Promise((resolve, reject) => reject(foo[5])); + `, + ` + declare const foo: ReadonlyArray; + new Promise((resolve, reject) => reject(foo[5])); + `, + ` + declare const foo: [Error]; + new Promise((resolve, reject) => reject(foo[0])); + `, ` class Foo extends Promise {} Foo.reject(new Error()); @@ -530,6 +554,86 @@ foo.reject(); }, ], }, + { + code: ` +declare const foo: never; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: unknown; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Error[]; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: ReadonlyArray; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: [Error]; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, { code: ` @@ -785,6 +889,86 @@ new foo((resolve, reject) => reject(5)); }, { code: ` +declare const foo: never; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: unknown; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Error[]; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: ReadonlyArray; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: [Error]; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` class Foo extends Promise {} Foo.reject(5); `, From 232642964a8677e133ab283b15f096c8050763f8 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 5 Jan 2024 12:25:22 +0000 Subject: [PATCH 30/31] feat: add support for `Readonly` and similar --- .../src/rules/prefer-promise-reject-errors.ts | 24 +- .../prefer-promise-reject-errors.test.ts | 418 ++++++++++++++++++ packages/type-utils/src/builtinSymbolLikes.ts | 120 ++++- 3 files changed, 538 insertions(+), 24 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 1552496b9a69..69494823e023 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -10,6 +10,7 @@ import { isIdentifier, isPromiseConstructorLike, isPromiseLike, + isReadonlyErrorLike, } from '../util'; export type MessageIds = 'rejectAnError'; @@ -55,19 +56,22 @@ export default createRule({ function checkRejectCall(callExpression: TSESTree.CallExpression): void { const argument = callExpression.arguments.at(0); - if (!argument && options.allowEmptyReject) { + if (argument) { + const type = services.getTypeAtLocation(argument); + if ( + isErrorLike(services.program, type) || + isReadonlyErrorLike(services.program, type) + ) { + return; + } + } else if (options.allowEmptyReject) { return; } - if ( - !argument || - !isErrorLike(services.program, services.getTypeAtLocation(argument)) - ) { - context.report({ - node: callExpression, - messageId: 'rejectAnError', - }); - } + context.report({ + node: callExpression, + messageId: 'rejectAnError', + }); } function skipChainExpression( diff --git a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts index 8b6de5dc3a5c..69eed002ebf7 100644 --- a/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-promise-reject-errors.test.ts @@ -51,6 +51,53 @@ ruleTester.run('prefer-promise-reject-errors', rule, { const foo = false; Promise.reject(false || new Error()); `, + ` + declare const foo: Readonly; + Promise.reject(foo); + `, + ` + declare const foo: Readonly | Readonly; + Promise.reject(foo); + `, + ` + declare const foo: Readonly & Readonly; + Promise.reject(foo); + `, + ` + declare const foo: Readonly & { foo: 'bar' }; + Promise.reject(foo); + `, + ` + declare const foo: Readonly & { foo: 'bar' }; + Promise.reject(foo); + `, + ` + declare const foo: Readonly>; + Promise.reject(foo); + `, + ` + declare const foo: Readonly>>; + Promise.reject(foo); + `, + ` + declare const foo: Readonly< + Readonly & { foo: 'bar' }> & { + fooBar: 'barFoo'; + } + > & { barFoo: 'fooBar' }; + Promise.reject(foo); + `, + ` + declare const foo: + | Readonly | Readonly> + | Readonly; + Promise.reject(foo); + `, + ` + type Wrapper = { foo: Readonly[] }; + declare const foo: Wrapper['foo'][5]; + Promise.reject(foo); + `, ` declare const foo: Error[]; Promise.reject(foo[5]); @@ -142,6 +189,53 @@ ruleTester.run('prefer-promise-reject-errors', rule, { const foo = Promise; new foo((resolve, reject) => reject(new Error())); `, + ` + declare const foo: Readonly; + new Promise((resolve, reject) => reject(foo)); + `, + ` + declare const foo: Readonly | Readonly; + new Promise((resolve, reject) => reject(foo)); + `, + ` + declare const foo: Readonly & Readonly; + new Promise((resolve, reject) => reject(foo)); + `, + ` + declare const foo: Readonly & { foo: 'bar' }; + new Promise((resolve, reject) => reject(foo)); + `, + ` + declare const foo: Readonly & { foo: 'bar' }; + new Promise((resolve, reject) => reject(foo)); + `, + ` + declare const foo: Readonly>; + new Promise((resolve, reject) => reject(foo)); + `, + ` + declare const foo: Readonly>>; + new Promise((resolve, reject) => reject(foo)); + `, + ` + declare const foo: Readonly< + Readonly & { foo: 'bar' }> & { + fooBar: 'barFoo'; + } + > & { barFoo: 'fooBar' }; + new Promise((resolve, reject) => reject(foo)); + `, + ` + declare const foo: + | Readonly | Readonly> + | Readonly; + new Promise((resolve, reject) => reject(foo)); + `, + ` + type Wrapper = { foo: Readonly[] }; + declare const foo: Wrapper['foo'][5]; + new Promise((resolve, reject) => reject(foo)); + `, ` declare const foo: Error[]; new Promise((resolve, reject) => reject(foo[5])); @@ -588,6 +682,168 @@ Promise.reject(foo); }, { code: ` +type FakeReadonly = { 'fake readonly': T }; +declare const foo: FakeReadonly; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 4, + endLine: 4, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Readonly<'error'>; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Readonly; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Readonly | 'error'; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Readonly | Readonly | Readonly<'error'>; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Readonly>; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Readonly | 'error'>>; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Readonly & TypeError>> | 'error'; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +declare const foo: Readonly> | Readonly | 'error'; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` +type Wrapper = { foo: Readonly[] }; +declare const foo: Wrapper['foo'][5]; +Promise.reject(foo); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 4, + endLine: 4, + column: 1, + endColumn: 20, + }, + ], + }, + { + code: ` declare const foo: Error[]; Promise.reject(foo); `, @@ -921,6 +1177,168 @@ new Promise((resolve, reject) => reject(foo)); }, { code: ` +type FakeReadonly = { 'fake readonly': T }; +declare const foo: FakeReadonly; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 4, + endLine: 4, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Readonly<'error'>; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Readonly; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Readonly | 'error'; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Readonly | Readonly | Readonly<'error'>; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Readonly>; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Readonly | 'error'>>; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Readonly & TypeError>> | 'error'; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +declare const foo: Readonly> | Readonly | 'error'; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 3, + endLine: 3, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` +type Wrapper = { foo: Readonly[] }; +declare const foo: Wrapper['foo'][5]; +new Promise((resolve, reject) => reject(foo)); + `, + errors: [ + { + messageId: 'rejectAnError', + type: AST_NODE_TYPES.CallExpression, + line: 4, + endLine: 4, + column: 34, + endColumn: 45, + }, + ], + }, + { + code: ` declare const foo: Error[]; new Promise((resolve, reject) => reject(foo)); `, diff --git a/packages/type-utils/src/builtinSymbolLikes.ts b/packages/type-utils/src/builtinSymbolLikes.ts index 5f5b2fd76b86..730527811a7f 100644 --- a/packages/type-utils/src/builtinSymbolLikes.ts +++ b/packages/type-utils/src/builtinSymbolLikes.ts @@ -32,39 +32,131 @@ export function isErrorLike(program: ts.Program, type: ts.Type): boolean { return isBuiltinSymbolLike(program, type, 'Error'); } +/** + * type T = Readonly + * ^ ReadonlyErrorLike + */ +export function isReadonlyErrorLike( + program: ts.Program, + type: ts.Type, +): boolean { + return isReadonlyTypeLike(program, type, subtype => { + const [typeArgument] = subtype.aliasTypeArguments; + return ( + isErrorLike(program, typeArgument) || + isReadonlyErrorLike(program, typeArgument) + ); + }); +} + +/** + * type T = Readonly<{ foo: 'bar' }> + * ^ ReadonlyTypeLike + */ +export function isReadonlyTypeLike( + program: ts.Program, + type: ts.Type, + predicate?: ( + subType: ts.Type & { + aliasSymbol: Symbol; + aliasTypeArguments: readonly ts.Type[]; + }, + ) => boolean, +) { + return isBuiltinTypeAliasLike(program, type, subtype => { + return ( + subtype.aliasSymbol.getEscapedName() === 'Readonly' && + !!predicate?.(subtype) + ); + }); +} +export function isBuiltinTypeAliasLike( + program: ts.Program, + type: ts.Type, + predicate: ( + subType: ts.Type & { + aliasSymbol: Symbol; + aliasTypeArguments: readonly ts.Type[]; + }, + ) => boolean, +) { + return isBuiltinSymbolLikeRecurser(program, type, subtype => { + const { aliasSymbol, aliasTypeArguments } = subtype; + + if (!aliasSymbol || !aliasTypeArguments) { + return false; + } + + if ( + isSymbolFromDefaultLibrary(program, aliasSymbol) && + predicate( + subtype as ts.Type & { + aliasSymbol: Symbol; + aliasTypeArguments: readonly ts.Type[]; + }, + ) + ) { + return true; + } + + return null; + }); +} + export function isBuiltinSymbolLike( program: ts.Program, type: ts.Type, symbolName: string, +): boolean { + return isBuiltinSymbolLikeRecurser(program, type, subType => { + const symbol = subType.getSymbol(); + if (!symbol) { + return false; + } + + if ( + symbol.getName() === symbolName && + isSymbolFromDefaultLibrary(program, symbol) + ) { + return true; + } + + return null; + }); +} + +export function isBuiltinSymbolLikeRecurser( + program: ts.Program, + type: ts.Type, + predicate: (subType: ts.Type) => boolean | null, ): boolean { if (type.isIntersection()) { - return type.types.some(t => isBuiltinSymbolLike(program, t, symbolName)); + return type.types.some(t => + isBuiltinSymbolLikeRecurser(program, t, predicate), + ); } if (type.isUnion()) { - return type.types.every(t => isBuiltinSymbolLike(program, t, symbolName)); + return type.types.every(t => + isBuiltinSymbolLikeRecurser(program, t, predicate), + ); } - const symbol = type.getSymbol(); - if (!symbol) { - return false; + const predicateResult = predicate(type); + if (typeof predicateResult === 'boolean') { + return predicateResult; } + const symbol = type.getSymbol(); if ( - symbol.getName() === symbolName && - isSymbolFromDefaultLibrary(program, symbol) + symbol && + symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface) ) { - return true; - } - - if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) { const checker = program.getTypeChecker(); - for (const baseType of checker.getBaseTypes(type as ts.InterfaceType)) { - if (isBuiltinSymbolLike(program, baseType, symbolName)) { + if (isBuiltinSymbolLikeRecurser(program, baseType, predicate)) { return true; } } } - return false; } From f33a84ac218109cd4f015cb861867f6d6d171e4e Mon Sep 17 00:00:00 2001 From: auvred Date: Sat, 6 Jan 2024 12:05:32 +0000 Subject: [PATCH 31/31] chore: fix lint issues --- packages/type-utils/src/builtinSymbolLikes.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/type-utils/src/builtinSymbolLikes.ts b/packages/type-utils/src/builtinSymbolLikes.ts index 730527811a7f..3443a0d0382e 100644 --- a/packages/type-utils/src/builtinSymbolLikes.ts +++ b/packages/type-utils/src/builtinSymbolLikes.ts @@ -58,15 +58,14 @@ export function isReadonlyTypeLike( type: ts.Type, predicate?: ( subType: ts.Type & { - aliasSymbol: Symbol; + aliasSymbol: ts.Symbol; aliasTypeArguments: readonly ts.Type[]; }, ) => boolean, -) { +): boolean { return isBuiltinTypeAliasLike(program, type, subtype => { return ( - subtype.aliasSymbol.getEscapedName() === 'Readonly' && - !!predicate?.(subtype) + subtype.aliasSymbol.getName() === 'Readonly' && !!predicate?.(subtype) ); }); } @@ -75,11 +74,11 @@ export function isBuiltinTypeAliasLike( type: ts.Type, predicate: ( subType: ts.Type & { - aliasSymbol: Symbol; + aliasSymbol: ts.Symbol; aliasTypeArguments: readonly ts.Type[]; }, ) => boolean, -) { +): boolean { return isBuiltinSymbolLikeRecurser(program, type, subtype => { const { aliasSymbol, aliasTypeArguments } = subtype; @@ -91,7 +90,7 @@ export function isBuiltinTypeAliasLike( isSymbolFromDefaultLibrary(program, aliasSymbol) && predicate( subtype as ts.Type & { - aliasSymbol: Symbol; + aliasSymbol: ts.Symbol; aliasTypeArguments: readonly ts.Type[]; }, ) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy