Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 61ffa9e

Browse files
feat(eslint-plugin): [no-misused-promises] warn when spreading promises (typescript-eslint#5053)
* feat(eslint-plugin): warn when spreading promises * feat(eslint-plugin): fix typo * feat(eslint-plugin): handle logical expressions * feat(eslint-plugin): test spreading promises in arrays Co-authored-by: Josh Goldberg <[email protected]>
1 parent bc90ce0 commit 61ffa9e

File tree

3 files changed

+193
-5
lines changed

3 files changed

+193
-5
lines changed

packages/eslint-plugin/docs/rules/no-misused-promises.md

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ See [`no-floating-promises`](./no-floating-promises.md) for detecting unhandled
1313

1414
## Rule Details
1515

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

2020
## Options
2121

@@ -24,6 +24,7 @@ type Options = [
2424
{
2525
checksConditionals?: boolean;
2626
checksVoidReturn?: boolean | ChecksVoidReturnOptions;
27+
checksSpreads?: boolean;
2728
},
2829
];
2930

@@ -39,6 +40,7 @@ const defaultOptions: Options = [
3940
{
4041
checksConditionals: true,
4142
checksVoidReturn: true,
43+
checksSpreads: true,
4244
},
4345
];
4446
```
@@ -101,6 +103,21 @@ For example, if you don't mind that passing a `() => Promise<void>` to a `() =>
101103
}
102104
```
103105

106+
### `"checksSpreads"`
107+
108+
If you don't want to check object spreads, you can add this configuration:
109+
110+
```json
111+
{
112+
"@typescript-eslint/no-misused-promises": [
113+
"error",
114+
{
115+
"checksSpreads": false
116+
}
117+
]
118+
}
119+
```
120+
104121
### `checksConditionals: true`
105122

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

213230
<!--/tabs-->
214231

232+
### `checksSpreads: true`
233+
234+
Examples of code for this rule with `checksSpreads: true`:
235+
236+
<!--tabs-->
237+
238+
#### ❌ Incorrect
239+
240+
```ts
241+
const getData = () => someAsyncOperation({ myArg: 'foo' });
242+
243+
return { foo: 42, ...getData() };
244+
245+
const getData2 = async () => {
246+
await someAsyncOperation({ myArg: 'foo' });
247+
};
248+
249+
return { foo: 42, ...getData2() };
250+
```
251+
252+
#### ✅ Correct
253+
254+
```ts
255+
const getData = () => someAsyncOperation({ myArg: 'foo' });
256+
257+
return { foo: 42, ...(await getData()) };
258+
259+
const getData2 = async () => {
260+
await someAsyncOperation({ myArg: 'foo' });
261+
};
262+
263+
return { foo: 42, ...(await getData2()) };
264+
```
265+
266+
<!--tabs-->
267+
215268
## When Not To Use It
216269

217270
If you do not use Promises in your codebase or are not concerned with possible

packages/eslint-plugin/src/rules/no-misused-promises.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type Options = [
88
{
99
checksConditionals?: boolean;
1010
checksVoidReturn?: boolean | ChecksVoidReturnOptions;
11+
checksSpreads?: boolean;
1112
},
1213
];
1314

@@ -25,7 +26,8 @@ type MessageId =
2526
| 'voidReturnVariable'
2627
| 'voidReturnProperty'
2728
| 'voidReturnReturnValue'
28-
| 'voidReturnAttribute';
29+
| 'voidReturnAttribute'
30+
| 'spread';
2931

3032
function parseChecksVoidReturn(
3133
checksVoidReturn: boolean | ChecksVoidReturnOptions | undefined,
@@ -75,6 +77,7 @@ export default util.createRule<Options, MessageId>({
7577
voidReturnAttribute:
7678
'Promise-returning function provided to attribute where a void return was expected.',
7779
conditional: 'Expected non-Promise value in a boolean conditional.',
80+
spread: 'Expected a non-Promise value to be spreaded in an object.',
7881
},
7982
schema: [
8083
{
@@ -99,6 +102,9 @@ export default util.createRule<Options, MessageId>({
99102
},
100103
],
101104
},
105+
checksSpreads: {
106+
type: 'boolean',
107+
},
102108
},
103109
},
104110
],
@@ -108,10 +114,11 @@ export default util.createRule<Options, MessageId>({
108114
{
109115
checksConditionals: true,
110116
checksVoidReturn: true,
117+
checksSpreads: true,
111118
},
112119
],
113120

114-
create(context, [{ checksConditionals, checksVoidReturn }]) {
121+
create(context, [{ checksConditionals, checksVoidReturn, checksSpreads }]) {
115122
const parserServices = util.getParserServices(context);
116123
const checker = parserServices.program.getTypeChecker();
117124

@@ -153,6 +160,10 @@ export default util.createRule<Options, MessageId>({
153160
}
154161
: {};
155162

163+
const spreadChecks: TSESLint.RuleListener = {
164+
SpreadElement: checkSpread,
165+
};
166+
156167
function checkTestConditional(node: {
157168
test: TSESTree.Expression | null;
158169
}): void {
@@ -376,13 +387,37 @@ export default util.createRule<Options, MessageId>({
376387
}
377388
}
378389

390+
function checkSpread(node: TSESTree.SpreadElement): void {
391+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
392+
393+
if (isSometimesThenable(checker, tsNode.expression)) {
394+
context.report({
395+
messageId: 'spread',
396+
node: node.argument,
397+
});
398+
}
399+
}
400+
379401
return {
380402
...(checksConditionals ? conditionalChecks : {}),
381403
...(checksVoidReturn ? voidReturnChecks : {}),
404+
...(checksSpreads ? spreadChecks : {}),
382405
};
383406
},
384407
});
385408

409+
function isSometimesThenable(checker: ts.TypeChecker, node: ts.Node): boolean {
410+
const type = checker.getTypeAtLocation(node);
411+
412+
for (const subType of tsutils.unionTypeParts(checker.getApparentType(type))) {
413+
if (tsutils.isThenableType(checker, node, subType)) {
414+
return true;
415+
}
416+
}
417+
418+
return false;
419+
}
420+
386421
// Variation on the thenable check which requires all forms of the type (read:
387422
// alternates in a union) to be thenable. Otherwise, you might be trying to
388423
// check if something is defined or undefined and get caught because one of the

packages/eslint-plugin/tests/rules/no-misused-promises.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,63 @@ const _ = <Component onEvent={async () => {}} />;
316316
`,
317317
filename: 'react.tsx',
318318
},
319+
`
320+
console.log({ ...(await Promise.resolve({ key: 42 })) });
321+
`,
322+
`
323+
const getData = Promise.resolve({ key: 42 });
324+
325+
console.log({
326+
someData: 42,
327+
...(await getData()),
328+
});
329+
`,
330+
`
331+
declare const condition: boolean;
332+
333+
console.log({ ...(condition && (await Promise.resolve({ key: 42 }))) });
334+
console.log({ ...(condition || (await Promise.resolve({ key: 42 }))) });
335+
console.log({ ...(condition ? {} : await Promise.resolve({ key: 42 })) });
336+
console.log({ ...(condition ? await Promise.resolve({ key: 42 }) : {}) });
337+
`,
338+
`
339+
console.log([...(await Promise.resolve(42))]);
340+
`,
341+
{
342+
code: `
343+
console.log({ ...Promise.resolve({ key: 42 }) });
344+
`,
345+
options: [{ checksSpreads: false }],
346+
},
347+
{
348+
code: `
349+
const getData = Promise.resolve({ key: 42 });
350+
351+
console.log({
352+
someData: 42,
353+
...getData(),
354+
});
355+
`,
356+
options: [{ checksSpreads: false }],
357+
},
358+
{
359+
code: `
360+
declare const condition: boolean;
361+
362+
console.log({ ...(condition && Promise.resolve({ key: 42 })) });
363+
console.log({ ...(condition || Promise.resolve({ key: 42 })) });
364+
console.log({ ...(condition ? {} : Promise.resolve({ key: 42 })) });
365+
console.log({ ...(condition ? Promise.resolve({ key: 42 }) : {}) });
366+
`,
367+
options: [{ checksSpreads: false }],
368+
},
369+
{
370+
code: `
371+
// This is invalid Typescript, but it shouldn't trigger this linter specifically
372+
console.log([...Promise.resolve(42)]);
373+
`,
374+
options: [{ checksSpreads: false }],
375+
},
319376
],
320377

