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 107f369260ec..ea625b32ec48 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -151,6 +151,7 @@ export = { '@typescript-eslint/return-await': '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 7cf867b382f2..ff44f49bab1e 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -63,6 +63,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', '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off', }, diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index 4a5c7adfc93e..ce20c7a1fdd8 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -104,6 +104,7 @@ export = { 'error', 'error-handling-correctness-only', ], + '@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 72c62f3e0122..06c7ec06f595 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -122,6 +122,7 @@ import returnAwait from './return-await'; import sortTypeConstituents from './sort-type-constituents'; 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 typedef from './typedef'; import unboundMethod from './unbound-method'; @@ -252,6 +253,7 @@ const rules = { 'sort-type-constituents': sortTypeConstituents, 'strict-boolean-expressions': strictBooleanExpressions, 'switch-exhaustiveness-check': switchExhaustivenessCheck, + 'thenable-in-promise-aggregators': thenableInPromiseAggregators, 'triple-slash-reference': tripleSlashReference, typedef, 'unbound-method': unboundMethod, 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..6840d94e7ca8 --- /dev/null +++ b/packages/eslint-plugin/src/rules/thenable-in-promise-aggregators.ts @@ -0,0 +1,215 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +import { + isBuiltinSymbolLike, + isTypeAnyType, + isTypeUnknownType, +} from '@typescript-eslint/type-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', 'allSettled', 'any', 'race']); + +export default createRule({ + name: 'thenable-in-promise-aggregators', + meta: { + type: 'problem', + docs: { + description: + 'Disallow passing non-Thenable values to promise aggregators', + recommended: 'strict', + requiresTypeChecking: true, + }, + messages: { + 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?).', + inArray: + 'Unexpected non-Thenable value in array passed to promise aggregator.', + }, + schema: [], + }, + 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, [ + 'PromiseConstructor', + '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({ + node: arg, + messageId: 'emptyArrayElement', + }); + 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({ + node: element, + messageId: 'inArray', + }); + } + 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({ + node: arg, + messageId: 'arrayArg', + }); + }, + }; + }, +}); 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..f9a9c6076c55 --- /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({ + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }, + }, +}); + +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 7686531a40d3..bd729dc2b9bd 100644 --- a/packages/type-utils/src/builtinSymbolLikes.ts +++ b/packages/type-utils/src/builtinSymbolLikes.ts @@ -118,6 +118,19 @@ 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 + * @param symbolName the name or names of the symbol to match + */ export function isBuiltinSymbolLike( program: ts.Program, type: ts.Type,