From 3492cb6d14df0df47bd21aa3bdd76e09e52a182b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sat, 9 Mar 2024 17:04:29 +0900 Subject: [PATCH 01/28] feat: ignoreVoidInVoid option add --- .../src/rules/no-confusing-void-expression.ts | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index 38df4fb0dc44..c5ee30ae5634 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -19,6 +19,7 @@ export type Options = [ { ignoreArrowShorthand?: boolean; ignoreVoidOperator?: boolean; + ignoreVoidInVoid?: boolean; }, ]; @@ -72,6 +73,7 @@ export default createRule({ properties: { ignoreArrowShorthand: { type: 'boolean' }, ignoreVoidOperator: { type: 'boolean' }, + ignoreVoidInVoid: { type: 'boolean' }, }, additionalProperties: false, }, @@ -80,7 +82,13 @@ export default createRule({ fixable: 'code', hasSuggestions: true, }, - defaultOptions: [{ ignoreArrowShorthand: false, ignoreVoidOperator: false }], + defaultOptions: [ + { + ignoreArrowShorthand: false, + ignoreVoidOperator: false, + ignoreVoidInVoid: false, + }, + ], create(context, [options]) { return { @@ -112,6 +120,15 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { // handle arrow function shorthand + if (options.ignoreVoidInVoid) { + if ( + invalidAncestor.returnType?.typeAnnotation.type === + AST_NODE_TYPES.TSVoidKeyword + ) { + return; + } + } + if (options.ignoreVoidOperator) { // handle wrapping with `void` return context.report({ @@ -167,6 +184,26 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) { // handle return statement + if (options.ignoreVoidInVoid) { + if ( + invalidAncestor.parent.parent?.type === + AST_NODE_TYPES.FunctionDeclaration && + invalidAncestor.parent.parent.returnType?.typeAnnotation.type === + AST_NODE_TYPES.TSVoidKeyword + ) { + return; + } + + if ( + invalidAncestor.parent.parent?.type === + AST_NODE_TYPES.ArrowFunctionExpression && + invalidAncestor.parent.parent.returnType?.typeAnnotation.type === + AST_NODE_TYPES.TSVoidKeyword + ) { + return; + } + } + if (options.ignoreVoidOperator) { // handle wrapping with `void` return context.report({ From 7bc38a40eafa5592dcb881cddcfe2ac1fde59fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sat, 9 Mar 2024 17:04:44 +0900 Subject: [PATCH 02/28] test: ignoreVoidInVoid test case add --- .../no-confusing-void-expression.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index c03d89ed6a61..5ec82f072e8d 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -119,6 +119,34 @@ function cool(input: string) { } `, }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function returnsVoid(): void {} + +function test1(): void { + return returnsVoid(); // should be fine +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function returnsVoid(): void {} + +const test2 = (): void => returnsVoid(); + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function returnsVoid(): void {} + +const test2 = (): void => { + return returnsVoid(); +}; + `, + }, ], invalid: [ From 8c4b72f3ec726b263234b85a658cf33209c1fb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sat, 9 Mar 2024 17:05:01 +0900 Subject: [PATCH 03/28] docs: ignoreVoidInVoid description add --- .../rules/no-confusing-void-expression.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md index ef2b51b64724..df7cb7442270 100644 --- a/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md @@ -109,6 +109,29 @@ function doSomething() { console.log(void alert('Hello, world!')); ``` +### `ignoreVoidInVoid` + +Allow using `void` type expressions in return value of a function that specified as `void` + +Examples of additional **correct** code with this option enabled: + +```ts option='{ "ignoreVoidInVoid": true }' showPlaygroundButton +function returnsVoid(): void {} + +// type is correct with void, so not evoke error +function test1(): void { + return returnsVoid(); +} + +// type is correct with void, so not evoke error +const test2 = (): void => returnsVoid(); + +// type is correct with void, so not evoke error +const test3 = (): void => { + return returnsVoid(); +}; +``` + ## When Not To Use It The return type of a function can be inspected by going to its definition or hovering over it in an IDE. From 028aa935fbfd520bf90f9bc787284388dab3195d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sat, 9 Mar 2024 18:13:51 +0900 Subject: [PATCH 04/28] fix: snapshot update --- .../tests/schema-snapshots/no-confusing-void-expression.shot | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-confusing-void-expression.shot b/packages/eslint-plugin/tests/schema-snapshots/no-confusing-void-expression.shot index b7481d1000ce..9342d33fb981 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-confusing-void-expression.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-confusing-void-expression.shot @@ -11,6 +11,9 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "ignoreArrowShorthand": { "type": "boolean" }, + "ignoreVoidInVoid": { + "type": "boolean" + }, "ignoreVoidOperator": { "type": "boolean" } @@ -25,6 +28,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { ignoreArrowShorthand?: boolean; + ignoreVoidInVoid?: boolean; ignoreVoidOperator?: boolean; }, ]; From 7babaf94790b621152298c885f0cf1e07848756c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sat, 9 Mar 2024 20:00:02 +0900 Subject: [PATCH 05/28] test: add test code --- .../no-confusing-void-expression.test.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index 5ec82f072e8d..a7f0c2c5b9cc 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -501,5 +501,73 @@ function notcool(input: string) { }, ], }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function returnsVoid(): void {} + +function test1() { + return returnsVoid(); // should be fine +} + `, + errors: [ + { + line: 5, + column: 10, + messageId: 'invalidVoidExprReturnLast', + }, + ], + output: ` +function returnsVoid(): void {} + +function test1() { + returnsVoid(); // should be fine +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function returnsVoid(): void {} + +const test2 = () => returnsVoid(); + `, + errors: [ + { + line: 4, + column: 21, + messageId: 'invalidVoidExprArrow', + }, + ], + output: ` +function returnsVoid(): void {} + +const test2 = () => { returnsVoid(); }; + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function returnsVoid(): void {} + +const test3 = () => { + return returnsVoid(); +}; + `, + errors: [ + { + line: 5, + column: 10, + messageId: 'invalidVoidExprReturnLast', + }, + ], + output: ` +function returnsVoid(): void {} + +const test3 = () => { + returnsVoid(); +}; + `, + }, ], }); From 0030a7b813ad916e5ef2b24a826769ffc8612342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sun, 10 Mar 2024 13:25:49 +0900 Subject: [PATCH 06/28] docs: edit example --- .../docs/rules/no-confusing-void-expression.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md index df7cb7442270..59d4b0174a6c 100644 --- a/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md @@ -116,19 +116,17 @@ Allow using `void` type expressions in return value of a function that specified Examples of additional **correct** code with this option enabled: ```ts option='{ "ignoreVoidInVoid": true }' showPlaygroundButton -function returnsVoid(): void {} - // type is correct with void, so not evoke error -function test1(): void { - return returnsVoid(); +function test1(value: string): void { + return window.postMessage(value); } // type is correct with void, so not evoke error -const test2 = (): void => returnsVoid(); +const test2 = (value: string): void => window.postMessage(value); // type is correct with void, so not evoke error -const test3 = (): void => { - return returnsVoid(); +const test3 = (value: string): void => { + return window.postMessage(value); }; ``` From 3ed4122718f3e955e996064c43fc550b7c282ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sun, 10 Mar 2024 13:33:51 +0900 Subject: [PATCH 07/28] fix: functionExprssion and inside block return fix --- .../src/rules/no-confusing-void-expression.ts | 49 ++++++++++++++----- .../no-confusing-void-expression.test.ts | 46 +++++++++++++++++ 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index c5ee30ae5634..72404296a9f2 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -186,19 +186,11 @@ export default createRule({ if (options.ignoreVoidInVoid) { if ( - invalidAncestor.parent.parent?.type === - AST_NODE_TYPES.FunctionDeclaration && - invalidAncestor.parent.parent.returnType?.typeAnnotation.type === - AST_NODE_TYPES.TSVoidKeyword - ) { - return; - } - - if ( - invalidAncestor.parent.parent?.type === - AST_NODE_TYPES.ArrowFunctionExpression && - invalidAncestor.parent.parent.returnType?.typeAnnotation.type === - AST_NODE_TYPES.TSVoidKeyword + targetNodeAncestorHasVoidReturnType(invalidAncestor, [ + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionExpression, + ]) ) { return; } @@ -413,5 +405,36 @@ export default createRule({ const type = getConstrainedTypeAtLocation(services, targetNode); return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike); } + + function targetNodeAncestorHasVoidReturnType( + node: TSESTree.Node, + targets: ( + | AST_NODE_TYPES.FunctionDeclaration + | AST_NODE_TYPES.ArrowFunctionExpression + | AST_NODE_TYPES.FunctionExpression + )[], + ): boolean { + if (!node.parent) { + return false; + } + + const filter = targets.filter(target => { + if (target === node.parent.type) { + if ( + node.parent.returnType?.typeAnnotation.type === + AST_NODE_TYPES.TSVoidKeyword + ) { + return true; + } + } + return false; + }); + + if (filter.length > 0) { + return true; + } + + return targetNodeAncestorHasVoidReturnType(node.parent, targets); + } }, }); diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index a7f0c2c5b9cc..7fbe294ca645 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -144,6 +144,52 @@ function returnsVoid(): void {} const test2 = (): void => { return returnsVoid(); +}; + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +declare function returnsVoid(): void; + +function test(): void { + { + return returnsVoid(); + } +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +declare function returnsVoid(): void; + +const data = { + foo(): void { + return returnsVoid(); + }, +}; + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +declare function returnsVoid(): void; + +class Foo { + foo(): void { + return returnsVoid(); + } +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +declare function returnsVoid(): void; + +const foo = function (): void { + return returnsVoid(); }; `, }, From b76656a78629d4ff0ac6724ee7835a42d0cd0d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sun, 10 Mar 2024 14:09:26 +0900 Subject: [PATCH 08/28] test: change void expression to console.log --- .../no-confusing-void-expression.test.ts | 84 ++++++------------- 1 file changed, 26 insertions(+), 58 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index 7fbe294ca645..c3eb2f14ddef 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -122,39 +122,29 @@ function cool(input: string) { { options: [{ ignoreVoidInVoid: true }], code: ` -function returnsVoid(): void {} - -function test1(): void { - return returnsVoid(); // should be fine +function test(): void { + return console.log('foo'); } `, }, { options: [{ ignoreVoidInVoid: true }], - code: ` -function returnsVoid(): void {} - -const test2 = (): void => returnsVoid(); - `, + code: "const test = (): void => console.log('foo');", }, { options: [{ ignoreVoidInVoid: true }], code: ` -function returnsVoid(): void {} - -const test2 = (): void => { - return returnsVoid(); +const test = (): void => { + return console.log('foo'); }; `, }, { options: [{ ignoreVoidInVoid: true }], code: ` -declare function returnsVoid(): void; - function test(): void { { - return returnsVoid(); + return console.log('foo'); } } `, @@ -162,11 +152,9 @@ function test(): void { { options: [{ ignoreVoidInVoid: true }], code: ` -declare function returnsVoid(): void; - const data = { - foo(): void { - return returnsVoid(); + test(): void { + return console.log('foo'); }, }; `, @@ -174,11 +162,9 @@ const data = { { options: [{ ignoreVoidInVoid: true }], code: ` -declare function returnsVoid(): void; - class Foo { - foo(): void { - return returnsVoid(); + test(): void { + return console.log('foo'); } } `, @@ -186,10 +172,8 @@ class Foo { { options: [{ ignoreVoidInVoid: true }], code: ` -declare function returnsVoid(): void; - -const foo = function (): void { - return returnsVoid(); +const test = function (): void { + return console.log('foo'); }; `, }, @@ -550,68 +534,52 @@ function notcool(input: string) { { options: [{ ignoreVoidInVoid: true }], code: ` -function returnsVoid(): void {} - -function test1() { - return returnsVoid(); // should be fine +function test() { + return console.log('foo'); } `, errors: [ { - line: 5, + line: 3, column: 10, messageId: 'invalidVoidExprReturnLast', }, ], output: ` -function returnsVoid(): void {} - -function test1() { - returnsVoid(); // should be fine +function test() { + console.log('foo'); } `, }, { options: [{ ignoreVoidInVoid: true }], - code: ` -function returnsVoid(): void {} - -const test2 = () => returnsVoid(); - `, + code: "const test = () => console.log('foo');", errors: [ { - line: 4, - column: 21, + line: 1, + column: 20, messageId: 'invalidVoidExprArrow', }, ], - output: ` -function returnsVoid(): void {} - -const test2 = () => { returnsVoid(); }; - `, + output: "const test = () => { console.log('foo'); };", }, { options: [{ ignoreVoidInVoid: true }], code: ` -function returnsVoid(): void {} - -const test3 = () => { - return returnsVoid(); +const test = () => { + return console.log('foo'); }; `, errors: [ { - line: 5, + line: 3, column: 10, messageId: 'invalidVoidExprReturnLast', }, ], output: ` -function returnsVoid(): void {} - -const test3 = () => { - returnsVoid(); +const test = () => { + console.log('foo'); }; `, }, From 27d8d5f0595dc9e8f15b54f6f56e2357f211a4fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sun, 10 Mar 2024 22:31:28 +0900 Subject: [PATCH 09/28] fix: apply code review --- .../src/rules/no-confusing-void-expression.ts | 58 +++++++++---------- .../no-confusing-void-expression.test.ts | 24 ++++++++ 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index 72404296a9f2..53f96684b6c0 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -120,13 +120,12 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { // handle arrow function shorthand - if (options.ignoreVoidInVoid) { - if ( - invalidAncestor.returnType?.typeAnnotation.type === + if ( + options.ignoreVoidInVoid && + invalidAncestor.returnType?.typeAnnotation.type === AST_NODE_TYPES.TSVoidKeyword - ) { - return; - } + ) { + return; } if (options.ignoreVoidOperator) { @@ -186,11 +185,7 @@ export default createRule({ if (options.ignoreVoidInVoid) { if ( - targetNodeAncestorHasVoidReturnType(invalidAncestor, [ - AST_NODE_TYPES.FunctionDeclaration, - AST_NODE_TYPES.ArrowFunctionExpression, - AST_NODE_TYPES.FunctionExpression, - ]) + targetNodeFunctionAncestorNodeHasVoidReturnType(invalidAncestor) ) { return; } @@ -406,35 +401,38 @@ export default createRule({ return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike); } - function targetNodeAncestorHasVoidReturnType( + function targetNodeFunctionAncestorNodeHasVoidReturnType( node: TSESTree.Node, - targets: ( - | AST_NODE_TYPES.FunctionDeclaration - | AST_NODE_TYPES.ArrowFunctionExpression - | AST_NODE_TYPES.FunctionExpression - )[], ): boolean { + const targets: [ + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionExpression, + ] = [ + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionExpression, + ]; + if (!node.parent) { return false; } - const filter = targets.filter(target => { - if (target === node.parent.type) { - if ( - node.parent.returnType?.typeAnnotation.type === - AST_NODE_TYPES.TSVoidKeyword - ) { - return true; - } + if ( + targets[0] === node.parent.type || + targets[1] === node.parent.type || + targets[2] === node.parent.type + ) { + if ( + node.parent.returnType?.typeAnnotation.type === + AST_NODE_TYPES.TSVoidKeyword + ) { + return true; } return false; - }); - - if (filter.length > 0) { - return true; } - return targetNodeAncestorHasVoidReturnType(node.parent, targets); + return targetNodeFunctionAncestorNodeHasVoidReturnType(node.parent); } }, }); diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index c3eb2f14ddef..9ec2942be997 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -583,5 +583,29 @@ const test = () => { }; `, }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function foo(): void { + const bar = () => { + return console.log(); + }; +} + `, + errors: [ + { + line: 4, + column: 12, + messageId: 'invalidVoidExprReturnLast', + }, + ], + output: ` +function foo(): void { + const bar = () => { + console.log(); + }; +} + `, + }, ], }); From 39c208058b00d8fa816066b8675cf3ede473afe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Mon, 11 Mar 2024 23:04:29 +0900 Subject: [PATCH 10/28] fix: add code review --- .../src/rules/no-confusing-void-expression.ts | 35 +++++------- .../no-confusing-void-expression.test.ts | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index 53f96684b6c0..7a6a468fc5b0 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -183,12 +183,11 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) { // handle return statement - if (options.ignoreVoidInVoid) { - if ( - targetNodeFunctionAncestorNodeHasVoidReturnType(invalidAncestor) - ) { - return; - } + if ( + options.ignoreVoidInVoid && + targetNodeFunctionAncestorNodeHasVoidReturnType(invalidAncestor) + ) { + return; } if (options.ignoreVoidOperator) { @@ -404,28 +403,22 @@ export default createRule({ function targetNodeFunctionAncestorNodeHasVoidReturnType( node: TSESTree.Node, ): boolean { - const targets: [ - AST_NODE_TYPES.FunctionDeclaration, - AST_NODE_TYPES.ArrowFunctionExpression, - AST_NODE_TYPES.FunctionExpression, - ] = [ - AST_NODE_TYPES.FunctionDeclaration, - AST_NODE_TYPES.ArrowFunctionExpression, - AST_NODE_TYPES.FunctionExpression, - ]; - if (!node.parent) { return false; } if ( - targets[0] === node.parent.type || - targets[1] === node.parent.type || - targets[2] === node.parent.type + AST_NODE_TYPES.FunctionDeclaration === node.parent.type || + AST_NODE_TYPES.ArrowFunctionExpression === node.parent.type || + AST_NODE_TYPES.FunctionExpression === node.parent.type ) { + const services = getParserServices(context); if ( - node.parent.returnType?.typeAnnotation.type === - AST_NODE_TYPES.TSVoidKeyword + node.parent.returnType && + !tsutils.isTypeFlagSet( + getConstrainedTypeAtLocation(services, node.parent.returnType), + ts.TypeFlags.VoidLike, + ) ) { return true; } diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index 9ec2942be997..3d7ac950273b 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -177,6 +177,47 @@ const test = function (): void { }; `, }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function test() { + function nestedTest(): void { + return console.log('foo'); + } +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +type Foo = void; + +function test(): Foo { + return console.log(); +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function returnVoid(): void {} + +function test(): void { + return returnVoid(); +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +type Foo = void; +function returnVoid(): void {} + +function test(): Foo { + return returnVoid(); +} + `, + }, ], invalid: [ @@ -607,5 +648,21 @@ function foo(): void { } `, }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +return console.log('foo'); + `, + errors: [ + { + line: 2, + column: 8, + messageId: 'invalidVoidExprReturn', + }, + ], + output: ` +{ console.log('foo'); return; } + `, + }, ], }); From 0f88a30acf490921b019293bdcc5b0ba04511d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Tue, 12 Mar 2024 00:48:59 +0900 Subject: [PATCH 11/28] test: add additional test code --- .../no-confusing-void-expression.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index 3d7ac950273b..8acf8aeb65f9 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -218,6 +218,31 @@ function test(): Foo { } `, }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function test(): void & void { + return console.log('foo'); +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +type Foo = void; +declare function foo(): Foo; +function test(): Foo { + return foo(); +} + `, + }, + { + options: [{ ignoreVoidInVoid: true, ignoreArrowShorthand: true }], + code: ` +type Foo = void; +const test: Foo = () => console.log('err'); + `, + }, ], invalid: [ From 0d547c942e992750ab3dbf567ff1c6eeeebe807d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Tue, 12 Mar 2024 00:57:49 +0900 Subject: [PATCH 12/28] test add test case --- .../tests/rules/no-confusing-void-expression.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index 8acf8aeb65f9..f87d0f33d57b 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -243,6 +243,13 @@ type Foo = void; const test: Foo = () => console.log('err'); `, }, + { + options: [{ ignoreVoidInVoid: true, ignoreArrowShorthand: true }], + code: ` +type Foo = void; +const test = (): Foo => console.log('err'); + `, + }, ], invalid: [ From a7e7de732c6f257ab585e8214b7fbb42a94719f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Tue, 12 Mar 2024 00:58:25 +0900 Subject: [PATCH 13/28] refactor: if statement to single return --- .../src/rules/no-confusing-void-expression.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index 7a6a468fc5b0..7dfe79b29a04 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -413,16 +413,13 @@ export default createRule({ AST_NODE_TYPES.FunctionExpression === node.parent.type ) { const services = getParserServices(context); - if ( - node.parent.returnType && + return ( + !!node.parent.returnType && !tsutils.isTypeFlagSet( getConstrainedTypeAtLocation(services, node.parent.returnType), ts.TypeFlags.VoidLike, ) - ) { - return true; - } - return false; + ); } return targetNodeFunctionAncestorNodeHasVoidReturnType(node.parent); From d72be7b6a33d6c90fa92abd028fc204765df2441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Tue, 12 Mar 2024 09:57:14 +0900 Subject: [PATCH 14/28] fix: variable declator expection --- .../src/rules/no-confusing-void-expression.ts | 45 ++++++++++++++----- .../no-confusing-void-expression.test.ts | 13 +++++- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index 7dfe79b29a04..d8640ca4eb82 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -106,6 +106,7 @@ export default createRule({ } const invalidAncestor = findInvalidAncestor(node); + if (invalidAncestor == null) { // void expression is in valid position return; @@ -120,12 +121,23 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { // handle arrow function shorthand - if ( - options.ignoreVoidInVoid && - invalidAncestor.returnType?.typeAnnotation.type === - AST_NODE_TYPES.TSVoidKeyword - ) { - return; + // console.log(invalidAncestor.parent.type === AST_NODE_TYPES.VariableDeclaration) + + if (options.ignoreVoidInVoid) { + if ( + invalidAncestor.returnType && + isVoidLikeType(invalidAncestor.returnType) + ) { + return; + } + if ( + !invalidAncestor.returnType && + invalidAncestor.parent.type === + AST_NODE_TYPES.VariableDeclarator && + isVoidLikeType(invalidAncestor.parent.id.typeAnnotation) + ) { + return; + } } if (options.ignoreVoidOperator) { @@ -400,6 +412,17 @@ export default createRule({ return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike); } + function isVoidLikeType(typeNode?: TSESTree.TSTypeAnnotation): boolean { + const services = getParserServices(context); + return ( + !!typeNode && + !tsutils.isTypeFlagSet( + getConstrainedTypeAtLocation(services, typeNode), + ts.TypeFlags.VoidLike, + ) + ); + } + function targetNodeFunctionAncestorNodeHasVoidReturnType( node: TSESTree.Node, ): boolean { @@ -412,13 +435,11 @@ export default createRule({ AST_NODE_TYPES.ArrowFunctionExpression === node.parent.type || AST_NODE_TYPES.FunctionExpression === node.parent.type ) { - const services = getParserServices(context); return ( - !!node.parent.returnType && - !tsutils.isTypeFlagSet( - getConstrainedTypeAtLocation(services, node.parent.returnType), - ts.TypeFlags.VoidLike, - ) + (node.parent.returnType && isVoidLikeType(node.parent.returnType)) || + (!node.parent.returnType && + node.parent.parent.type === AST_NODE_TYPES.VariableDeclarator && + isVoidLikeType(node.parent.parent.id.typeAnnotation)) ); } diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index f87d0f33d57b..6f4e580b3850 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -237,19 +237,28 @@ function test(): Foo { `, }, { - options: [{ ignoreVoidInVoid: true, ignoreArrowShorthand: true }], + options: [{ ignoreVoidInVoid: true }], code: ` type Foo = void; const test: Foo = () => console.log('err'); `, }, { - options: [{ ignoreVoidInVoid: true, ignoreArrowShorthand: true }], + options: [{ ignoreVoidInVoid: true }], code: ` type Foo = void; const test = (): Foo => console.log('err'); `, }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +type Foo = () => void; +const test: Foo = function () { + return console.log('err'); +}; + `, + }, ], invalid: [ From 48eb39425667377808896d62efbcb0ed106b354a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Wed, 13 Mar 2024 00:29:33 +0900 Subject: [PATCH 15/28] fix: code reivew apply --- .../src/rules/no-confusing-void-expression.ts | 15 +-------------- .../rules/no-confusing-void-expression.test.ts | 16 ---------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index d8640ca4eb82..a95881e5a0d8 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -121,8 +121,6 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { // handle arrow function shorthand - // console.log(invalidAncestor.parent.type === AST_NODE_TYPES.VariableDeclaration) - if (options.ignoreVoidInVoid) { if ( invalidAncestor.returnType && @@ -130,14 +128,6 @@ export default createRule({ ) { return; } - if ( - !invalidAncestor.returnType && - invalidAncestor.parent.type === - AST_NODE_TYPES.VariableDeclarator && - isVoidLikeType(invalidAncestor.parent.id.typeAnnotation) - ) { - return; - } } if (options.ignoreVoidOperator) { @@ -436,10 +426,7 @@ export default createRule({ AST_NODE_TYPES.FunctionExpression === node.parent.type ) { return ( - (node.parent.returnType && isVoidLikeType(node.parent.returnType)) || - (!node.parent.returnType && - node.parent.parent.type === AST_NODE_TYPES.VariableDeclarator && - isVoidLikeType(node.parent.parent.id.typeAnnotation)) + !!node.parent.returnType && isVoidLikeType(node.parent.returnType) ); } diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index 6f4e580b3850..55c227434634 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -240,25 +240,9 @@ function test(): Foo { options: [{ ignoreVoidInVoid: true }], code: ` type Foo = void; -const test: Foo = () => console.log('err'); - `, - }, - { - options: [{ ignoreVoidInVoid: true }], - code: ` -type Foo = void; const test = (): Foo => console.log('err'); `, }, - { - options: [{ ignoreVoidInVoid: true }], - code: ` -type Foo = () => void; -const test: Foo = function () { - return console.log('err'); -}; - `, - }, ], invalid: [ From 8a7a8e70712031a355403ac8242ded470547dbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Wed, 13 Mar 2024 22:43:08 +0900 Subject: [PATCH 16/28] docs: remove comment --- .../eslint-plugin/docs/rules/no-confusing-void-expression.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md index 59d4b0174a6c..0ca1ea9a0045 100644 --- a/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md @@ -116,15 +116,12 @@ Allow using `void` type expressions in return value of a function that specified Examples of additional **correct** code with this option enabled: ```ts option='{ "ignoreVoidInVoid": true }' showPlaygroundButton -// type is correct with void, so not evoke error function test1(value: string): void { return window.postMessage(value); } -// type is correct with void, so not evoke error const test2 = (value: string): void => window.postMessage(value); -// type is correct with void, so not evoke error const test3 = (value: string): void => { return window.postMessage(value); }; From f9c30dc9595e245afbddc7aa0d51926b28b816d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sun, 24 Mar 2024 18:07:46 +0900 Subject: [PATCH 17/28] feat: allow arrow function type --- .../src/rules/no-confusing-void-expression.ts | 53 ++++++- .../no-confusing-void-expression.test.ts | 142 ++++++++++++++++++ 2 files changed, 187 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index a95881e5a0d8..f2faf11fd502 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -14,6 +14,10 @@ import { nullThrows, NullThrowsReasons, } from '../util'; +import { + ancestorHasReturnType, + isValidFunctionExpressionReturnType, +} from '../util/explicitReturnTypeUtils'; export type Options = [ { @@ -33,6 +37,21 @@ export type MessageId = | 'invalidVoidExprWrapVoid' | 'voidExprWrapVoid'; +function findFunction( + node: TSESTree.Node | undefined, +): TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression | null { + if (!node) { + return null; + } + if ( + node.type === AST_NODE_TYPES.FunctionExpression || + node.type === AST_NODE_TYPES.ArrowFunctionExpression + ) { + return node; + } + return findFunction(node.parent); +} + export default createRule({ name: 'no-confusing-void-expression', meta: { @@ -89,7 +108,6 @@ export default createRule({ ignoreVoidInVoid: false, }, ], - create(context, [options]) { return { 'AwaitExpression, CallExpression, TaggedTemplateExpression'( @@ -123,8 +141,13 @@ export default createRule({ if (options.ignoreVoidInVoid) { if ( - invalidAncestor.returnType && - isVoidLikeType(invalidAncestor.returnType) + (invalidAncestor.returnType && + isVoidLikeType(invalidAncestor.returnType)) || + isValidFunctionExpressionReturnType(invalidAncestor, { + allowDirectConstAssertionInArrowFunctions: true, + allowTypedFunctionExpressions: true, + }) || + ancestorHasReturnType(invalidAncestor) ) { return; } @@ -185,11 +208,25 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) { // handle return statement - if ( - options.ignoreVoidInVoid && - targetNodeFunctionAncestorNodeHasVoidReturnType(invalidAncestor) - ) { - return; + if (options.ignoreVoidInVoid) { + const functionNode = findFunction(invalidAncestor); + + if ( + targetNodeFunctionAncestorNodeHasVoidReturnType(invalidAncestor) + ) { + return; + } + + if ( + functionNode != null && + (isValidFunctionExpressionReturnType(functionNode, { + allowDirectConstAssertionInArrowFunctions: true, + allowTypedFunctionExpressions: true, + }) || + ancestorHasReturnType(functionNode)) + ) { + return; + } } if (options.ignoreVoidOperator) { diff --git a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index 55c227434634..86c733ffb895 100644 --- a/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -243,6 +243,148 @@ type Foo = void; const test = (): Foo => console.log('err'); `, }, + { + code: ` + type Foo = () => void; + const arrowFn: Foo = () => console.log('foo'); + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type Foo = () => void; + const funcExpr: Foo = function () { + return console.log(); + }; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type Foo = () => void; + const funcExpr: Foo = () => { + return console.log(); + }; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type Foo = () => void; + const x = (() => console.log()) as Foo; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type Foo = { + foo: () => void; + }; + const q = { + foo: () => console.log(), + } as Foo; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type Foo = { + foo: () => void; + }; + const x: Foo = { + foo: () => console.log(), + }; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + const q = { + foo: () => console.log(), + } as { + foo: () => void; + }; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + const x: { + foo: () => void; + } = { + foo: () => console.log(), + }; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type Foo = { + foo: { bar: () => void }; + }; + const q = { + foo: { bar: () => console.log() }, + } as Foo; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type Foo = { + foo: { bar: () => void }; + }; + const x: Foo = { + foo: { bar: () => console.log() }, + }; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type MethodType = () => void; + + class App { + private method: MethodType = () => console.log(); + } + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + interface Foo { + foo: () => void; + } + function bar(): Foo { + return { + foo: () => console.log(), + }; + } + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + type HigherOrderType = () => () => () => void; + const x: HigherOrderType = () => () => () => console.log(); + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` + declare function foo(arg: () => void): void; + foo(() => console.log()); + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: 'const foo =