Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(eslint-plugin): [thenable-in-promise-aggregators] add rule #10163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5302e72
feat(eslint-plugin): [thenable-in-promise-aggregators] resolve #1804
Tjstretchalot Dec 18, 2023
3c79e83
Working tests/spelling/lint
Tjstretchalot Dec 18, 2023
ac00bbf
Remove accidental linebreak mid-sentence
Tjstretchalot Dec 20, 2023
453b5c3
Remove from recommended
Tjstretchalot Dec 20, 2023
c2a10e2
Support Promise aliases
Tjstretchalot Dec 20, 2023
e3e2a4a
Support tuple args
Tjstretchalot Dec 20, 2023
056dfe1
Handle empty arrays / omitted elements in array
Tjstretchalot Dec 20, 2023
03c4fc9
Assert impossible missing type args
Tjstretchalot Dec 20, 2023
00909d3
Test array of any/unknown in valid
Tjstretchalot Dec 20, 2023
da403ff
Remove unnecessary suggestions line
Tjstretchalot Dec 20, 2023
fada233
yarn lint --fix
Tjstretchalot Dec 20, 2023
d918f42
Remove unnecessary aliasing on messageId
Tjstretchalot Dec 20, 2023
9859e6d
Split dual-purpose test cases
Tjstretchalot Dec 20, 2023
2cfbf76
Tests for [] syntax instead of dot syntax
Tjstretchalot Dec 20, 2023
3e0e442
Explicit tests for union types
Tjstretchalot Dec 20, 2023
20e1545
Use shorter valid testcase syntax
Tjstretchalot Dec 20, 2023
7a3cc01
Fix including = in declare const
Tjstretchalot Dec 20, 2023
304356e
Use latest helpers from #8011
Tjstretchalot Dec 24, 2023
d58111d
Carefully handle complicated promise-likes, remove nonArrayArg
Tjstretchalot Dec 24, 2023
609db97
Rename callerType calleeType for consistency
Tjstretchalot Dec 24, 2023
698e579
Add any to list of promise aggregators
Tjstretchalot Dec 24, 2023
4028718
Update to pull in type util functions
Tjstretchalot Jan 10, 2024
82447b7
emptyArrayElement, refactoring, typed member names
Tjstretchalot Jan 12, 2024
c5aef50
Minor documentation cleanup
Tjstretchalot Jan 24, 2024
2bbe8fa
Avoid backward incompat lib signature change
Tjstretchalot Feb 2, 2024
ea65212
Satisfy linter
Tjstretchalot Feb 2, 2024
63f3d1e
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into t…
abrahamguo Oct 17, 2024
3cfd876
revert some stuff
abrahamguo Oct 17, 2024
9268db6
cleanup
abrahamguo Oct 17, 2024
15f08c2
lint
abrahamguo Oct 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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

<!--tabs-->

### ❌ 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.
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/disable-type-checked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/strict-type-checked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
215 changes: 215 additions & 0 deletions packages/eslint-plugin/src/rules/thenable-in-promise-aggregators.ts
Original file line number Diff line number Diff line change
@@ -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<T extends TSESTree.Node>(
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',
});
},
};
},
});
Loading
Loading