From 359e0527c44d1922423e346029612e3ce05e4359 Mon Sep 17 00:00:00 2001 From: gyumong Date: Sun, 27 Oct 2024 17:30:53 +0900 Subject: [PATCH 01/14] fix(eslint-plugin): correctly report errors for async methods returning Promise where void is expected --- .../src/rules/no-misused-promises.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index c80b253c650e..d07645905bb5 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -6,6 +6,7 @@ import * as ts from 'typescript'; import { createRule, + getFunctionHeadLoc, getParserServices, isArrayMethodCallWithPredicate, isFunction, @@ -490,10 +491,32 @@ export default createRule({ ); if (isVoidReturningFunctionType(checker, tsNode.name, contextualType)) { - context.report({ - node: node.value, - messageId: 'voidReturnProperty', - }); + const signature = checker.getSignatureFromDeclaration(tsNode); + if (signature) { + const returnType = checker.getReturnTypeOfSignature(signature); + if (tsutils.isThenableType(checker, tsNode, returnType)) { + const functionNode = node.value; + if (isFunction(functionNode)) { + if (functionNode.returnType) { + context.report({ + node: functionNode.returnType, + messageId: 'voidReturnProperty', + }); + } else { + context.report({ + loc: getFunctionHeadLoc(functionNode, context.sourceCode), + node: functionNode, + messageId: 'voidReturnProperty', + }); + } + } else { + context.report({ + node: node.value, + messageId: 'voidReturnProperty', + }); + } + } + } } return; } From d8c6635a9a1e8e415bdb4e4b993ba24e7347f868 Mon Sep 17 00:00:00 2001 From: gyumong Date: Sun, 27 Oct 2024 18:49:53 +0900 Subject: [PATCH 02/14] test add coverage --- .../tests/rules/no-misused-promises.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index ed685906ee99..c6c9e23cdbe5 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -2429,5 +2429,43 @@ arrayFn<() => void>( }, ], }, + { + code: ` +type HasVoidMethod = { + f(): void; +}; + +const o: HasVoidMethod = { + async f() { + return 3; + }, +}; + `, + errors: [ + { + line: 7, + messageId: 'voidReturnProperty', + }, + ], + }, + { + code: ` +type HasVoidMethod = { + f(): void; +}; + +const o: HasVoidMethod = { + async f(): Promise { + return 3; + }, +}; + `, + errors: [ + { + line: 7, + messageId: 'voidReturnProperty', + }, + ], + }, ], }); From 3d26e12a0cbd6b18a28a443e70ef2354be93197e Mon Sep 17 00:00:00 2001 From: gyumong Date: Sun, 27 Oct 2024 20:23:13 +0900 Subject: [PATCH 03/14] fix: remove unreachable code --- packages/eslint-plugin/src/rules/no-misused-promises.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index d07645905bb5..d52f4b03a438 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -509,11 +509,6 @@ export default createRule({ messageId: 'voidReturnProperty', }); } - } else { - context.report({ - node: node.value, - messageId: 'voidReturnProperty', - }); } } } From a5dbd8761d018d5cc1620a3b3a06d9ed7975eff7 Mon Sep 17 00:00:00 2001 From: gyumong Date: Mon, 28 Oct 2024 03:45:55 +0900 Subject: [PATCH 04/14] fix: simplify redundant if checks in context.report logic --- .../src/rules/no-misused-promises.ts | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index d52f4b03a438..80d377a9b25d 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -7,6 +7,7 @@ import * as ts from 'typescript'; import { createRule, getFunctionHeadLoc, + getFunctionHeadLocation, getParserServices, isArrayMethodCallWithPredicate, isFunction, @@ -43,6 +44,19 @@ type MessageId = | 'voidReturnReturnValue' | 'voidReturnVariable'; +function findFunctionNode( + node: TSESTree.Node, +): + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression { + let current: TSESTree.Node | undefined = node; + while (current && !isFunction(current)) { + current = current.parent; + } + return nullThrows(current, NullThrowsReasons.MissingParent); +} + function parseChecksVoidReturn( checksVoidReturn: boolean | ChecksVoidReturnOptions | undefined, ): ChecksVoidReturnOptions | false { @@ -491,26 +505,19 @@ export default createRule({ ); if (isVoidReturningFunctionType(checker, tsNode.name, contextualType)) { - const signature = checker.getSignatureFromDeclaration(tsNode); - if (signature) { - const returnType = checker.getReturnTypeOfSignature(signature); - if (tsutils.isThenableType(checker, tsNode, returnType)) { - const functionNode = node.value; - if (isFunction(functionNode)) { - if (functionNode.returnType) { - context.report({ - node: functionNode.returnType, - messageId: 'voidReturnProperty', - }); - } else { - context.report({ - loc: getFunctionHeadLoc(functionNode, context.sourceCode), - node: functionNode, - messageId: 'voidReturnProperty', - }); - } - } - } + const functionNode = findFunctionNode(node.value); + + if (functionNode.returnType) { + context.report({ + node: functionNode.returnType, + messageId: 'voidReturnProperty', + }); + } else { + context.report({ + loc: getFunctionHeadLoc(functionNode, context.sourceCode), + node: functionNode, + messageId: 'voidReturnProperty', + }); } } return; @@ -524,13 +531,8 @@ export default createRule({ } // syntactically ignore some known-good cases to avoid touching type info - const functionNode = (() => { - let current: TSESTree.Node | undefined = node.parent; - while (current && !isFunction(current)) { - current = current.parent; - } - return nullThrows(current, NullThrowsReasons.MissingParent); - })(); + const functionNode = findFunctionNode(node); + if ( functionNode.returnType && !isPossiblyFunctionType(functionNode.returnType) From a762f40e1b7d4ee01567c78942fddded4050208e Mon Sep 17 00:00:00 2001 From: gyumong Date: Mon, 28 Oct 2024 03:47:10 +0900 Subject: [PATCH 05/14] test: specify error location in voidReturnProperty test cases --- .../eslint-plugin/tests/rules/no-misused-promises.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index c6c9e23cdbe5..ccb5803ab4b9 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -2443,6 +2443,9 @@ const o: HasVoidMethod = { `, errors: [ { + column: 3, + endColumn: 10, + endLine: 7, line: 7, messageId: 'voidReturnProperty', }, @@ -2462,6 +2465,9 @@ const o: HasVoidMethod = { `, errors: [ { + column: 12, + endColumn: 29, + endLine: 7, line: 7, messageId: 'voidReturnProperty', }, From 432b792003a8ff98120a3140ef1e48b5d1169852 Mon Sep 17 00:00:00 2001 From: gyumong Date: Mon, 28 Oct 2024 04:28:05 +0900 Subject: [PATCH 06/14] fix lint --- packages/eslint-plugin/src/rules/no-misused-promises.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 80d377a9b25d..7cd53204b033 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -7,7 +7,6 @@ import * as ts from 'typescript'; import { createRule, getFunctionHeadLoc, - getFunctionHeadLocation, getParserServices, isArrayMethodCallWithPredicate, isFunction, @@ -47,9 +46,9 @@ type MessageId = function findFunctionNode( node: TSESTree.Node, ): + | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.ArrowFunctionExpression { + | TSESTree.FunctionExpression { let current: TSESTree.Node | undefined = node; while (current && !isFunction(current)) { current = current.parent; From 4c7d78741ac7ba9789c7239eb45c6b417fe90058 Mon Sep 17 00:00:00 2001 From: gyumong Date: Thu, 31 Oct 2024 20:18:20 +0900 Subject: [PATCH 07/14] =?UTF-8?q?test:=20add=20full=20loc=20(line,=20endLi?= =?UTF-8?q?ne,=20column,=20endColumn)=20to=20existing=20test=20cases?= =?UTF-8?q?=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/no-misused-promises.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index ccb5803ab4b9..5c51e0c367a2 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -1451,6 +1451,9 @@ const obj: O = { `, errors: [ { + column: 3, + endColumn: 10, + endLine: 4, line: 4, messageId: 'voidReturnProperty', }, @@ -1472,14 +1475,23 @@ function f(): O { `, errors: [ { + column: 5, + endColumn: 12, + endLine: 6, line: 6, messageId: 'voidReturnProperty', }, { + column: 8, + endColumn: 21, + endLine: 9, line: 9, messageId: 'voidReturnProperty', }, { + column: 5, + endColumn: 6, + endLine: 10, line: 10, messageId: 'voidReturnProperty', }, @@ -2465,7 +2477,7 @@ const o: HasVoidMethod = { `, errors: [ { - column: 12, + column: 14, endColumn: 29, endLine: 7, line: 7, From 65ae6285f95193c83a33625b73f96de7fda449e5 Mon Sep 17 00:00:00 2001 From: gyumong Date: Thu, 31 Oct 2024 20:24:41 +0900 Subject: [PATCH 08/14] test: add new invalid test cases for promise-returning methods without async --- .../tests/rules/no-misused-promises.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 5c51e0c367a2..4d2d525f40db 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -2485,5 +2485,47 @@ const o: HasVoidMethod = { }, ], }, + { + code: ` +type HasVoidMethod = { + f(): void; +}; +const obj: HasVoidMethod = { + f() { + return Promise.resolve('foo'); + }, +}; + `, + errors: [ + { + column: 2, + endColumn: 3, + endLine: 6, + line: 6, + messageId: 'voidReturnProperty', + }, + ], + }, + { + code: ` +type HasVoidMethod = { + f(): void; +}; +const obj: HasVoidMethod = { + f(): Promise { + throw new Error(); + }, +}; + `, + errors: [ + { + column: 8, + endColumn: 21, + endLine: 6, + line: 6, + messageId: 'voidReturnProperty', + }, + ], + }, ], }); From 801bf4c6d12eb948e5ff0ac9e5d08af364b386d1 Mon Sep 17 00:00:00 2001 From: gyumong Date: Thu, 31 Oct 2024 20:30:37 +0900 Subject: [PATCH 09/14] fix : test col --- .../eslint-plugin/tests/rules/no-misused-promises.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 4d2d525f40db..8c0ddbe42074 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -2498,8 +2498,8 @@ const obj: HasVoidMethod = { `, errors: [ { - column: 2, - endColumn: 3, + column: 3, + endColumn: 4, endLine: 6, line: 6, messageId: 'voidReturnProperty', From f367c202d94b1d2084309a4091786dc0c8641540 Mon Sep 17 00:00:00 2001 From: gyumong Date: Thu, 31 Oct 2024 20:33:09 +0900 Subject: [PATCH 10/14] fix: log --- .../src/rules/no-misused-promises.ts | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 7cd53204b033..db00b2786fcd 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -43,19 +43,6 @@ type MessageId = | 'voidReturnReturnValue' | 'voidReturnVariable'; -function findFunctionNode( - node: TSESTree.Node, -): - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression { - let current: TSESTree.Node | undefined = node; - while (current && !isFunction(current)) { - current = current.parent; - } - return nullThrows(current, NullThrowsReasons.MissingParent); -} - function parseChecksVoidReturn( checksVoidReturn: boolean | ChecksVoidReturnOptions | undefined, ): ChecksVoidReturnOptions | false { @@ -504,11 +491,11 @@ export default createRule({ ); if (isVoidReturningFunctionType(checker, tsNode.name, contextualType)) { - const functionNode = findFunctionNode(node.value); + const functionNode = node.value as TSESTree.FunctionExpression; if (functionNode.returnType) { context.report({ - node: functionNode.returnType, + node: functionNode.returnType.typeAnnotation, messageId: 'voidReturnProperty', }); } else { @@ -530,7 +517,13 @@ export default createRule({ } // syntactically ignore some known-good cases to avoid touching type info - const functionNode = findFunctionNode(node); + const functionNode = (() => { + let current: TSESTree.Node | undefined = node.parent; + while (current && !isFunction(current)) { + current = current.parent; + } + return nullThrows(current, NullThrowsReasons.MissingParent); + })(); if ( functionNode.returnType && From e79283cfb9e6f4a7e0e3d060a68636fb37984731 Mon Sep 17 00:00:00 2001 From: Gyumong <60845910+Gyumong@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:29:49 +0900 Subject: [PATCH 11/14] Update packages/eslint-plugin/src/rules/no-misused-promises.ts Co-authored-by: Kirk Waiblinger --- packages/eslint-plugin/src/rules/no-misused-promises.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index db00b2786fcd..199777bddb98 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -501,7 +501,6 @@ export default createRule({ } else { context.report({ loc: getFunctionHeadLoc(functionNode, context.sourceCode), - node: functionNode, messageId: 'voidReturnProperty', }); } From 2bc76666733cec2db3ffe2953b6d87ef41ee9996 Mon Sep 17 00:00:00 2001 From: gyumong Date: Mon, 4 Nov 2024 16:07:01 +0900 Subject: [PATCH 12/14] refactor(no-misused-promises): improve function type checking in void return validation --- .../src/rules/no-misused-promises.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 199777bddb98..f21870ea97cf 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -437,10 +437,25 @@ export default createRule({ ) && returnsThenable(checker, tsNode.initializer) ) { - context.report({ - node: node.value, - messageId: 'voidReturnProperty', - }); + if (isFunction(node.value)) { + const functionNode = node.value; + if (functionNode.returnType) { + context.report({ + node: functionNode.returnType.typeAnnotation, + messageId: 'voidReturnProperty', + }); + } else { + context.report({ + loc: getFunctionHeadLoc(functionNode, context.sourceCode), + messageId: 'voidReturnProperty', + }); + } + } else { + context.report({ + node: node.value, + messageId: 'voidReturnProperty', + }); + } } } else if (ts.isShorthandPropertyAssignment(tsNode)) { const contextualType = checker.getContextualType(tsNode.name); From 2da0b5cce529eab70d38b54fa3987b165ec56790 Mon Sep 17 00:00:00 2001 From: gyumong Date: Mon, 4 Nov 2024 16:33:25 +0900 Subject: [PATCH 13/14] test(no-misused-promises): add test cases for isFunction utility usage --- .../tests/rules/no-misused-promises.test.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 8c0ddbe42074..9cc0e46fc878 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -1405,6 +1405,9 @@ const obj: O = { `, errors: [ { + column: 3, + endColumn: 12, + endLine: 4, line: 4, messageId: 'voidReturnProperty', }, @@ -1419,6 +1422,9 @@ const obj: O = { `, errors: [ { + column: 3, + endColumn: 12, + endLine: 4, line: 4, messageId: 'voidReturnProperty', }, @@ -1482,8 +1488,8 @@ function f(): O { messageId: 'voidReturnProperty', }, { - column: 8, - endColumn: 21, + column: 5, + endColumn: 14, endLine: 9, line: 9, messageId: 'voidReturnProperty', @@ -1795,7 +1801,15 @@ const test: ReturnsRecord = () => { return { asynchronous: async () => {} }; }; `, - errors: [{ line: 5, messageId: 'voidReturnProperty' }], + errors: [ + { + column: 12, + endColumn: 32, + endLine: 5, + line: 5, + messageId: 'voidReturnProperty', + }, + ], }, { code: ` From c7ac4e67a0bf9890ed9a3056b915d63189e9992e Mon Sep 17 00:00:00 2001 From: gyumong Date: Tue, 5 Nov 2024 22:01:44 +0900 Subject: [PATCH 14/14] test(no-misused-promises): add test cases --- .../tests/rules/no-misused-promises.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 9cc0e46fc878..16ee46b5f01d 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -2541,5 +2541,40 @@ const obj: HasVoidMethod = { }, ], }, + { + code: ` +type O = { f: () => void }; +const asyncFunction = async () => 'foo'; +const obj: O = { + f: asyncFunction, +}; + `, + errors: [ + { + column: 6, + endColumn: 19, + endLine: 5, + line: 5, + messageId: 'voidReturnProperty', + }, + ], + }, + { + code: ` +type O = { f: () => void }; +const obj: O = { + f: async (): Promise => 'foo', +}; + `, + errors: [ + { + column: 16, + endColumn: 31, + endLine: 4, + line: 4, + messageId: 'voidReturnProperty', + }, + ], + }, ], });