Skip to content

Commit 30a6951

Browse files
fix(eslint-plugin): [no-misused-promises] False negative in LogicalExpression (typescript-eslint#2682)
Fix typescript-eslint#2544
1 parent a2a2514 commit 30a6951

File tree

2 files changed

+119
-21
lines changed

2 files changed

+119
-21
lines changed

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

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
1+
import {
2+
AST_NODE_TYPES,
3+
TSESLint,
4+
TSESTree,
5+
} from '@typescript-eslint/experimental-utils';
26
import * as tsutils from 'tsutils';
37
import * as ts from 'typescript';
48

@@ -51,20 +55,16 @@ export default util.createRule<Options, 'conditional' | 'voidReturn'>({
5155
const parserServices = util.getParserServices(context);
5256
const checker = parserServices.program.getTypeChecker();
5357

58+
const checkedNodes = new Set<TSESTree.Node>();
59+
5460
const conditionalChecks: TSESLint.RuleListener = {
5561
ConditionalExpression: checkTestConditional,
5662
DoWhileStatement: checkTestConditional,
5763
ForStatement: checkTestConditional,
5864
IfStatement: checkTestConditional,
59-
LogicalExpression(node) {
60-
// We only check the lhs of a logical expression because the rhs might
61-
// be the return value of a short circuit expression.
62-
checkConditional(node.left);
63-
},
64-
UnaryExpression(node) {
65-
if (node.operator === '!') {
66-
checkConditional(node.argument);
67-
}
65+
LogicalExpression: checkConditional,
66+
'UnaryExpression[operator="!"]'(node: TSESTree.UnaryExpression) {
67+
checkConditional(node.argument, true);
6868
},
6969
WhileStatement: checkTestConditional,
7070
};
@@ -78,11 +78,37 @@ export default util.createRule<Options, 'conditional' | 'voidReturn'>({
7878
test: TSESTree.Expression | null;
7979
}): void {
8080
if (node.test) {
81-
checkConditional(node.test);
81+
checkConditional(node.test, true);
8282
}
8383
}
8484

85-
function checkConditional(node: TSESTree.Expression): void {
85+
/**
86+
* This function analyzes the type of a node and checks if it is a Promise in a boolean conditional.
87+
* It uses recursion when checking nested logical operators.
88+
* @param node The AST node to check.
89+
* @param isTestExpr Whether the node is a descendant of a test expression.
90+
*/
91+
function checkConditional(
92+
node: TSESTree.Expression,
93+
isTestExpr = false,
94+
): void {
95+
// prevent checking the same node multiple times
96+
if (checkedNodes.has(node)) {
97+
return;
98+
}
99+
checkedNodes.add(node);
100+
101+
if (node.type === AST_NODE_TYPES.LogicalExpression) {
102+
// ignore the left operand for nullish coalescing expressions not in a context of a test expression
103+
if (node.operator !== '??' || isTestExpr) {
104+
checkConditional(node.left, isTestExpr);
105+
}
106+
// we ignore the right operand when not in a context of a test expression
107+
if (isTestExpr) {
108+
checkConditional(node.right, isTestExpr);
109+
}
110+
return;
111+
}
86112
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
87113
if (isAlwaysThenable(checker, tsNode)) {
88114
context.report({

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

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ if (!Promise.resolve()) {
8383
options: [{ checksConditionals: false }],
8484
},
8585
'false || (true && Promise.resolve());',
86+
'(true && Promise.resolve()) || false;',
8687
`
8788
async function test() {
8889
if (await Promise.resolve()) {
@@ -139,6 +140,30 @@ if (returnsPromise?.()) {
139140
`
140141
declare const returnsPromise: { call: () => Promise<void> } | null;
141142
if (returnsPromise?.call()) {
143+
}
144+
`,
145+
'Promise.resolve() ?? false;',
146+
`
147+
function test(a: Promise<void> | undefinded) {
148+
const foo = a ?? Promise.reject();
149+
}
150+
`,
151+
`
152+
function test(p: Promise<boolean> | undefined, bool: boolean) {
153+
if (p ?? bool) {
154+
}
155+
}
156+
`,
157+
`
158+
async function test(p: Promise<boolean | undefined>, bool: boolean) {
159+
if ((await p) ?? bool) {
160+
}
161+
}
162+
`,
163+
`
164+
async function test(p: Promise<boolean> | undefined) {
165+
if (await (p ?? Promise.reject())) {
166+
}
142167
}
143168
`,
144169
],
@@ -231,15 +256,6 @@ if (!Promise.resolve()) {
231256
},
232257
],
233258
},
234-
{
235-
code: '(true && Promise.resolve()) || false;',
236-
errors: [
237-
{
238-
line: 1,
239-
messageId: 'conditional',
240-
},
241-
],
242-
},
243259
{
244260
code: `
245261
[Promise.resolve(), Promise.reject()].forEach(async val => {
@@ -360,5 +376,61 @@ fnWithCallback('val', (err, res) => {
360376
},
361377
],
362378
},
379+
{
380+
code: `
381+
function test(bool: boolean, p: Promise<void>) {
382+
if (bool || p) {
383+
}
384+
}
385+
`,
386+
errors: [
387+
{
388+
line: 3,
389+
messageId: 'conditional',
390+
},
391+
],
392+
},
393+
{
394+
code: `
395+
function test(bool: boolean, p: Promise<void>) {
396+
if (bool && p) {
397+
}
398+
}
399+
`,
400+
errors: [
401+
{
402+
line: 3,
403+
messageId: 'conditional',
404+
},
405+
],
406+
},
407+
{
408+
code: `
409+
function test(a: any, p: Promise<void>) {
410+
if (a ?? p) {
411+
}
412+
}
413+
`,
414+
errors: [
415+
{
416+
line: 3,
417+
messageId: 'conditional',
418+
},
419+
],
420+
},
421+
{
422+
code: `
423+
function test(p: Promise<void> | undefined) {
424+
if (p ?? Promise.reject()) {
425+
}
426+
}
427+
`,
428+
errors: [
429+
{
430+
line: 3,
431+
messageId: 'conditional',
432+
},
433+
],
434+
},
363435
],
364436
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy