diff --git a/packages/eslint-plugin/docs/rules/no-confusing-void-expression.mdx b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.mdx index 043b07521c6b..b791705dd348 100644 --- a/packages/eslint-plugin/docs/rules/no-confusing-void-expression.mdx +++ b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.mdx @@ -115,6 +115,24 @@ 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 test1(value: string): void { + return window.postMessage(value); +} + +const test2 = (value: string): void => window.postMessage(value); + +const test3 = (value: string): void => { + return window.postMessage(value); +}; +``` + ## 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. 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..b28fc4011221 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -14,11 +14,16 @@ import { nullThrows, NullThrowsReasons, } from '../util'; +import { + ancestorHasReturnType, + isValidFunctionExpressionReturnType, +} from '../util/explicitReturnTypeUtils'; export type Options = [ { ignoreArrowShorthand?: boolean; ignoreVoidOperator?: boolean; + ignoreVoidInVoid?: boolean; }, ]; @@ -32,6 +37,26 @@ export type MessageId = | 'invalidVoidExprWrapVoid' | 'voidExprWrapVoid'; +function findFunction( + node: TSESTree.Node | undefined, +): + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | null { + if (!node) { + return null; + } + if ( + node.type === AST_NODE_TYPES.FunctionExpression || + node.type === AST_NODE_TYPES.ArrowFunctionExpression || + node.type === AST_NODE_TYPES.FunctionDeclaration + ) { + return node; + } + return findFunction(node.parent); +} + export default createRule({ name: 'no-confusing-void-expression', meta: { @@ -72,6 +97,7 @@ export default createRule({ properties: { ignoreArrowShorthand: { type: 'boolean' }, ignoreVoidOperator: { type: 'boolean' }, + ignoreVoidInVoid: { type: 'boolean' }, }, additionalProperties: false, }, @@ -80,8 +106,13 @@ export default createRule({ fixable: 'code', hasSuggestions: true, }, - defaultOptions: [{ ignoreArrowShorthand: false, ignoreVoidOperator: false }], - + defaultOptions: [ + { + ignoreArrowShorthand: false, + ignoreVoidOperator: false, + ignoreVoidInVoid: false, + }, + ], create(context, [options]) { return { 'AwaitExpression, CallExpression, TaggedTemplateExpression'( @@ -98,6 +129,7 @@ export default createRule({ } const invalidAncestor = findInvalidAncestor(node); + if (invalidAncestor == null) { // void expression is in valid position return; @@ -112,6 +144,12 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { // handle arrow function shorthand + if (options.ignoreVoidInVoid) { + if (hasValidReturnType(invalidAncestor)) { + return; + } + } + if (options.ignoreVoidOperator) { // handle wrapping with `void` return context.report({ @@ -167,6 +205,14 @@ export default createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) { // handle return statement + if (options.ignoreVoidInVoid) { + const functionNode = findFunction(invalidAncestor); + + if (hasValidReturnType(functionNode)) { + return; + } + } + if (options.ignoreVoidOperator) { // handle wrapping with `void` return context.report({ @@ -376,5 +422,44 @@ export default createRule({ const type = getConstrainedTypeAtLocation(services, targetNode); return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike); } + + function hasValidReturnType( + node: + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | null, + ): boolean { + const services = getParserServices(context); + + if (node != null) { + const functionTSNode = services.esTreeNodeToTSNodeMap.get(node); + const functionType = services.getTypeAtLocation(node); + + if (functionTSNode.type) { + const signatures = tsutils.getCallSignaturesOfType(functionType); + + return !signatures.every(signature => + tsutils.isTypeFlagSet( + signature.getReturnType(), + ts.TypeFlags.Any | ts.TypeFlags.Unknown, + ), + ); + } + + if ( + ((node.type === AST_NODE_TYPES.FunctionExpression || + node.type === AST_NODE_TYPES.ArrowFunctionExpression) && + isValidFunctionExpressionReturnType(node, { + allowTypedFunctionExpressions: true, + })) || + ancestorHasReturnType(node) + ) { + return true; + } + } + + return false; + } }, }); diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-confusing-void-expression.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-confusing-void-expression.shot index bf20e61945c3..d044ead177c9 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-confusing-void-expression.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-confusing-void-expression.shot @@ -80,3 +80,18 @@ function doSomething() { console.log(void alert('Hello, world!')); " `; + +exports[`Validating rule docs no-confusing-void-expression.mdx code examples ESLint output 5`] = ` +"Options: { "ignoreVoidInVoid": true } + +function test1(value: string): void { + return window.postMessage(value); +} + +const test2 = (value: string): void => window.postMessage(value); + +const test3 = (value: string): void => { + return window.postMessage(value); +}; +" +`; 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 921ee833894c..6948cd15b781 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,325 @@ function cool(input: string) { } `, }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function test(): void { + return console.log('foo'); +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: "const test = (): void => console.log('foo');", + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +const test = (): void => { + return console.log('foo'); +}; + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +function test(): void { + { + return console.log('foo'); + } +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +const data = { + test(): void { + return console.log('foo'); + }, +}; + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +class Foo { + test(): void { + return console.log('foo'); + } +} + `, + }, + { + options: [{ ignoreVoidInVoid: true }], + code: ` +const test = function (): void { + return console.log('foo'); +}; + `, + }, + { + 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(); +} + `, + }, + { + 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 }], + code: ` +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 test: Foo = () => { + return console.log(); +}; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` +type Foo = () => void; +const test = (() => console.log()) as Foo; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` +type Foo = { + foo: () => void; +}; +const test = { + foo: () => console.log(), +} as Foo; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` +type Foo = { + foo: () => void; +}; +const test: Foo = { + foo: () => console.log(), +}; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` +const test = { + foo: () => console.log(), +} as { + foo: () => void; +}; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` +const test: { + foo: () => void; +} = { + foo: () => console.log(), +}; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` +type Foo = { + foo: { bar: () => void }; +}; +const test = { + foo: { bar: () => console.log() }, +} as Foo; + `, + options: [{ ignoreVoidInVoid: true }], + }, + { + code: ` +type Foo = { + foo: { bar: () => void }; +}; +const test: 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 =