Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [no-misused-promises] warn when spreading promises #5053

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 56 additions & 3 deletions packages/eslint-plugin/docs/rules/no-misused-promises.md
Expand Up @@ -13,9 +13,9 @@ See [`no-floating-promises`](./no-floating-promises.md) for detecting unhandled

## Rule Details

This rule accepts a single option which is an object with `checksConditionals`
and `checksVoidReturn` properties indicating which types of misuse to flag.
Both are enabled by default.
This rule accepts a single option which is an object with `checksConditionals`,
`checksVoidReturn`, and `checksSpreads` properties indicating which types of
misuse to flag. All are enabled by default.

## Options

Expand All @@ -24,6 +24,7 @@ type Options = [
{
checksConditionals?: boolean;
checksVoidReturn?: boolean | ChecksVoidReturnOptions;
checksSpreads?: boolean;
},
];

Expand All @@ -39,6 +40,7 @@ const defaultOptions: Options = [
{
checksConditionals: true,
checksVoidReturn: true,
checksSpreads: true,
},
];
```
Expand Down Expand Up @@ -101,6 +103,21 @@ For example, if you don't mind that passing a `() => Promise<void>` to a `() =>
}
```

### `"checksSpreads"`

If you don't want to check object spreads, you can add this configuration:

```json
{
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksSpreads": false
}
]
}
```

### `checksConditionals: true`

Examples of code for this rule with `checksConditionals: true`:
Expand Down Expand Up @@ -212,6 +229,42 @@ eventEmitter.on('some-event', () => {

<!--/tabs-->

### `checksSpreads: true`

Examples of code for this rule with `checksSpreads: true`:

<!--tabs-->

#### ❌ Incorrect

```ts
const getData = () => someAsyncOperation({ myArg: 'foo' });

return { foo: 42, ...getData() };

const getData2 = async () => {
await someAsyncOperation({ myArg: 'foo' });
};

return { foo: 42, ...getData2() };
```

#### ✅ Correct

```ts
const getData = () => someAsyncOperation({ myArg: 'foo' });

return { foo: 42, ...(await getData()) };

const getData2 = async () => {
await someAsyncOperation({ myArg: 'foo' });
};

return { foo: 42, ...(await getData2()) };
```

<!--tabs-->

## When Not To Use It

If you do not use Promises in your codebase or are not concerned with possible
Expand Down
34 changes: 32 additions & 2 deletions packages/eslint-plugin/src/rules/no-misused-promises.ts
Expand Up @@ -8,6 +8,7 @@ type Options = [
{
checksConditionals?: boolean;
checksVoidReturn?: boolean | ChecksVoidReturnOptions;
checksSpreads?: boolean;
},
];

Expand All @@ -25,7 +26,8 @@ type MessageId =
| 'voidReturnVariable'
| 'voidReturnProperty'
| 'voidReturnReturnValue'
| 'voidReturnAttribute';
| 'voidReturnAttribute'
| 'spread';

function parseChecksVoidReturn(
checksVoidReturn: boolean | ChecksVoidReturnOptions | undefined,
Expand Down Expand Up @@ -75,6 +77,7 @@ export default util.createRule<Options, MessageId>({
voidReturnAttribute:
'Promise-returning function provided to attribute where a void return was expected.',
conditional: 'Expected non-Promise value in a boolean conditional.',
spread: 'Expected a non-Promise value to be spreaded in an object.',
},
schema: [
{
Expand All @@ -99,6 +102,9 @@ export default util.createRule<Options, MessageId>({
},
],
},
checksSpreads: {
type: 'boolean',
},
},
},
],
Expand All @@ -108,10 +114,11 @@ export default util.createRule<Options, MessageId>({
{
checksConditionals: true,
checksVoidReturn: true,
checksSpreads: true,
},
],

create(context, [{ checksConditionals, checksVoidReturn }]) {
create(context, [{ checksConditionals, checksVoidReturn, checksSpreads }]) {
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();

Expand Down Expand Up @@ -153,6 +160,10 @@ export default util.createRule<Options, MessageId>({
}
: {};

const spreadChecks: TSESLint.RuleListener = {
SpreadElement: checkSpread,
};

function checkTestConditional(node: {
test: TSESTree.Expression | null;
}): void {
Expand Down Expand Up @@ -376,9 +387,28 @@ export default util.createRule<Options, MessageId>({
}
}

function checkSpread(node: TSESTree.SpreadElement): void {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);

if (
node.argument.type === AST_NODE_TYPES.CallExpression &&
tsutils.isThenableType(
checker,
tsNode.expression,
checker.getTypeAtLocation(tsNode.expression),
)
) {
context.report({
messageId: 'spread',
node: node.argument,
});
}
}

return {
...(checksConditionals ? conditionalChecks : {}),
...(checksVoidReturn ? voidReturnChecks : {}),
...(checksSpreads ? spreadChecks : {}),
};
},
});
Expand Down
55 changes: 55 additions & 0 deletions packages/eslint-plugin/tests/rules/no-misused-promises.test.ts
Expand Up @@ -316,6 +316,34 @@ const _ = <Component onEvent={async () => {}} />;
`,
filename: 'react.tsx',
},
`
console.log({ ...(await Promise.resolve({ key: 42 })) });
`,
`
const getData = Promise.resolve({ key: 42 });

console.log({
someData: 42,
...(await getData()),
});
`,
{
code: `
console.log({ ...Promise.resolve({ key: 42 }) });
`,
options: [{ checksSpreads: false }],
},
{
code: `
const getData = Promise.resolve({ key: 42 });

console.log({
someData: 42,
...getData(),
});
`,
options: [{ checksSpreads: false }],
},
],

invalid: [
Expand Down Expand Up @@ -870,5 +898,32 @@ it('', async () => {});
},
],
},
{
code: `
console.log({ ...Promise.resolve({ key: 42 }) });
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
`,
errors: [
{
line: 2,
messageId: 'spread',
},
],
},
{
code: `
const getData = () => Promise.resolve({ key: 42 });

console.log({
someData: 42,
...getData(),
});
`,
errors: [
{
line: 6,
messageId: 'spread',
},
],
},
],
});