From 476f867d9e041d9e1a39071bb2722e9acfc2fc27 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Sat, 17 Feb 2024 21:05:43 +0530 Subject: [PATCH 01/35] feat: [no-floating-promises] add an 'allowForKnownSafePromises' option fixes: #7008 --- .../docs/rules/no-floating-promises.md | 26 +++++++++ .../src/rules/no-floating-promises.ts | 53 +++++++++++++++++++ .../tests/rules/no-floating-promises.test.ts | 13 +++++ .../no-floating-promises.shot | 37 +++++++++++++ .../type-utils/src/TypeOrValueSpecifier.ts | 15 ++++-- .../tests/TypeOrValueSpecifier.test.ts | 8 +++ packages/type-utils/typings/typescript.d.ts | 2 + 7 files changed, 150 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.md b/packages/eslint-plugin/docs/rules/no-floating-promises.md index da46d7f98d18..742e2e07f8bf 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.md +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.md @@ -109,6 +109,32 @@ await (async function () { })(); ``` +### `allowForKnownSafePromises` + +This allows you to skip checking of the promise returning functions, which are supposed to be unhandled and unreturned, documented by an external package/module. + +Each item must be a : + +- A function from a package (`{from: "package", name: "test", package: "node:test"}`) + +Examples of code for this rule with: + +```json +{ + "allowForKnownSafePromises": [ + { "from": "package", "name": "fetch", "package": "foo" } + ] +} +``` + + + +#### ❌ Incorrect + +```ts option='{"allowForKnownSafePromises":[{"from":"package","name":"fetch","package":"foo"}]}' +fetch('https://typescript-eslint.io/'); +``` + ## 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 b3ac65296992..0dad53d6cf62 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -8,12 +8,18 @@ import { getOperatorPrecedence, getParserServices, OperatorPrecedence, + typeMatchesSpecifier, } from '../util'; type Options = [ { ignoreVoid?: boolean; ignoreIIFE?: boolean; + allowForKnownSafePromises?: { + from: 'package'; + name: string[] | string; + package: string; + }[]; }, ]; @@ -79,6 +85,40 @@ export default createRule({ 'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).', type: 'boolean', }, + allowForKnownSafePromises: { + description: + 'The list of promises which should be floating as per an external package/module.', + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + from: { + type: 'string', + enum: ['package'], + }, + name: { + oneOf: [ + { + type: 'string', + }, + { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + }, + }, + ], + }, + package: { + type: 'string', + }, + }, + required: ['from', 'name', 'package'], + }, + }, }, additionalProperties: false, }, @@ -89,6 +129,7 @@ export default createRule({ { ignoreVoid: true, ignoreIIFE: false, + allowForKnownSafePromises: [], }, ], @@ -262,6 +303,18 @@ export default createRule({ } if (node.type === AST_NODE_TYPES.CallExpression) { + if ( + options.allowForKnownSafePromises?.some(specifier => + typeMatchesSpecifier( + services.getTypeAtLocation(node.callee), + specifier, + services.program, + ), + ) + ) { + return { isUnhandled: false }; + } + // If the outer expression is a call, a `.catch()` or `.then()` with // rejection handler handles the promise. 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 434c9735a46d..1934460b7e8a 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -1806,5 +1806,18 @@ cursed(); `, errors: [{ line: 3, messageId: 'floatingPromiseArrayVoid' }], }, + { + code: ` + fetch('https://typescript-eslint.io/'); + `, + options: [ + { + allowForKnownSafePromises: [ + { from: 'package', name: 'fetch', package: 'foo' }, + ], + }, + ], + errors: [{ line: 2, messageId: 'floatingVoid' }], + }, ], }); 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 ac6669f651b4..a9f89c70b2f7 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,38 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos { "additionalProperties": false, "properties": { + "allowForKnownSafePromises": { + "items": { + "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" + }, "ignoreIIFE": { "description": "Whether to ignore async IIFEs (Immediately Invoked Function Expressions).", "type": "boolean" @@ -26,6 +58,11 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { + allowForKnownSafePromises?: { + from: 'package'; + name: [string, ...string[]] | string; + package: string; + }[]; /** Whether to ignore async IIFEs (Immediately Invoked Function Expressions). */ ignoreIIFE?: boolean; /** Whether to ignore \`void\` expressions. */ diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 16958370f57b..f7e7a1623ab1 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -160,12 +160,19 @@ function typeDeclaredInPackage( // Handle scoped packages - if the name starts with @, remove it and replace / with __ const typesPackageName = packageName.replace(/^@([^/]+)\//, '$1__'); - const matcher = new RegExp(`${packageName}|${typesPackageName}`); + let matcher = new RegExp(`${packageName}|${typesPackageName}`); + + if (packageName.startsWith('node:')) { + matcher = new RegExp(packageName.substring(5)); + } return declarationFiles.some(declaration => { - const packageIdName = program.sourceFileToPackageName.get(declaration.path); + const packageIdName = + program.resolvedModules.has(declaration.path) || + program.sourceFileToPackageName.has(declaration.path); + return ( - packageIdName !== undefined && - matcher.test(packageIdName) && + packageIdName && + matcher.test(declaration.path) && program.isSourceFileFromExternalLibrary(declaration) ); }); diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 8ddca54d3b34..f405f35e4ed6 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -337,6 +337,14 @@ describe('TypeOrValueSpecifier', () => { package: '@babel/code-frame', }, ], + [ + 'import test from "node:test"; type Test = typeof test;', + { + from: 'package', + name: 'test', + package: 'node:test', + }, + ], ])('matches a matching package specifier: %s', runTestPositive); it.each<[string, TypeOrValueSpecifier]>([ diff --git a/packages/type-utils/typings/typescript.d.ts b/packages/type-utils/typings/typescript.d.ts index 335b12b86fe5..ee3b516447ad 100644 --- a/packages/type-utils/typings/typescript.d.ts +++ b/packages/type-utils/typings/typescript.d.ts @@ -36,6 +36,8 @@ declare module 'typescript' { * Maps from a SourceFile's `.path` to the name of the package it was imported with. */ readonly sourceFileToPackageName: ReadonlyMap; + + readonly resolvedModules: ReadonlyMap; } interface SourceFile extends Declaration, LocalsContainer { From 7bbba2667f219d01f18bb789da9793b7379999f2 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Sun, 18 Feb 2024 22:49:35 +0530 Subject: [PATCH 02/35] chore: snapshot errors --- .../tests/schema-snapshots/no-floating-promises.shot | 2 ++ 1 file changed, 2 insertions(+) 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 a9f89c70b2f7..b5c64e3a7d02 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot @@ -9,6 +9,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "additionalProperties": false, "properties": { "allowForKnownSafePromises": { + "description": "The list of promises which should be floating as per an external package/module.", "items": { "additionalProperties": false, "properties": { @@ -58,6 +59,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { + /** The list of promises which should be floating as per an external package/module. */ allowForKnownSafePromises?: { from: 'package'; name: [string, ...string[]] | string; From 38ea99f93c2eb82fa0e69cd1c9bec9c064c82528 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Mon, 19 Feb 2024 23:24:15 +0530 Subject: [PATCH 03/35] chore: add a test --- .../tests/rules/no-floating-promises.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 1934460b7e8a..02429eb1c070 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -504,6 +504,19 @@ void promiseArray; ['I', 'am', 'just', 'an', 'array']; `, }, + { + code: ` +import test from 'node:test'; +test('Hi', () => Promise.reject(5)); + `, + options: [ + { + allowForKnownSafePromises: [ + { from: 'package', name: 'test', package: 'node:test' }, + ], + }, + ], + }, ], invalid: [ From f15a0bb4f0f7ffdc50f326b9316cab43ca61891b Mon Sep 17 00:00:00 2001 From: arka1002 Date: Mon, 19 Feb 2024 23:39:38 +0530 Subject: [PATCH 04/35] chore: add another test --- .../tests/rules/no-floating-promises.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 02429eb1c070..cd057c2a7d15 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -1832,5 +1832,19 @@ cursed(); ], errors: [{ line: 2, messageId: 'floatingVoid' }], }, + { + code: ` +import test from 'node:test'; +test('Hi', () => Promise.reject(5)); + `, + options: [ + { + allowForKnownSafePromises: [ + { from: 'package', name: 'url', package: 'node:url' }, + ], + }, + ], + errors: [{ line: 3, messageId: 'floatingVoid' }], + }, ], }); From 00196782249545c8abdc0f7eb840e5f42e3ead0d Mon Sep 17 00:00:00 2001 From: arka1002 Date: Wed, 21 Feb 2024 21:39:57 +0530 Subject: [PATCH 05/35] chore: rewrite --- .../src/rules/no-floating-promises.ts | 107 ++++++++------- .../tests/rules/no-floating-promises.test.ts | 126 ++++++++++++++---- .../prefer-readonly-parameter-types.test.ts | 13 ++ .../no-floating-promises.shot | 126 ++++++++++++++---- .../type-utils/src/TypeOrValueSpecifier.ts | 8 +- .../tests/TypeOrValueSpecifier.test.ts | 5 + 6 files changed, 280 insertions(+), 105 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 0dad53d6cf62..ae7192f9effd 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -1,4 +1,8 @@ -import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type { + TSESLint, + TSESTree, + ParserServicesWithTypeInformation, +} from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -8,18 +12,17 @@ import { getOperatorPrecedence, getParserServices, OperatorPrecedence, + readonlynessOptionsDefaults, + readonlynessOptionsSchema, typeMatchesSpecifier, + TypeOrValueSpecifier, } from '../util'; type Options = [ { ignoreVoid?: boolean; ignoreIIFE?: boolean; - allowForKnownSafePromises?: { - from: 'package'; - name: string[] | string; - package: string; - }[]; + allowForKnownSafePromises?: TypeOrValueSpecifier[]; }, ]; @@ -85,40 +88,7 @@ export default createRule({ 'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).', type: 'boolean', }, - allowForKnownSafePromises: { - description: - 'The list of promises which should be floating as per an external package/module.', - type: 'array', - items: { - type: 'object', - additionalProperties: false, - properties: { - from: { - type: 'string', - enum: ['package'], - }, - name: { - oneOf: [ - { - type: 'string', - }, - { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - }, - }, - ], - }, - package: { - type: 'string', - }, - }, - required: ['from', 'name', 'package'], - }, - }, + allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow, }, additionalProperties: false, }, @@ -129,7 +99,7 @@ export default createRule({ { ignoreVoid: true, ignoreIIFE: false, - allowForKnownSafePromises: [], + allowForKnownSafePromises: readonlynessOptionsDefaults.allow, }, ], @@ -303,13 +273,15 @@ export default createRule({ } if (node.type === AST_NODE_TYPES.CallExpression) { + const member = + node.callee.type === AST_NODE_TYPES.MemberExpression + ? node.callee.object + : node; if ( - options.allowForKnownSafePromises?.some(specifier => - typeMatchesSpecifier( - services.getTypeAtLocation(node.callee), - specifier, - services.program, - ), + doesTypeMatchesSpecifier( + services, + member, + options.allowForKnownSafePromises, ) ) { return { isUnhandled: false }; @@ -356,6 +328,15 @@ export default createRule({ node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.NewExpression ) { + if ( + doesTypeMatchesSpecifier( + services, + node, + options.allowForKnownSafePromises, + ) + ) { + 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. @@ -366,6 +347,20 @@ export default createRule({ return leftResult; } return isUnhandledPromise(checker, node.right); + } else if ( + node.type === AST_NODE_TYPES.TSAsExpression || + node.type === AST_NODE_TYPES.TSTypeAssertion + ) { + if ( + doesTypeMatchesSpecifier( + services, + node, + options.allowForKnownSafePromises, + ) + ) { + return { isUnhandled: false }; + } + return isUnhandledPromise(checker, node.expression); } // We conservatively return false for all other types of expressions because @@ -376,6 +371,24 @@ export default createRule({ }, }); +function doesTypeMatchesSpecifier( + services: ParserServicesWithTypeInformation, + node: TSESTree.Node, + options: TypeOrValueSpecifier[] | undefined, +) { + if (Array.isArray(options) && options.length > 0) { + const result = options.some(specifier => + typeMatchesSpecifier( + services.getTypeAtLocation(node), + specifier, + services.program, + ), + ); + return result; + } + return false; +} + function isPromiseArray(checker: ts.TypeChecker, node: ts.Node): boolean { const type = checker.getTypeAtLocation(node); for (const ty of tsutils 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 cd057c2a7d15..0114212fb2fd 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -506,17 +506,76 @@ void promiseArray; }, { code: ` -import test from 'node:test'; -test('Hi', () => Promise.reject(5)); +type Foo = Promise & { hey?: string }; +let guzz: Foo = Promise.resolve(5); +guzz; +guzz.then(() => {}); +guzz.catch(); +guzz.finally(); +0 ? guzz.catch() : 2; +null ?? guzz.catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); +guzz as Foo; +(guzz as Foo).then(() => {}); +(guzz as Foo).catch(); +(guzz as Foo).finally(); +0 ? (guzz as Foo).catch() : 2; +null ?? (guzz as Foo).catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); +guzz() as Foo; +(guzz() as Foo).then(() => {}); +(guzz() as Foo).catch(); +(guzz() as Foo).finally(); +0 ? (guzz() as Foo).catch() : 2; +null ?? (guzz() as Foo).catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: () => Foo = () => Promise.resolve(5); +guzz(); +guzz().then(() => {}); +guzz().catch(); +guzz().finally(); +0 ? guzz().catch() : 2; +null ?? guzz().catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); +(guzz() as Foo).then(() => {}, () => {}); +(guzz() as Foo).catch(() => {}); +0 ? (guzz() as Foo).catch(() => {}) : 2; +null ?? (guzz() as Foo).catch(() => {}); `, - options: [ - { - allowForKnownSafePromises: [ - { from: 'package', name: 'test', package: 'node:test' }, - ], - }, - ], }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); +(guzz as Foo).then(() => {}, () => {}); +(guzz as Foo).catch(() => {}); +0 ? (guzz as Foo).catch(() => {}) : 2; +null ?? (guzz as Foo).catch(() => {}); + `, + } ], invalid: [ @@ -1821,30 +1880,43 @@ cursed(); }, { code: ` - fetch('https://typescript-eslint.io/'); +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); +guzz as Foo; +(guzz as Foo).then((x) => {}); +(guzz as Foo).catch(); +(guzz as Foo).finally(); +0 ? (guzz as Foo).catch() : 2; +null ?? (guzz as Foo).catch(); `, - options: [ - { - allowForKnownSafePromises: [ - { from: 'package', name: 'fetch', package: 'foo' }, - ], - }, + errors: [ + { line: 4, messageId: 'floatingVoid' }, + { line: 5, messageId: 'floatingVoid' }, + { line: 6, messageId: 'floatingVoid' }, + { line: 7, messageId: 'floatingVoid' }, + { line: 8, messageId: 'floatingVoid' }, + { line: 9, messageId: 'floatingVoid' }, ], - errors: [{ line: 2, messageId: 'floatingVoid' }], }, { code: ` -import test from 'node:test'; -test('Hi', () => Promise.reject(5)); +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); +guzz() as Foo; +(guzz() as Foo).then((x) => {}); +(guzz() as Foo).catch(); +(guzz() as Foo).finally(); +0 ? (guzz() as Foo).catch() : 2; +null ?? (guzz() as Foo).catch(); `, - options: [ - { - allowForKnownSafePromises: [ - { from: 'package', name: 'url', package: 'node:url' }, - ], - }, + errors: [ + { line: 4, messageId: 'floatingVoid' }, + { line: 5, messageId: 'floatingVoid' }, + { line: 6, messageId: 'floatingVoid' }, + { line: 7, messageId: 'floatingVoid' }, + { line: 8, messageId: 'floatingVoid' }, + { line: 9, messageId: 'floatingVoid' }, ], - errors: [{ line: 3, messageId: 'floatingVoid' }], - }, + } ], }); diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts index d392a5232fd6..f7e297070635 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts @@ -479,6 +479,19 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { }, ], }, + { + code: ` +import { EventEmitter } from 'node:events'; +const shawl = (foo: EventEmitter) => {}; + `, + options: [ + { + allow: [ + { from: 'package', name: 'EventEmitter', package: 'node:events' }, + ], + }, + ], + }, ], invalid: [ // arrays 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 b5c64e3a7d02..a708c7001d5b 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot @@ -9,35 +9,96 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "additionalProperties": false, "properties": { "allowForKnownSafePromises": { - "description": "The list of promises which should be floating as per an external package/module.", "items": { - "additionalProperties": false, - "properties": { - "from": { - "enum": ["package"], + "oneOf": [ + { "type": "string" }, - "name": { - "oneOf": [ - { + { + "additionalProperties": false, + "properties": { + "from": { + "enum": ["file"], "type": "string" }, - { - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array", - "uniqueItems": true + "name": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + ] + }, + "path": { + "type": "string" } - ] + }, + "required": ["from", "name"], + "type": "object" }, - "package": { - "type": "string" + { + "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" } - }, - "required": ["from", "name", "package"], - "type": "object" + ] }, "type": "array" }, @@ -59,12 +120,23 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { - /** The list of promises which should be floating as per an external package/module. */ - allowForKnownSafePromises?: { - from: 'package'; - name: [string, ...string[]] | string; - package: string; - }[]; + allowForKnownSafePromises?: ( + | { + from: 'file'; + name: [string, ...string[]] | string; + path?: string; + } + | { + from: 'lib'; + name: [string, ...string[]] | string; + } + | { + from: 'package'; + name: [string, ...string[]] | string; + package: string; + } + | string + )[]; /** Whether to ignore async IIFEs (Immediately Invoked Function Expressions). */ ignoreIIFE?: boolean; /** Whether to ignore \`void\` expressions. */ diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index f7e7a1623ab1..edb3b9bd777a 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -162,8 +162,8 @@ function typeDeclaredInPackage( let matcher = new RegExp(`${packageName}|${typesPackageName}`); - if (packageName.startsWith('node:')) { - matcher = new RegExp(packageName.substring(5)); + if (packageName.includes(':')) { + matcher = new RegExp(packageName.substring(0, packageName.indexOf(":"))); } return declarationFiles.some(declaration => { const packageIdName = @@ -207,9 +207,9 @@ export function typeMatchesSpecifier( if (!specifierNameMatches(type, specifier.name)) { return false; } + const symbol = type.getSymbol() || type.aliasSymbol; const declarationFiles = - type - .getSymbol() + symbol ?.getDeclarations() ?.map(declaration => declaration.getSourceFile()) ?? []; switch (specifier.from) { diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index f405f35e4ed6..d8bacf469676 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -202,6 +202,11 @@ describe('TypeOrValueSpecifier', () => { 'type Foo = {prop: string}; type Test = Foo;', { from: 'file', name: 'Foo', path: 'tests/fixtures/file.ts' }, ], + [ + 'type Foo = Promise & {hey?: string}; let Guzz: Foo = Promise.resolve(5); type Test = typeof Guzz;', + // type.getSymbol() doesn't work here, .aliasSymbol does + { from: 'file', name: 'Foo' }, + ], [ 'interface Foo {prop: string}; type Test = Foo;', { From 379a2d0077d38f719cad5394fbfb5fba484a2fa0 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Tue, 19 Mar 2024 19:08:13 +0530 Subject: [PATCH 06/35] chore: docs & test --- .../docs/rules/no-floating-promises.mdx | 50 ++++++++++++++++--- .../tests/rules/no-floating-promises.test.ts | 14 +++++- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index 9b3cbb6f239d..333787cf4247 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -117,30 +117,64 @@ await (async function () { ### `allowForKnownSafePromises` -This allows you to skip checking of the promise returning functions, which are supposed to be unhandled and unreturned, documented by an external package/module. +This allows you to skip checking of those promises which are marked with some kind of branded type. The branding of a type can be done either by using a type alias or an interface. -Each item must be a : +Each item must be one of : -- A function from a package (`{from: "package", name: "test", package: "node:test"}`) +- 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). + +Additionally, a type may be defined just as a simple string, which then matches the type independently of its origin. Examples of code for this rule with: ```json { "allowForKnownSafePromises": [ - { "from": "package", "name": "fetch", "package": "foo" } + "$", + { "from": "file", "name": "Foo" }, + { "from": "file", "name": "Shaw" }, + { "from": "lib", "name": "PromiseLike" }, + { "from": "package", "name": "Bar", "package": "bar-lib" } ] } ``` - + + + +```ts option='{"allowForKnownSafePromises":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +type Foo = Promise & { __linterBrands?: string }; // an example of a branded type +let Bazz = Promise.resolve(2); +Bazz; +Bazz.then(() => {}); +Bazz.catch(); +Bazz.finally(); +``` -#### ❌ Incorrect + + -```ts option='{"allowForKnownSafePromises":[{"from":"package","name":"fetch","package":"foo"}]}' -fetch('https://typescript-eslint.io/'); +```ts option='{"allowForKnownSafePromises":["$",{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +type Foo = Promise & { __linterBrands?: string }; // an example of a branded type +let Bazz = Promise.resolve(2); +Bazz as Foo; // then apply the branded type as a type assertion +(Bazz as Foo).then(() => {}); +(Bazz as Foo).catch(); +(Bazz as Foo).finally(); + +// it also works in the form of type annotations +type Shaw = Promise & { __linterBrands?: string }; // an example of a branded type +let Guzz: Shaw = Promise.resolve(2); // can be used as type annotation too +Guzz.then(() => {}); +Guzz.catch(); +Guzz.finally(); ``` + + + ## 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/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts index 0114212fb2fd..68d03ce55005 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -558,6 +558,16 @@ null ?? guzz().catch(); }, { code: ` +let guzz = () => Promise.resolve(5); +guzz() as PromiseLike; +(guzz() as PromiseLike).then(() => {}); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'lib', name: 'PromiseLike' }] }, + ], + }, + { + code: ` type Foo = Promise & { hey?: string }; let guzz = () => Promise.resolve(5); (guzz() as Foo).then(() => {}, () => {}); @@ -575,7 +585,7 @@ let guzz = Promise.resolve(5); 0 ? (guzz as Foo).catch(() => {}) : 2; null ?? (guzz as Foo).catch(() => {}); `, - } + }, ], invalid: [ @@ -1917,6 +1927,6 @@ null ?? (guzz() as Foo).catch(); { line: 8, messageId: 'floatingVoid' }, { line: 9, messageId: 'floatingVoid' }, ], - } + }, ], }); From 035484bd9bccf60ec290167919a277ceb0348cad Mon Sep 17 00:00:00 2001 From: arka1002 Date: Tue, 19 Mar 2024 19:16:41 +0530 Subject: [PATCH 07/35] chore: add jsdoc comment --- .../src/rules/no-floating-promises.ts | 13 ++++++++++--- .../tests/rules/no-floating-promises.test.ts | 14 ++++++++++---- packages/type-utils/src/TypeOrValueSpecifier.ts | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index ae7192f9effd..dd502b27e73a 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -1,12 +1,13 @@ import type { + ParserServicesWithTypeInformation, TSESLint, TSESTree, - ParserServicesWithTypeInformation, } 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, @@ -15,7 +16,6 @@ import { readonlynessOptionsDefaults, readonlynessOptionsSchema, typeMatchesSpecifier, - TypeOrValueSpecifier, } from '../util'; type Options = [ @@ -371,11 +371,18 @@ export default createRule({ }, }); +/** + * It checks whether a node's type matches one of the types listed in the `allowForKnownSafePromises` config + * @param services services variable passed from context function to check the type of a node + * @param node the node whose type is to be calculated to know whether it is a safe promise or not + * @param options The config object of `allowForKnownSafePromises` + * @returns `true` if the type matches, `false` if it isnt + */ function doesTypeMatchesSpecifier( services: ParserServicesWithTypeInformation, node: TSESTree.Node, options: TypeOrValueSpecifier[] | undefined, -) { +): boolean { if (Array.isArray(options) && options.length > 0) { const result = options.some(specifier => typeMatchesSpecifier( 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 68d03ce55005..4d9b57c9025f 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -570,7 +570,10 @@ guzz() as PromiseLike; code: ` type Foo = Promise & { hey?: string }; let guzz = () => Promise.resolve(5); -(guzz() as Foo).then(() => {}, () => {}); +(guzz() as Foo).then( + () => {}, + () => {}, +); (guzz() as Foo).catch(() => {}); 0 ? (guzz() as Foo).catch(() => {}) : 2; null ?? (guzz() as Foo).catch(() => {}); @@ -580,7 +583,10 @@ null ?? (guzz() as Foo).catch(() => {}); code: ` type Foo = Promise & { hey?: string }; let guzz = Promise.resolve(5); -(guzz as Foo).then(() => {}, () => {}); +(guzz as Foo).then( + () => {}, + () => {}, +); (guzz as Foo).catch(() => {}); 0 ? (guzz as Foo).catch(() => {}) : 2; null ?? (guzz as Foo).catch(() => {}); @@ -1893,7 +1899,7 @@ cursed(); type Foo = Promise & { hey?: string }; let guzz = Promise.resolve(5); guzz as Foo; -(guzz as Foo).then((x) => {}); +(guzz as Foo).then(x => {}); (guzz as Foo).catch(); (guzz as Foo).finally(); 0 ? (guzz as Foo).catch() : 2; @@ -1913,7 +1919,7 @@ null ?? (guzz as Foo).catch(); type Foo = Promise & { hey?: string }; let guzz = () => Promise.resolve(5); guzz() as Foo; -(guzz() as Foo).then((x) => {}); +(guzz() as Foo).then(x => {}); (guzz() as Foo).catch(); (guzz() as Foo).finally(); 0 ? (guzz() as Foo).catch() : 2; diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index edb3b9bd777a..ee13ddc19c8d 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -163,7 +163,7 @@ function typeDeclaredInPackage( let matcher = new RegExp(`${packageName}|${typesPackageName}`); if (packageName.includes(':')) { - matcher = new RegExp(packageName.substring(0, packageName.indexOf(":"))); + matcher = new RegExp(packageName.substring(0, packageName.indexOf(':'))); } return declarationFiles.some(declaration => { const packageIdName = From 6fe2efaefda99a1b4f32978a32e6f3cce62492cb Mon Sep 17 00:00:00 2001 From: arka1002 Date: Tue, 19 Mar 2024 19:46:36 +0530 Subject: [PATCH 08/35] chore: cspell --- packages/eslint-plugin/src/rules/no-floating-promises.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index dd502b27e73a..8c0e67345bbd 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -376,7 +376,7 @@ export default createRule({ * @param services services variable passed from context function to check the type of a node * @param node the node whose type is to be calculated to know whether it is a safe promise or not * @param options The config object of `allowForKnownSafePromises` - * @returns `true` if the type matches, `false` if it isnt + * @returns `true` if the type matches, `false` if it isn't */ function doesTypeMatchesSpecifier( services: ParserServicesWithTypeInformation, From 2269319a9a29f80395195238f4902a65e75d29fb Mon Sep 17 00:00:00 2001 From: arka1002 Date: Tue, 19 Mar 2024 20:18:18 +0530 Subject: [PATCH 09/35] chore: lint --- packages/type-utils/src/TypeOrValueSpecifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index ee13ddc19c8d..6eb83d257cb9 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -207,7 +207,7 @@ export function typeMatchesSpecifier( if (!specifierNameMatches(type, specifier.name)) { return false; } - const symbol = type.getSymbol() || type.aliasSymbol; + const symbol = type.getSymbol() ?? type.aliasSymbol; const declarationFiles = symbol ?.getDeclarations() From 27b463faaac80b7cc8c684bb4b682ead8732c420 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Thu, 4 Apr 2024 17:58:10 +0530 Subject: [PATCH 10/35] chore: address reviews --- .../src/rules/no-floating-promises.ts | 46 +++++++++++++--- .../tests/rules/no-floating-promises.test.ts | 54 +++++++++++++++++++ .../prefer-readonly-parameter-types.test.ts | 13 ----- .../type-utils/src/TypeOrValueSpecifier.ts | 15 ++---- .../tests/TypeOrValueSpecifier.test.ts | 8 --- packages/type-utils/typings/typescript.d.ts | 2 - 6 files changed, 96 insertions(+), 42 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index dd74d0a05728..17ccf43a0043 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -264,7 +264,14 @@ export default createRule({ // Check the type. At this point it can't be unhandled if it isn't a promise // or array thereof. - if (isPromiseArray(checker, tsNode)) { + if ( + isPromiseArray( + services, + options.allowForKnownSafePromises, + checker, + tsNode, + ) + ) { return { isUnhandled: true, promiseArray: true }; } @@ -278,7 +285,7 @@ export default createRule({ ? node.callee.object : node; if ( - doesTypeMatchesSpecifier( + doesTypeMatcheSpecifier( services, member, options.allowForKnownSafePromises, @@ -331,7 +338,7 @@ export default createRule({ node.type === AST_NODE_TYPES.NewExpression ) { if ( - doesTypeMatchesSpecifier( + doesTypeMatcheSpecifier( services, node, options.allowForKnownSafePromises, @@ -354,7 +361,7 @@ export default createRule({ node.type === AST_NODE_TYPES.TSTypeAssertion ) { if ( - doesTypeMatchesSpecifier( + doesTypeMatcheSpecifier( services, node, options.allowForKnownSafePromises, @@ -380,15 +387,16 @@ export default createRule({ * @param options The config object of `allowForKnownSafePromises` * @returns `true` if the type matches, `false` if it isn't */ -function doesTypeMatchesSpecifier( +function doesTypeMatcheSpecifier( services: ParserServicesWithTypeInformation, - node: TSESTree.Node, + node: TSESTree.Node | undefined, options: TypeOrValueSpecifier[] | undefined, + type?: ts.Type, ): boolean { if (Array.isArray(options) && options.length > 0) { const result = options.some(specifier => typeMatchesSpecifier( - services.getTypeAtLocation(node), + type ?? services.getTypeAtLocation(node!), specifier, services.program, ), @@ -398,13 +406,25 @@ function doesTypeMatchesSpecifier( return false; } -function isPromiseArray(checker: ts.TypeChecker, node: ts.Node): boolean { +function isPromiseArray( + services: ParserServicesWithTypeInformation, + options: TypeOrValueSpecifier[] | undefined, + 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 (Array.isArray(options) && options.length > 0) { + return !tsutils + .unionTypeParts(arrayType) + .some(type => + doesTypeMatcheSpecifier(services, undefined, options, type), + ); + } if (isPromiseLike(checker, node, arrayType)) { return true; } @@ -412,6 +432,16 @@ function isPromiseArray(checker: ts.TypeChecker, node: ts.Node): boolean { if (checker.isTupleType(ty)) { for (const tupleElementType of checker.getTypeArguments(ty)) { + if ( + doesTypeMatcheSpecifier( + services, + undefined, + options, + tupleElementType, + ) + ) { + return false; + } if (isPromiseLike(checker, node, tupleElementType)) { return true; } 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 3bb43f056194..93c1939cd9e1 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -519,6 +519,21 @@ null ?? guzz.catch(); }, { code: ` +class SafePromise extends Promise {}; +let guzz: SafePromise = Promise.resolve(5); +guzz; +guzz.then(() => {}); +guzz.catch(); +guzz.finally(); +0 ? guzz.catch() : 2; +null ?? guzz.catch(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` type Foo = Promise & { hey?: string }; let guzz = Promise.resolve(5); guzz as Foo; @@ -568,6 +583,22 @@ guzz() as PromiseLike; }, { code: ` +declare const arrayOrPromiseTuple: Foo[]; +arrayOrPromiseTuple; +type Foo = Promise & { hey?: string }; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +declare const arrayOrPromiseTuple: [Foo, 5]; +arrayOrPromiseTuple; +type Foo = Promise & { hey?: string }; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` type Foo = Promise & { hey?: string }; let guzz = () => Promise.resolve(5); (guzz() as Foo).then( @@ -1983,5 +2014,28 @@ null ?? (guzz() as Foo).catch(); { line: 9, messageId: 'floatingVoid' }, ], }, + { + code: ` +class UnsafePromise extends Promise {}; +let guzz: UnsafePromise = Promise.resolve(5); +guzz; +guzz.then(() => {}); +guzz.catch(); +guzz.finally(); +0 ? guzz.catch() : 2; +null ?? guzz.catch(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + errors: [ + { line: 4, messageId: 'floatingVoid' }, + { line: 5, messageId: 'floatingVoid' }, + { line: 6, messageId: 'floatingVoid' }, + { line: 7, messageId: 'floatingVoid' }, + { line: 8, messageId: 'floatingVoid' }, + { line: 9, messageId: 'floatingVoid' }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts index f7e297070635..d392a5232fd6 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts @@ -479,19 +479,6 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { }, ], }, - { - code: ` -import { EventEmitter } from 'node:events'; -const shawl = (foo: EventEmitter) => {}; - `, - options: [ - { - allow: [ - { from: 'package', name: 'EventEmitter', package: 'node:events' }, - ], - }, - ], - }, ], invalid: [ // arrays diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 6eb83d257cb9..cf23b059c802 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -160,19 +160,12 @@ function typeDeclaredInPackage( // Handle scoped packages - if the name starts with @, remove it and replace / with __ const typesPackageName = packageName.replace(/^@([^/]+)\//, '$1__'); - let matcher = new RegExp(`${packageName}|${typesPackageName}`); - - if (packageName.includes(':')) { - matcher = new RegExp(packageName.substring(0, packageName.indexOf(':'))); - } + const matcher = new RegExp(`${packageName}|${typesPackageName}`); return declarationFiles.some(declaration => { - const packageIdName = - program.resolvedModules.has(declaration.path) || - program.sourceFileToPackageName.has(declaration.path); - + const packageIdName = program.sourceFileToPackageName.get(declaration.path); return ( - packageIdName && - matcher.test(declaration.path) && + packageIdName !== undefined && + matcher.test(packageIdName) && program.isSourceFileFromExternalLibrary(declaration) ); }); diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index d8bacf469676..dad38e22a6cf 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -342,14 +342,6 @@ describe('TypeOrValueSpecifier', () => { package: '@babel/code-frame', }, ], - [ - 'import test from "node:test"; type Test = typeof test;', - { - from: 'package', - name: 'test', - package: 'node:test', - }, - ], ])('matches a matching package specifier: %s', runTestPositive); it.each<[string, TypeOrValueSpecifier]>([ diff --git a/packages/type-utils/typings/typescript.d.ts b/packages/type-utils/typings/typescript.d.ts index ee3b516447ad..335b12b86fe5 100644 --- a/packages/type-utils/typings/typescript.d.ts +++ b/packages/type-utils/typings/typescript.d.ts @@ -36,8 +36,6 @@ declare module 'typescript' { * Maps from a SourceFile's `.path` to the name of the package it was imported with. */ readonly sourceFileToPackageName: ReadonlyMap; - - readonly resolvedModules: ReadonlyMap; } interface SourceFile extends Declaration, LocalsContainer { From 6e221bee91364796e8563dfbb2384b00ede8a444 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Thu, 4 Apr 2024 20:30:40 +0530 Subject: [PATCH 11/35] chore: lint errors --- .../src/rules/no-floating-promises.ts | 23 ++++++++++++++----- .../tests/rules/no-floating-promises.test.ts | 4 ++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 17ccf43a0043..60352a684fa6 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -289,6 +289,7 @@ export default createRule({ services, member, options.allowForKnownSafePromises, + undefined, ) ) { return { isUnhandled: false }; @@ -342,6 +343,7 @@ export default createRule({ services, node, options.allowForKnownSafePromises, + undefined, ) ) { return { isUnhandled: false }; @@ -365,6 +367,7 @@ export default createRule({ services, node, options.allowForKnownSafePromises, + undefined, ) ) { return { isUnhandled: false }; @@ -385,21 +388,29 @@ export default createRule({ * @param services services variable passed from context function to check the type of a node * @param node the node whose type is to be calculated to know whether it is a safe promise or not * @param options The config object of `allowForKnownSafePromises` + * @param type The type of the node, either provide this or node, if both are given this param is going to get used, if both are `undefined`, `false` is returned * @returns `true` if the type matches, `false` if it isn't */ function doesTypeMatcheSpecifier( services: ParserServicesWithTypeInformation, node: TSESTree.Node | undefined, options: TypeOrValueSpecifier[] | undefined, - type?: ts.Type, + type: ts.Type | undefined, ): boolean { + let typeOfNode: ts.Type; + if (!type && !node) { + return false; + } else if (!type && node) { + typeOfNode = services.getTypeAtLocation(node); + } else if (!node && type) { + typeOfNode = type; + } else if (node && type) { + typeOfNode = type; + } + if (Array.isArray(options) && options.length > 0) { const result = options.some(specifier => - typeMatchesSpecifier( - type ?? services.getTypeAtLocation(node!), - specifier, - services.program, - ), + typeMatchesSpecifier(typeOfNode, specifier, services.program), ); return result; } 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 93c1939cd9e1..aeb4c92a27f6 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -519,7 +519,7 @@ null ?? guzz.catch(); }, { code: ` -class SafePromise extends Promise {}; +class SafePromise extends Promise {} let guzz: SafePromise = Promise.resolve(5); guzz; guzz.then(() => {}); @@ -2016,7 +2016,7 @@ null ?? (guzz() as Foo).catch(); }, { code: ` -class UnsafePromise extends Promise {}; +class UnsafePromise extends Promise {} let guzz: UnsafePromise = Promise.resolve(5); guzz; guzz.then(() => {}); From 4400e3b61454a7602502b540aaf82883cdd0cce2 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Fri, 5 Apr 2024 21:10:18 +0530 Subject: [PATCH 12/35] chore: remove invalid js/ts code --- .../eslint-plugin/tests/rules/no-floating-promises.test.ts | 5 ----- 1 file changed, 5 deletions(-) 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 aeb4c92a27f6..ab4ba478a9b6 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -486,11 +486,6 @@ declare const promiseArray: Array>; void promiseArray; `, }, - { - code: ` -[Promise.reject(), Promise.reject()].then(() => {}); - `, - }, { // Expressions aren't checked by this rule, so this just becomes an array // of number | undefined, which is fine regardless of the ignoreVoid setting. From 856a1a078c774976aafce398782116010974dd39 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Fri, 5 Apr 2024 21:43:46 +0530 Subject: [PATCH 13/35] chore: add thenables --- .../tests/rules/no-floating-promises.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 ab4ba478a9b6..e9195bdc34fb 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -514,7 +514,7 @@ null ?? guzz.catch(); }, { code: ` -class SafePromise extends Promise {} +class SafePromise extends Promise {}; let guzz: SafePromise = Promise.resolve(5); guzz; guzz.then(() => {}); @@ -529,6 +529,23 @@ null ?? guzz.catch(); }, { code: ` +interface SecureThenable { then(onfulfilled?: ((value: T) => TResult1 | SecureThenable) | undefined | null, onrejected?: ((reason: any) => TResult2 | SecureThenable) | undefined | null): SecureThenable; }; +let guzz: SecureThenable = Promise.resolve(5); +guzz; +guzz.then(() => {}); +guzz.catch(); +guzz.finally(); +0 ? guzz.catch() : 2; +null ?? guzz.catch(); + `, + options: [ + { + allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + }, + ], + }, + { + code: ` type Foo = Promise & { hey?: string }; let guzz = Promise.resolve(5); guzz as Foo; From b7ac1fdf1e724507dbb1875269af049f7ca067a3 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Fri, 5 Apr 2024 22:10:28 +0530 Subject: [PATCH 14/35] chore: fix failures --- .../no-floating-promises.shot | 37 +++++++++++++++++++ .../tests/rules/no-floating-promises.test.ts | 15 +++++++- 2 files changed, 50 insertions(+), 2 deletions(-) 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 0dc935b52782..72c43f118ce1 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 @@ -70,3 +70,40 @@ await (async function () { })(); " `; + +exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 5`] = ` +"Incorrect +Options: {"allowForKnownSafePromises":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} + +type Foo = Promise & { __linterBrands?: string }; // an example of a branded type +let Bazz = Promise.resolve(2); +Bazz; +~~~~~ 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. +Bazz.then(() => {}); +~~~~~~~~~~~~~~~~~~~~ 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. +Bazz.catch(); +~~~~~~~~~~~~~ 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. +Bazz.finally(); +~~~~~~~~~~~~~~~ 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 6`] = ` +"Correct +Options: {"allowForKnownSafePromises":["$",{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} + +type Foo = Promise & { __linterBrands?: string }; // an example of a branded type +let Bazz = Promise.resolve(2); +Bazz as Foo; // then apply the branded type as a type assertion +(Bazz as Foo).then(() => {}); +(Bazz as Foo).catch(); +(Bazz as Foo).finally(); + +// it also works in the form of type annotations +type Shaw = Promise & { __linterBrands?: string }; // an example of a branded type +let Guzz: Shaw = Promise.resolve(2); // can be used as type annotation too +Guzz.then(() => {}); +Guzz.catch(); +Guzz.finally(); +" +`; 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 e9195bdc34fb..7b1c81545a55 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -514,7 +514,7 @@ null ?? guzz.catch(); }, { code: ` -class SafePromise extends Promise {}; +class SafePromise extends Promise {} let guzz: SafePromise = Promise.resolve(5); guzz; guzz.then(() => {}); @@ -529,7 +529,18 @@ null ?? guzz.catch(); }, { code: ` -interface SecureThenable { then(onfulfilled?: ((value: T) => TResult1 | SecureThenable) | undefined | null, onrejected?: ((reason: any) => TResult2 | SecureThenable) | undefined | null): SecureThenable; }; +interface SecureThenable { + then( + onfulfilled?: + | ((value: T) => TResult1 | SecureThenable) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | SecureThenable) + | undefined + | null, + ): SecureThenable; +} let guzz: SecureThenable = Promise.resolve(5); guzz; guzz.then(() => {}); From 0fbc581bda94005f04dda839ea27c9312049c873 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Mon, 8 Apr 2024 11:06:17 +0530 Subject: [PATCH 15/35] chore: re-write tests --- .../tests/rules/no-floating-promises.test.ts | 317 +++++++++++++++++- 1 file changed, 305 insertions(+), 12 deletions(-) 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 7b1c81545a55..7a20ab6f387e 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -499,34 +499,117 @@ void promiseArray; ['I', 'am', 'just', 'an', 'array']; `, }, + // Branded type annotations in `allowForKnownSafePromises` { code: ` type Foo = Promise & { hey?: string }; let guzz: Foo = Promise.resolve(5); guzz; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: Foo = Promise.resolve(5); guzz.then(() => {}); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: Foo = Promise.resolve(5); guzz.catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: Foo = Promise.resolve(5); guzz.finally(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: Foo = Promise.resolve(5); 0 ? guzz.catch() : 2; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: Foo = Promise.resolve(5); null ?? guzz.catch(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, + // extended classes as type annotations in `allowForKnownSafePromises` { code: ` class SafePromise extends Promise {} let guzz: SafePromise = Promise.resolve(5); guzz; + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` +class SafePromise extends Promise {} +let guzz: SafePromise = Promise.resolve(5); guzz.then(() => {}); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` +class SafePromise extends Promise {} +let guzz: SafePromise = Promise.resolve(5); guzz.catch(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` +class SafePromise extends Promise {} +let guzz: SafePromise = Promise.resolve(5); guzz.finally(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` +class SafePromise extends Promise {} +let guzz: SafePromise = Promise.resolve(5); 0 ? guzz.catch() : 2; + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` +class SafePromise extends Promise {} +let guzz: SafePromise = Promise.resolve(5); null ?? guzz.catch(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], }, + // thenable interfaces as type annotations in `allowForKnownSafePromises` { code: ` interface SecureThenable { @@ -543,11 +626,29 @@ interface SecureThenable { } let guzz: SecureThenable = Promise.resolve(5); guzz; + `, + options: [ + { + allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + }, + ], + }, + { + code: ` +interface SecureThenable { + then( + onfulfilled?: + | ((value: T) => TResult1 | SecureThenable) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | SecureThenable) + | undefined + | null, + ): SecureThenable; +} +let guzz: SecureThenable = Promise.resolve(5); guzz.then(() => {}); -guzz.catch(); -guzz.finally(); -0 ? guzz.catch() : 2; -null ?? guzz.catch(); `, options: [ { @@ -557,43 +658,198 @@ null ?? guzz.catch(); }, { code: ` +interface SecureThenable { + then( + onfulfilled?: + | ((value: T) => TResult1 | SecureThenable) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | SecureThenable) + | undefined + | null, + ): SecureThenable; +} +let guzz: SecureThenable = Promise.resolve(5); +0 ? guzz.then(() => {}) : 2; + `, + options: [ + { + allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + }, + ], + }, + { + code: ` +interface SecureThenable { + then( + onfulfilled?: + | ((value: T) => TResult1 | SecureThenable) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | SecureThenable) + | undefined + | null, + ): SecureThenable; +} +let guzz: SecureThenable = Promise.resolve(5); +null ?? guzz.then(() => {}); + `, + options: [ + { + allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + }, + ], + }, + // branded type annotations on promise returning functions (or async functions) in `allowForKnownSafePromises` + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: () => Foo = () => Promise.resolve(5); +guzz(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: () => Foo = () => Promise.resolve(5); +guzz().then(() => {}); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: () => Foo = () => Promise.resolve(5); +guzz().catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: () => Foo = () => Promise.resolve(5); +guzz().finally(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: () => Foo = () => Promise.resolve(5); +0 ? guzz().catch() : 2; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: () => Foo = () => Promise.resolve(5); +null ?? guzz().catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + // type assertions of branded types in `allowForKnownSafePromises` + { + code: ` type Foo = Promise & { hey?: string }; let guzz = Promise.resolve(5); guzz as Foo; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); (guzz as Foo).then(() => {}); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); (guzz as Foo).catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); (guzz as Foo).finally(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); 0 ? (guzz as Foo).catch() : 2; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); null ?? (guzz as Foo).catch(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, + // type assertions of branded types on promise returning functions (or async functions) in `allowForKnownSafePromises` { code: ` type Foo = Promise & { hey?: string }; let guzz = () => Promise.resolve(5); guzz() as Foo; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); (guzz() as Foo).then(() => {}); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); (guzz() as Foo).catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); (guzz() as Foo).finally(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); 0 ? (guzz() as Foo).catch() : 2; -null ?? (guzz() as Foo).catch(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, { code: ` type Foo = Promise & { hey?: string }; -let guzz: () => Foo = () => Promise.resolve(5); -guzz(); -guzz().then(() => {}); -guzz().catch(); -guzz().finally(); -0 ? guzz().catch() : 2; -null ?? guzz().catch(); +let guzz = () => Promise.resolve(5); +null ?? (guzz() as Foo).catch(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, + // type from es5.d.ts using `allowForKnownSafePromises` { code: ` let guzz = () => Promise.resolve(5); @@ -604,6 +860,7 @@ guzz() as PromiseLike; { allowForKnownSafePromises: [{ from: 'lib', name: 'PromiseLike' }] }, ], }, + // promises in array using `allowForKnownSafePromises` { code: ` declare const arrayOrPromiseTuple: Foo[]; @@ -628,8 +885,26 @@ let guzz = () => Promise.resolve(5); () => {}, () => {}, ); + `, + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); (guzz() as Foo).catch(() => {}); + `, + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); 0 ? (guzz() as Foo).catch(() => {}) : 2; + `, + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); null ?? (guzz() as Foo).catch(() => {}); `, }, @@ -641,8 +916,26 @@ let guzz = Promise.resolve(5); () => {}, () => {}, ); + `, + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); (guzz as Foo).catch(() => {}); + `, + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); 0 ? (guzz as Foo).catch(() => {}) : 2; + `, + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); null ?? (guzz as Foo).catch(() => {}); `, }, From c50ae5b09be20b21d8059e9adb21119fe1544b6f Mon Sep 17 00:00:00 2001 From: arka1002 Date: Mon, 8 Apr 2024 16:39:51 +0530 Subject: [PATCH 16/35] chore: more tests --- .../tests/rules/no-floating-promises.test.ts | 149 +++++++++++++++--- 1 file changed, 124 insertions(+), 25 deletions(-) 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 7a20ab6f387e..948f56a70eb7 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -2295,63 +2295,162 @@ cursed(); type Foo = Promise & { hey?: string }; let guzz = Promise.resolve(5); guzz as Foo; -(guzz as Foo).then(x => {}); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); +(guzz as Foo).then(() => {}); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); (guzz as Foo).catch(); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); (guzz as Foo).finally(); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); 0 ? (guzz as Foo).catch() : 2; + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = Promise.resolve(5); null ?? (guzz as Foo).catch(); `, - errors: [ - { line: 4, messageId: 'floatingVoid' }, - { line: 5, messageId: 'floatingVoid' }, - { line: 6, messageId: 'floatingVoid' }, - { line: 7, messageId: 'floatingVoid' }, - { line: 8, messageId: 'floatingVoid' }, - { line: 9, messageId: 'floatingVoid' }, - ], + errors: [{ line: 4, messageId: 'floatingVoid' }], }, { code: ` type Foo = Promise & { hey?: string }; let guzz = () => Promise.resolve(5); guzz() as Foo; + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); (guzz() as Foo).then(x => {}); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); (guzz() as Foo).catch(); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); (guzz() as Foo).finally(); + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); 0 ? (guzz() as Foo).catch() : 2; + `, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz = () => Promise.resolve(5); null ?? (guzz() as Foo).catch(); `, - errors: [ - { line: 4, messageId: 'floatingVoid' }, - { line: 5, messageId: 'floatingVoid' }, - { line: 6, messageId: 'floatingVoid' }, - { line: 7, messageId: 'floatingVoid' }, - { line: 8, messageId: 'floatingVoid' }, - { line: 9, messageId: 'floatingVoid' }, - ], + errors: [{ line: 4, messageId: 'floatingVoid' }], }, { code: ` class UnsafePromise extends Promise {} let guzz: UnsafePromise = Promise.resolve(5); guzz; + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +class UnsafePromise extends Promise {} +let guzz: UnsafePromise = Promise.resolve(5); guzz.then(() => {}); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +class UnsafePromise extends Promise {} +let guzz: UnsafePromise = Promise.resolve(5); guzz.catch(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +class UnsafePromise extends Promise {} +let guzz: UnsafePromise = Promise.resolve(5); guzz.finally(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +class UnsafePromise extends Promise {} +let guzz: UnsafePromise = Promise.resolve(5); 0 ? guzz.catch() : 2; -null ?? guzz.catch(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [ - { line: 4, messageId: 'floatingVoid' }, - { line: 5, messageId: 'floatingVoid' }, - { line: 6, messageId: 'floatingVoid' }, - { line: 7, messageId: 'floatingVoid' }, - { line: 8, messageId: 'floatingVoid' }, - { line: 9, messageId: 'floatingVoid' }, + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, + { + code: ` +class UnsafePromise extends Promise {} +let guzz: UnsafePromise = Promise.resolve(5); +null ?? guzz.catch(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], + errors: [{ line: 4, messageId: 'floatingVoid' }], }, ], }); From af882ddc5fc7293ea1e3ed030a1d9da4c850d361 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Mon, 8 Apr 2024 17:45:58 +0530 Subject: [PATCH 17/35] chore: fix schemas --- .../docs/rules/no-floating-promises.mdx | 5 +- .../src/rules/no-floating-promises.ts | 12 +-- .../no-floating-promises.shot | 4 +- .../no-floating-promises.shot | 4 - .../type-utils/src/TypeOrValueSpecifier.ts | 89 +++++++++++++++++++ 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index ec4e5687c20f..1d1284ea0796 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -132,7 +132,6 @@ Examples of code for this rule with: ```json { "allowForKnownSafePromises": [ - "$", { "from": "file", "name": "Foo" }, { "from": "file", "name": "Shaw" }, { "from": "lib", "name": "PromiseLike" }, @@ -144,7 +143,7 @@ Examples of code for this rule with: -```ts option='{"allowForKnownSafePromises":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' type Foo = Promise & { __linterBrands?: string }; // an example of a branded type let Bazz = Promise.resolve(2); Bazz; @@ -156,7 +155,7 @@ Bazz.finally(); -```ts option='{"allowForKnownSafePromises":["$",{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' type Foo = Promise & { __linterBrands?: string }; // an example of a branded type let Bazz = Promise.resolve(2); Bazz as Foo; // then apply the branded type as a type assertion diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 60352a684fa6..55930fdc52e5 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -13,16 +13,15 @@ import { getOperatorPrecedence, getParserServices, OperatorPrecedence, - readonlynessOptionsDefaults, - readonlynessOptionsSchema, typeMatchesSpecifier, + typeOrValueSpecifierSchema_v2, } from '../util'; type Options = [ { ignoreVoid?: boolean; ignoreIIFE?: boolean; - allowForKnownSafePromises?: TypeOrValueSpecifier[]; + allowForKnownSafePromises?: Exclude[]; }, ]; @@ -88,7 +87,10 @@ export default createRule({ 'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).', type: 'boolean', }, - allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow, + allowForKnownSafePromises: { + type: 'array', + items: typeOrValueSpecifierSchema_v2, + }, }, additionalProperties: false, }, @@ -99,7 +101,7 @@ export default createRule({ { ignoreVoid: true, ignoreIIFE: false, - allowForKnownSafePromises: readonlynessOptionsDefaults.allow, + allowForKnownSafePromises: [], }, ], 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 72c43f118ce1..8f1ebb9ccb5e 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 @@ -73,7 +73,7 @@ await (async function () { exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 5`] = ` "Incorrect -Options: {"allowForKnownSafePromises":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} +Options: {"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} type Foo = Promise & { __linterBrands?: string }; // an example of a branded type let Bazz = Promise.resolve(2); @@ -90,7 +90,7 @@ Bazz.finally(); exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 6`] = ` "Correct -Options: {"allowForKnownSafePromises":["$",{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} +Options: {"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} type Foo = Promise & { __linterBrands?: string }; // an example of a branded type let Bazz = Promise.resolve(2); 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..2618c8666705 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot @@ -11,9 +11,6 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "allowForKnownSafePromises": { "items": { "oneOf": [ - { - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -135,7 +132,6 @@ type Options = [ name: [string, ...string[]] | string; package: string; } - | string )[]; /** Whether to ignore async IIFEs (Immediately Invoked Function Expressions). */ ignoreIIFE?: boolean; diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index cf23b059c802..cd5f5bea6b04 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -119,6 +119,95 @@ export const typeOrValueSpecifierSchema: JSONSchema4 = { ], }; +export const typeOrValueSpecifierSchema_v2: JSONSchema4 = { + oneOf: [ + { + type: 'object', + additionalProperties: false, + properties: { + from: { + type: 'string', + enum: ['file'], + }, + name: { + oneOf: [ + { + type: 'string', + }, + { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + }, + }, + ], + }, + path: { + type: 'string', + }, + }, + required: ['from', 'name'], + }, + { + type: 'object', + additionalProperties: false, + properties: { + from: { + type: 'string', + enum: ['lib'], + }, + name: { + oneOf: [ + { + type: 'string', + }, + { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + }, + }, + ], + }, + }, + required: ['from', 'name'], + }, + { + type: 'object', + additionalProperties: false, + properties: { + from: { + type: 'string', + enum: ['package'], + }, + name: { + oneOf: [ + { + type: 'string', + }, + { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + }, + }, + ], + }, + package: { + type: 'string', + }, + }, + required: ['from', 'name', 'package'], + }, + ], +}; + function specifierNameMatches(type: ts.Type, name: string[] | string): boolean { if (typeof name === 'string') { name = [name]; From 4c51ce32fd2ce9380d6860b17d478cbaa6d809df Mon Sep 17 00:00:00 2001 From: arka1002 Date: Mon, 8 Apr 2024 18:24:45 +0530 Subject: [PATCH 18/35] chore: typo --- packages/eslint-plugin/docs/rules/no-floating-promises.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index 1d1284ea0796..433d97143595 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -125,8 +125,6 @@ Each item must be one of : - 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). -Additionally, a type may be defined just as a simple string, which then matches the type independently of its origin. - Examples of code for this rule with: ```json From 52019b2990c668af1adfa0b7be12537db96d3df7 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Tue, 9 Apr 2024 13:27:50 +0530 Subject: [PATCH 19/35] fix: docs --- packages/eslint-plugin/docs/rules/no-floating-promises.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index 433d97143595..4a58d7ad9a6d 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -117,7 +117,7 @@ await (async function () { ### `allowForKnownSafePromises` -This allows you to skip checking of those promises which are marked with some kind of branded type. The branding of a type can be done either by using a type alias or an interface. +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 : From e5ffd3d5bf002d95c643d7e9151d529e7b0796cc Mon Sep 17 00:00:00 2001 From: arka1002 Date: Tue, 9 Apr 2024 14:14:59 +0530 Subject: [PATCH 20/35] chore: nits --- .../eslint-plugin/docs/rules/no-floating-promises.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index 4a58d7ad9a6d..a159e8e290c9 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -142,7 +142,7 @@ Examples of code for this rule with: ```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' -type Foo = Promise & { __linterBrands?: string }; // an example of a branded type +type Foo = Promise & { __linterBrands?: string }; let Bazz = Promise.resolve(2); Bazz; Bazz.then(() => {}); @@ -154,15 +154,15 @@ Bazz.finally(); ```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' -type Foo = Promise & { __linterBrands?: string }; // an example of a branded type +type Foo = Promise & { __linterBrands?: string }; let Bazz = Promise.resolve(2); -Bazz as Foo; // then apply the branded type as a type assertion +Bazz as Foo; // apply the safe type as a type assertion (Bazz as Foo).then(() => {}); (Bazz as Foo).catch(); (Bazz as Foo).finally(); // it also works in the form of type annotations -type Shaw = Promise & { __linterBrands?: string }; // an example of a branded type +type Shaw = Promise & { __linterBrands?: string }; let Guzz: Shaw = Promise.resolve(2); // can be used as type annotation too Guzz.then(() => {}); Guzz.catch(); From 345a65d2931ad18f4543ce997e6ef43300991641 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Tue, 9 Apr 2024 16:22:06 +0530 Subject: [PATCH 21/35] snapshots --- .../no-floating-promises.shot | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 8f1ebb9ccb5e..d5a15084b37c 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 @@ -75,7 +75,7 @@ exports[`Validating rule docs no-floating-promises.mdx code examples ESLint outp "Incorrect Options: {"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} -type Foo = Promise & { __linterBrands?: string }; // an example of a branded type +type Foo = Promise & { __linterBrands?: string }; let Bazz = Promise.resolve(2); Bazz; ~~~~~ 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. @@ -92,15 +92,15 @@ exports[`Validating rule docs no-floating-promises.mdx code examples ESLint outp "Correct Options: {"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} -type Foo = Promise & { __linterBrands?: string }; // an example of a branded type +type Foo = Promise & { __linterBrands?: string }; let Bazz = Promise.resolve(2); -Bazz as Foo; // then apply the branded type as a type assertion +Bazz as Foo; // apply the safe type as a type assertion (Bazz as Foo).then(() => {}); (Bazz as Foo).catch(); (Bazz as Foo).finally(); // it also works in the form of type annotations -type Shaw = Promise & { __linterBrands?: string }; // an example of a branded type +type Shaw = Promise & { __linterBrands?: string }; let Guzz: Shaw = Promise.resolve(2); // can be used as type annotation too Guzz.then(() => {}); Guzz.catch(); From 304a53c19dac919ac8a9d749b96e10c187e23a0a Mon Sep 17 00:00:00 2001 From: arka1002 Date: Wed, 24 Apr 2024 17:05:25 +0530 Subject: [PATCH 22/35] chore: address reviews --- .../src/rules/no-floating-promises.ts | 76 ++--- .../tests/rules/no-floating-promises.test.ts | 261 +----------------- .../no-floating-promises.shot | 4 + .../type-utils/src/TypeOrValueSpecifier.ts | 89 ------ 4 files changed, 23 insertions(+), 407 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 55930fdc52e5..a2655e628c2f 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -13,15 +13,16 @@ import { getOperatorPrecedence, getParserServices, OperatorPrecedence, + readonlynessOptionsDefaults, + readonlynessOptionsSchema, typeMatchesSpecifier, - typeOrValueSpecifierSchema_v2, } from '../util'; type Options = [ { ignoreVoid?: boolean; ignoreIIFE?: boolean; - allowForKnownSafePromises?: Exclude[]; + allowForKnownSafePromises?: TypeOrValueSpecifier[]; }, ]; @@ -87,10 +88,7 @@ export default createRule({ 'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).', type: 'boolean', }, - allowForKnownSafePromises: { - type: 'array', - items: typeOrValueSpecifierSchema_v2, - }, + allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow, }, additionalProperties: false, }, @@ -101,7 +99,7 @@ export default createRule({ { ignoreVoid: true, ignoreIIFE: false, - allowForKnownSafePromises: [], + allowForKnownSafePromises: readonlynessOptionsDefaults.allow, }, ], @@ -289,9 +287,8 @@ export default createRule({ if ( doesTypeMatcheSpecifier( services, - member, options.allowForKnownSafePromises, - undefined, + services.getTypeAtLocation(member), ) ) { return { isUnhandled: false }; @@ -343,9 +340,8 @@ export default createRule({ if ( doesTypeMatcheSpecifier( services, - node, options.allowForKnownSafePromises, - undefined, + services.getTypeAtLocation(node), ) ) { return { isUnhandled: false }; @@ -360,21 +356,6 @@ export default createRule({ return leftResult; } return isUnhandledPromise(checker, node.right); - } else if ( - node.type === AST_NODE_TYPES.TSAsExpression || - node.type === AST_NODE_TYPES.TSTypeAssertion - ) { - if ( - doesTypeMatcheSpecifier( - services, - node, - options.allowForKnownSafePromises, - undefined, - ) - ) { - return { isUnhandled: false }; - } - return isUnhandledPromise(checker, node.expression); } // We conservatively return false for all other types of expressions because @@ -388,35 +369,21 @@ export default createRule({ /** * It checks whether a node's type matches one of the types listed in the `allowForKnownSafePromises` config * @param services services variable passed from context function to check the type of a node - * @param node the node whose type is to be calculated to know whether it is a safe promise or not * @param options The config object of `allowForKnownSafePromises` - * @param type The type of the node, either provide this or node, if both are given this param is going to get used, if both are `undefined`, `false` is returned + * @param type The type of the node * @returns `true` if the type matches, `false` if it isn't */ function doesTypeMatcheSpecifier( services: ParserServicesWithTypeInformation, - node: TSESTree.Node | undefined, options: TypeOrValueSpecifier[] | undefined, - type: ts.Type | undefined, + type: ts.Type, ): boolean { - let typeOfNode: ts.Type; - if (!type && !node) { - return false; - } else if (!type && node) { - typeOfNode = services.getTypeAtLocation(node); - } else if (!node && type) { - typeOfNode = type; - } else if (node && type) { - typeOfNode = type; - } - - if (Array.isArray(options) && options.length > 0) { - const result = options.some(specifier => - typeMatchesSpecifier(typeOfNode, specifier, services.program), - ); - return result; - } - return false; + return ( + !!options?.length && + options.some(specifier => + typeMatchesSpecifier(type, specifier, services.program), + ) + ); } function isPromiseArray( @@ -434,9 +401,7 @@ function isPromiseArray( if (Array.isArray(options) && options.length > 0) { return !tsutils .unionTypeParts(arrayType) - .some(type => - doesTypeMatcheSpecifier(services, undefined, options, type), - ); + .some(type => doesTypeMatcheSpecifier(services, options, type)); } if (isPromiseLike(checker, node, arrayType)) { return true; @@ -445,14 +410,7 @@ function isPromiseArray( if (checker.isTupleType(ty)) { for (const tupleElementType of checker.getTypeArguments(ty)) { - if ( - doesTypeMatcheSpecifier( - services, - undefined, - options, - tupleElementType, - ) - ) { + if (doesTypeMatcheSpecifier(services, options, tupleElementType)) { return false; } if (isPromiseLike(checker, node, tupleElementType)) { 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 948f56a70eb7..4613456dd208 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -751,110 +751,11 @@ null ?? guzz().catch(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, - // type assertions of branded types in `allowForKnownSafePromises` - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -guzz as Foo; - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -(guzz as Foo).then(() => {}); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -(guzz as Foo).catch(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -(guzz as Foo).finally(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -0 ? (guzz as Foo).catch() : 2; - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -null ?? (guzz as Foo).catch(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - // type assertions of branded types on promise returning functions (or async functions) in `allowForKnownSafePromises` - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -guzz() as Foo; - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -(guzz() as Foo).then(() => {}); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -(guzz() as Foo).catch(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -(guzz() as Foo).finally(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -0 ? (guzz() as Foo).catch() : 2; - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -null ?? (guzz() as Foo).catch(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, // type from es5.d.ts using `allowForKnownSafePromises` { code: ` -let guzz = () => Promise.resolve(5); -guzz() as PromiseLike; -(guzz() as PromiseLike).then(() => {}); +let guzz: () => PromiseLike = () => Promise.resolve(5); +guzz().then(() => {}); `, options: [ { allowForKnownSafePromises: [{ from: 'lib', name: 'PromiseLike' }] }, @@ -879,68 +780,6 @@ type Foo = Promise & { hey?: string }; }, { code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -(guzz() as Foo).then( - () => {}, - () => {}, -); - `, - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -(guzz() as Foo).catch(() => {}); - `, - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -0 ? (guzz() as Foo).catch(() => {}) : 2; - `, - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -null ?? (guzz() as Foo).catch(() => {}); - `, - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -(guzz as Foo).then( - () => {}, - () => {}, -); - `, - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -(guzz as Foo).catch(() => {}); - `, - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -0 ? (guzz as Foo).catch(() => {}) : 2; - `, - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -null ?? (guzz as Foo).catch(() => {}); - `, - }, - { - code: ` declare const myTag: (strings: TemplateStringsArray) => Promise; myTag\`abc\`.catch(() => {}); `, @@ -2292,102 +2131,6 @@ cursed(); }, { code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -guzz as Foo; - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -(guzz as Foo).then(() => {}); - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -(guzz as Foo).catch(); - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -(guzz as Foo).finally(); - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -0 ? (guzz as Foo).catch() : 2; - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = Promise.resolve(5); -null ?? (guzz as Foo).catch(); - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -guzz() as Foo; - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -(guzz() as Foo).then(x => {}); - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -(guzz() as Foo).catch(); - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -(guzz() as Foo).finally(); - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -0 ? (guzz() as Foo).catch() : 2; - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz = () => Promise.resolve(5); -null ?? (guzz() as Foo).catch(); - `, - errors: [{ line: 4, messageId: 'floatingVoid' }], - }, - { - code: ` class UnsafePromise extends Promise {} let guzz: UnsafePromise = Promise.resolve(5); guzz; 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 2618c8666705..a708c7001d5b 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot @@ -11,6 +11,9 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "allowForKnownSafePromises": { "items": { "oneOf": [ + { + "type": "string" + }, { "additionalProperties": false, "properties": { @@ -132,6 +135,7 @@ type Options = [ name: [string, ...string[]] | string; package: string; } + | string )[]; /** Whether to ignore async IIFEs (Immediately Invoked Function Expressions). */ ignoreIIFE?: boolean; diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index cd5f5bea6b04..cf23b059c802 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -119,95 +119,6 @@ export const typeOrValueSpecifierSchema: JSONSchema4 = { ], }; -export const typeOrValueSpecifierSchema_v2: JSONSchema4 = { - oneOf: [ - { - type: 'object', - additionalProperties: false, - properties: { - from: { - type: 'string', - enum: ['file'], - }, - name: { - oneOf: [ - { - type: 'string', - }, - { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - }, - }, - ], - }, - path: { - type: 'string', - }, - }, - required: ['from', 'name'], - }, - { - type: 'object', - additionalProperties: false, - properties: { - from: { - type: 'string', - enum: ['lib'], - }, - name: { - oneOf: [ - { - type: 'string', - }, - { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - }, - }, - ], - }, - }, - required: ['from', 'name'], - }, - { - type: 'object', - additionalProperties: false, - properties: { - from: { - type: 'string', - enum: ['package'], - }, - name: { - oneOf: [ - { - type: 'string', - }, - { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - }, - }, - ], - }, - package: { - type: 'string', - }, - }, - required: ['from', 'name', 'package'], - }, - ], -}; - function specifierNameMatches(type: ts.Type, name: string[] | string): boolean { if (typeof name === 'string') { name = [name]; From 497e8f7a7f081a44ae3a26dd3a5e31146374bee7 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Wed, 24 Apr 2024 23:08:58 +0530 Subject: [PATCH 23/35] chore: reduce tests --- .../tests/rules/no-floating-promises.test.ts | 246 +++++++----------- 1 file changed, 93 insertions(+), 153 deletions(-) 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 4613456dd208..454ef7d783e1 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -499,117 +499,7 @@ void promiseArray; ['I', 'am', 'just', 'an', 'array']; `, }, - // Branded type annotations in `allowForKnownSafePromises` - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz: Foo = Promise.resolve(5); -guzz; - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz: Foo = Promise.resolve(5); -guzz.then(() => {}); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz: Foo = Promise.resolve(5); -guzz.catch(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz: Foo = Promise.resolve(5); -guzz.finally(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz: Foo = Promise.resolve(5); -0 ? guzz.catch() : 2; - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz: Foo = Promise.resolve(5); -null ?? guzz.catch(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - // extended classes as type annotations in `allowForKnownSafePromises` - { - code: ` -class SafePromise extends Promise {} -let guzz: SafePromise = Promise.resolve(5); -guzz; - `, - options: [ - { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, - ], - }, - { - code: ` -class SafePromise extends Promise {} -let guzz: SafePromise = Promise.resolve(5); -guzz.then(() => {}); - `, - options: [ - { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, - ], - }, - { - code: ` -class SafePromise extends Promise {} -let guzz: SafePromise = Promise.resolve(5); -guzz.catch(); - `, - options: [ - { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, - ], - }, - { - code: ` -class SafePromise extends Promise {} -let guzz: SafePromise = Promise.resolve(5); -guzz.finally(); - `, - options: [ - { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, - ], - }, - { - code: ` -class SafePromise extends Promise {} -let guzz: SafePromise = Promise.resolve(5); -0 ? guzz.catch() : 2; - `, - options: [ - { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, - ], - }, - { - code: ` -class SafePromise extends Promise {} -let guzz: SafePromise = Promise.resolve(5); -null ?? guzz.catch(); - `, - options: [ - { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, - ], - }, - // thenable interfaces as type annotations in `allowForKnownSafePromises` + // Branded type annotations on variables containing promises { code: ` interface SecureThenable { @@ -658,6 +548,43 @@ guzz.then(() => {}); }, { code: ` +class SafePromise extends Promise {} +let guzz: SafePromise = Promise.resolve(5); +guzz.catch(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` +class SafePromise extends Promise {} +let guzz: SafePromise = Promise.resolve(5); +guzz.finally(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: Foo = Promise.resolve(5); +0 ? guzz.catch() : 2; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + { + code: ` +type Foo = Promise & { hey?: string }; +let guzz: Foo = Promise.resolve(5); +null ?? guzz.catch(); + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + }, + // branded type annotations on promise returning functions (or async functions) + { + code: ` interface SecureThenable { then( onfulfilled?: @@ -670,8 +597,8 @@ interface SecureThenable { | null, ): SecureThenable; } -let guzz: SecureThenable = Promise.resolve(5); -0 ? guzz.then(() => {}) : 2; +let guzz: () => SecureThenable = () => Promise.resolve(5); +guzz(); `, options: [ { @@ -693,8 +620,8 @@ interface SecureThenable { | null, ): SecureThenable; } -let guzz: SecureThenable = Promise.resolve(5); -null ?? guzz.then(() => {}); +let guzz: () => SecureThenable = () => Promise.resolve(5); +guzz().then(() => {}); `, options: [ { @@ -702,23 +629,6 @@ null ?? guzz.then(() => {}); }, ], }, - // branded type annotations on promise returning functions (or async functions) in `allowForKnownSafePromises` - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz: () => Foo = () => Promise.resolve(5); -guzz(); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, - { - code: ` -type Foo = Promise & { hey?: string }; -let guzz: () => Foo = () => Promise.resolve(5); -guzz().then(() => {}); - `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - }, { code: ` type Foo = Promise & { hey?: string }; @@ -730,26 +640,30 @@ guzz().catch(); { code: ` type Foo = Promise & { hey?: string }; -let guzz: () => Foo = () => Promise.resolve(5); +let guzz: () => Foo = async () => 5; guzz().finally(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, { code: ` -type Foo = Promise & { hey?: string }; -let guzz: () => Foo = () => Promise.resolve(5); +class SafePromise extends Promise {} +let guzz: () => SafePromise = async () => 5; 0 ? guzz().catch() : 2; `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], }, { code: ` -type Foo = Promise & { hey?: string }; -let guzz: () => Foo = () => Promise.resolve(5); +class SafePromise extends Promise {} +let guzz: () => SafePromise = async () => 5; null ?? guzz().catch(); `, - options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], }, // type from es5.d.ts using `allowForKnownSafePromises` { @@ -2131,23 +2045,49 @@ cursed(); }, { code: ` -class UnsafePromise extends Promise {} -let guzz: UnsafePromise = Promise.resolve(5); +interface InsecureThenable { + then( + onfulfilled?: + | ((value: T) => TResult1 | InsecureThenable) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | InsecureThenable) + | undefined + | null, + ): InsecureThenable; +} +let guzz: InsecureThenable = Promise.resolve(5); guzz; `, options: [ - { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + { + allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + }, ], errors: [{ line: 4, messageId: 'floatingVoid' }], }, { code: ` -class UnsafePromise extends Promise {} -let guzz: UnsafePromise = Promise.resolve(5); -guzz.then(() => {}); +interface InsecureThenable { + then( + onfulfilled?: + | ((value: T) => TResult1 | InsecureThenable) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | InsecureThenable) + | undefined + | null, + ): InsecureThenable; +} +let guzz: () => InsecureThenable = () => Promise.resolve(5); +guzz().then(() => {}); `, options: [ - { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + { + allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + }, ], errors: [{ line: 4, messageId: 'floatingVoid' }], }, @@ -2165,8 +2105,8 @@ guzz.catch(); { code: ` class UnsafePromise extends Promise {} -let guzz: UnsafePromise = Promise.resolve(5); -guzz.finally(); +let guzz: () => UnsafePromise = async () => 5; +guzz().finally(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -2175,8 +2115,8 @@ guzz.finally(); }, { code: ` -class UnsafePromise extends Promise {} -let guzz: UnsafePromise = Promise.resolve(5); +type UnsafePromise = Promise & { hey?: string }; +let guzz: UnsafePromise = Promise.resolve(5); 0 ? guzz.catch() : 2; `, options: [ @@ -2186,9 +2126,9 @@ let guzz: UnsafePromise = Promise.resolve(5); }, { code: ` -class UnsafePromise extends Promise {} -let guzz: UnsafePromise = Promise.resolve(5); -null ?? guzz.catch(); +type UnsafePromise = Promise & { hey?: string }; +let guzz: () => UnsafePromise = async () => 5; +null ?? guzz().catch(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, From 842bd2db2e482f0bb42676ffb9b3ae26b9b1ab2f Mon Sep 17 00:00:00 2001 From: arka1002 Date: Wed, 24 Apr 2024 23:23:08 +0530 Subject: [PATCH 24/35] chore: line numbers --- .../eslint-plugin/tests/rules/no-floating-promises.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 454ef7d783e1..b8f2704c1c75 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -2065,7 +2065,7 @@ guzz; allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [{ line: 15, messageId: 'floatingVoid' }], }, { code: ` @@ -2089,7 +2089,7 @@ guzz().then(() => {}); allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [{ line: 15, messageId: 'floatingVoid' }], }, { code: ` From 6b01c41c5a05f3aba1d7fea6a4264f8438194f51 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Thu, 25 Apr 2024 09:55:42 +0530 Subject: [PATCH 25/35] chore: docs --- .../docs/rules/no-floating-promises.mdx | 37 ++++++--------- .../no-floating-promises.shot | 46 ++++++++----------- 2 files changed, 33 insertions(+), 50 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index a159e8e290c9..67a22d1eba6c 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -130,8 +130,7 @@ Examples of code for this rule with: ```json { "allowForKnownSafePromises": [ - { "from": "file", "name": "Foo" }, - { "from": "file", "name": "Shaw" }, + { "from": "file", "name": "SafePromise" }, { "from": "lib", "name": "PromiseLike" }, { "from": "package", "name": "Bar", "package": "bar-lib" } ] @@ -141,32 +140,24 @@ Examples of code for this rule with: -```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' -type Foo = Promise & { __linterBrands?: string }; -let Bazz = Promise.resolve(2); -Bazz; -Bazz.then(() => {}); -Bazz.catch(); -Bazz.finally(); +```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +type SafePromise = Promise & { __linterBrands?: string }; +let promise = Promise.resolve(2); +promise; +promise.then(() => {}); +promise.catch(); +promise.finally(); ``` -```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' -type Foo = Promise & { __linterBrands?: string }; -let Bazz = Promise.resolve(2); -Bazz as Foo; // apply the safe type as a type assertion -(Bazz as Foo).then(() => {}); -(Bazz as Foo).catch(); -(Bazz as Foo).finally(); - -// it also works in the form of type annotations -type Shaw = Promise & { __linterBrands?: string }; -let Guzz: Shaw = Promise.resolve(2); // can be used as type annotation too -Guzz.then(() => {}); -Guzz.catch(); -Guzz.finally(); +```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +type SafePromise = Promise & { __linterBrands?: string }; // promises can be marked as safe by using branded types +let promise: SafePromise = Promise.resolve(2); +promise.then(() => {}); +promise.catch(); +promise.finally(); ``` 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 d5a15084b37c..71838d722d7f 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 @@ -73,37 +73,29 @@ await (async function () { exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 5`] = ` "Incorrect -Options: {"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} - -type Foo = Promise & { __linterBrands?: string }; -let Bazz = Promise.resolve(2); -Bazz; -~~~~~ 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. -Bazz.then(() => {}); -~~~~~~~~~~~~~~~~~~~~ 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. -Bazz.catch(); -~~~~~~~~~~~~~ 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. -Bazz.finally(); -~~~~~~~~~~~~~~~ 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. +Options: {"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} + +type SafePromise = Promise & { __linterBrands?: string }; +let promise = Promise.resolve(2); +promise; +~~~~~~~~ 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. +promise.then(() => {}); +~~~~~~~~~~~~~~~~~~~~~~~ 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. +promise.catch(); +~~~~~~~~~~~~~~~~ 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. +promise.finally(); +~~~~~~~~~~~~~~~~~~ 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 6`] = ` "Correct -Options: {"allowForKnownSafePromises":[{"from":"file","name":"Foo"},{"from":"file","name":"Shaw"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} - -type Foo = Promise & { __linterBrands?: string }; -let Bazz = Promise.resolve(2); -Bazz as Foo; // apply the safe type as a type assertion -(Bazz as Foo).then(() => {}); -(Bazz as Foo).catch(); -(Bazz as Foo).finally(); - -// it also works in the form of type annotations -type Shaw = Promise & { __linterBrands?: string }; -let Guzz: Shaw = Promise.resolve(2); // can be used as type annotation too -Guzz.then(() => {}); -Guzz.catch(); -Guzz.finally(); +Options: {"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} + +type SafePromise = Promise & { __linterBrands?: string }; // promises can be marked as safe by using branded types +let promise: SafePromise = Promise.resolve(2); +promise.then(() => {}); +promise.catch(); +promise.finally(); " `; From f10ae89b2f3eb0fc54e92493a200e527ec92b553 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Thu, 25 Apr 2024 10:20:46 +0530 Subject: [PATCH 26/35] chore: typos --- .../eslint-plugin/src/rules/no-floating-promises.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index a2655e628c2f..a209f4a7e9ee 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -285,7 +285,7 @@ export default createRule({ ? node.callee.object : node; if ( - doesTypeMatcheSpecifier( + doesTypeMatchSpecifier( services, options.allowForKnownSafePromises, services.getTypeAtLocation(member), @@ -338,7 +338,7 @@ export default createRule({ node.type === AST_NODE_TYPES.NewExpression ) { if ( - doesTypeMatcheSpecifier( + doesTypeMatchSpecifier( services, options.allowForKnownSafePromises, services.getTypeAtLocation(node), @@ -373,7 +373,7 @@ export default createRule({ * @param type The type of the node * @returns `true` if the type matches, `false` if it isn't */ -function doesTypeMatcheSpecifier( +function doesTypeMatchSpecifier( services: ParserServicesWithTypeInformation, options: TypeOrValueSpecifier[] | undefined, type: ts.Type, @@ -401,7 +401,7 @@ function isPromiseArray( if (Array.isArray(options) && options.length > 0) { return !tsutils .unionTypeParts(arrayType) - .some(type => doesTypeMatcheSpecifier(services, options, type)); + .some(type => doesTypeMatchSpecifier(services, options, type)); } if (isPromiseLike(checker, node, arrayType)) { return true; @@ -410,7 +410,7 @@ function isPromiseArray( if (checker.isTupleType(ty)) { for (const tupleElementType of checker.getTypeArguments(ty)) { - if (doesTypeMatcheSpecifier(services, options, tupleElementType)) { + if (doesTypeMatchSpecifier(services, options, tupleElementType)) { return false; } if (isPromiseLike(checker, node, tupleElementType)) { From 4a7d5164ab50361004b297775cfbe198a5c53b7b Mon Sep 17 00:00:00 2001 From: arka1002 Date: Sat, 27 Apr 2024 14:02:08 +0530 Subject: [PATCH 27/35] chore: address reviews --- .../src/rules/no-floating-promises.ts | 34 ++++++------------- .../tests/rules/no-floating-promises.test.ts | 18 ++++++++++ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index a209f4a7e9ee..523f382a06ed 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -106,6 +106,9 @@ export default createRule({ 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!; return { ExpressionStatement(node): void { @@ -265,12 +268,7 @@ export default createRule({ // or array thereof. if ( - isPromiseArray( - services, - options.allowForKnownSafePromises, - checker, - tsNode, - ) + isPromiseArray(services, allowForKnownSafePromises, checker, tsNode) ) { return { isUnhandled: true, promiseArray: true }; } @@ -287,7 +285,7 @@ export default createRule({ if ( doesTypeMatchSpecifier( services, - options.allowForKnownSafePromises, + allowForKnownSafePromises, services.getTypeAtLocation(member), ) ) { @@ -340,7 +338,7 @@ export default createRule({ if ( doesTypeMatchSpecifier( services, - options.allowForKnownSafePromises, + allowForKnownSafePromises, services.getTypeAtLocation(node), ) ) { @@ -366,29 +364,19 @@ export default createRule({ }, }); -/** - * It checks whether a node's type matches one of the types listed in the `allowForKnownSafePromises` config - * @param services services variable passed from context function to check the type of a node - * @param options The config object of `allowForKnownSafePromises` - * @param type The type of the node - * @returns `true` if the type matches, `false` if it isn't - */ function doesTypeMatchSpecifier( services: ParserServicesWithTypeInformation, - options: TypeOrValueSpecifier[] | undefined, + options: TypeOrValueSpecifier[], type: ts.Type, ): boolean { - return ( - !!options?.length && - options.some(specifier => - typeMatchesSpecifier(type, specifier, services.program), - ) + return options.some(specifier => + typeMatchesSpecifier(type, specifier, services.program), ); } function isPromiseArray( services: ParserServicesWithTypeInformation, - options: TypeOrValueSpecifier[] | undefined, + options: TypeOrValueSpecifier[], checker: ts.TypeChecker, node: ts.Node, ): boolean { @@ -398,7 +386,7 @@ function isPromiseArray( .map(t => checker.getApparentType(t))) { if (checker.isArrayType(ty)) { const arrayType = checker.getTypeArguments(ty)[0]; - if (Array.isArray(options) && options.length > 0) { + if (options.length > 0) { return !tsutils .unionTypeParts(arrayType) .some(type => doesTypeMatchSpecifier(services, options, type)); 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 b8f2704c1c75..7c27dbc1a1a9 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -2135,5 +2135,23 @@ null ?? guzz().catch(); ], errors: [{ line: 4, messageId: 'floatingVoid' }], }, + { + code: ` +declare const arrayOrPromiseTuple: Foo[]; +arrayOrPromiseTuple; +type Foo = Promise & { hey?: string }; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Bar' }] }], + errors: [{ line: 3, messageId: 'floatingPromiseArrayVoid' }], + }, + { + code: ` +declare const arrayOrPromiseTuple: [Foo, 5]; +arrayOrPromiseTuple; +type Foo = Promise & { hey?: string }; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Bar' }] }], + errors: [{ line: 3, messageId: 'floatingPromiseArrayVoid' }], + }, ], }); From aa94a28de74c225c8e7338c73f56330eb1015227 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Sat, 27 Apr 2024 21:30:03 +0530 Subject: [PATCH 28/35] fix: condition issues --- packages/eslint-plugin/src/rules/no-floating-promises.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 523f382a06ed..b3a2150aa242 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -386,10 +386,13 @@ function isPromiseArray( .map(t => checker.getApparentType(t))) { if (checker.isArrayType(ty)) { const arrayType = checker.getTypeArguments(ty)[0]; - if (options.length > 0) { - return !tsutils + if ( + options.length > 0 && + tsutils .unionTypeParts(arrayType) - .some(type => doesTypeMatchSpecifier(services, options, type)); + .some(type => doesTypeMatchSpecifier(services, options, type)) + ) { + return false; } if (isPromiseLike(checker, node, arrayType)) { return true; From 171701bf5a1dafb24d1bfab581f212fbaa2abfdd Mon Sep 17 00:00:00 2001 From: Arka Pratim Chaudhuri <105232141+arka1002@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:09:50 +0530 Subject: [PATCH 29/35] chore: update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- packages/eslint-plugin/docs/rules/no-floating-promises.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index 67a22d1eba6c..d83dab2c9673 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -119,7 +119,7 @@ await (async function () { 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 : +Each item must be one of: - 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"}`) From ecd31e3439fb3aad312f7216f6fb5bd44df65367 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Mon, 29 Apr 2024 16:10:04 +0530 Subject: [PATCH 30/35] chore: tag function --- .../src/rules/no-floating-promises.ts | 9 +++++++++ .../tests/rules/no-floating-promises.test.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index b3a2150aa242..3a321b7a9b59 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -321,6 +321,15 @@ export default createRule({ // 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 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 7c27dbc1a1a9..519327360656 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -694,6 +694,16 @@ type Foo = Promise & { hey?: string }; }, { code: ` +type SafePromise = Promise & { __linterBrands?: string }; +declare const myTag: (strings: TemplateStringsArray) => SafePromise; +myTag\`abc\`; + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + }, + { + code: ` declare const myTag: (strings: TemplateStringsArray) => Promise; myTag\`abc\`.catch(() => {}); `, @@ -2153,5 +2163,14 @@ type Foo = Promise & { hey?: string }; options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Bar' }] }], errors: [{ line: 3, messageId: 'floatingPromiseArrayVoid' }], }, + { + code: ` +type SafePromise = Promise & { __linterBrands?: string }; +declare const myTag: (strings: TemplateStringsArray) => SafePromise; +myTag\`abc\`; + `, + options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], + errors: [{ line: 4, messageId: 'floatingVoid' }], + }, ], }); From a7fc9a2f5f6a0225c35436d9c2a5ddc389b858ef Mon Sep 17 00:00:00 2001 From: arka1002 Date: Wed, 1 May 2024 15:31:26 +0530 Subject: [PATCH 31/35] chore: address reviews --- .../docs/rules/no-floating-promises.mdx | 4 +- .../src/rules/no-floating-promises.ts | 12 +- .../no-floating-promises.shot | 4 +- .../tests/rules/no-floating-promises.test.ts | 152 +++++++++--------- 4 files changed, 85 insertions(+), 87 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index d83dab2c9673..efa911f3d74d 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -141,8 +141,8 @@ Examples of code for this rule with: ```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' -type SafePromise = Promise & { __linterBrands?: string }; -let promise = Promise.resolve(2); +type UnsafePromise = Promise & { __linterBrands?: string }; +let promise: UnsafePromise = Promise.resolve(2); promise; promise.then(() => {}); promise.catch(); diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 3a321b7a9b59..923891b846c4 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -108,7 +108,7 @@ export default createRule({ const checker = services.program.getTypeChecker(); // TODO: #5439 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const allowForKnownSafePromises = options.allowForKnownSafePromises!; + const safePromisesSpecifiers = options.allowForKnownSafePromises!; return { ExpressionStatement(node): void { @@ -267,9 +267,7 @@ export default createRule({ // 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) - ) { + if (isPromiseArray(services, safePromisesSpecifiers, checker, tsNode)) { return { isUnhandled: true, promiseArray: true }; } @@ -285,7 +283,7 @@ export default createRule({ if ( doesTypeMatchSpecifier( services, - allowForKnownSafePromises, + safePromisesSpecifiers, services.getTypeAtLocation(member), ) ) { @@ -324,7 +322,7 @@ export default createRule({ if ( doesTypeMatchSpecifier( services, - allowForKnownSafePromises, + safePromisesSpecifiers, services.getTypeAtLocation(node), ) ) { @@ -347,7 +345,7 @@ export default createRule({ if ( doesTypeMatchSpecifier( services, - allowForKnownSafePromises, + safePromisesSpecifiers, services.getTypeAtLocation(node), ) ) { 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 71838d722d7f..9a3ea6ceca61 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 @@ -75,8 +75,8 @@ exports[`Validating rule docs no-floating-promises.mdx code examples ESLint outp "Incorrect Options: {"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]} -type SafePromise = Promise & { __linterBrands?: string }; -let promise = Promise.resolve(2); +type UnsafePromise = Promise & { __linterBrands?: string }; +let promise: UnsafePromise = Promise.resolve(2); promise; ~~~~~~~~ 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. promise.then(() => {}); 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 519327360656..d65e3b81659a 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -499,58 +499,58 @@ void promiseArray; ['I', 'am', 'just', 'an', 'array']; `, }, - // Branded type annotations on variables containing promises + // type annotations on variables containing promises { code: ` -interface SecureThenable { +interface SafeThenable { then( onfulfilled?: - | ((value: T) => TResult1 | SecureThenable) + | ((value: T) => TResult1 | SafeThenable) | undefined | null, onrejected?: - | ((reason: any) => TResult2 | SecureThenable) + | ((reason: any) => TResult2 | SafeThenable) | undefined | null, - ): SecureThenable; + ): SafeThenable; } -let guzz: SecureThenable = Promise.resolve(5); -guzz; +let promise: SafeThenable = Promise.resolve(5); +promise; `, options: [ { - allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }], }, ], }, { code: ` -interface SecureThenable { +interface SafeThenable { then( onfulfilled?: - | ((value: T) => TResult1 | SecureThenable) + | ((value: T) => TResult1 | SafeThenable) | undefined | null, onrejected?: - | ((reason: any) => TResult2 | SecureThenable) + | ((reason: any) => TResult2 | SafeThenable) | undefined | null, - ): SecureThenable; + ): SafeThenable; } -let guzz: SecureThenable = Promise.resolve(5); -guzz.then(() => {}); +let promise: SafeThenable = Promise.resolve(5); +promise.then(() => {}); `, options: [ { - allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }], }, ], }, { code: ` class SafePromise extends Promise {} -let guzz: SafePromise = Promise.resolve(5); -guzz.catch(); +let promise: SafePromise = Promise.resolve(5); +promise.catch(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -559,8 +559,8 @@ guzz.catch(); { code: ` class SafePromise extends Promise {} -let guzz: SafePromise = Promise.resolve(5); -guzz.finally(); +let promise: SafePromise = Promise.resolve(5); +promise.finally(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -569,87 +569,87 @@ guzz.finally(); { code: ` type Foo = Promise & { hey?: string }; -let guzz: Foo = Promise.resolve(5); -0 ? guzz.catch() : 2; +let promise: Foo = Promise.resolve(5); +0 ? promise.catch() : 2; `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, { code: ` type Foo = Promise & { hey?: string }; -let guzz: Foo = Promise.resolve(5); -null ?? guzz.catch(); +let promise: Foo = Promise.resolve(5); +null ?? promise.catch(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, - // branded type annotations on promise returning functions (or async functions) + // type annotations on promise returning functions (or async functions) { code: ` -interface SecureThenable { +interface SafeThenable { then( onfulfilled?: - | ((value: T) => TResult1 | SecureThenable) + | ((value: T) => TResult1 | SafeThenable) | undefined | null, onrejected?: - | ((reason: any) => TResult2 | SecureThenable) + | ((reason: any) => TResult2 | SafeThenable) | undefined | null, - ): SecureThenable; + ): SafeThenable; } -let guzz: () => SecureThenable = () => Promise.resolve(5); -guzz(); +let promise: () => SafeThenable = () => Promise.resolve(5); +promise(); `, options: [ { - allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }], }, ], }, { code: ` -interface SecureThenable { +interface SafeThenable { then( onfulfilled?: - | ((value: T) => TResult1 | SecureThenable) + | ((value: T) => TResult1 | SafeThenable) | undefined | null, onrejected?: - | ((reason: any) => TResult2 | SecureThenable) + | ((reason: any) => TResult2 | SafeThenable) | undefined | null, - ): SecureThenable; + ): SafeThenable; } -let guzz: () => SecureThenable = () => Promise.resolve(5); -guzz().then(() => {}); +let promise: () => SafeThenable = () => Promise.resolve(5); +promise().then(() => {}); `, options: [ { - allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }], }, ], }, { code: ` type Foo = Promise & { hey?: string }; -let guzz: () => Foo = () => Promise.resolve(5); -guzz().catch(); +let promise: () => Foo = () => Promise.resolve(5); +promise().catch(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, { code: ` type Foo = Promise & { hey?: string }; -let guzz: () => Foo = async () => 5; -guzz().finally(); +let promise: () => Foo = async () => 5; +promise().finally(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, { code: ` class SafePromise extends Promise {} -let guzz: () => SafePromise = async () => 5; -0 ? guzz().catch() : 2; +let promise: () => SafePromise = async () => 5; +0 ? promise().catch() : 2; `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -658,8 +658,8 @@ let guzz: () => SafePromise = async () => 5; { code: ` class SafePromise extends Promise {} -let guzz: () => SafePromise = async () => 5; -null ?? guzz().catch(); +let promise: () => SafePromise = async () => 5; +null ?? promise().catch(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -668,8 +668,8 @@ null ?? guzz().catch(); // type from es5.d.ts using `allowForKnownSafePromises` { code: ` -let guzz: () => PromiseLike = () => Promise.resolve(5); -guzz().then(() => {}); +let promise: () => PromiseLike = () => Promise.resolve(5); +promise().then(() => {}); `, options: [ { allowForKnownSafePromises: [{ from: 'lib', name: 'PromiseLike' }] }, @@ -678,17 +678,17 @@ guzz().then(() => {}); // promises in array using `allowForKnownSafePromises` { code: ` +type Foo = Promise & { hey?: string }; declare const arrayOrPromiseTuple: Foo[]; arrayOrPromiseTuple; -type Foo = Promise & { hey?: string }; `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, { code: ` +type Foo = Promise & { hey?: string }; declare const arrayOrPromiseTuple: [Foo, 5]; arrayOrPromiseTuple; -type Foo = Promise & { hey?: string }; `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, @@ -2055,48 +2055,48 @@ cursed(); }, { code: ` -interface InsecureThenable { +interface UnsafeThenable { then( onfulfilled?: - | ((value: T) => TResult1 | InsecureThenable) + | ((value: T) => TResult1 | UnsafeThenable) | undefined | null, onrejected?: - | ((reason: any) => TResult2 | InsecureThenable) + | ((reason: any) => TResult2 | UnsafeThenable) | undefined | null, - ): InsecureThenable; + ): UnsafeThenable; } -let guzz: InsecureThenable = Promise.resolve(5); -guzz; +let promise: UnsafeThenable = Promise.resolve(5); +promise; `, options: [ { - allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }], }, ], errors: [{ line: 15, messageId: 'floatingVoid' }], }, { code: ` -interface InsecureThenable { +interface UnsafeThenable { then( onfulfilled?: - | ((value: T) => TResult1 | InsecureThenable) + | ((value: T) => TResult1 | UnsafeThenable) | undefined | null, onrejected?: - | ((reason: any) => TResult2 | InsecureThenable) + | ((reason: any) => TResult2 | UnsafeThenable) | undefined | null, - ): InsecureThenable; + ): UnsafeThenable; } -let guzz: () => InsecureThenable = () => Promise.resolve(5); -guzz().then(() => {}); +let promise: () => UnsafeThenable = () => Promise.resolve(5); +promise().then(() => {}); `, options: [ { - allowForKnownSafePromises: [{ from: 'file', name: 'SecureThenable' }], + allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }], }, ], errors: [{ line: 15, messageId: 'floatingVoid' }], @@ -2104,8 +2104,8 @@ guzz().then(() => {}); { code: ` class UnsafePromise extends Promise {} -let guzz: UnsafePromise = Promise.resolve(5); -guzz.catch(); +let promise: UnsafePromise = Promise.resolve(5); +promise.catch(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -2115,8 +2115,8 @@ guzz.catch(); { code: ` class UnsafePromise extends Promise {} -let guzz: () => UnsafePromise = async () => 5; -guzz().finally(); +let promise: () => UnsafePromise = async () => 5; +promise().finally(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -2126,8 +2126,8 @@ guzz().finally(); { code: ` type UnsafePromise = Promise & { hey?: string }; -let guzz: UnsafePromise = Promise.resolve(5); -0 ? guzz.catch() : 2; +let promise: UnsafePromise = Promise.resolve(5); +0 ? promise.catch() : 2; `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -2137,8 +2137,8 @@ let guzz: UnsafePromise = Promise.resolve(5); { code: ` type UnsafePromise = Promise & { hey?: string }; -let guzz: () => UnsafePromise = async () => 5; -null ?? guzz().catch(); +let promise: () => UnsafePromise = async () => 5; +null ?? promise().catch(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -2147,21 +2147,21 @@ null ?? guzz().catch(); }, { code: ` +type Foo = Promise & { hey?: string }; declare const arrayOrPromiseTuple: Foo[]; arrayOrPromiseTuple; -type Foo = Promise & { hey?: string }; `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Bar' }] }], - errors: [{ line: 3, messageId: 'floatingPromiseArrayVoid' }], + errors: [{ line: 4, messageId: 'floatingPromiseArrayVoid' }], }, { code: ` +type Foo = Promise & { hey?: string }; declare const arrayOrPromiseTuple: [Foo, 5]; arrayOrPromiseTuple; -type Foo = Promise & { hey?: string }; `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Bar' }] }], - errors: [{ line: 3, messageId: 'floatingPromiseArrayVoid' }], + errors: [{ line: 4, messageId: 'floatingPromiseArrayVoid' }], }, { code: ` From 0ab70605a42b1e635cf6c0b2386617a12aa3a528 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Wed, 1 May 2024 15:55:30 +0530 Subject: [PATCH 32/35] chore: revert var name --- .../eslint-plugin/src/rules/no-floating-promises.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 923891b846c4..3a321b7a9b59 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -108,7 +108,7 @@ export default createRule({ const checker = services.program.getTypeChecker(); // TODO: #5439 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const safePromisesSpecifiers = options.allowForKnownSafePromises!; + const allowForKnownSafePromises = options.allowForKnownSafePromises!; return { ExpressionStatement(node): void { @@ -267,7 +267,9 @@ export default createRule({ // Check the type. At this point it can't be unhandled if it isn't a promise // or array thereof. - if (isPromiseArray(services, safePromisesSpecifiers, checker, tsNode)) { + if ( + isPromiseArray(services, allowForKnownSafePromises, checker, tsNode) + ) { return { isUnhandled: true, promiseArray: true }; } @@ -283,7 +285,7 @@ export default createRule({ if ( doesTypeMatchSpecifier( services, - safePromisesSpecifiers, + allowForKnownSafePromises, services.getTypeAtLocation(member), ) ) { @@ -322,7 +324,7 @@ export default createRule({ if ( doesTypeMatchSpecifier( services, - safePromisesSpecifiers, + allowForKnownSafePromises, services.getTypeAtLocation(node), ) ) { @@ -345,7 +347,7 @@ export default createRule({ if ( doesTypeMatchSpecifier( services, - safePromisesSpecifiers, + allowForKnownSafePromises, services.getTypeAtLocation(node), ) ) { From 441e4672c5c258b2ce34dacb7312334cdf788b89 Mon Sep 17 00:00:00 2001 From: arka1002 Date: Sat, 11 May 2024 10:38:04 +0530 Subject: [PATCH 33/35] chore: address reviews --- .../src/rules/no-floating-promises.ts | 8 +++ .../tests/rules/no-floating-promises.test.ts | 52 ++++++++++++------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 3a321b7a9b59..940369d9a3d2 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -282,7 +282,15 @@ export default createRule({ node.callee.type === AST_NODE_TYPES.MemberExpression ? node.callee.object : node; + const calledByThenOrCatch = + (node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'catch') || + (node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'then'); if ( + !calledByThenOrCatch && doesTypeMatchSpecifier( services, allowForKnownSafePromises, 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 d65e3b81659a..682aac999bd7 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -515,7 +515,7 @@ interface SafeThenable { ): SafeThenable; } let promise: SafeThenable = Promise.resolve(5); -promise; +0, promise; `, options: [ { @@ -538,7 +538,7 @@ interface SafeThenable { ): SafeThenable; } let promise: SafeThenable = Promise.resolve(5); -promise.then(() => {}); +0 ? promise : 3; `, options: [ { @@ -549,8 +549,8 @@ promise.then(() => {}); { code: ` class SafePromise extends Promise {} -let promise: SafePromise = Promise.resolve(5); -promise.catch(); +let promise: { a: SafePromise} = { a: Promise.resolve(5) }; +promise.a; `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -560,7 +560,7 @@ promise.catch(); code: ` class SafePromise extends Promise {} let promise: SafePromise = Promise.resolve(5); -promise.finally(); +promise; `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -570,7 +570,7 @@ promise.finally(); code: ` type Foo = Promise & { hey?: string }; let promise: Foo = Promise.resolve(5); -0 ? promise.catch() : 2; +0 || promise; `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, @@ -578,7 +578,7 @@ let promise: Foo = Promise.resolve(5); code: ` type Foo = Promise & { hey?: string }; let promise: Foo = Promise.resolve(5); -null ?? promise.catch(); +promise.finally(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, @@ -598,7 +598,7 @@ interface SafeThenable { ): SafeThenable; } let promise: () => SafeThenable = () => Promise.resolve(5); -promise(); +0, promise(); `, options: [ { @@ -621,7 +621,7 @@ interface SafeThenable { ): SafeThenable; } let promise: () => SafeThenable = () => Promise.resolve(5); -promise().then(() => {}); +0 ? promise() : 3; `, options: [ { @@ -633,7 +633,7 @@ promise().then(() => {}); code: ` type Foo = Promise & { hey?: string }; let promise: () => Foo = () => Promise.resolve(5); -promise().catch(); +promise(); `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], }, @@ -649,7 +649,7 @@ promise().finally(); code: ` class SafePromise extends Promise {} let promise: () => SafePromise = async () => 5; -0 ? promise().catch() : 2; +0 || promise(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -659,7 +659,7 @@ let promise: () => SafePromise = async () => 5; code: ` class SafePromise extends Promise {} let promise: () => SafePromise = async () => 5; -null ?? promise().catch(); +null ?? promise(); `, options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, @@ -669,7 +669,7 @@ null ?? promise().catch(); { code: ` let promise: () => PromiseLike = () => Promise.resolve(5); -promise().then(() => {}); +promise(); `, options: [ { allowForKnownSafePromises: [{ from: 'lib', name: 'PromiseLike' }] }, @@ -2079,19 +2079,19 @@ promise; }, { code: ` -interface UnsafeThenable { +interface SafeThenable { then( onfulfilled?: - | ((value: T) => TResult1 | UnsafeThenable) + | ((value: T) => TResult1 | SafeThenable) | undefined | null, onrejected?: - | ((reason: any) => TResult2 | UnsafeThenable) + | ((reason: any) => TResult2 | SafeThenable) | undefined | null, - ): UnsafeThenable; + ): SafeThenable; } -let promise: () => UnsafeThenable = () => Promise.resolve(5); +let promise: () => SafeThenable = () => Promise.resolve(5); promise().then(() => {}); `, options: [ @@ -2103,8 +2103,8 @@ promise().then(() => {}); }, { code: ` -class UnsafePromise extends Promise {} -let promise: UnsafePromise = Promise.resolve(5); +class SafePromise extends Promise {} +let promise: SafePromise = Promise.resolve(5); promise.catch(); `, options: [ @@ -2156,6 +2156,18 @@ arrayOrPromiseTuple; }, { code: ` +type SafePromise = Promise & { hey?: string; }; +let foo: SafePromise = Promise.resolve(1); +let bar = [ Promise.resolve(2), foo ]; +bar; + `, + options: [ + { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, + ], + errors: [{ line: 5, messageId: 'floatingPromiseArrayVoid' }], + }, + { + code: ` type Foo = Promise & { hey?: string }; declare const arrayOrPromiseTuple: [Foo, 5]; arrayOrPromiseTuple; From 7422cf504ba51cc2e1855f0352e151877720755e Mon Sep 17 00:00:00 2001 From: arka1002 Date: Sat, 11 May 2024 17:32:58 +0530 Subject: [PATCH 34/35] chore: lint & tests --- packages/eslint-plugin/docs/rules/no-floating-promises.mdx | 5 +---- packages/eslint-plugin/src/rules/no-floating-promises.ts | 4 ++-- .../docs-eslint-output-snapshots/no-floating-promises.shot | 7 +------ .../eslint-plugin/tests/rules/no-floating-promises.test.ts | 6 +++--- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index efa911f3d74d..8199e11efa92 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -144,8 +144,6 @@ Examples of code for this rule with: type UnsafePromise = Promise & { __linterBrands?: string }; let promise: UnsafePromise = Promise.resolve(2); promise; -promise.then(() => {}); -promise.catch(); promise.finally(); ``` @@ -155,8 +153,7 @@ promise.finally(); ```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' type SafePromise = Promise & { __linterBrands?: string }; // promises can be marked as safe by using branded types let promise: SafePromise = Promise.resolve(2); -promise.then(() => {}); -promise.catch(); +promise; promise.finally(); ``` diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 940369d9a3d2..89558307cef7 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -284,10 +284,10 @@ export default createRule({ : node; const calledByThenOrCatch = (node.callee.type === AST_NODE_TYPES.MemberExpression && - node.callee.property.type === 'Identifier' && + node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === 'catch') || (node.callee.type === AST_NODE_TYPES.MemberExpression && - node.callee.property.type === 'Identifier' && + node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === 'then'); if ( !calledByThenOrCatch && 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 9a3ea6ceca61..00cfbc962882 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 @@ -79,10 +79,6 @@ type UnsafePromise = Promise & { __linterBrands?: string }; let promise: UnsafePromise = Promise.resolve(2); promise; ~~~~~~~~ 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. -promise.then(() => {}); -~~~~~~~~~~~~~~~~~~~~~~~ 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. -promise.catch(); -~~~~~~~~~~~~~~~~ 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. promise.finally(); ~~~~~~~~~~~~~~~~~~ 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. " @@ -94,8 +90,7 @@ Options: {"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"fr type SafePromise = Promise & { __linterBrands?: string }; // promises can be marked as safe by using branded types let promise: SafePromise = Promise.resolve(2); -promise.then(() => {}); -promise.catch(); +promise; promise.finally(); " `; 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 682aac999bd7..491dc2acb0f1 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -549,7 +549,7 @@ let promise: SafeThenable = Promise.resolve(5); { code: ` class SafePromise extends Promise {} -let promise: { a: SafePromise} = { a: Promise.resolve(5) }; +let promise: { a: SafePromise } = { a: Promise.resolve(5) }; promise.a; `, options: [ @@ -2156,9 +2156,9 @@ arrayOrPromiseTuple; }, { code: ` -type SafePromise = Promise & { hey?: string; }; +type SafePromise = Promise & { hey?: string }; let foo: SafePromise = Promise.resolve(1); -let bar = [ Promise.resolve(2), foo ]; +let bar = [Promise.resolve(2), foo]; bar; `, options: [ From cbbda8a4b5a7608617a8c361d40262d9bbf5ac6c Mon Sep 17 00:00:00 2001 From: arka1002 Date: Sun, 26 May 2024 15:14:53 +0530 Subject: [PATCH 35/35] chore: comments --- packages/eslint-plugin/tests/rules/no-floating-promises.test.ts | 1 + 1 file changed, 1 insertion(+) 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 491dc2acb0f1..6ec6f4f883d5 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -2154,6 +2154,7 @@ arrayOrPromiseTuple; options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Bar' }] }], errors: [{ line: 4, messageId: 'floatingPromiseArrayVoid' }], }, + // an array, which contains elements of `Promise` type and a branded promise type, its type will be reduced to `Promise`, see - https://github.com/typescript-eslint/typescript-eslint/pull/8502#issuecomment-2105734406 { code: ` type SafePromise = Promise & { hey?: string };