diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index e3f047e6e4f0..c3d051151d6f 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -176,6 +176,44 @@ returnsSafePromise(); +### `allowForKnownSafeCalls` + +This option allows marking specific functions as "safe" to be called to create floating Promises. +For example, you may need to do this in the case of libraries whose APIs may be called without handling the resultant Promises. + +This option takes the same array format as [`allowForKnownSafePromises`](#allowForKnownSafePromises). + +Examples of code for this rule with: + +```json +{ + "allowForKnownSafeCalls": [ + { "from": "file", "name": "safe", "path": "input.ts" } + ] +} +``` + + + + +```ts option='{"allowForKnownSafeCalls":[{"from":"file","name":"safe","path":"input.ts"}]}' +declare function unsafe(...args: unknown[]): Promise; + +unsafe('...', () => {}); +``` + + + + +```ts option='{"allowForKnownSafeCalls":[{"from":"file","name":"safe","path":"input.ts"}]}' skipValidation +declare function safe(...args: unknown[]): Promise; + +safe('...', () => {}); +``` + + + + ## When Not To Use It This rule can be difficult to enable on large existing projects that set up many floating Promises. diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 1ae5e602ae0e..ad25d241be55 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -19,6 +19,7 @@ type Options = [ ignoreVoid?: boolean; ignoreIIFE?: boolean; allowForKnownSafePromises?: TypeOrValueSpecifier[]; + allowForKnownSafeCalls?: TypeOrValueSpecifier[]; }, ]; @@ -85,6 +86,7 @@ export default createRule({ type: 'boolean', }, allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow, + allowForKnownSafeCalls: readonlynessOptionsSchema.properties.allow, }, additionalProperties: false, }, @@ -96,6 +98,7 @@ export default createRule({ ignoreVoid: true, ignoreIIFE: false, allowForKnownSafePromises: readonlynessOptionsDefaults.allow, + allowForKnownSafeCalls: readonlynessOptionsDefaults.allow, }, ], @@ -103,8 +106,10 @@ export default createRule({ const services = getParserServices(context); const checker = services.program.getTypeChecker(); // TODO: #5439 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + /* eslint-disable @typescript-eslint/no-non-null-assertion */ const allowForKnownSafePromises = options.allowForKnownSafePromises!; + const allowForKnownSafeCalls = options.allowForKnownSafeCalls!; + /* eslint-enable @typescript-eslint/no-non-null-assertion */ return { ExpressionStatement(node): void { @@ -118,6 +123,10 @@ export default createRule({ expression = expression.expression; } + if (isKnownSafePromiseReturn(expression)) { + return; + } + const { isUnhandled, nonFunctionHandler, promiseArray } = isUnhandledPromise(checker, expression); @@ -197,6 +206,18 @@ export default createRule({ }, }; + function isKnownSafePromiseReturn(node: TSESTree.Node): boolean { + if (node.type !== AST_NODE_TYPES.CallExpression) { + return false; + } + + const type = services.getTypeAtLocation(node.callee); + + return allowForKnownSafeCalls.some(allowedType => + typeMatchesSpecifier(type, allowedType, services.program), + ); + } + function isHigherPrecedenceThanUnary(node: ts.Node): boolean { const operator = ts.isBinaryExpression(node) ? node.operatorToken.kind diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot index 44e303fc3131..e1f38e71fe3e 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot @@ -108,3 +108,25 @@ function returnsSafePromise(): SafePromise { returnsSafePromise(); " `; + +exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 7`] = ` +"Incorrect +Options: {"allowForKnownSafeCalls":[{"from":"file","name":"safe","path":"input.ts"}]} + +declare function unsafe(...args: unknown[]): Promise; + +unsafe('...', () => {}); +~~~~~~~~~~~~~~~~~~~~~~~~ Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the \`void\` operator. +" +`; + +exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 8`] = ` +"Correct +Options: {"allowForKnownSafeCalls":[{"from":"file","name":"safe","path":"input.ts"}]} + +declare function safe(...args: unknown[]): Promise; + +safe('...', () => {}); +~~~~~~~~~~~~~~~~~~~~~~ Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the \`void\` operator. +" +`; diff --git a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts index e6087e512265..d5d62eeddf56 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -698,6 +698,27 @@ myTag\`abc\`; { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], }, + { + code: ` + declare function it(...args: unknown[]): Promise; + + it('...', () => {}); + `, + options: [ + { + allowForKnownSafeCalls: [ + { + from: 'file', + name: 'it', + // https://github.com/typescript-eslint/typescript-eslint/pull/9234/files#r1626465054 + path: process.env.TYPESCRIPT_ESLINT_PROJECT_SERVICE + ? 'file.ts' + : 'tests/fixtures/file.ts', + }, + ], + }, + ], + }, { code: ` declare const myTag: (strings: TemplateStringsArray) => Promise; @@ -2181,5 +2202,71 @@ myTag\`abc\`; options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], errors: [{ line: 4, messageId: 'floatingVoid' }], }, + { + code: ` + declare function unsafe(...args: unknown[]): Promise; + + unsafe('...', () => {}); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + options: [ + { + allowForKnownSafeCalls: [ + { + from: 'file', + name: 'it', + // https://github.com/typescript-eslint/typescript-eslint/pull/9234/files#r1626465054 + path: process.env.TYPESCRIPT_ESLINT_PROJECT_SERVICE + ? 'file.ts' + : 'tests/fixtures/file.ts', + }, + ], + }, + ], + }, + { + code: ` + declare function it(...args: unknown[]): Promise; + + it('...', () => {}).then(() => {}); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + options: [ + { + allowForKnownSafeCalls: [ + { + from: 'file', + name: 'it', + // https://github.com/typescript-eslint/typescript-eslint/pull/9234/files#r1626465054 + path: process.env.TYPESCRIPT_ESLINT_PROJECT_SERVICE + ? 'file.ts' + : 'tests/fixtures/file.ts', + }, + ], + }, + ], + }, + { + code: ` + declare function it(...args: unknown[]): Promise; + + it('...', () => {}).finally(() => {}); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + options: [ + { + allowForKnownSafeCalls: [ + { + from: 'file', + name: 'it', + // https://github.com/typescript-eslint/typescript-eslint/pull/9234/files#r1626465054 + path: process.env.TYPESCRIPT_ESLINT_PROJECT_SERVICE + ? 'file.ts' + : 'tests/fixtures/file.ts', + }, + ], + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot index a708c7001d5b..b73c2638b92e 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot @@ -8,6 +8,100 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos { "additionalProperties": false, "properties": { + "allowForKnownSafeCalls": { + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "from": { + "enum": ["file"], + "type": "string" + }, + "name": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + ] + }, + "path": { + "type": "string" + } + }, + "required": ["from", "name"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "from": { + "enum": ["lib"], + "type": "string" + }, + "name": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + ] + } + }, + "required": ["from", "name"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "from": { + "enum": ["package"], + "type": "string" + }, + "name": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + ] + }, + "package": { + "type": "string" + } + }, + "required": ["from", "name", "package"], + "type": "object" + } + ] + }, + "type": "array" + }, "allowForKnownSafePromises": { "items": { "oneOf": [ @@ -120,6 +214,23 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { + allowForKnownSafeCalls?: ( + | { + from: 'file'; + name: [string, ...string[]] | string; + path?: string; + } + | { + from: 'lib'; + name: [string, ...string[]] | string; + } + | { + from: 'package'; + name: [string, ...string[]] | string; + package: string; + } + | string + )[]; allowForKnownSafePromises?: ( | { from: 'file';