forked from typescript-eslint/typescript-eslint
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): add new rule no-floating-promises
Adds the equivalent of TSLint's `no-floating-promises` rule. Fixes typescript-eslint#464
- Loading branch information
Showing
5 changed files
with
616 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# Requires Promise-like values to be handled appropriately (no-floating-promises) | ||
|
||
This rule forbids usage of Promise-like values in statements without handling | ||
their errors appropriately. Unhandled promises can cause several issues, such | ||
as improperly sequenced operations, ignored Promise rejections and more. Valid | ||
ways of handling a Promise-valued statement include `await`ing, returning, and | ||
either calling `.then()` with two arguments or `.catch()` with one argument. | ||
|
||
## Rule Details | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```ts | ||
const promise = new Promise((resolve, reject) => resolve('value')); | ||
promise; | ||
|
||
async function returnsPromise() { | ||
return 'value'; | ||
} | ||
returnsPromise().then(() => {}); | ||
|
||
Promise.reject('value').catch(); | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```ts | ||
const promise = new Promise((resolve, reject) => resolve('value')); | ||
await promise; | ||
|
||
async function returnsPromise() { | ||
return 'value'; | ||
} | ||
returnsPromise().then(() => {}, () => {}); | ||
|
||
Promise.reject('value').catch(() => {}); | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
If you do not use Promise-like values in your codebase or want to allow them to | ||
remain unhandled. | ||
|
||
## Related to | ||
|
||
- Tslint: ['no-floating-promises'](https://palantir.github.io/tslint/rules/no-floating-promises/) |
127 changes: 127 additions & 0 deletions
127
packages/eslint-plugin/src/rules/no-floating-promises.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import * as tsutils from 'tsutils'; | ||
import * as ts from 'typescript'; | ||
|
||
import * as util from '../util'; | ||
|
||
export default util.createRule({ | ||
name: 'no-floating-promises', | ||
meta: { | ||
docs: { | ||
description: | ||
'Requires promises to be awaited or have a rejection handler.', | ||
category: 'Best Practices', | ||
recommended: false, | ||
tslintName: 'no-floating-promises', | ||
}, | ||
messages: { | ||
floating: 'Promises must be handled appropriately', | ||
}, | ||
schema: [], | ||
type: 'problem', | ||
}, | ||
defaultOptions: [], | ||
|
||
create(context) { | ||
const parserServices = util.getParserServices(context); | ||
const checker = parserServices.program.getTypeChecker(); | ||
|
||
return { | ||
ExpressionStatement(node) { | ||
const { expression } = parserServices.esTreeNodeToTSNodeMap.get( | ||
node, | ||
) as ts.ExpressionStatement; | ||
const type = checker.getTypeAtLocation(expression); | ||
|
||
if ( | ||
isPromiseLike(checker, expression, type) && | ||
(!tsutils.isCallExpression(expression) || | ||
(!isPromiseCatchCallWithHandler(expression) && | ||
!isPromiseThenCallWithRejectionHandler(expression))) | ||
) { | ||
context.report({ | ||
messageId: 'floating', | ||
node, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); | ||
|
||
// Modified from tsutils.isThenable() to only consider thenables which can be | ||
// rejected/caught via a second parameter. Original source (MIT licensed): | ||
// | ||
// https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125 | ||
function isPromiseLike( | ||
checker: ts.TypeChecker, | ||
node: ts.Node, | ||
type: ts.Type, | ||
): boolean { | ||
for (const ty of tsutils.unionTypeParts(checker.getApparentType(type))) { | ||
const then = ty.getProperty('then'); | ||
if (then === undefined) { | ||
continue; | ||
} | ||
|
||
const thenType = checker.getTypeOfSymbolAtLocation(then, node); | ||
if ( | ||
hasMatchingSignature( | ||
thenType, | ||
signature => | ||
signature.parameters.length >= 2 && | ||
isFunctionParam(checker, signature.parameters[0], node) && | ||
isFunctionParam(checker, signature.parameters[1], node), | ||
) | ||
) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
function hasMatchingSignature( | ||
type: ts.Type, | ||
matcher: (signature: ts.Signature) => boolean, | ||
): boolean { | ||
for (const t of tsutils.unionTypeParts(type)) { | ||
if (t.getCallSignatures().some(matcher)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function isFunctionParam( | ||
checker: ts.TypeChecker, | ||
param: ts.Symbol, | ||
node: ts.Node, | ||
): boolean { | ||
let type: ts.Type | undefined = checker.getApparentType( | ||
checker.getTypeOfSymbolAtLocation(param, node), | ||
); | ||
for (const t of tsutils.unionTypeParts(type)) { | ||
if (t.getCallSignatures().length !== 0) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
function isPromiseCatchCallWithHandler(expression: ts.CallExpression): boolean { | ||
return ( | ||
tsutils.isPropertyAccessExpression(expression.expression) && | ||
expression.expression.name.text === 'catch' && | ||
expression.arguments.length >= 1 | ||
); | ||
} | ||
|
||
function isPromiseThenCallWithRejectionHandler( | ||
expression: ts.CallExpression, | ||
): boolean { | ||
return ( | ||
tsutils.isPropertyAccessExpression(expression.expression) && | ||
expression.expression.name.text === 'then' && | ||
expression.arguments.length >= 2 | ||
); | ||
} |
Oops, something went wrong.