Skip to content

Commit

Permalink
feat: [no-floating-promises] add an
Browse files Browse the repository at this point in the history
'allowForKnownSafePromises' option

fixes: typescript-eslint#7008
  • Loading branch information
arka1002 committed Feb 18, 2024
1 parent ebd2959 commit 476f867
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 4 deletions.
26 changes: 26 additions & 0 deletions packages/eslint-plugin/docs/rules/no-floating-promises.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
]
}
```

<!--tabs-->

#### ❌ 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.
Expand Down
53 changes: 53 additions & 0 deletions packages/eslint-plugin/src/rules/no-floating-promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}[];
},
];

Expand Down Expand Up @@ -79,6 +85,40 @@ export default createRule<Options, MessageId>({
'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,
},
Expand All @@ -89,6 +129,7 @@ export default createRule<Options, MessageId>({
{
ignoreVoid: true,
ignoreIIFE: false,
allowForKnownSafePromises: [],
},
],

Expand Down Expand Up @@ -262,6 +303,18 @@ export default createRule<Options, MessageId>({
}

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.

Expand Down
13 changes: 13 additions & 0 deletions packages/eslint-plugin/tests/rules/no-floating-promises.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }],
},
],
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions packages/type-utils/src/TypeOrValueSpecifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
});
Expand Down
8 changes: 8 additions & 0 deletions packages/type-utils/tests/TypeOrValueSpecifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]>([
Expand Down
2 changes: 2 additions & 0 deletions packages/type-utils/typings/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path, string>;

readonly resolvedModules: ReadonlyMap<Path, unknown>;
}

interface SourceFile extends Declaration, LocalsContainer {
Expand Down

0 comments on commit 476f867

Please sign in to comment.