Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
feat(eslint-plugin): [thenable-in-promise-aggregators] disallow non-Thenables to promise aggregators#8094
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
feat(eslint-plugin): [thenable-in-promise-aggregators] disallow non-Thenables to promise aggregators#8094
Changes fromall commits
5302e723c79e83ac00bbf453b5c3c2a10e2e3e2a4a056dfe103c4fc900909d3da403fffada233d918f429859e6d2cfbf763e0e44220e15457a3cc01304356ed58111d609db97698e579402871882447b7c5aef502bbe8faea65212File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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. |
JoshuaKGoldberg marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page.
Tjstretchalot marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,215 @@ | ||||||
| import{ | ||||||
| isBuiltinSymbolLike, | ||||||
| isTypeAnyType, | ||||||
| isTypeUnknownType, | ||||||
| }from'@typescript-eslint/type-utils'; | ||||||
| importtype{TSESTree}from'@typescript-eslint/utils'; | ||||||
| import{AST_NODE_TYPES}from'@typescript-eslint/utils'; | ||||||
| import*astsutilsfrom'ts-api-utils'; | ||||||
| import*astsfrom'typescript'; | ||||||
| import{createRule,getParserServices}from'../util'; | ||||||
| constaggregateFunctionNames=newSet(['all','race','allSettled','any']); | ||||||
| exportdefaultcreateRule({ | ||||||
| name:'thenable-in-promise-aggregators', | ||||||
| meta:{ | ||||||
| docs:{ | ||||||
| description: | ||||||
| 'Disallow passing non-Thenable values to promise aggregators', | ||||||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [Docs] Interesting, we don't have a standard for whether we refer to Promises with an upper-case P or lower-case p... Outside the scope of this PR, maybe it'd be a good followup to file an issue about standardizing this in docs? Not a blocker or request for changes 😄 just ruminating. | ||||||
| recommended:'strict', | ||||||
| requiresTypeChecking:true, | ||||||
| }, | ||||||
| messages:{ | ||||||
| inArray: | ||||||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [Question] Is there a use case for adding an option to allow users to provide an array/tuple where only some of the elements are thenables? Like I can't think of one 🤔 so maybe this is a non-actionable comment? Btw, sorry if I missed this in previous discussion - I looked through and couldn't find anything. Author
| ||||||
| '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){ | ||||||
| constservices=getParserServices(context); | ||||||
| constchecker=services.program.getTypeChecker(); | ||||||
| functionskipChainExpression<TextendsTSESTree.Node>( | ||||||
Tjstretchalot marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||||||
| node:T, | ||||||
| ):T|TSESTree.ChainElement{ | ||||||
| returnnode.type===AST_NODE_TYPES.ChainExpression | ||||||
| ?node.expression | ||||||
| :node; | ||||||
| } | ||||||
| functionisPartiallyLikeType( | ||||||
| type:ts.Type, | ||||||
| predicate:(type:ts.Type)=>boolean, | ||||||
| ):boolean{ | ||||||
| if(isTypeAnyType(type)||isTypeUnknownType(type)){ | ||||||
Tjstretchalot marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||||||
| returntrue; | ||||||
| } | ||||||
| if(type.isIntersection()||type.isUnion()){ | ||||||
| returntype.types.some(t=>isPartiallyLikeType(t,predicate)); | ||||||
| } | ||||||
| returnpredicate(type); | ||||||
| } | ||||||
| functionisIndexableWithSomeElementsLike( | ||||||
| type:ts.Type, | ||||||
| predicate:(type:ts.Type)=>boolean, | ||||||
| ):boolean{ | ||||||
| if(isTypeAnyType(type)||isTypeUnknownType(type)){ | ||||||
| returntrue; | ||||||
| } | ||||||
| if(type.isIntersection()||type.isUnion()){ | ||||||
| returntype.types.some(t=> | ||||||
| isIndexableWithSomeElementsLike(t,predicate), | ||||||
| ); | ||||||
| } | ||||||
| if(!checker.isArrayType(type)&&!checker.isTupleType(type)){ | ||||||
| constindexType=checker.getIndexTypeOfType(type,ts.IndexKind.Number); | ||||||
| if(indexType===undefined){ | ||||||
| returnfalse; | ||||||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [Testing] Missing unit test coverage for this and a few other lines. If it's a case where weknow the value is definitely not nullish (e.g. an array type having a numeric index) then a | ||||||
| } | ||||||
| returnisPartiallyLikeType(indexType,predicate); | ||||||
| } | ||||||
| consttypeArgs=type.typeArguments; | ||||||
| if(typeArgs===undefined){ | ||||||
| thrownewError( | ||||||
| 'Expected to find type arguments for an array or tuple.', | ||||||
| ); | ||||||
| } | ||||||
| returntypeArgs.some(t=>isPartiallyLikeType(t,predicate)); | ||||||
| } | ||||||
| functionisStringLiteralMatching( | ||||||
| type:ts.Type, | ||||||
| predicate:(value:string)=>boolean, | ||||||
| ):boolean{ | ||||||
| if(type.isIntersection()){ | ||||||
| returntype.types.some(t=>isStringLiteralMatching(t,predicate)); | ||||||
| } | ||||||
| if(type.isUnion()){ | ||||||
| returntype.types.every(t=>isStringLiteralMatching(t,predicate)); | ||||||
| } | ||||||
| if(!type.isStringLiteral()){ | ||||||
| returnfalse; | ||||||
| } | ||||||
| returnpredicate(type.value); | ||||||
| } | ||||||
| functionisMemberName( | ||||||
| node: | ||||||
| |TSESTree.MemberExpressionComputedName | ||||||
| |TSESTree.MemberExpressionNonComputedName, | ||||||
| predicate:(name:string)=>boolean, | ||||||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [Refactor] Similar to the Suggested change
| ||||||
| ):boolean{ | ||||||
| if(!node.computed){ | ||||||
| returnpredicate(node.property.name); | ||||||
| } | ||||||
| if(node.property.type!==AST_NODE_TYPES.Literal){ | ||||||
| consttypeOfProperty=services.getTypeAtLocation(node.property); | ||||||
| returnisStringLiteralMatching(typeOfProperty,predicate); | ||||||
| } | ||||||
| const{ value}=node.property; | ||||||
| if(typeofvalue!=='string'){ | ||||||
| returnfalse; | ||||||
| } | ||||||
| returnpredicate(value); | ||||||
| } | ||||||
| return{ | ||||||
| CallExpression(node:TSESTree.CallExpression):void{ | ||||||
| constcallee=skipChainExpression(node.callee); | ||||||
| if(callee.type!==AST_NODE_TYPES.MemberExpression){ | ||||||
| return; | ||||||
| } | ||||||
| if(!isMemberName(callee,n=>aggregateFunctionNames.has(n))){ | ||||||
| return; | ||||||
| } | ||||||
| constargs=node.arguments; | ||||||
| if(args.length<1){ | ||||||
| return; | ||||||
| } | ||||||
| constcalleeType=services.getTypeAtLocation(callee.object); | ||||||
| if( | ||||||
| !isBuiltinSymbolLike( | ||||||
| services.program, | ||||||
| calleeType, | ||||||
| name=>name==='PromiseConstructor'||name==='Promise', | ||||||
| ) | ||||||
| ){ | ||||||
| return; | ||||||
| } | ||||||
| constarg=args[0]; | ||||||
| if(arg.type===AST_NODE_TYPES.ArrayExpression){ | ||||||
| const{ elements}=arg; | ||||||
| if(elements.length===0){ | ||||||
| return; | ||||||
| } | ||||||
Tjstretchalot marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||||||
| for(constelementofelements){ | ||||||
| if(element==null){ | ||||||
| context.report({ | ||||||
| messageId:'emptyArrayElement', | ||||||
| node:arg, | ||||||
| }); | ||||||
| return; | ||||||
| } | ||||||
| constelementType=services.getTypeAtLocation(element); | ||||||
| if(isTypeAnyType(elementType)||isTypeUnknownType(elementType)){ | ||||||
| continue; | ||||||
| } | ||||||
| constoriginalNode=services.esTreeNodeToTSNodeMap.get(element); | ||||||
| if(tsutils.isThenableType(checker,originalNode,elementType)){ | ||||||
| continue; | ||||||
| } | ||||||
Comment on lines +180 to +188 Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. this logic (if Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Hmm; I refactored around a bit, though I still have 3 calls like this. However, I don't think something like this would add anything besides some mental overhead: functionisTypeAnyOrUnknownType(type:ts.Type):boolean{returnisTypeAnyType(type)||isTypeUnknownType(type);} For this to be useful from my current perspective there would need to be a more salient name for the method that is a more useful concept than | ||||||
| context.report({ | ||||||
| messageId:'inArray', | ||||||
| node:element, | ||||||
| }); | ||||||
| } | ||||||
| return; | ||||||
| } | ||||||
| constargType=services.getTypeAtLocation(arg); | ||||||
| constoriginalNode=services.esTreeNodeToTSNodeMap.get(arg); | ||||||
| if( | ||||||
| isIndexableWithSomeElementsLike(argType,elementType=>{ | ||||||
| returntsutils.isThenableType(checker,originalNode,elementType); | ||||||
| }) | ||||||
| ){ | ||||||
| return; | ||||||
| } | ||||||
| context.report({ | ||||||
| messageId:'arrayArg', | ||||||
| node:arg, | ||||||
| }); | ||||||
| }, | ||||||
| }; | ||||||
| }, | ||||||
| }); | ||||||
Uh oh!
There was an error while loading.Please reload this page.