321378
invalid: [
@@ -870,5 +927,48 @@ it('', async () => {});
870927
},
871928
],
872929
},
930+
{
931+
code: `
932+
console.log({ ...Promise.resolve({ key: 42 }) });
933+
`,
934+
errors: [
935+
{
936+
line: 2,
937+
messageId: 'spread',
938+
},
939+
],
940+
},
941+
{
942+
code: `
943+
const getData = () => Promise.resolve({ key: 42 });
944+
945+
console.log({
946+
someData: 42,
947+
...getData(),
948+
});
949+
`,
950+
errors: [
951+
{
952+
line: 6,
953+
messageId: 'spread',
954+
},
955+
],
956+
},
957+
{
958+
code: `
959+
declare const condition: boolean;
960+
961+
console.log({ ...(condition && Promise.resolve({ key: 42 })) });
962+
console.log({ ...(condition || Promise.resolve({ key: 42 })) });
963+
console.log({ ...(condition ? {} : Promise.resolve({ key: 42 })) });
964+
console.log({ ...(condition ? Promise.resolve({ key: 42 }) : {}) });
965+
`,
966+
errors: [
967+
{ line: 4, messageId: 'spread' },
968+
{ line: 5, messageId: 'spread' },
969+
{ line: 6, messageId: 'spread' },
970+
{ line: 7, messageId: 'spread' },
971+
],
972+
},
873973
],
874974
});

0 commit comments

Comments
 (0)