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): [no-floating-promises] add an 'allowForKnownSafePromises' option#8502
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.
Changes fromall commits
476f867
7bbba26
38ea99f
f15a0bb
0019678
d231660
379a2d0
035484b
6fe2efa
2269319
2f7ed24
27b463f
6e221be
4400e3b
856a1a0
9bd2105
b7ac1fd
0fbc581
c50ae5b
af882dd
4c51ce3
52019b2
e5ffd3d
345a65d
304a53c
497e8f7
842bd2d
6b01c41
f10ae89
4a7d516
aa94a28
171701b
ecd31e3
a7fc9a2
0ab7060
441e467
7422cf5
cbbda8a
File 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 |
---|---|---|
@@ -115,6 +115,51 @@ await (async function () { | ||
})(); | ||
``` | ||
### `allowForKnownSafePromises` | ||
This option allows marking specific types as "safe" to be floating. For example, you may need to do this in the case of libraries whose APIs return Promises whose rejections are safely handled by the library. | ||
Each item must be one of: | ||
kirkwaiblinger marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
- A type defined in a file (`{from: "file", name: "Foo", path: "src/foo-file.ts"}` with `path` being an optional path relative to the project root directory) | ||
- A type from the default library (`{from: "lib", name: "PromiseLike"}`) | ||
- A type from a package (`{from: "package", name: "Foo", package: "foo-lib"}`, this also works for types defined in a typings package). | ||
Examples of code for this rule with: | ||
```json | ||
{ | ||
"allowForKnownSafePromises": [ | ||
{ "from": "file", "name": "SafePromise" }, | ||
{ "from": "lib", "name": "PromiseLike" }, | ||
{ "from": "package", "name": "Bar", "package": "bar-lib" } | ||
] | ||
} | ||
``` | ||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' | ||
type UnsafePromise = Promise<number> & { __linterBrands?: string }; | ||
let promise: UnsafePromise = Promise.resolve(2); | ||
promise; | ||
promise.finally(); | ||
``` | ||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' | ||
type SafePromise = Promise<number> & { __linterBrands?: string }; // promises can be marked as safe by using branded types | ||
let promise: SafePromise = Promise.resolve(2); | ||
promise; | ||
promise.finally(); | ||
``` | ||
</TabItem> | ||
</Tabs> | ||
## When Not To Use It | ||
This rule can be difficult to enable on large existing projects that set up many floating Promises. | ||
arkapratimc marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page.
JoshuaKGoldberg 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 |
---|---|---|
@@ -1,19 +1,28 @@ | ||
import type { | ||
ParserServicesWithTypeInformation, | ||
TSESLint, | ||
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 type { TypeOrValueSpecifier } from '../util'; | ||
import { | ||
createRule, | ||
getOperatorPrecedence, | ||
getParserServices, | ||
OperatorPrecedence, | ||
readonlynessOptionsDefaults, | ||
readonlynessOptionsSchema, | ||
typeMatchesSpecifier, | ||
} from '../util'; | ||
type Options = [ | ||
{ | ||
ignoreVoid?: boolean; | ||
ignoreIIFE?: boolean; | ||
allowForKnownSafePromises?: TypeOrValueSpecifier[]; | ||
}, | ||
]; | ||
@@ -79,6 +88,7 @@ export default createRule<Options, MessageId>({ | ||
'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).', | ||
type: 'boolean', | ||
}, | ||
allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow, | ||
}, | ||
JoshuaKGoldberg marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
additionalProperties: false, | ||
}, | ||
@@ -89,12 +99,16 @@ export default createRule<Options, MessageId>({ | ||
{ | ||
ignoreVoid: true, | ||
ignoreIIFE: false, | ||
allowForKnownSafePromises: readonlynessOptionsDefaults.allow, | ||
}, | ||
], | ||
create(context, [options]) { | ||
const services = getParserServices(context); | ||
const checker = services.program.getTypeChecker(); | ||
// TODO: #5439 | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const allowForKnownSafePromises = options.allowForKnownSafePromises!; | ||
arkapratimc marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
return { | ||
ExpressionStatement(node): void { | ||
@@ -253,7 +267,9 @@ export default createRule<Options, MessageId>({ | ||
// Check the type. At this point it can't be unhandled if it isn't a promise | ||
// or array thereof. | ||
if ( | ||
isPromiseArray(services, allowForKnownSafePromises, checker, tsNode) | ||
) { | ||
return { isUnhandled: true, promiseArray: true }; | ||
} | ||
@@ -262,6 +278,28 @@ export default createRule<Options, MessageId>({ | ||
} | ||
if (node.type === AST_NODE_TYPES.CallExpression) { | ||
const member = | ||
node.callee.type === AST_NODE_TYPES.MemberExpression | ||
? node.callee.object | ||
: node; | ||
const calledByThenOrCatch = | ||
(node.callee.type === AST_NODE_TYPES.MemberExpression && | ||
node.callee.property.type === AST_NODE_TYPES.Identifier && | ||
node.callee.property.name === 'catch') || | ||
(node.callee.type === AST_NODE_TYPES.MemberExpression && | ||
node.callee.property.type === AST_NODE_TYPES.Identifier && | ||
node.callee.property.name === 'then'); | ||
if ( | ||
!calledByThenOrCatch && | ||
doesTypeMatchSpecifier( | ||
services, | ||
allowForKnownSafePromises, | ||
services.getTypeAtLocation(member), | ||
) | ||
) { | ||
return { isUnhandled: false }; | ||
} | ||
// If the outer expression is a call, a `.catch()` or `.then()` with | ||
// rejection handler handles the promise. | ||
@@ -291,6 +329,15 @@ export default createRule<Options, MessageId>({ | ||
// All other cases are unhandled. | ||
return { isUnhandled: true }; | ||
} else if (node.type === AST_NODE_TYPES.TaggedTemplateExpression) { | ||
if ( | ||
doesTypeMatchSpecifier( | ||
services, | ||
allowForKnownSafePromises, | ||
services.getTypeAtLocation(node), | ||
) | ||
) { | ||
return { isUnhandled: false }; | ||
} | ||
return { isUnhandled: true }; | ||
} else if (node.type === AST_NODE_TYPES.ConditionalExpression) { | ||
// We must be getting the promise-like value from one of the branches of the | ||
@@ -305,6 +352,15 @@ export default createRule<Options, MessageId>({ | ||
node.type === AST_NODE_TYPES.Identifier || | ||
node.type === AST_NODE_TYPES.NewExpression | ||
) { | ||
if ( | ||
doesTypeMatchSpecifier( | ||
services, | ||
allowForKnownSafePromises, | ||
services.getTypeAtLocation(node), | ||
) | ||
) { | ||
return { isUnhandled: false }; | ||
} | ||
// If it is just a property access chain or a `new` call (e.g. `foo.bar` or | ||
// `new Promise()`), the promise is not handled because it doesn't have the | ||
// necessary then/catch call at the end of the chain. | ||
@@ -325,20 +381,46 @@ export default createRule<Options, MessageId>({ | ||
}, | ||
}); | ||
function doesTypeMatchSpecifier( | ||
services: ParserServicesWithTypeInformation, | ||
kirkwaiblinger marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
options: TypeOrValueSpecifier[], | ||
type: ts.Type, | ||
): boolean { | ||
return options.some(specifier => | ||
typeMatchesSpecifier(type, specifier, services.program), | ||
); | ||
} | ||
function isPromiseArray( | ||
services: ParserServicesWithTypeInformation, | ||
options: TypeOrValueSpecifier[], | ||
checker: ts.TypeChecker, | ||
node: ts.Node, | ||
): boolean { | ||
const type = checker.getTypeAtLocation(node); | ||
for (const ty of tsutils | ||
.unionTypeParts(type) | ||
.map(t => checker.getApparentType(t))) { | ||
if (checker.isArrayType(ty)) { | ||
const arrayType = checker.getTypeArguments(ty)[0]; | ||
if ( | ||
options.length > 0 && | ||
tsutils | ||
.unionTypeParts(arrayType) | ||
.some(type => doesTypeMatchSpecifier(services, options, type)) | ||
) { | ||
return false; | ||
} | ||
if (isPromiseLike(checker, node, arrayType)) { | ||
return true; | ||
} | ||
} | ||
if (checker.isTupleType(ty)) { | ||
for (const tupleElementType of checker.getTypeArguments(ty)) { | ||
if (doesTypeMatchSpecifier(services, options, tupleElementType)) { | ||
return false; | ||
} | ||
if (isPromiseLike(checker, node, tupleElementType)) { | ||
return true; | ||
} | ||
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.