diff --git a/packages/eslint-plugin/docs/rules/thenable-in-promise-aggregators.md b/packages/eslint-plugin/docs/rules/thenable-in-promise-aggregators.md new file mode 100644 index 000000000000..820a36e511a8 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/thenable-in-promise-aggregators.md @@ -0,0 +1,48 @@ +--- +description: 'Disallow passing non-Thenable values to promise aggregators.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/thenable-in-promise-aggregators** for documentation. + +A "Thenable" value is an object which has a `then` method, such as a Promise. +The `await` keyword is generally used to retrieve the result of calling a Thenable's `then` method. + +When multiple Thenables are running at the same time, it is sometimes desirable to wait until any one of them resolves (`Promise.race`), all of them resolve or any of them reject (`Promise.all`), or all of them resolve or reject (`Promise.allSettled`). + +Each of these functions accept an iterable of promises as input and return a single Promise. +If a non-Thenable is passed, it is ignored. +While doing so is valid JavaScript, it is often a programmer error, such as forgetting to unwrap a wrapped promise, or using the `await` keyword on the individual promises, which defeats the purpose of using one of these Promise aggregators. + +## Examples + + + +### ❌ Incorrect + +```ts +await Promise.race(['value1', 'value2']); + +await Promise.race([ + await new Promise(resolve => setTimeout(resolve, 3000)), + await new Promise(resolve => setTimeout(resolve, 6000)), +]); +``` + +### ✅ Correct + +```ts +await Promise.race([Promise.resolve('value1'), Promise.resolve('value2')]); + +await Promise.race([ + new Promise(resolve => setTimeout(resolve, 3000)), + new Promise(resolve => setTimeout(resolve, 6000)), +]); +``` + +## When Not To Use It + +If you want to allow code to use `Promise.race`, `Promise.all`, or `Promise.allSettled` on arrays of non-Thenable values. +This is generally not preferred but can sometimes be useful for visual consistency. +You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 3a5e6343b197..0fbbc691e96c 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -146,6 +146,7 @@ export = { '@typescript-eslint/sort-type-constituents': 'error', '@typescript-eslint/strict-boolean-expressions': 'error', '@typescript-eslint/switch-exhaustiveness-check': 'error', + '@typescript-eslint/thenable-in-promise-aggregators': 'error', '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/typedef': 'error', '@typescript-eslint/unbound-method': 'error', diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index 4cd82bf2414e..ebf6d21e9ee9 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -57,6 +57,7 @@ export = { '@typescript-eslint/return-await': 'off', '@typescript-eslint/strict-boolean-expressions': 'off', '@typescript-eslint/switch-exhaustiveness-check': 'off', + '@typescript-eslint/thenable-in-promise-aggregators': 'off', '@typescript-eslint/unbound-method': 'off', }, }; diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index a0f82563b1f3..7a90194e3cfe 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -71,6 +71,7 @@ export = { '@typescript-eslint/require-await': 'error', '@typescript-eslint/restrict-plus-operands': 'error', '@typescript-eslint/restrict-template-expressions': 'error', + '@typescript-eslint/thenable-in-promise-aggregators': 'error', '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/unbound-method': 'error', '@typescript-eslint/unified-signatures': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 94cce184242d..06ff2a764d92 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -132,6 +132,7 @@ import spaceBeforeFunctionParen from './space-before-function-paren'; import spaceInfixOps from './space-infix-ops'; import strictBooleanExpressions from './strict-boolean-expressions'; import switchExhaustivenessCheck from './switch-exhaustiveness-check'; +import thenableInPromiseAggregators from './thenable-in-promise-aggregators'; import tripleSlashReference from './triple-slash-reference'; import typeAnnotationSpacing from './type-annotation-spacing'; import typedef from './typedef'; @@ -273,6 +274,7 @@ export default { 'space-infix-ops': spaceInfixOps, 'strict-boolean-expressions': strictBooleanExpressions, 'switch-exhaustiveness-check': switchExhaustivenessCheck, + 'thenable-in-promise-aggregators': thenableInPromiseAggregators, 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, typedef: typedef, diff --git a/packages/eslint-plugin/src/rules/thenable-in-promise-aggregators.ts b/packages/eslint-plugin/src/rules/thenable-in-promise-aggregators.ts new file mode 100644 index 000000000000..e6eeac2e882b --- /dev/null +++ b/packages/eslint-plugin/src/rules/thenable-in-promise-aggregators.ts @@ -0,0 +1,215 @@ +import { + isBuiltinSymbolLike, + isTypeAnyType, + isTypeUnknownType, +} from '@typescript-eslint/type-utils'; +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import { createRule, getParserServices } from '../util'; + +const aggregateFunctionNames = new Set(['all', 'race', 'allSettled', 'any']); + +export default createRule({ + name: 'thenable-in-promise-aggregators', + meta: { + docs: { + description: + 'Disallow passing non-Thenable values to promise aggregators', + recommended: 'strict', + requiresTypeChecking: true, + }, + messages: { + inArray: + 'Unexpected non-Thenable value in array passed to promise aggregator.', + arrayArg: + 'Unexpected array of non-Thenable values passed to promise aggregator.', + emptyArrayElement: + 'Unexpected empty element in array passed to promise aggregator (do you have an extra comma?).', + }, + schema: [], + type: 'problem', + }, + defaultOptions: [], + + create(context) { + const services = getParserServices(context); + const checker = services.program.getTypeChecker(); + + function skipChainExpression( + node: T, + ): T | TSESTree.ChainElement { + return node.type === AST_NODE_TYPES.ChainExpression + ? node.expression + : node; + } + + function isPartiallyLikeType( + type: ts.Type, + predicate: (type: ts.Type) => boolean, + ): boolean { + if (isTypeAnyType(type) || isTypeUnknownType(type)) { + return true; + } + if (type.isIntersection() || type.isUnion()) { + return type.types.some(t => isPartiallyLikeType(t, predicate)); + } + return predicate(type); + } + + function isIndexableWithSomeElementsLike( + type: ts.Type, + predicate: (type: ts.Type) => boolean, + ): boolean { + if (isTypeAnyType(type) || isTypeUnknownType(type)) { + return true; + } + + if (type.isIntersection() || type.isUnion()) { + return type.types.some(t => + isIndexableWithSomeElementsLike(t, predicate), + ); + } + + if (!checker.isArrayType(type) && !checker.isTupleType(type)) { + const indexType = checker.getIndexTypeOfType(type, ts.IndexKind.Number); + if (indexType === undefined) { + return false; + } + + return isPartiallyLikeType(indexType, predicate); + } + + const typeArgs = type.typeArguments; + if (typeArgs === undefined) { + throw new Error( + 'Expected to find type arguments for an array or tuple.', + ); + } + + return typeArgs.some(t => isPartiallyLikeType(t, predicate)); + } + + function isStringLiteralMatching( + type: ts.Type, + predicate: (value: string) => boolean, + ): boolean { + if (type.isIntersection()) { + return type.types.some(t => isStringLiteralMatching(t, predicate)); + } + + if (type.isUnion()) { + return type.types.every(t => isStringLiteralMatching(t, predicate)); + } + + if (!type.isStringLiteral()) { + return false; + } + + return predicate(type.value); + } + + function isMemberName( + node: + | TSESTree.MemberExpressionComputedName + | TSESTree.MemberExpressionNonComputedName, + predicate: (name: string) => boolean, + ): boolean { + if (!node.computed) { + return predicate(node.property.name); + } + + if (node.property.type !== AST_NODE_TYPES.Literal) { + const typeOfProperty = services.getTypeAtLocation(node.property); + return isStringLiteralMatching(typeOfProperty, predicate); + } + + const { value } = node.property; + if (typeof value !== 'string') { + return false; + } + + return predicate(value); + } + + return { + CallExpression(node: TSESTree.CallExpression): void { + const callee = skipChainExpression(node.callee); + if (callee.type !== AST_NODE_TYPES.MemberExpression) { + return; + } + + if (!isMemberName(callee, n => aggregateFunctionNames.has(n))) { + return; + } + + const args = node.arguments; + if (args.length < 1) { + return; + } + + const calleeType = services.getTypeAtLocation(callee.object); + + if ( + !isBuiltinSymbolLike( + services.program, + calleeType, + name => name === 'PromiseConstructor' || name === 'Promise', + ) + ) { + return; + } + + const arg = args[0]; + if (arg.type === AST_NODE_TYPES.ArrayExpression) { + const { elements } = arg; + if (elements.length === 0) { + return; + } + + for (const element of elements) { + if (element == null) { + context.report({ + messageId: 'emptyArrayElement', + node: arg, + }); + return; + } + const elementType = services.getTypeAtLocation(element); + if (isTypeAnyType(elementType) || isTypeUnknownType(elementType)) { + continue; + } + + const originalNode = services.esTreeNodeToTSNodeMap.get(element); + if (tsutils.isThenableType(checker, originalNode, elementType)) { + continue; + } + + context.report({ + messageId: 'inArray', + node: element, + }); + } + return; + } + + const argType = services.getTypeAtLocation(arg); + const originalNode = services.esTreeNodeToTSNodeMap.get(arg); + if ( + isIndexableWithSomeElementsLike(argType, elementType => { + return tsutils.isThenableType(checker, originalNode, elementType); + }) + ) { + return; + } + + context.report({ + messageId: 'arrayArg', + node: arg, + }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/thenable-in-promise-aggregators.test.ts b/packages/eslint-plugin/tests/rules/thenable-in-promise-aggregators.test.ts new file mode 100644 index 000000000000..8c912b05a99a --- /dev/null +++ b/packages/eslint-plugin/tests/rules/thenable-in-promise-aggregators.test.ts @@ -0,0 +1,797 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/thenable-in-promise-aggregators'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('thenable-in-promise-aggregators', rule, { + valid: [ + 'await Promise.race([Promise.resolve(3)]);', + 'await Promise.all([Promise.resolve(3)]);', + 'await Promise.allSettled([Promise.resolve(3)]);', + 'await Promise.any([Promise.resolve(3)]);', + 'await Promise.all([]);', + 'await Promise.race([Promise.reject(3)]);', + "await Promise['all']([Promise.resolve(3)]);", + 'await Promise[`all`]([Promise.resolve(3)]);', + "await Promise.all([Promise['resolve'](3)]);", + 'await Promise.race([(async () => true)()]);', + ` +function returnsPromise() { + return Promise.resolve('value'); +} +await Promise.race([returnsPromise()]); + `, + ` +async function returnsPromiseAsync() {} +await Promise.race([returnsPromiseAsync()]); + `, + ` +declare const anyValue: any; +await Promise.race([anyValue]); + `, + ` +const key = 'all'; +await Promise[key]([Promise.resolve(3)]); + `, + ` +declare const key: 'race'; +await Promise[key]([Promise.resolve(3)]); + `, + ` +declare const key: 'race' | 'any'; +await Promise[key]([Promise.resolve(3)]); + `, + ` +declare const key: any; +await Promise[key]([3]); + `, + ` +declare const key: 'all' | 'unrelated'; +await Promise[key]([3]); + `, + ` +declare const key: [string] & 'all'; +await Promise[key]([Promise.resolve(3)]); + `, + ` +declare const unknownValue: unknown; +await Promise.race([unknownValue]); + `, + ` +declare const numberPromise: Promise; +await Promise.race([numberPromise]); + `, + ` +class Foo extends Promise {} +const foo: Foo = Foo.resolve(2); +await Promise.race([foo]); + `, + ` +class Foo extends Promise {} +class Bar extends Foo {} +const bar: Bar = Bar.resolve(2); +await Promise.race([bar]); + `, + 'await Promise.race([Math.random() > 0.5 ? nonExistentSymbol : 0]);', + ` +declare const intersectionPromise: Promise & number; +await Promise.race([intersectionPromise]); + `, + ` +declare const unionPromise: Promise | number; +await Promise.race([unionPromise]); + `, + ` +class Thenable { + then(callback: () => {}) {} +} + +const thenable = new Thenable(); +await Promise.race([thenable]); + `, + ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + await Promise.all([ + obj1.a?.b?.c?.(), + obj2.a?.b?.c(), + obj3.a?.b.c?.(), + obj4.a.b.c?.(), + obj5.a?.().b?.c?.(), + obj6?.a.b.c?.(), + callback(), + ]); +}; + `, + ` +declare const promiseArr: Promise[]; +await Promise.all(promiseArr); + `, + ` +declare const intersectionArr: (Promise & number)[]; +await Promise.all(intersectionArr); + `, + ` +const values = [1, 2, 3]; +await Promise.all(values.map(value => Promise.resolve(value))); + `, + ` +const values = [1, 2, 3]; +await Promise.all(values.map(async value => {})); + `, + ` +const foo = Promise; +await foo.all([Promise.resolve(3)]); + `, + ` +const foo = Promise; +await Promise.all([foo.resolve(3)]); + `, + ` +class Foo extends Promise {} +await Foo.all([Foo.resolve(3)]); + `, + ` +const foo = Promise; +await Promise.all([ + new foo(resolve => { + resolve(); + }), +]); + `, + ` +class Foo extends Promise {} +const myfn = () => + new Foo(resolve => { + resolve(3); + }); +await Promise.all([myfn()]); + `, + 'await Promise.resolve?.([Promise.resolve(3)]);', + 'await Promise?.resolve?.([Promise.resolve(3)]);', + ` +const foo = Promise; +await foo.resolve?.([foo.resolve(3)]); + `, + ` +const promisesTuple: [Promise] = [Promise.resolve(3)]; +await Promise.all(promisesTuple); + `, + 'await Promise.all([Promise.resolve(6)] as const);', + ` +const foo = Array(); +await Promise.all(foo); + `, + ` +declare const arrOfAny: any[]; +await Promise.all(arrOfAny); + `, + ` +declare const arrOfUnknown: unknown[]; +await Promise.all(arrOfAny); + `, + ` +declare const arrOfIntersection: (Promise & number)[]; +await Promise.all(arrOfIntersection); + `, + ` +declare const arrOfUnion: (Promise | number)[]; +await Promise.all(arrOfUnion); + `, + ` +declare const unionOfArr: Promise[] | Promise[]; +await Promise.all(unionOfArr); + `, + ` +declare const unionOfTuple: [Promise] | [Promise]; +await Promise.all(unionOfTuple); + `, + ` +declare const intersectionOfArr: Promise[] & Promise[]; +await Promise.all(intersectionOfArr); + `, + ` +declare const intersectionOfTuple: [Promise] & [Promise]; +await Promise.all(intersectionOfTuple); + `, + ` +declare const readonlyArr: ReadonlyArray>; +await Promise.all(readonlyArr); + `, + ` +declare const unionOfPromiseArrAndArr: Promise[] | number[]; +await Promise.all(unionOfPromiseArrAndArr); + `, + ` +declare const readonlyTuple: readonly [Promise]; +await Promise.all(readonlyTuple); + `, + ` +declare const readonlyTupleWithOneValid: readonly [number, Promise]; +await Promise.all(readonlyTupleWithOneValid); + `, + ` +declare const unionOfReadonlyTuples: + | readonly [number] + | readonly [Promise]; +await Promise.all(unionOfReadonlyTuples); + `, + ` +declare const readonlyTupleOfUnion: readonly [Promise | number]; +await Promise.all(readonlyTupleOfUnion); + `, + ` +class Foo extends Array> {} +declare const foo: Foo; +await Promise.all(foo); + `, + ` +class Foo extends Array {} +declare const foo: Foo; +await Promise.all(foo); + `, + ` +class Foo extends Array {} +declare const foo: Foo; +await Promise.all(foo); + `, + ` +class Foo extends Array {} +declare const foo: Foo; +await Promise.all(foo); + `, + ` +class Foo extends Array> {} +declare const foo: Foo; +await Promise.all(foo); + `, + ` +type Foo = { new (): ReadonlyArray> }; +declare const foo: Foo; +class Baz extends foo {} +declare const baz: Baz; +await Promise.all(baz); + `, + ` +type Foo = { new (): [Promise] }; +declare const foo: Foo; +class Baz extends foo {} +declare const baz: Baz; +await Promise.all(baz); + `, + ` +type Foo = { new (): [number | Promise] }; +declare const foo: Foo; +class Baz extends foo {} +declare const baz: Baz; +await Promise.all(baz); + `, + ], + + invalid: [ + { + code: 'await Promise.race([0]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise.all([0]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise.allSettled([0]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise.any([0]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise.race([Promise.resolve(3), 0]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: 'async () => await Promise.race([await Promise.resolve(3)]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: "async () => await Promise.race([Math.random() > 0.5 ? '' : 0]);", + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: ` +class NonPromise extends Array {} +await Promise.race([new NonPromise()]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +async function test() { + class IncorrectThenable { + then() {} + } + const thenable = new IncorrectThenable(); + + await Promise.race([thenable]); +} + `, + errors: [ + { + line: 8, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const callback: (() => void) | undefined; +await Promise.race([callback?.()]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const obj: { a?: { b?: () => void } }; +await Promise.race([obj.a?.b?.()]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const obj: { a: { b: { c?: () => void } } } | undefined; +await Promise.race([obj?.a.b.c?.()]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const wrappedPromise: { promise: Promise }; +declare const stdPromise: Promise; +await Promise.all([wrappedPromise, stdPromise]); + `, + errors: [ + { + line: 4, + messageId: 'inArray', + }, + ], + }, + { + code: ` +const foo = Promise; +await foo.race([0]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +class Foo extends Promise {} +await Foo.all([0]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +const foo = (() => Promise)(); +await foo.all([0]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise.race?.([0]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise?.race([0]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise?.race?.([0]);', + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const foo: never; +await Promise.all([foo]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise.all([,]);', + errors: [ + { + line: 1, + messageId: 'emptyArrayElement', + }, + ], + }, + { + code: "await Promise['all']([3]);", + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: "await Promise['race']([3]);", + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: "await Promise['allSettled']([3]);", + errors: [ + { + line: 1, + messageId: 'inArray', + }, + ], + }, + { + code: ` +const key = 'all'; +await Promise[key]([3]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const key: 'all'; +await Promise[key]([3]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const key: 'all' | 'race'; +await Promise[key]([3]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const key: 'all' & Promise; +await Promise[key]([3]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: ` +declare const badUnion: number | string; +await Promise.all([badUnion]); + `, + errors: [ + { + line: 3, + messageId: 'inArray', + }, + ], + }, + { + code: 'await Promise.all([0, 1].map(v => v));', + errors: [ + { + line: 1, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +declare const promiseArr: Promise[]; +await Promise.all(promiseArr.map(v => await v)); + `, + errors: [ + { + line: 3, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +declare const arr: number[]; +await Promise.all?.(arr); + `, + errors: [ + { + line: 3, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +declare const foo: [number]; +await Promise.race(foo); + `, + errors: [ + { + line: 3, + messageId: 'arrayArg', + }, + ], + }, + { + code: 'await Promise.race([0] as const);', + errors: [ + { + line: 1, + messageId: 'arrayArg', + }, + ], + }, + { + code: "await Promise['all']([0, 1].map(v => v));", + errors: [ + { + line: 1, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +declare const badUnionArr: (number | string)[]; +await Promise.all(badUnionArr); + `, + errors: [ + { + line: 3, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +declare const badArrUnion: number[] | string[]; +await Promise.all(badArrUnion); + `, + errors: [ + { + line: 3, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +declare const badReadonlyArr: ReadonlyArray; +await Promise.all(badReadonlyArr); + `, + errors: [ + { + line: 3, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +declare const badArrIntersection: number[] & string[]; +await Promise.all(badArrIntersection); + `, + errors: [ + { + line: 3, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +declare const badReadonlyTuple: readonly [number, string]; +await Promise.all(badReadonlyTuple); + `, + errors: [ + { + line: 3, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +class Foo extends Array {} +declare const foo: Foo; +await Promise.all(foo); + `, + errors: [ + { + line: 4, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +class Foo extends Array {} +declare const foo: Foo; +await Promise.all(foo); + `, + errors: [ + { + line: 4, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +type Foo = [number]; +declare const foo: Foo; +await Promise.all(foo); + `, + errors: [ + { + line: 4, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +class Bar {} +type Foo = { new (): Bar & [number] }; +declare const foo: Foo; +class Baz extends foo {} +declare const baz: Baz; +await Promise.all(baz); + `, + errors: [ + { + line: 7, + messageId: 'arrayArg', + }, + ], + }, + { + code: ` +type Foo = { new (): ReadonlyArray }; +declare const foo: Foo; +class Baz extends foo {} +declare const baz: Baz; +await Promise.all(baz); + `, + errors: [ + { + line: 6, + messageId: 'arrayArg', + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/thenable-in-promise-aggregators.shot b/packages/eslint-plugin/tests/schema-snapshots/thenable-in-promise-aggregators.shot new file mode 100644 index 000000000000..9f966c14d5e2 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/thenable-in-promise-aggregators.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes thenable-in-promise-aggregators 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; diff --git a/packages/type-utils/src/builtinSymbolLikes.ts b/packages/type-utils/src/builtinSymbolLikes.ts index 3443a0d0382e..14e5f5c4480b 100644 --- a/packages/type-utils/src/builtinSymbolLikes.ts +++ b/packages/type-utils/src/builtinSymbolLikes.ts @@ -8,7 +8,11 @@ import { isSymbolFromDefaultLibrary } from './isSymbolFromDefaultLibrary'; * ^ PromiseLike */ export function isPromiseLike(program: ts.Program, type: ts.Type): boolean { - return isBuiltinSymbolLike(program, type, 'Promise'); + return isBuiltinSymbolLike( + program, + type, + symbolName => symbolName === 'Promise', + ); } /** @@ -20,7 +24,11 @@ export function isPromiseConstructorLike( program: ts.Program, type: ts.Type, ): boolean { - return isBuiltinSymbolLike(program, type, 'PromiseConstructor'); + return isBuiltinSymbolLike( + program, + type, + symbolName => symbolName === 'PromiseConstructor', + ); } /** @@ -29,7 +37,11 @@ export function isPromiseConstructorLike( * ^ ErrorLike */ export function isErrorLike(program: ts.Program, type: ts.Type): boolean { - return isBuiltinSymbolLike(program, type, 'Error'); + return isBuiltinSymbolLike( + program, + type, + symbolName => symbolName === 'Error', + ); } /** @@ -102,11 +114,30 @@ export function isBuiltinTypeAliasLike( }); } +/** + * Checks if the given type is an instance of a built-in type whose name matches + * the given predicate, i.e., it either is that type or extends it. + * + * This will return false if the type is _potentially_ an instance of the given + * type but might not be, e.g., if it's a union type where only some of the + * members are instances of a built-in type matching the predicate, this returns + * false. + * + * @param program The program the type is defined in + * @param type The type + * @param predicateOrSymbolName A predicate which returns true if the name of a + * symbol is a match and false otherwise, or the name of the symbol to match + */ export function isBuiltinSymbolLike( program: ts.Program, type: ts.Type, - symbolName: string, + predicateOrSymbolName: string | ((symbolName: string) => boolean), ): boolean { + const predicate = + typeof predicateOrSymbolName === 'string' + ? (symbolName: string): boolean => symbolName === predicateOrSymbolName + : predicateOrSymbolName; + return isBuiltinSymbolLikeRecurser(program, type, subType => { const symbol = subType.getSymbol(); if (!symbol) { @@ -114,7 +145,7 @@ export function isBuiltinSymbolLike( } if ( - symbol.getName() === symbolName && + predicate(symbol.getName()) && isSymbolFromDefaultLibrary(program, symbol) ) { return true; 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