From 76c295be512e11cb8fa46893a770aee87dc11eb8 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Aug 2024 03:13:01 +0200 Subject: [PATCH 01/33] feat(eslint-plugin): add rule [strict-void-return] --- .../docs/rules/no-misused-promises.mdx | 3 +- .../docs/rules/strict-void-return.mdx | 433 +++ packages/eslint-plugin/src/configs/all.ts | 1 + .../src/configs/disable-type-checked.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-confusing-void-expression.ts | 128 +- .../src/rules/strict-void-return.ts | 940 ++++++ .../src/util/addBracesToArrowFix.ts | 14 + .../src/util/discardReturnValueFix.ts | 178 ++ .../src/util/getBaseClassMember.ts | 31 + .../src/util/getRangeWithParens.ts | 40 + .../src/util/getWrappingFixer.ts | 8 +- packages/eslint-plugin/src/util/index.ts | 8 +- .../eslint-plugin/src/util/walkStatements.ts | 60 + .../strict-void-return.shot | 302 ++ .../tests/fixtures/tsconfig.dom.json | 11 + .../tests/rules/strict-void-return.test.ts | 2763 +++++++++++++++++ .../schema-snapshots/strict-void-return.shot | 52 + 18 files changed, 4860 insertions(+), 115 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/strict-void-return.mdx create mode 100644 packages/eslint-plugin/src/rules/strict-void-return.ts create mode 100644 packages/eslint-plugin/src/util/addBracesToArrowFix.ts create mode 100644 packages/eslint-plugin/src/util/discardReturnValueFix.ts create mode 100644 packages/eslint-plugin/src/util/getBaseClassMember.ts create mode 100644 packages/eslint-plugin/src/util/getRangeWithParens.ts create mode 100644 packages/eslint-plugin/src/util/walkStatements.ts create mode 100644 packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot create mode 100644 packages/eslint-plugin/tests/fixtures/tsconfig.dom.json create mode 100644 packages/eslint-plugin/tests/rules/strict-void-return.test.ts create mode 100644 packages/eslint-plugin/tests/schema-snapshots/strict-void-return.shot diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index 3621829cb977..77a6c18fe2ff 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -249,4 +249,5 @@ You might consider using [ESLint disable comments](https://eslint.org/docs/lates ## Related To -- [`no-floating-promises`](./no-floating-promises.mdx) +- [`strict-void-return`](./strict-void-return.mdx) - A superset of this rule's `checksVoidReturn` option which also checks for non-Promise values. +- [`no-floating-promises`](./no-floating-promises.mdx) - Warns about unhandled promises in _statement_ positions. diff --git a/packages/eslint-plugin/docs/rules/strict-void-return.mdx b/packages/eslint-plugin/docs/rules/strict-void-return.mdx new file mode 100644 index 000000000000..4db97a1bfab2 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/strict-void-return.mdx @@ -0,0 +1,433 @@ +--- +description: 'Disallow passing a value-returning function in a position accepting a void function.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/strict-void-return** for documentation. + +## Rule Details + +TypeScript considers functions returning a value to be assignable to a function returning void. +Using this feature of TypeScript can lead to bugs or confusing code. + +## Examples + +### Unsafety + +Passing a value-returning function in a place expecting a void function can be unsound. + + + + +```ts +const bad: () => void = () => 2137; +const func = Math.random() > 0.1 ? bad : prompt; +const val = func(); +if (val) console.log(val.toUpperCase()); // ❌ Crash if bad was called +``` + + + + +```ts +const good: () => void = () => {}; +const func = Math.random() > 0.1 ? good : prompt; +const val = func(); +if (val) console.log(val.toUpperCase()); // ✅ No crash +``` + + + + +### Unhandled promises + +If a promise is returned from a callback that should return void, +it won't be awaited and its rejection will be silently ignored or crash the process depending on runtime. + + + + +```ts +declare function takesCallback(cb: () => void): void; + +takesCallback(async () => { + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); +}); +``` + + + + +```ts +declare function takesCallback(cb: () => void): void; + +takesCallback(() => { + (async () => { + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); + })().catch(console.error); +}); +``` + + + + +:::info +If you only care about promises, you can use the [`no-misused-promises`](no-misused-promises.mdx) rule instead. +::: + +:::tip +Use [`no-floating-promises`](no-floating-promises.mdx) to also enforce error handling of non-awaited promises. +::: + +### Ignored generators + +If a generator is returned from a void function it won't even be started. + + + + +```ts +declare function takesCallback(cb: () => void): void; + +takesCallback(function* () { + console.log('Hello'); + yield; + console.log('World'); +}); +``` + + + + +```ts +declare function takesCallback(cb: () => void): void; + +takesCallback(() => { + function* gen() { + console.log('Hello'); + yield; + console.log('World'); + } + for (const _ of gen()); +}); +``` + + + + +### Probable mistakes + +Returning a value from a void function is likely a mistake on part of the programmer. +This rule will often warn you early when using a function in a wrong way. + + + + +```ts +['Kazik', 'Zenek'].forEach(name => `Hello, ${name}!`); +``` + + + + +```ts +['Kazik', 'Zenek'].forEach(name => console.log(`Hello, ${name}!`)); +``` + + + + +## Options + +### `considerOtherOverloads` + +Whether to assume that an any-returning callback argument should be treated as a void callback, +if there exists another overload where it is typed as returning void. + +This is required to correctly detect `addEventListener`'s callback as void callback, +because otherwise the call always resolves to the any-returning signature. + +Additional incorrect code when the option is **enabled**: + + + + +```ts option='{ "considerOtherOverloads": true }' +/// + +document.addEventListener('click', () => { + return 'Clicked'; +}); +``` + + + + +```ts option='{ "considerOtherOverloads": true }' +/// + +document.addEventListener('click', () => { + console.log('Clicked'); +}); +``` + + + + +### `considerBaseClass` + +Whether to enforce class methods which override a void method to also be void. + +Additional incorrect code when the option is **enabled**: + + + + +```ts option='{ "considerBaseClass": true }' +/// + +class MyElement extends HTMLElement { + click() { + super.click(); + return 'Clicked'; + } +} +``` + + + + +```ts option='{ "considerBaseClass": true }' +/// + +class MyElement extends HTMLElement { + click() { + super.click(); + console.log('Clicked'); + } +} +``` + + + + +### `considerImplementedInterfaces` + +Whether to enforce class methods which implement a void method to also be void. + +Additional incorrect code when the option is **enabled**: + + + + +```ts option='{ "considerImplementedInterfaces": true }' +/// + +class FooListener implements EventListenerObject { + handleEvent() { + return 'Handled'; + } +} +``` + + + + +```ts option='{ "considerImplementedInterfaces": true }' +/// + +class FooListener implements EventListenerObject { + handleEvent() { + console.log('Handled'); + } +} +``` + + + + +### `allowReturnPromiseIfTryCatch` + +Whether to allow returned promises +if they are returned from an async function expression +whose whole body is wrapped in a try-catch block. + +This offers an alternative to an async IIFE for handling errors in async callbacks. + +Additional incorrect code when the option is **disabled**: + + + + +```ts option='{ "allowReturnPromiseIfTryCatch": false }' +const cb: () => void = async () => { + try { + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); + } catch (error) { + console.error(error); + } +}; +``` + + + + +```ts option='{ "allowReturnPromiseIfTryCatch": false }' +const cb: () => void = () => { + (async () => { + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); + })().catch(console.error); +}; +``` + + + + +### `allowReturnUndefined` + +Whether to allow a function returning `undefined` to be used in place expecting a `void` function. +When disabled, `void` operator can't be used to discard the return value, because it evaluates to undefined. +Disable this to enforce a consistent style across the codebase. + +:::note +Note that even when disabled it will still allow `return functionReturningVoid()`. +If you want to disallow that too, use the [`no-confusing-void-expression`](./no-confusing-void-expression.mdx) rule. +::: + +Additional incorrect code when the option is **disabled**: + + + + +```ts option='{ "allowReturnUndefined": false }' +let cb: () => void; + +cb = () => undefined; + +cb = () => { + return void 0; +}; +``` + + + + +```ts option='{ "allowReturnUndefined": false }' +let cb: () => void; + +cb = () => {}; + +cb = () => { + return; +}; +``` + + + + +### `allowReturnNull` + +Whether to allow a function returning `null` to be used in place expecting a `void` function. + +Additional incorrect code when the option is **disabled**: + + + + +```ts option='{ "allowReturnNull": false }' +let cb: () => void; + +cb = () => null; + +cb = () => { + return null; +}; +``` + + + + +```ts option='{ "allowReturnNull": false }' +let cb: () => void; + +cb = () => {}; + +cb = () => { + return; +}; +``` + + + + +### `allowReturnAny` + +Whether to allow a function returning `any` to be used in place expecting a `void` function. + +Additional incorrect code when the option is **disabled**: + + + + +```ts option='{ "allowReturnAny": false }' +declare function fn(cb: () => void): void; + +fn(() => JSON.parse('{}')); + +fn(() => { + return someUntypedApi(); +}); +``` + + + + +```ts option='{ "allowReturnAny": false }' +declare function fn(cb: () => void): void; + +fn(() => void JSON.parse('{}')); + +fn(() => { + someUntypedApi(); +}); +``` + + + + +## When Not To Use It + +Primitive values returned from void functions are usually safe. +If you don't care about returning them you can use [`no-misused-promises`](./no-misused-promises.mdx) instead. + +In browser context, an unhandled promise will be reported as an error in the console. +It's a always a good idea to also show some kind of indicator on the page that something went wrong, +but if you are just prototyping or don't care about that, the default behavior might be acceptable. +In such case, instead of handling the promises and `console.error`ing them anyways, you can just disable this rule. + +Similarly, the default behavior of crashing the process on unhandled promise rejection +might be acceptable when developing, for example, a CLI tool. +If your promise handlers simply call `process.exit(1)` on rejection, +you might as well not use this rule and rely on the default behavior. + +## Related To + +- [`no-misused-promises`](./no-misused-promises.mdx) - A subset of this rule which only cares about promises. +- [`no-floating-promises`](./no-floating-promises.mdx) - Warns about unhandled promises in _statement_ positions. +- [`no-confusing-void-expression`](./no-confusing-void-expression.mdx) - Disallows returning _void_ values. + +## Further Reading + +- [TypeScript FAQ - Void function assignability](https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-returning-non-void-assignable-to-function-returning-void) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 51b511f32074..07274b045697 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -153,6 +153,7 @@ export = { 'no-return-await': 'off', '@typescript-eslint/return-await': 'error', '@typescript-eslint/strict-boolean-expressions': 'error', + '@typescript-eslint/strict-void-return': 'error', '@typescript-eslint/switch-exhaustiveness-check': 'error', '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/typedef': 'error', diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index f0ba1bc0225e..457942bb617f 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -67,6 +67,7 @@ export = { '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/return-await': 'off', '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/strict-void-return': 'off', '@typescript-eslint/switch-exhaustiveness-check': 'off', '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index f7f064b0b34c..f5d63a1a8e96 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -142,6 +142,7 @@ import spaceBeforeBlocks from './space-before-blocks'; import spaceBeforeFunctionParen from './space-before-function-paren'; import spaceInfixOps from './space-infix-ops'; import strictBooleanExpressions from './strict-boolean-expressions'; +import strictVoidReturn from './strict-void-return'; import switchExhaustivenessCheck from './switch-exhaustiveness-check'; import tripleSlashReference from './triple-slash-reference'; import typeAnnotationSpacing from './type-annotation-spacing'; @@ -294,6 +295,7 @@ export default { 'space-before-function-paren': spaceBeforeFunctionParen, 'space-infix-ops': spaceInfixOps, 'strict-boolean-expressions': strictBooleanExpressions, + 'strict-void-return': strictVoidReturn, 'switch-exhaustiveness-check': switchExhaustivenessCheck, 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, 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..70bbe3e9a8f0 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -5,14 +5,15 @@ import * as ts from 'typescript'; import type { MakeRequired } from '../util'; import { + addBracesToArrowFix, createRule, getConstrainedTypeAtLocation, getParserServices, - isClosingParenToken, - isOpeningParenToken, - isParenthesized, + isFinalReturn, + moveValueBeforeReturnFix, nullThrows, NullThrowsReasons, + removeReturnLeaveValueFix, } from '../util'; export type Options = [ @@ -122,44 +123,18 @@ export default createRule({ } // handle wrapping with braces - const arrowFunction = invalidAncestor; return context.report({ node, messageId: 'invalidVoidExprArrow', fix(fixer) { - if (!canFix(arrowFunction)) { + if (!canFix(invalidAncestor)) { return null; } - const arrowBody = arrowFunction.body; - const arrowBodyText = context.sourceCode.getText(arrowBody); - const newArrowBodyText = `{ ${arrowBodyText}; }`; - if (isParenthesized(arrowBody, context.sourceCode)) { - const bodyOpeningParen = nullThrows( - context.sourceCode.getTokenBefore( - arrowBody, - isOpeningParenToken, - ), - NullThrowsReasons.MissingToken( - 'opening parenthesis', - 'arrow body', - ), - ); - const bodyClosingParen = nullThrows( - context.sourceCode.getTokenAfter( - arrowBody, - isClosingParenToken, - ), - NullThrowsReasons.MissingToken( - 'closing parenthesis', - 'arrow body', - ), - ); - return fixer.replaceTextRange( - [bodyOpeningParen.range[0], bodyClosingParen.range[1]], - newArrowBodyText, - ); - } - return fixer.replaceText(arrowBody, newArrowBodyText); + return addBracesToArrowFix( + fixer, + context.sourceCode, + invalidAncestor, + ); }, }); } @@ -185,14 +160,11 @@ export default createRule({ if (!canFix(invalidAncestor)) { return null; } - const returnValue = invalidAncestor.argument; - const returnValueText = context.sourceCode.getText(returnValue); - let newReturnStmtText = `${returnValueText};`; - if (isPreventingASI(returnValue)) { - // put a semicolon at the beginning of the line - newReturnStmtText = `;${newReturnStmtText}`; - } - return fixer.replaceText(invalidAncestor, newReturnStmtText); + return removeReturnLeaveValueFix( + fixer, + context.sourceCode, + invalidAncestor, + ); }, }); } @@ -202,21 +174,11 @@ export default createRule({ node, messageId: 'invalidVoidExprReturn', fix(fixer) { - const returnValue = invalidAncestor.argument; - const returnValueText = context.sourceCode.getText(returnValue); - let newReturnStmtText = `${returnValueText}; return;`; - if (isPreventingASI(returnValue)) { - // put a semicolon at the beginning of the line - newReturnStmtText = `;${newReturnStmtText}`; - } - if ( - invalidAncestor.parent.type !== AST_NODE_TYPES.BlockStatement - ) { - // e.g. `if (cond) return console.error();` - // add braces if not inside a block - newReturnStmtText = `{ ${newReturnStmtText} }`; - } - return fixer.replaceText(invalidAncestor, newReturnStmtText); + return moveValueBeforeReturnFix( + fixer, + context.sourceCode, + invalidAncestor, + ); }, }); } @@ -313,56 +275,6 @@ export default createRule({ return parent as InvalidAncestor; } - /** Checks whether the return statement is the last statement in a function body. */ - function isFinalReturn(node: TSESTree.ReturnStatement): boolean { - // the parent must be a block - const block = nullThrows(node.parent, NullThrowsReasons.MissingParent); - if (block.type !== AST_NODE_TYPES.BlockStatement) { - // e.g. `if (cond) return;` (not in a block) - return false; - } - - // the block's parent must be a function - const blockParent = nullThrows( - block.parent, - NullThrowsReasons.MissingParent, - ); - if ( - ![ - AST_NODE_TYPES.FunctionDeclaration, - AST_NODE_TYPES.FunctionExpression, - AST_NODE_TYPES.ArrowFunctionExpression, - ].includes(blockParent.type) - ) { - // e.g. `if (cond) { return; }` - // not in a top-level function block - return false; - } - - // must be the last child of the block - if (block.body.indexOf(node) < block.body.length - 1) { - // not the last statement in the block - return false; - } - - return true; - } - - /** - * Checks whether the given node, if placed on its own line, - * would prevent automatic semicolon insertion on the line before. - * - * This happens if the line begins with `(`, `[` or `` ` `` - */ - function isPreventingASI(node: TSESTree.Expression): boolean { - const startToken = nullThrows( - context.sourceCode.getFirstToken(node), - NullThrowsReasons.MissingToken('first token', node.type), - ); - - return ['(', '[', '`'].includes(startToken.value); - } - function canFix( node: ReturnStatementWithArgument | TSESTree.ArrowFunctionExpression, ): boolean { diff --git a/packages/eslint-plugin/src/rules/strict-void-return.ts b/packages/eslint-plugin/src/rules/strict-void-return.ts new file mode 100644 index 000000000000..4ae6be8edb32 --- /dev/null +++ b/packages/eslint-plugin/src/rules/strict-void-return.ts @@ -0,0 +1,940 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { + AST_NODE_TYPES, + AST_TOKEN_TYPES, + ASTUtils, +} from '@typescript-eslint/utils'; +import assert from 'assert'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import * as util from '../util'; + +type Options = [ + { + considerOtherOverloads?: boolean; + considerBaseClass?: boolean; + considerImplementedInterfaces?: boolean; + allowReturnPromiseIfTryCatch?: boolean; + allowReturnUndefined?: boolean; + allowReturnNull?: boolean; + allowReturnAny?: boolean; + }, +]; + +type ErrorPlaceId = + | 'Arg' + | 'ArgOverload' + | 'Attr' + | 'Var' + | 'Prop' + | 'Return' + | 'ExtMember' + | 'ImplMember'; + +type ErrorMessageId = + | `nonVoidReturnIn${ErrorPlaceId}` + | `asyncFuncIn${ErrorPlaceId}` + | `asyncNoTryCatchFuncIn${ErrorPlaceId}` + | `genFuncIn${ErrorPlaceId}` + | `nonVoidFuncIn${ErrorPlaceId}`; + +type SuggestionMessageId = 'suggestWrapInAsyncIIFE' | 'suggestWrapInTryCatch'; + +type MessageId = ErrorMessageId | SuggestionMessageId; + +export default util.createRule({ + name: 'strict-void-return', + meta: { + type: 'problem', + fixable: 'code', + hasSuggestions: true, + docs: { + description: + 'Disallow passing a value-returning function in a position accepting a void function', + requiresTypeChecking: true, + }, + messages: { + nonVoidReturnInArg: + 'Value returned in a callback argument where a void callback was expected.', + asyncFuncInArg: + 'Async callback passed as an argument where a void callback was expected.', + asyncNoTryCatchFuncInArg: + 'Async callback not wrapped with a try-catch block and passed as an argument where a void callback was expected.', + genFuncInArg: + 'Generator callback passed as an argument where a void callback was expected.', + nonVoidFuncInArg: + 'Value-returning callback passed as an argument where a void callback was expected.', + + nonVoidReturnInArgOverload: + 'Value returned in a callback argument where one of the function signatures suggests it should be a void callback.', + asyncFuncInArgOverload: + 'Async callback passed as an argument where one of the function signatures suggests it should be a void callback.', + asyncNoTryCatchFuncInArgOverload: + 'Async callback not wrapped with a try-catch block and passed as an argument where one of the function signatures suggests it should be a void callback.', + genFuncInArgOverload: + 'Generator callback passed as an argument where one of the function signatures suggests it should be a void callback.', + nonVoidFuncInArgOverload: + 'Value-returning callback passed as an argument where one of the function signatures suggests it should be a void callback.', + + nonVoidReturnInAttr: + 'Value returned in a callback attribute where a void callback was expected.', + asyncFuncInAttr: + 'Async callback passed as an attribute where a void callback was expected.', + asyncNoTryCatchFuncInAttr: + 'Async callback not wrapped with a try-catch block and passed as an attribute where a void callback was expected.', + genFuncInAttr: + 'Generator callback passed as an attribute where a void callback was expected.', + nonVoidFuncInAttr: + 'Value-returning callback passed as an attribute where a void callback was expected.', + + // Also used for array elements + nonVoidReturnInVar: + 'Value returned in a context where a void return was expected.', + asyncFuncInVar: + 'Async function used in a context where a void function is expected.', + asyncNoTryCatchFuncInVar: + 'Async function not wrapped with a try-catch block and used in a context where a void function is expected.', + genFuncInVar: + 'Generator function used in a context where a void function is expected.', + nonVoidFuncInVar: + 'Value-returning function used in a context where a void function is expected.', + + nonVoidReturnInProp: + 'Value returned in an object method which must be a void method.', + asyncFuncInProp: + 'Async function provided as an object method which must be a void method.', + asyncNoTryCatchFuncInProp: + 'Async function not wrapped with a try-catch block and provided as an object method which must be a void method.', + genFuncInProp: + 'Generator function provided as an object method which must be a void method.', + nonVoidFuncInProp: + 'Value-returning function provided as an object method which must be a void method.', + + nonVoidReturnInReturn: + 'Value returned in a callback returned from a function which must return a void callback', + asyncFuncInReturn: + 'Async callback returned from a function which must return a void callback.', + asyncNoTryCatchFuncInReturn: + 'Async callback not wrapped with a try-catch block and returned from a function which must return a void callback.', + genFuncInReturn: + 'Generator callback returned from a function which must return a void callback.', + nonVoidFuncInReturn: + 'Value-returning callback returned from a function which must return a void callback.', + + nonVoidReturnInExtMember: + 'Value returned in a method which overrides a void method.', + asyncFuncInExtMember: + 'Overriding a void method with an async method is forbidden.', + asyncNoTryCatchFuncInExtMember: + 'Overriding a void method with an async method requires wrapping the body in a try-catch block.', + genFuncInExtMember: + 'Overriding a void method with a generator method is forbidden.', + nonVoidFuncInExtMember: + 'Overriding a void method with a value-returning function is forbidden.', + + nonVoidReturnInImplMember: + 'Value returned in a method which implements a void method.', + asyncFuncInImplMember: + 'Implementing a void method as an async method is forbidden.', + asyncNoTryCatchFuncInImplMember: + 'Implementing a void method as an async method requires wrapping the body in a try-catch block.', + genFuncInImplMember: + 'Implementing a void method as a generator method is forbidden.', + nonVoidFuncInImplMember: + 'Implementing a void method as a value-returning function is forbidden.', + + suggestWrapInAsyncIIFE: + 'Wrap the function body in an immediately-invoked async function expression.', + suggestWrapInTryCatch: 'Wrap the function body in a try-catch block.', + }, + schema: [ + { + type: 'object', + properties: { + considerOtherOverloads: { type: 'boolean' }, + considerBaseClass: { type: 'boolean' }, + considerImplementedInterfaces: { type: 'boolean' }, + allowReturnPromiseIfTryCatch: { type: 'boolean' }, + allowReturnUndefined: { type: 'boolean' }, + allowReturnNull: { type: 'boolean' }, + allowReturnAny: { type: 'boolean' }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + considerOtherOverloads: true, + considerBaseClass: true, + considerImplementedInterfaces: true, + allowReturnPromiseIfTryCatch: true, + allowReturnUndefined: true, + allowReturnNull: true, + }, + ], + + create(context, [options]) { + const sourceCode = context.sourceCode; + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + return { + 'CallExpression, NewExpression': ( + node: TSESTree.CallExpression | TSESTree.NewExpression, + ): void => { + checkFunctionCallNode(node); + }, + JSXExpressionContainer: (node): void => { + if (node.expression.type !== AST_NODE_TYPES.JSXEmptyExpression) { + checkExpressionNode(node.expression, 'Attr'); + } + }, + VariableDeclarator: (node): void => { + if (node.init != null) { + checkExpressionNode(node.init, 'Var'); + } + }, + AssignmentExpression: (node): void => { + if (['=', '||=', '&&=', '??='].includes(node.operator)) { + checkExpressionNode(node.right, 'Var'); + } + }, + ObjectExpression: (node): void => { + for (const propNode of node.properties) { + if (propNode.type !== AST_NODE_TYPES.SpreadElement) { + checkObjectPropertyNode(propNode); + } + } + }, + ArrayExpression: (node): void => { + for (const elemNode of node.elements) { + if ( + elemNode != null && + elemNode.type !== AST_NODE_TYPES.SpreadElement + ) { + checkExpressionNode(elemNode, 'Var'); + } + } + }, + ArrowFunctionExpression: (node): void => { + if (node.body.type !== AST_NODE_TYPES.BlockStatement) { + checkExpressionNode(node.body, 'Return'); + } + }, + ReturnStatement: (node): void => { + if (node.argument != null) { + checkExpressionNode(node.argument, 'Return'); + } + }, + PropertyDefinition: (node): void => { + checkClassPropertyNode(node); + }, + MethodDefinition: (node): void => { + checkClassMethodNode(node); + }, + }; + + /** Checks whether the type is a void-returning function type. */ + function isVoidReturningFunctionType(type: ts.Type): boolean { + const returnTypes = tsutils + .getCallSignaturesOfType(type) + .flatMap(signature => + tsutils.unionTypeParts(signature.getReturnType()), + ); + return ( + returnTypes.some(type => + tsutils.isTypeFlagSet(type, ts.TypeFlags.Void), + ) && + returnTypes.every(type => + tsutils.isTypeFlagSet( + type, + ts.TypeFlags.VoidLike | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.Any | + ts.TypeFlags.Never, + ), + ) + ); + } + + /** + * Finds errors in any expression node. + * + * Compares the type of the node against the contextual (expected) type. + * + * @returns `true` if the expected type was void function. + */ + function checkExpressionNode( + node: TSESTree.Expression, + msgId: ErrorPlaceId, + ): boolean { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + assert(ts.isExpression(tsNode)); + const expectedType = checker.getContextualType(tsNode); + + if (expectedType != null && isVoidReturningFunctionType(expectedType)) { + reportIfNonVoidFunction(node, msgId); + return true; + } + + return false; + } + + /** + * Finds errors in function calls. + * + * When checking arguments, we also manually figure out the argument types + * by iterating over all the function signatures. + * Thanks to this, we can find arguments like `(() => void) | (() => any)` + * and treat them as void too. + * This is done to also support checking functions like `addEventListener` + * which have overloads where one callback returns any. + * + * Implementation mostly based on no-misused-promises, + * which does this to find `(() => void) | (() => NotThenable)` + * and report them too. + */ + function checkFunctionCallNode( + callNode: TSESTree.CallExpression | TSESTree.NewExpression, + ): void { + const callTsNode = parserServices.esTreeNodeToTSNodeMap.get(callNode); + for (const [argIdx, argNode] of callNode.arguments.entries()) { + if (argNode.type === AST_NODE_TYPES.SpreadElement) { + continue; + } + + // Check against the contextual type first + if (checkExpressionNode(argNode, 'Arg')) { + continue; + } + + // Check against the types from all of the call signatures + if (options.considerOtherOverloads) { + const funcType = checker.getTypeAtLocation(callTsNode.expression); + const funcSignatures = tsutils + .unionTypeParts(funcType) + .flatMap(type => + ts.isCallExpression(callTsNode) + ? type.getCallSignatures() + : type.getConstructSignatures(), + ); + const argExpectedReturnTypes = funcSignatures + .map(s => s.parameters[argIdx]) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Indexing can return undefined + .filter(param => param != null) + .map(param => + checker.getTypeOfSymbolAtLocation(param, callTsNode.expression), + ) + .flatMap(paramType => tsutils.unionTypeParts(paramType)) + .flatMap(paramType => paramType.getCallSignatures()) + .map(paramSignature => paramSignature.getReturnType()); + if ( + // At least one return type is void + argExpectedReturnTypes.some(type => + tsutils.isTypeFlagSet(type, ts.TypeFlags.Void), + ) && + // The rest are nullish or any + argExpectedReturnTypes.every(type => + tsutils.isTypeFlagSet( + type, + ts.TypeFlags.VoidLike | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.Any | + ts.TypeFlags.Never, + ), + ) + ) { + // We treat this argument as void even though it might be technically any. + reportIfNonVoidFunction(argNode, 'ArgOverload'); + } + continue; + } + } + } + + /** + * Finds errors in an object property. + * + * Object properties require different logic + * when the property is a method shorthand. + */ + function checkObjectPropertyNode( + propNode: TSESTree.Property | TSESTree.MethodDefinition, + ): void { + if ( + propNode.value.type === AST_NODE_TYPES.AssignmentPattern || + propNode.value.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression + ) { + return; + } + const propTsNode = parserServices.esTreeNodeToTSNodeMap.get(propNode); + + if (propTsNode.kind === ts.SyntaxKind.MethodDeclaration) { + // Object property is a method shorthand. + + if (propTsNode.name.kind === ts.SyntaxKind.ComputedPropertyName) { + // Don't check object methods with computed name. + return; + } + const objTsNode = propTsNode.parent; + assert(ts.isObjectLiteralExpression(objTsNode)); + const objType = checker.getContextualType(objTsNode); + if (objType == null) { + // Expected object type is unknown. + return; + } + const propSymbol = checker.getPropertyOfType( + objType, + propTsNode.name.text, + ); + if (propSymbol == null) { + // Expected object type is known, but it doesn't have this method. + return; + } + const propExpectedType = checker.getTypeOfSymbolAtLocation( + propSymbol, + propTsNode, + ); + if (isVoidReturningFunctionType(propExpectedType)) { + reportIfNonVoidFunction(propNode.value, 'Prop'); + } + return; + } + + // Object property is a regular property. + checkExpressionNode(propNode.value, 'Prop'); + } + + /** + * Finds errors in a class property. + * + * In addition to the regular check against the contextual type, + * we also check against the base class property (when the class extends another class) + * and the implemented interfaces (when the class implements an interface). + * + * This can produce 3 errors at once. + */ + function checkClassPropertyNode( + propNode: TSESTree.PropertyDefinition, + ): void { + if (propNode.value == null) { + return; + } + const propTsNode = parserServices.esTreeNodeToTSNodeMap.get(propNode); + + // Check in comparison to the base class property. + if (options.considerBaseClass) { + const basePropSymbol = util.getBaseClassMember(propTsNode, checker); + if (basePropSymbol != null && propTsNode.initializer != null) { + const basePropType = checker.getTypeOfSymbolAtLocation( + basePropSymbol, + propTsNode.initializer, + ); + if (isVoidReturningFunctionType(basePropType)) { + reportIfNonVoidFunction(propNode.value, 'ExtMember'); + } + } + } + + // Check in comparison to the implemented interfaces. + if (options.considerImplementedInterfaces) { + const classTsNode = propTsNode.parent; + if (classTsNode.heritageClauses != null) { + const propSymbol = checker.getSymbolAtLocation(propTsNode.name); + if (propSymbol != null) { + const valueTsNode = parserServices.esTreeNodeToTSNodeMap.get( + propNode.value, + ); + for (const heritageTsNode of classTsNode.heritageClauses) { + if (heritageTsNode.token !== ts.SyntaxKind.ImplementsKeyword) { + continue; + } + for (const heritageTypeTsNode of heritageTsNode.types) { + const interfaceType = + checker.getTypeAtLocation(heritageTypeTsNode); + const interfacePropSymbol = checker.getPropertyOfType( + interfaceType, + propSymbol.name, + ); + if (interfacePropSymbol == null) { + continue; + } + const interfacePropType = checker.getTypeOfSymbolAtLocation( + interfacePropSymbol, + valueTsNode, + ); + if (isVoidReturningFunctionType(interfacePropType)) { + reportIfNonVoidFunction(propNode.value, 'ImplMember'); + } + } + } + } + } + } + + // Check in comparison to the contextual type. + checkExpressionNode(propNode.value, 'Prop'); + } + + /** + * Finds errors in a class method. + * + * We check against the base class method (when the class extends another class) + * and the implemented interfaces (when the class implements an interface). + * + * This can produce 2 errors at once. + */ + function checkClassMethodNode(methodNode: TSESTree.MethodDefinition): void { + if ( + methodNode.value.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression + ) { + return; + } + const methodTsNode = parserServices.esTreeNodeToTSNodeMap.get(methodNode); + if ( + methodTsNode.kind === ts.SyntaxKind.Constructor || + methodTsNode.kind === ts.SyntaxKind.GetAccessor || + methodTsNode.kind === ts.SyntaxKind.SetAccessor + ) { + return; + } + + // Check in comparison to the base class method. + if (options.considerBaseClass) { + const baseMethodSymbol = util.getBaseClassMember(methodTsNode, checker); + if (baseMethodSymbol != null) { + const baseMethodType = checker.getTypeOfSymbolAtLocation( + baseMethodSymbol, + methodTsNode, + ); + if (isVoidReturningFunctionType(baseMethodType)) { + reportIfNonVoidFunction(methodNode.value, 'ExtMember'); + } + } + } + + // Check in comparison to the implemented interfaces. + if (options.considerImplementedInterfaces) { + const classTsNode = methodTsNode.parent; + assert(ts.isClassLike(classTsNode)); + if (classTsNode.heritageClauses != null) { + const methodSymbol = checker.getSymbolAtLocation(methodTsNode.name); + if (methodSymbol != null) { + for (const heritageTsNode of classTsNode.heritageClauses) { + if (heritageTsNode.token !== ts.SyntaxKind.ImplementsKeyword) { + continue; + } + for (const heritageTypeTsNode of heritageTsNode.types) { + const interfaceType = + checker.getTypeAtLocation(heritageTypeTsNode); + const interfaceMethodSymbol = checker.getPropertyOfType( + interfaceType, + methodSymbol.name, + ); + if (interfaceMethodSymbol == null) { + continue; + } + const interfaceMethodType = checker.getTypeOfSymbolAtLocation( + interfaceMethodSymbol, + methodTsNode, + ); + if (isVoidReturningFunctionType(interfaceMethodType)) { + reportIfNonVoidFunction(methodNode.value, 'ImplMember'); + } + } + } + } + } + } + } + + /** + * Reports an error if the provided node is not allowed in a void function context. + */ + function reportIfNonVoidFunction( + funcNode: TSESTree.Expression, + msgId: ErrorPlaceId, + ): void { + const allowedReturnType = + ts.TypeFlags.Void | + ts.TypeFlags.Never | + (options.allowReturnUndefined ? ts.TypeFlags.Undefined : 0) | + (options.allowReturnNull ? ts.TypeFlags.Null : 0) | + (options.allowReturnAny ? ts.TypeFlags.Any : 0); + + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(funcNode); + const actualType = checker.getApparentType( + checker.getTypeAtLocation(tsNode), + ); + + if ( + tsutils + .getCallSignaturesOfType(actualType) + .map(signature => signature.getReturnType()) + .flatMap(returnType => tsutils.unionTypeParts(returnType)) + .every(type => tsutils.isTypeFlagSet(type, allowedReturnType)) + ) { + // The function is already void. + return; + } + + if ( + funcNode.type !== AST_NODE_TYPES.ArrowFunctionExpression && + funcNode.type !== AST_NODE_TYPES.FunctionExpression + ) { + // The provided function is not a function literal. + // Report a generic error. + return context.report({ + node: funcNode, + messageId: `nonVoidFuncIn${msgId}`, + }); + } + + // The provided function is a function literal. + + if (funcNode.generator) { + // The provided function is a generator function. + // Generator functions are not allowed. + + assert(funcNode.body.type === AST_NODE_TYPES.BlockStatement); + if (funcNode.body.body.length === 0) { + // Function body is empty. + // Fix it by removing the generator star. + return context.report({ + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `genFuncIn${msgId}`, + fix: fixer => removeGeneratorStarFix(fixer, funcNode), + }); + } + + // Function body is not empty. + // Report an error. + return context.report({ + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `genFuncIn${msgId}`, + }); + } + + if (funcNode.async) { + // The provided function is an async function. + + if ( + funcNode.body.type === AST_NODE_TYPES.BlockStatement + ? funcNode.body.body.length === 0 + : !ASTUtils.hasSideEffect(funcNode.body, sourceCode) + ) { + // Function body is empty or has no side effects. + // Fix it by removing the body and the async keyword. + return context.report({ + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `asyncFuncIn${msgId}`, + fix: fixer => emptyFuncFix(fixer, funcNode), + }); + } + + if (funcNode.body.type === AST_NODE_TYPES.BlockStatement) { + // This async function has a block body. + + if (options.allowReturnPromiseIfTryCatch) { + // Async functions are allowed if they are wrapped in a try-catch block. + + if ( + funcNode.body.body.length > 1 || + funcNode.body.body[0].type !== AST_NODE_TYPES.TryStatement || + funcNode.body.body[0].handler == null + ) { + // Function is not wrapped in a try-catch block. + // Suggest wrapping it in a try-catch block in addition to async IIFE. + return context.report({ + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `asyncNoTryCatchFuncIn${msgId}`, + suggest: [ + { + messageId: 'suggestWrapInTryCatch', + fix: fixer => wrapFuncInTryCatchFix(fixer, funcNode), + }, + { + messageId: 'suggestWrapInAsyncIIFE', + fix: fixer => wrapFuncInAsyncIIFEFix(fixer, funcNode), + }, + ], + }); + } + } else { + // Async functions are never allowed. + // Suggest wrapping its body in an async IIFE. + return context.report({ + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `asyncFuncIn${msgId}`, + suggest: [ + { + messageId: 'suggestWrapInAsyncIIFE', + fix: fixer => wrapFuncInAsyncIIFEFix(fixer, funcNode), + }, + ], + }); + } + } + } + + // At this point the function is either: + // a regular function, + // async with block body wrapped in try-catch (if allowed), + // or async arrow shorthand without braces. + + if (funcNode.body.type !== AST_NODE_TYPES.BlockStatement) { + // The provided function is an arrow function expression shorthand without braces. + assert(funcNode.type === AST_NODE_TYPES.ArrowFunctionExpression); + + if (!ASTUtils.hasSideEffect(funcNode.body, sourceCode)) { + // Function return value has no side effects. + // Fix it by removing the body and the async keyword. + return context.report({ + node: funcNode.body, + messageId: `nonVoidReturnIn${msgId}`, + fix: fixer => emptyFuncFix(fixer, funcNode), + }); + } + if (options.allowReturnUndefined) { + // Fix it by adding a void operator. + return context.report({ + node: funcNode.body, + messageId: `nonVoidReturnIn${msgId}`, + fix: function* (fixer) { + if (funcNode.async) { + yield removeAsyncKeywordFix(fixer, funcNode); + } + if (funcNode.returnType != null) { + yield fixer.replaceText( + funcNode.returnType.typeAnnotation, + 'void', + ); + } + yield util.getWrappingFixer({ + node: funcNode.body, + sourceCode, + wrap: code => `void ${code}`, + })(fixer); + }, + }); + } + // Fix it by adding braces to function body. + return context.report({ + node: funcNode.body, + messageId: `nonVoidReturnIn${msgId}`, + fix: function* (fixer) { + if (funcNode.async) { + yield removeAsyncKeywordFix(fixer, funcNode); + } + if (funcNode.returnType != null) { + yield fixer.replaceText( + funcNode.returnType.typeAnnotation, + 'void', + ); + } + yield util.addBracesToArrowFix(fixer, sourceCode, funcNode); + }, + }); + } + + // The function is a regular or arrow function with a block body. + // Possibly async and wrapped in try-catch if allowed. + + if (funcNode.returnType != null) { + // The provided function has an explicit return type annotation. + const typeAnnotationNode = funcNode.returnType.typeAnnotation; + if ( + !( + typeAnnotationNode.type === AST_NODE_TYPES.TSVoidKeyword || + (funcNode.async && + typeAnnotationNode.type === AST_NODE_TYPES.TSTypeReference && + typeAnnotationNode.typeName.type === AST_NODE_TYPES.Identifier && + typeAnnotationNode.typeName.name === 'Promise' && + typeAnnotationNode.typeArguments?.params[0].type === + AST_NODE_TYPES.TSVoidKeyword) + ) + ) { + // The explicit return type is not `void` or `Promise`. + // Fix it by changing the return type to `void` or `Promise`. + return context.report({ + node: typeAnnotationNode, + messageId: `nonVoidFuncIn${msgId}`, + fix: fixer => { + if (funcNode.async) { + return fixer.replaceText(typeAnnotationNode, 'Promise'); + } + return fixer.replaceText(typeAnnotationNode, 'void'); + }, + }); + } + } + + // Iterate over all function's statements recursively. + for (const statement of util.walkStatements([funcNode.body])) { + if ( + statement.type !== AST_NODE_TYPES.ReturnStatement || + statement.argument == null + ) { + // We only care about return statements with a value. + continue; + } + + const returnType = checker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(statement.argument), + ); + if (tsutils.isTypeFlagSet(returnType, allowedReturnType)) { + // Only visit return statements with invalid type. + continue; + } + + const returnKeyword = util.nullThrows( + sourceCode.getFirstToken(statement, { + filter: token => token.value === 'return', + }), + util.NullThrowsReasons.MissingToken('return keyword', statement.type), + ); + + // This return statement causes the non-void return type. + context.report({ + node: returnKeyword, + messageId: `nonVoidReturnIn${msgId}`, + fix: fixer => + util.discardReturnValueFix( + fixer, + sourceCode, + statement, + options.allowReturnUndefined, + ), + }); + } + } + + function removeGeneratorStarFix( + fixer: TSESLint.RuleFixer, + funcNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + ): TSESLint.RuleFix { + const funcHeadNode = + funcNode.parent.type === AST_NODE_TYPES.Property || + funcNode.parent.type === AST_NODE_TYPES.MethodDefinition + ? funcNode.parent + : funcNode; + const starToken = util.nullThrows( + sourceCode.getFirstToken(funcHeadNode, { + filter: token => token.value === '*', + }), + util.NullThrowsReasons.MissingToken('generator star', funcNode.type), + ); + const beforeStarToken = util.nullThrows( + sourceCode.getTokenBefore(starToken), + util.NullThrowsReasons.MissingToken( + 'token before generator star', + funcNode.type, + ), + ); + const afterStarToken = util.nullThrows( + sourceCode.getTokenAfter(starToken), + util.NullThrowsReasons.MissingToken( + 'token after generator star', + funcNode.type, + ), + ); + if ( + sourceCode.isSpaceBetween(beforeStarToken, starToken) || + sourceCode.isSpaceBetween(starToken, afterStarToken) || + afterStarToken.type === AST_TOKEN_TYPES.Punctuator + ) { + return fixer.remove(starToken); + } + return fixer.replaceText(starToken, ' '); + } + + function removeAsyncKeywordFix( + fixer: TSESLint.RuleFixer, + funcNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + ): TSESLint.RuleFix { + const funcHeadNode = + funcNode.parent.type === AST_NODE_TYPES.Property || + funcNode.parent.type === AST_NODE_TYPES.MethodDefinition + ? funcNode.parent + : funcNode; + const asyncToken = util.nullThrows( + sourceCode.getFirstToken(funcHeadNode, { + filter: token => token.value === 'async', + }), + util.NullThrowsReasons.MissingToken('async keyword', funcNode.type), + ); + const afterAsyncToken = util.nullThrows( + sourceCode.getTokenAfter(asyncToken), + util.NullThrowsReasons.MissingToken( + 'token after async keyword', + funcNode.type, + ), + ); + return fixer.removeRange([asyncToken.range[0], afterAsyncToken.range[0]]); + } + + function* emptyFuncFix( + fixer: TSESLint.RuleFixer, + funcNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + ): Generator { + // Remove async keyword + if (funcNode.async) { + yield removeAsyncKeywordFix(fixer, funcNode); + } + // Replace return type with void + if (funcNode.returnType != null) { + yield fixer.replaceText(funcNode.returnType.typeAnnotation, 'void'); + } + // Replace body with empty block + const bodyRange = util.getRangeWithParens(funcNode.body, sourceCode); + yield fixer.replaceTextRange(bodyRange, '{}'); + } + + function* wrapFuncInTryCatchFix( + fixer: TSESLint.RuleFixer, + funcNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + ): Generator { + // Replace return type with Promise + if (funcNode.returnType != null) { + assert(funcNode.async); + yield fixer.replaceText( + funcNode.returnType.typeAnnotation, + 'Promise', + ); + } + // Wrap body in try-catch + assert(funcNode.body.type === AST_NODE_TYPES.BlockStatement); + const bodyRange = util.getRangeWithParens(funcNode.body, sourceCode); + yield fixer.insertTextBeforeRange(bodyRange, '{ try '); + yield fixer.insertTextAfterRange(bodyRange, ' catch {} }'); + } + + function* wrapFuncInAsyncIIFEFix( + fixer: TSESLint.RuleFixer, + funcNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + ): Generator { + // Remove async keyword + yield removeAsyncKeywordFix(fixer, funcNode); + // Replace return type with void + if (funcNode.returnType != null) { + yield fixer.replaceText(funcNode.returnType.typeAnnotation, 'void'); + } + // Wrap body in async IIFE + const bodyRange = util.getRangeWithParens(funcNode.body, sourceCode); + if ( + funcNode.type === AST_NODE_TYPES.ArrowFunctionExpression && + options.allowReturnUndefined + ) { + yield fixer.insertTextBeforeRange(bodyRange, 'void (async () => '); + yield fixer.insertTextAfterRange(bodyRange, ')()'); + } else { + yield fixer.insertTextBeforeRange(bodyRange, '{ (async () => '); + yield fixer.insertTextAfterRange(bodyRange, ')(); }'); + } + } + }, +}); diff --git a/packages/eslint-plugin/src/util/addBracesToArrowFix.ts b/packages/eslint-plugin/src/util/addBracesToArrowFix.ts new file mode 100644 index 000000000000..84f4661faf88 --- /dev/null +++ b/packages/eslint-plugin/src/util/addBracesToArrowFix.ts @@ -0,0 +1,14 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +import { getRangeWithParens } from './getRangeWithParens'; + +export function addBracesToArrowFix( + fixer: TSESLint.RuleFixer, + sourceCode: Readonly, + funcNode: TSESTree.ArrowFunctionExpression, +): TSESLint.RuleFix { + const funcBody = funcNode.body; + const newFuncBodyText = `{ ${sourceCode.getText(funcBody)}; }`; + const range = getRangeWithParens(funcBody, sourceCode); + return fixer.replaceTextRange(range, newFuncBodyText); +} diff --git a/packages/eslint-plugin/src/util/discardReturnValueFix.ts b/packages/eslint-plugin/src/util/discardReturnValueFix.ts new file mode 100644 index 000000000000..36d427c036cf --- /dev/null +++ b/packages/eslint-plugin/src/util/discardReturnValueFix.ts @@ -0,0 +1,178 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { + AST_NODE_TYPES, + ASTUtils, + ESLintUtils, +} from '@typescript-eslint/utils'; + +import { nullThrows } from '.'; +import { getRangeWithParens } from './getRangeWithParens'; +import { getWrappingFixer } from './getWrappingFixer'; + +const ASI_PREVENTING_TOKENS = new Set(['-', '+', '`', '<', '(', '[']); + +/** + * Rewrites a return statement with a value so that the value is discarded. + * + * Must be called only on return statements with a value! + * + * Does one of these things: + * - Removes whole return statement if possible. + * - {@link removeValueLeaveReturnFix} + * - {@link removeReturnLeaveValueFix} + * - {@link getWrappingFixer} with void operator if {@link useVoidOperator} is true + * - {@link moveValueBeforeReturnFix} if {@link useVoidOperator} is false + */ +export function discardReturnValueFix( + fixer: TSESLint.RuleFixer, + sourceCode: Readonly, + returnNode: TSESTree.ReturnStatement, + useVoidOperator = false, +): TSESLint.RuleFix { + const argumentNode = nullThrows( + returnNode.argument, + 'missing return argument', + ); + + if (!ASTUtils.hasSideEffect(argumentNode, sourceCode)) { + // Return value is not needed. + + if (isFinalReturn(returnNode)) { + // Return statement is not needed. + // We can remove everything. + return fixer.remove(returnNode); + } + + // Return statement must stay. + return removeValueLeaveReturnFix(fixer, sourceCode, returnNode); + } + + // Return value must stay. + + if (isFinalReturn(returnNode)) { + // Return statement is not needed. + return removeReturnLeaveValueFix(fixer, sourceCode, returnNode); + } + + // Both the statement and the value must stay. + + if (useVoidOperator) { + // Use void operator to discard the return value. + return getWrappingFixer({ + node: argumentNode, + sourceCode, + wrap: code => `void ${code}`, + })(fixer); + } + + return moveValueBeforeReturnFix(fixer, sourceCode, returnNode); +} + +/** + * Checks whether the return statement is the last statement in a function body. + */ +export function isFinalReturn(returnNode: TSESTree.ReturnStatement): boolean { + // Return's parent must be a block. + if (returnNode.parent.type !== AST_NODE_TYPES.BlockStatement) { + // E.g. `if (cond) return;` (not in a block) + return false; + } + // Block's parent must be a function. + if ( + returnNode.parent.parent.type !== AST_NODE_TYPES.FunctionDeclaration && + returnNode.parent.parent.type !== AST_NODE_TYPES.FunctionExpression && + returnNode.parent.parent.type !== AST_NODE_TYPES.ArrowFunctionExpression + ) { + // E.g. `if (cond) { return; }` + // Not in a top-level function block. + return false; + } + // Return must be the last child of the block. + if ( + returnNode.parent.body.indexOf(returnNode) < + returnNode.parent.body.length - 1 + ) { + // Not the last statement in the block. + return false; + } + return true; +} + +/** + * Removes the value from a return statement and leaves only the return keyword. + */ +export function removeValueLeaveReturnFix( + fixer: TSESLint.RuleFixer, + sourceCode: Readonly, + returnNode: TSESTree.ReturnStatement, +): TSESLint.RuleFix { + const returnToken = ESLintUtils.nullThrows( + sourceCode.getFirstToken(returnNode, { + filter: token => token.value === 'return', + }), + ESLintUtils.NullThrowsReasons.MissingToken( + 'return keyword', + returnNode.type, + ), + ); + const argumentNode = nullThrows( + returnNode.argument, + 'missing return argument', + ); + const argRange = getRangeWithParens(argumentNode, sourceCode); + return fixer.removeRange([returnToken.range[1], argRange[1]]); +} + +/** + * Removes the return keyword from a return statement and leaves only the value. + */ +export function removeReturnLeaveValueFix( + fixer: TSESLint.RuleFixer, + sourceCode: Readonly, + returnNode: TSESTree.ReturnStatement, +): TSESLint.RuleFix { + const argumentNode = nullThrows( + returnNode.argument, + 'missing return argument', + ); + let newReturnText = `${sourceCode.getText(argumentNode)};`; + if (ASI_PREVENTING_TOKENS.has(newReturnText[0])) { + // The line could be interpreted as a continuation of the previous line, so + // we put a semicolon at the beginning to not break semicolon-less code. + newReturnText = `;${newReturnText}`; + } + return fixer.replaceText(returnNode, newReturnText); +} + +/** + * Moves the return value before the return statement + * and leaves the return statement without a value. + */ +export function moveValueBeforeReturnFix( + fixer: TSESLint.RuleFixer, + sourceCode: Readonly, + returnNode: TSESTree.ReturnStatement, +): TSESLint.RuleFix { + const argumentNode = nullThrows( + returnNode.argument, + 'missing return argument', + ); + let newReturnText = sourceCode.getText(argumentNode); + if (newReturnText[0] === '{') { + // The value would be interpreted as a block statement, + // so we need to wrap it in parentheses. + newReturnText = `(${newReturnText})`; + } + if (ASI_PREVENTING_TOKENS.has(newReturnText[0])) { + // The line could be interpreted as a continuation of the previous line, + // so we put a semicolon at the beginning to not break semicolon-less code. + newReturnText = `;${newReturnText}`; + } + newReturnText = `${newReturnText}; return;`; + if (returnNode.parent.type !== AST_NODE_TYPES.BlockStatement) { + // E.g. `if (cond) return value;` + // Add braces if not inside a block. + newReturnText = `{ ${newReturnText} }`; + } + return fixer.replaceText(returnNode, newReturnText); +} diff --git a/packages/eslint-plugin/src/util/getBaseClassMember.ts b/packages/eslint-plugin/src/util/getBaseClassMember.ts new file mode 100644 index 000000000000..76519f33bbba --- /dev/null +++ b/packages/eslint-plugin/src/util/getBaseClassMember.ts @@ -0,0 +1,31 @@ +import * as ts from 'typescript'; + +/** + * When given a class property or method node, + * if the class extends another class, + * this function returns the corresponding property or method node in the base class. + */ +export function getBaseClassMember( + memberNode: ts.PropertyDeclaration | ts.MethodDeclaration, + checker: ts.TypeChecker, +): ts.Symbol | undefined { + const classNode = memberNode.parent; + const classType = checker.getTypeAtLocation(classNode); + if (!classType.isClassOrInterface()) { + // TODO: anonymous class expressions fail this check + return undefined; + } + const memberNameNode = memberNode.name; + if (ts.isComputedPropertyName(memberNameNode)) { + return undefined; + } + const memberName = memberNameNode.getText(); + const baseTypes = checker.getBaseTypes(classType); + for (const baseType of baseTypes) { + const basePropSymbol = checker.getPropertyOfType(baseType, memberName); + if (basePropSymbol != null) { + return basePropSymbol; + } + } + return undefined; +} diff --git a/packages/eslint-plugin/src/util/getRangeWithParens.ts b/packages/eslint-plugin/src/util/getRangeWithParens.ts new file mode 100644 index 000000000000..1e3f6cc45f5a --- /dev/null +++ b/packages/eslint-plugin/src/util/getRangeWithParens.ts @@ -0,0 +1,40 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { ASTUtils, ESLintUtils } from '@typescript-eslint/utils'; + +/** + * Gets the range of the node including any parentheses around it. + * + * For example, given a function node like `() => ({})`, + * calling this on `function.body` would return the range of `({})` instead of just `{}`. + */ +export function getRangeWithParens( + node: TSESTree.Node, + sourceCode: TSESLint.SourceCode, +): TSESTree.Range { + let startToken = ESLintUtils.nullThrows( + sourceCode.getFirstToken(node), + ESLintUtils.NullThrowsReasons.MissingToken('first token', node.type), + ); + let endToken = ESLintUtils.nullThrows( + sourceCode.getLastToken(node), + ESLintUtils.NullThrowsReasons.MissingToken('last token', node.type), + ); + + for (;;) { + const prevToken = sourceCode.getTokenBefore(startToken); + const nextToken = sourceCode.getTokenAfter(endToken); + if (prevToken == null || nextToken == null) { + break; + } + if ( + !ASTUtils.isOpeningParenToken(prevToken) || + !ASTUtils.isClosingParenToken(nextToken) + ) { + break; + } + startToken = prevToken; + endToken = nextToken; + } + + return [startToken.range[0], endToken.range[1]]; +} diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index d5d07b6ba7e1..19d8e64afc69 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -29,13 +29,11 @@ interface WrappingFixerParams { * Wraps node with some code. Adds parenthesis as necessary. * @returns Fixer which adds the specified code and parens if necessary. */ -export function getWrappingFixer( - params: WrappingFixerParams, -): TSESLint.ReportFixFunction { +export function getWrappingFixer(params: WrappingFixerParams) { const { sourceCode, node, innerNode = node, wrap } = params; const innerNodes = Array.isArray(innerNode) ? innerNode : [innerNode]; - return (fixer): TSESLint.RuleFix => { + return (fixer: TSESLint.RuleFixer): TSESLint.RuleFix => { const innerCodes = innerNodes.map(innerNode => { let code = sourceCode.getText(innerNode); @@ -67,7 +65,7 @@ export function getWrappingFixer( } // check if we need to insert semicolon - if (/^[`([]/.exec(code) && isMissingSemicolonBefore(node, sourceCode)) { + if (/^[-+`<([]/.exec(code) && isMissingSemicolonBefore(node, sourceCode)) { code = `;${code}`; } diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 317fe1af66e3..dfc726175de8 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -1,15 +1,21 @@ import { ESLintUtils } from '@typescript-eslint/utils'; +export * from './addBracesToArrowFix'; export * from './astUtils'; export * from './collectUnusedVariables'; export * from './createRule'; +export * from './discardReturnValueFix'; +export * from './getBaseClassMember'; export * from './getFunctionHeadLoc'; export * from './getOperatorPrecedence'; +export * from './getRangeWithParens'; export * from './getStaticStringValue'; export * from './getStringLength'; export * from './getTextWithParentheses'; export * from './getThisExpression'; export * from './getWrappingFixer'; +export * from './isAssignee'; +export * from './isNodeEqual'; export * from './isNodeEqual'; export * from './isNullLiteral'; export * from './isUndefinedIdentifier'; @@ -17,7 +23,7 @@ export * from './misc'; export * from './objectIterators'; export * from './scopeUtils'; export * from './types'; -export * from './isAssignee'; +export * from './walkStatements'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; diff --git a/packages/eslint-plugin/src/util/walkStatements.ts b/packages/eslint-plugin/src/util/walkStatements.ts new file mode 100644 index 000000000000..fbf9bfbfc77e --- /dev/null +++ b/packages/eslint-plugin/src/util/walkStatements.ts @@ -0,0 +1,60 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +/** + * Yields all statement nodes in a block, including nested blocks. + * + * You can use it to find all return statements in a function body. + */ +export function* walkStatements( + body: readonly TSESTree.Statement[], +): Generator { + for (const statement of body) { + switch (statement.type) { + case AST_NODE_TYPES.BlockStatement: { + yield* walkStatements(statement.body); + continue; + } + case AST_NODE_TYPES.SwitchStatement: { + for (const switchCase of statement.cases) { + yield* walkStatements(switchCase.consequent); + } + continue; + } + case AST_NODE_TYPES.IfStatement: { + yield* walkStatements([statement.consequent]); + if (statement.alternate) { + yield* walkStatements([statement.alternate]); + } + continue; + } + case AST_NODE_TYPES.WhileStatement: + case AST_NODE_TYPES.DoWhileStatement: + case AST_NODE_TYPES.ForStatement: + case AST_NODE_TYPES.ForInStatement: + case AST_NODE_TYPES.ForOfStatement: + case AST_NODE_TYPES.WithStatement: { + yield* walkStatements([statement.body]); + continue; + } + case AST_NODE_TYPES.TryStatement: { + yield* walkStatements([statement.block]); + if (statement.handler) { + yield* walkStatements([statement.handler.body]); + } + if (statement.finalizer) { + yield* walkStatements([statement.finalizer]); + } + continue; + } + case AST_NODE_TYPES.LabeledStatement: { + yield* walkStatements([statement.body]); + continue; + } + default: { + yield statement; + continue; + } + } + } +} diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot new file mode 100644 index 000000000000..da2c46ea83e9 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot @@ -0,0 +1,302 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 1`] = ` +"Incorrect + +const bad: () => void = () => 2137; + ~~~~ Value returned in a context where a void return was expected. +const func = Math.random() > 0.1 ? bad : prompt; +const val = func(); +if (val) console.log(val.toUpperCase()); // ❌ Crash if bad was called +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 2`] = ` +"Correct + +const good: () => void = () => {}; +const func = Math.random() > 0.1 ? good : prompt; +const val = func(); +if (val) console.log(val.toUpperCase()); // ✅ No crash +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 3`] = ` +"Incorrect + +declare function takesCallback(cb: () => void): void; + +takesCallback(async () => { + ~~ Async callback not wrapped with a try-catch block and passed as an argument where a void callback was expected. + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); +}); +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 4`] = ` +"Correct + +declare function takesCallback(cb: () => void): void; + +takesCallback(() => { + (async () => { + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); + })().catch(console.error); +}); +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 5`] = ` +"Incorrect + +declare function takesCallback(cb: () => void): void; + +takesCallback(function* () { + ~~~~~~~~~~ Generator callback passed as an argument where a void callback was expected. + console.log('Hello'); + yield; + console.log('World'); +}); +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 6`] = ` +"Correct + +declare function takesCallback(cb: () => void): void; + +takesCallback(() => { + function* gen() { + console.log('Hello'); + yield; + console.log('World'); + } + for (const _ of gen()); +}); +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 7`] = ` +"Incorrect + +['Kazik', 'Zenek'].forEach(name => \`Hello, \${name}!\`); + ~~~~~~~~~~~~~~~~~ Value returned in a callback argument where a void callback was expected. +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 8`] = ` +"Correct + +['Kazik', 'Zenek'].forEach(name => console.log(\`Hello, \${name}!\`)); +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 9`] = ` +"Incorrect +Options: { "considerOtherOverloads": true } + +/// + +document.addEventListener('click', () => { + return 'Clicked'; + ~~~~~~ Value returned in a callback argument where one of the function signatures suggests it should be a void callback. +}); +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 10`] = ` +"Correct +Options: { "considerOtherOverloads": true } + +/// + +document.addEventListener('click', () => { + console.log('Clicked'); +}); +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 11`] = ` +"Incorrect +Options: { "considerBaseClass": true } + +/// + +class MyElement extends HTMLElement { + click() { + super.click(); + return 'Clicked'; + ~~~~~~ Value returned in a method which overrides a void method. + } +} +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 12`] = ` +"Correct +Options: { "considerBaseClass": true } + +/// + +class MyElement extends HTMLElement { + click() { + super.click(); + console.log('Clicked'); + } +} +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 13`] = ` +"Incorrect +Options: { "considerImplementedInterfaces": true } + +/// + +class FooListener implements EventListenerObject { + handleEvent() { + return 'Handled'; + ~~~~~~ Value returned in a method which implements a void method. + } +} +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 14`] = ` +"Correct +Options: { "considerImplementedInterfaces": true } + +/// + +class FooListener implements EventListenerObject { + handleEvent() { + console.log('Handled'); + } +} +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 15`] = ` +"Incorrect +Options: { "allowReturnPromiseIfTryCatch": false } + +const cb: () => void = async () => { + ~~ Async function used in a context where a void function is expected. + try { + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); + } catch (error) { + console.error(error); + } +}; +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 16`] = ` +"Correct +Options: { "allowReturnPromiseIfTryCatch": false } + +const cb: () => void = () => { + (async () => { + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); + })().catch(console.error); +}; +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 17`] = ` +"Incorrect +Options: { "allowReturnUndefined": false } + +let cb: () => void; + +cb = () => undefined; + ~~~~~~~~~ Value returned in a context where a void return was expected. + +cb = () => { + return void 0; + ~~~~~~ Value returned in a context where a void return was expected. +}; +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 18`] = ` +"Correct +Options: { "allowReturnUndefined": false } + +let cb: () => void; + +cb = () => {}; + +cb = () => { + return; +}; +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 19`] = ` +"Incorrect +Options: { "allowReturnNull": false } + +let cb: () => void; + +cb = () => null; + ~~~~ Value returned in a context where a void return was expected. + +cb = () => { + return null; + ~~~~~~ Value returned in a context where a void return was expected. +}; +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 20`] = ` +"Correct +Options: { "allowReturnNull": false } + +let cb: () => void; + +cb = () => {}; + +cb = () => { + return; +}; +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 21`] = ` +"Incorrect +Options: { "allowReturnAny": false } + +declare function fn(cb: () => void): void; + +fn(() => JSON.parse('{}')); + ~~~~~~~~~~~~~~~~ Value returned in a callback argument where a void callback was expected. + +fn(() => { + return someUntypedApi(); + ~~~~~~ Value returned in a callback argument where a void callback was expected. +}); +" +`; + +exports[`Validating rule docs strict-void-return.mdx code examples ESLint output 22`] = ` +"Correct +Options: { "allowReturnAny": false } + +declare function fn(cb: () => void): void; + +fn(() => void JSON.parse('{}')); + +fn(() => { + someUntypedApi(); +}); +" +`; diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig.dom.json b/packages/eslint-plugin/tests/fixtures/tsconfig.dom.json new file mode 100644 index 000000000000..9eaad1181ee2 --- /dev/null +++ b/packages/eslint-plugin/tests/fixtures/tsconfig.dom.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "jsx": "preserve", + "target": "es5", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "lib": ["es2015", "es2017", "esnext", "dom"] + }, + "include": ["file.ts", "react.tsx"] +} diff --git a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts new file mode 100644 index 000000000000..b26bbc92488b --- /dev/null +++ b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts @@ -0,0 +1,2763 @@ +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/strict-void-return'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootDir, + project: './tsconfig.dom.json', + }, +}); + +ruleTester.run('strict-void-return', rule, { + valid: [ + { + code: ` + declare function foo(cb: {}): void; + foo(() => () => []); + `, + }, + { + code: ` + declare function foo(cb: any): void; + foo(() => () => []); + `, + }, + { + code: ` + declare class Foo { + constructor(cb: unknown): void; + } + new Foo(() => ({})); + `, + }, + { + options: [{ allowReturnAny: true }], + code: ` + declare function foo(cb: () => {}): void; + foo(() => 1 as any); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(() => { + throw new Error('boom'); + }); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + declare function boom(): never; + foo(() => boom()); + foo(boom); + `, + }, + { + code: ` + declare const Foo: { + new (cb: () => any): void; + }; + new Foo(function () { + return 1; + }); + `, + }, + { + code: ` + declare const Foo: { + new (cb: () => unknown): void; + }; + new Foo(function () { + return 1; + }); + `, + }, + { + code: ` + declare const foo: { + bar(cb1: () => unknown, cb2: () => void): void; + }; + foo.bar( + function () { + return 1; + }, + function () { + return; + }, + ); + `, + }, + { + code: ` + declare const Foo: { + new (cb: () => string | void): void; + }; + new Foo(() => { + if (maybe) { + return 'a'; + } else { + return 'b'; + } + }); + `, + }, + { + code: ` + declare function foo void>(cb: Cb): void; + foo(() => { + console.log('a'); + }); + `, + }, + { + code: ` + declare function foo(cb: (() => void) | (() => string)): void; + foo(() => { + label: while (maybe) { + for (let i = 0; i < 10; i++) { + switch (i) { + case 0: + continue; + case 1: + return 'a'; + } + } + } + }); + `, + }, + { + code: ` + declare function foo(cb: (() => void) | null): void; + foo(null); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(async () => { + try { + await Promise.resolve(); + } catch (err) { + console.error(err); + } + }); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(async () => { + try { + await Promise.resolve(); + } catch { + console.error('fail'); + } + }); + `, + }, + { + code: ` + interface Cb { + (): void; + (): string; + } + declare const Foo: { + new (cb: Cb): void; + }; + new Foo(() => { + do { + try { + throw 1; + } catch { + return 'a'; + } + } while (maybe); + }); + `, + }, + { + code: ` + declare const foo: ((cb: () => boolean) => void) | ((cb: () => void) => void); + foo(() => false); + `, + }, + { + code: ` + declare const foo: { + (cb: () => boolean): void; + (cb: () => void): void; + }; + foo(function () { + with ({}) { + return false; + } + }); + `, + }, + { + code: ` + declare const Foo: { + new (cb: () => void): void; + (cb: () => unknown): void; + }; + Foo(() => false); + `, + }, + { + code: ` + declare const Foo: { + new (cb: () => any): void; + (cb: () => void): void; + }; + new Foo(() => false); + `, + }, + { + code: ` + declare function foo(cb: () => boolean): void; + declare function foo(cb: () => void): void; + foo(() => false); + `, + }, + { + code: ` + declare function foo(cb: () => Promise): void; + declare function foo(cb: () => void): void; + foo(async () => {}); + `, + }, + { + options: [{ considerOtherOverloads: false }], + code: ` + document.addEventListener('click', async () => {}); + `, + }, + { + options: [{ considerOtherOverloads: false }], + code: ` + declare function foo(x: null, cb: () => void): void; + declare function foo(x: unknown, cb: () => any): void; + foo({}, async () => {}); + `, + }, + { + options: [{ allowReturnAny: true }], + code: ` + declare function foo(cb: () => void): void; + foo(() => 1 as any); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(() => {}); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + const cb = () => {}; + foo(cb); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(function () {}); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(cb); + function cb() {} + `, + }, + { + options: [{ allowReturnNull: false }], + code: ` + declare function foo(cb: () => void): void; + foo(() => undefined); + `, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + declare function foo(cb: () => void): void; + foo(() => null); + `, + }, + { + options: [{ allowReturnNull: false, allowReturnUndefined: false }], + code: ` + declare function foo(cb: () => void): void; + foo(function () { + return; + }); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(function () { + return void 0; + }); + `, + }, + { + options: [{ allowReturnNull: false }], + code: ` + declare function foo(cb: () => void): void; + foo(() => { + return; + }); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + declare function cb(): never; + foo(cb); + `, + }, + { + code: ` + declare class Foo { + constructor(cb: () => void): any; + } + declare function cb(): void; + new Foo(cb); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(cb); + function cb() { + throw new Error('boom'); + } + `, + }, + { + code: ` + declare function foo(arg: string, cb: () => void): void; + declare function cb(): undefined; + foo('arg', cb); + `, + }, + { + code: ` + declare function foo(cb?: () => void): void; + foo(); + `, + }, + { + code: ` + declare class Foo { + constructor(cb?: () => void): void; + } + declare function cb(): void; + new Foo(cb); + `, + }, + { + code: ` + declare function foo(...cbs: Array<() => void>): void; + foo( + () => {}, + () => null, + () => undefined, + ); + `, + }, + { + code: ` + declare function foo(...cbs: Array<() => void>): void; + declare const cbs: Array<() => void>; + foo(...cbs); + `, + }, + { + code: ` + declare function foo(...cbs: [() => any, () => void, (() => void)?]): void; + foo( + async () => {}, + () => null, + () => undefined, + ); + `, + }, + { + code: ` + let cb; + cb = async () => 10; + `, + }, + { + code: ` + const foo: () => void = () => {}; + `, + }, + { + code: ` + declare function cb(): void; + const foo: () => void = cb; + `, + }, + { + code: ` + const foo: () => void = function () { + throw new Error('boom'); + }; + `, + }, + { + code: ` + const foo: { (): string; (): void } = () => { + return 'a'; + }; + `, + }, + { + code: ` + const foo: (() => void) | (() => number) = () => { + return 1; + }; + `, + }, + { + code: ` + type Foo = () => void; + const foo: Foo = cb; + function cb() { + return null; + } + `, + }, + { + code: ` + interface Foo { + (): void; + } + const foo: Foo = cb; + function cb() { + return undefined; + } + `, + }, + { + code: ` + declare function cb(): void; + declare let foo: () => void; + foo = cb; + `, + }, + { + code: ` + declare function defaultCb(): object; + declare let foo: { cb?: () => void }; + // default doesn't have to be void + const { cb = defaultCb } = foo; + `, + }, + { + code: ` + let foo: (() => void) | null = null; + foo &&= null; + `, + }, + { + code: ` + declare function cb(): void; + let foo: (() => void) | boolean = false; + foo ||= cb; + `, + }, + { + filename: 'react.tsx', + code: ` + declare function Foo(props: { cb: () => void }): unknown; + return {}} />; + `, + }, + { + filename: 'react.tsx', + code: ` + type Cb = () => void; + declare function Foo(props: { cb: Cb; s: string }): unknown; + return ; + `, + }, + { + filename: 'react.tsx', + code: ` + type Cb = () => void; + declare function Foo(props: { x: number; cb?: Cb }): unknown; + return ; + `, + }, + { + filename: 'react.tsx', + code: ` + type Cb = (() => void) | (() => number); + declare function Foo(props: { cb?: Cb }): unknown; + return ( + + ); + `, + }, + { + filename: 'react.tsx', + code: ` + interface Props { + cb: ((arg: unknown) => void) | boolean; + } + declare function Foo(props: Props): unknown; + return ; + `, + }, + { + filename: 'react.tsx', + code: ` + interface Props { + cb: (() => void) | (() => Promise); + } + declare function Foo(props: Props): any; + const _ = {}} />; + `, + }, + { + filename: 'react.tsx', + code: ` + interface Props { + children: (arg: unknown) => void; + } + declare function Foo(props: Props): unknown; + declare function cb(): void; + return {cb}; + `, + }, + { + code: ` + declare function foo(cbs: { arg: number; cb: () => void }): void; + foo({ arg: 1, cb: () => null }); + `, + }, + { + options: [{ allowReturnAny: true }], + code: ` + declare let foo: { arg?: string; cb: () => void }; + foo = { + cb: () => { + return something; + }, + }; + `, + }, + { + options: [{ allowReturnAny: true }], + code: ` + declare let foo: { cb: () => void }; + foo = { + cb() { + return something; + }, + }; + `, + }, + { + code: ` + declare let foo: { cb: () => void }; + foo = { + // don't check this thing + cb = () => 1, + }; + `, + }, + { + code: ` + declare let foo: { cb: (n: number) => void }; + let method = 'cb'; + foo = { + // don't check computed methods + [method](n) { + return n; + }, + }; + `, + }, + { + code: ` + // no contextual type for object + let foo = { + cb(n) { + return n; + }, + }; + `, + }, + { + code: ` + interface Foo { + fn(): void; + } + // no symbol for method cb + let foo: Foo = { + cb(n) { + return n; + }, + }; + `, + }, + { + code: ` + declare let foo: { cb: (() => void) | number }; + foo = { + cb: 0, + }; + `, + }, + { + code: ` + declare function cb(): void; + const foo: Record void> = { + cb1: cb, + cb2: cb, + }; + `, + }, + { + code: ` + declare function cb(): void; + const foo: Array<(() => void) | false> = [false, cb, () => cb()]; + `, + }, + { + code: ` + declare function cb(): void; + const foo: [string, () => void, (() => void)?] = ['asd', cb]; + `, + }, + { + code: ` + const foo: { cbs: Array<() => void> | null } = { + cbs: [ + function () { + return undefined; + }, + () => { + return void 0; + }, + null, + ], + }; + `, + }, + { + code: ` + const foo: { cb: () => void } = class { + static cb = () => {}; + }; + `, + }, + { + code: ` + class Foo { + foo; + } + `, + }, + { + code: ` + class Foo { + foo: () => void = () => null; + } + `, + }, + { + code: ` + class Foo { + cb() { + console.log('siema'); + } + } + const method = 'cb' as const; + class Bar extends Foo { + [method]() { + return 'nara'; + } + } + `, + }, + { + code: ` + class Foo { + cb = () => { + console.log('siema'); + }; + } + class Bar extends Foo { + cb = () => { + console.log('nara'); + }; + } + `, + }, + { + code: ` + class Foo { + cb1 = () => {}; + } + class Bar extends Foo { + cb2() {} + } + class Baz extends Bar { + cb1 = () => { + console.log('siema'); + }; + cb2() { + console.log('nara'); + } + } + `, + }, + { + code: ` + class Foo { + fn() { + return 'a'; + } + cb() {} + } + void class extends Foo { + cb() { + if (maybe) { + console.log('siema'); + } else { + console.log('nara'); + } + } + }; + `, + }, + { + code: ` + abstract class Foo { + abstract cb(): void; + } + class Bar extends Foo { + cb() { + console.log('a'); + } + } + `, + }, + { + code: ` + class Bar implements Foo { + cb = () => 1; + } + `, + }, + { + code: ` + interface Foo { + cb: () => void; + } + class Bar implements Foo { + cb = () => {}; + } + `, + }, + { + code: ` + interface Foo { + cb: () => void; + } + class Bar implements Foo { + get cb() { + return () => {}; + } + } + `, + }, + { + code: ` + interface Foo { + cb(): void; + } + class Bar implements Foo { + cb() { + return undefined; + } + } + `, + }, + { + code: ` + interface Foo1 { + cb1(): void; + } + interface Foo2 { + cb2: () => void; + } + class Bar implements Foo1, Foo2 { + cb1() {} + cb2() {} + } + `, + }, + { + code: ` + interface Foo1 { + cb1(): void; + } + interface Foo2 extends Foo1 { + cb2: () => void; + } + class Bar implements Foo2 { + cb1() {} + cb2() {} + } + `, + }, + { + options: [ + { considerBaseClass: false, considerImplementedInterfaces: false }, + ], + code: ` + interface Foo { + cb(): void; + } + class Bar { + cb() {} + } + class Baz extends Bar implements Foo { + cb() { + return l; + } + } + `, + }, + { + code: ` + declare let foo: () => () => void; + foo = () => () => {}; + `, + }, + { + code: ` + declare let foo: { f(): () => void }; + foo = { + f() { + return () => null; + }, + }; + function cb() {} + `, + }, + { + code: ` + declare let foo: { f(): () => void }; + foo.f = function () { + return () => {}; + }; + `, + }, + { + code: ` + declare let foo: () => (() => void) | string; + foo = () => 'asd' + 'zxc'; + `, + }, + { + code: ` + declare function foo(cb: () => () => void): void; + foo(function () { + return () => {}; + }); + `, + }, + { + code: ` + declare function foo(cb: (arg: string) => () => void): void; + declare function foo(cb: (arg: number) => () => boolean): void; + foo((arg: number) => { + return cb; + }); + function cb() { + return true; + } + `, + }, + ], + invalid: [ + { + code: ` + declare function foo(cb: () => void): void; + foo(() => false); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 3, column: 19 }], + output: ` + declare function foo(cb: () => void): void; + foo(() => {}); + `, + }, + { + code: noFormat` + declare function foo(cb: () => void): void; + foo(() => (((true)))); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 3, column: 22 }], + output: ` + declare function foo(cb: () => void): void; + foo(() => {}); + `, + }, + { + code: noFormat` + declare function foo(cb: () => void): void; + foo(() => { + if (maybe) { + return (((1) + 1)); + } + }); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 5, column: 13 }], + output: ` + declare function foo(cb: () => void): void; + foo(() => { + if (maybe) { + return; + } + }); + `, + }, + { + code: ` + declare function foo(arg: number, cb: () => void): void; + foo(0, () => 0); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 3, column: 22 }], + output: ` + declare function foo(arg: number, cb: () => void): void; + foo(0, () => {}); + `, + }, + { + code: ` + declare function foo(cb?: { (): void }): void; + foo(() => () => {}); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 3, column: 19 }], + output: ` + declare function foo(cb?: { (): void }): void; + foo(() => {}); + `, + }, + { + code: ` + declare function foo(cb: { (): void }): void; + declare function cb(): string; + foo(cb); + `, + errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 13 }], + output: null, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + function foo(arg: T, cb: () => T); + function foo(arg: null, cb: () => void); + function foo(arg: any, cb: () => any) {} + + foo(null, () => Math.random()); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 6, column: 25 }], + output: ` + function foo(arg: T, cb: () => T); + function foo(arg: null, cb: () => void); + function foo(arg: any, cb: () => any) {} + + foo(null, () => { Math.random(); }); + `, + }, + { + code: ` + declare function foo(arg: T, cb: () => T): void; + declare function foo(arg: any, cb: () => void): void; + + foo(null, async () => {}); + `, + errors: [{ messageId: 'asyncFuncInArg', line: 5, column: 28 }], + output: ` + declare function foo(arg: T, cb: () => T): void; + declare function foo(arg: any, cb: () => void): void; + + foo(null, () => {}); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + declare function foo(cb: () => any): void; + foo(async () => { + return Math.random(); + }); + `, + errors: [ + { + messageId: 'asyncNoTryCatchFuncInArg', + line: 4, + column: 22, + suggestions: [ + { + messageId: 'suggestWrapInTryCatch', + output: ` + declare function foo(cb: () => void): void; + declare function foo(cb: () => any): void; + foo(async () => { try { + return Math.random(); + } catch {} }); + `, + }, + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + declare function foo(cb: () => void): void; + declare function foo(cb: () => any): void; + foo(() => void (async () => { + return Math.random(); + })()); + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` + declare function foo(cb: { (): void }): void; + foo(cb); + async function cb() {} + `, + errors: [{ messageId: 'nonVoidFuncInArg', line: 3, column: 13 }], + output: null, + }, + { + code: ` + declare function foo void>(cb: Cb): void; + foo(() => { + console.log('a'); + return 1; + }); + `, + errors: [ + { messageId: 'nonVoidReturnInArgOverload', line: 5, column: 11 }, + ], + output: ` + declare function foo void>(cb: Cb): void; + foo(() => { + console.log('a'); + \ + + }); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + function bar number>(cb: Cb) { + foo(cb); + } + `, + errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 15 }], + output: null, + }, + { + code: ` + declare function foo(cb: { (): void }): void; + const cb = () => dunno; + foo(cb); + `, + errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 13 }], + output: null, + }, + { + code: ` + declare const foo: { + (arg: boolean, cb: () => void): void; + }; + foo(false, () => Promise.resolve(undefined)); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 5, column: 26 }], + output: ` + declare const foo: { + (arg: boolean, cb: () => void): void; + }; + foo(false, () => void Promise.resolve(undefined)); + `, + }, + { + code: ` + declare const foo: { + bar(cb1: () => any, cb2: () => void): void; + }; + foo.bar( + () => Promise.resolve(1), + () => Promise.resolve(1), + ); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 7, column: 17 }], + output: ` + declare const foo: { + bar(cb1: () => any, cb2: () => void): void; + }; + foo.bar( + () => Promise.resolve(1), + () => void Promise.resolve(1), + ); + `, + }, + { + code: ` + declare const Foo: { + new (cb: () => void): void; + }; + new Foo(async () => {}); + `, + errors: [{ messageId: 'asyncFuncInArg', line: 5, column: 26 }], + output: ` + declare const Foo: { + new (cb: () => void): void; + }; + new Foo(() => {}); + `, + }, + { + options: [{ allowReturnNull: false }], + code: ` + declare function foo(cb: () => void): void; + foo(() => { + label: while (maybe) { + for (const i of [1, 2, 3]) { + if (maybe) return null; + else return null; + } + } + return void 0; + }); + `, + errors: [ + { messageId: 'nonVoidReturnInArg', line: 6, column: 26 }, + { messageId: 'nonVoidReturnInArg', line: 7, column: 20 }, + ], + output: ` + declare function foo(cb: () => void): void; + foo(() => { + label: while (maybe) { + for (const i of [1, 2, 3]) { + if (maybe) return; + else return; + } + } + return void 0; + }); + `, + }, + { + options: [{ allowReturnNull: false }], + code: ` + declare function foo(cb: () => void): void; + foo(() => { + do { + try { + throw 1; + } catch (e) { + return null; + } finally { + console.log('finally'); + } + } while (maybe); + }); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 8, column: 15 }], + output: ` + declare function foo(cb: () => void): void; + foo(() => { + do { + try { + throw 1; + } catch (e) { + return; + } finally { + console.log('finally'); + } + } while (maybe); + }); + `, + }, + { + options: [{ allowReturnPromiseIfTryCatch: false }], + code: ` + declare function foo(cb: () => void): void; + foo(async () => { + try { + await Promise.resolve(); + } catch { + console.error('fail'); + } + }); + `, + errors: [ + { + messageId: 'asyncFuncInArg', + line: 3, + column: 22, + suggestions: [ + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + declare function foo(cb: () => void): void; + foo(() => void (async () => { + try { + await Promise.resolve(); + } catch { + console.error('fail'); + } + })()); + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` + declare const Foo: { + new (cb: () => void): void; + (cb: () => unknown): void; + }; + new Foo(() => false); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 6, column: 23 }], + output: ` + declare const Foo: { + new (cb: () => void): void; + (cb: () => unknown): void; + }; + new Foo(() => {}); + `, + }, + { + code: ` + declare const Foo: { + new (cb: () => any): void; + (cb: () => void): void; + }; + Foo(() => false); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 6, column: 19 }], + output: ` + declare const Foo: { + new (cb: () => any): void; + (cb: () => void): void; + }; + Foo(() => {}); + `, + }, + { + code: ` + interface Cb { + (arg: string): void; + (arg: number): void; + } + declare function foo(cb: Cb): void; + foo(cb); + function cb() { + return true; + } + `, + errors: [{ messageId: 'nonVoidFuncInArg', line: 7, column: 13 }], + output: null, + }, + { + code: ` + declare function foo( + cb: ((arg: number) => void) | ((arg: string) => void), + ): void; + foo(cb); + function cb() { + return 1 + 1; + } + `, + errors: [{ messageId: 'nonVoidFuncInArg', line: 5, column: 13 }], + output: null, + }, + { + code: ` + declare function foo(cb: (() => void) | null): void; + declare function cb(): boolean; + foo(cb); + `, + errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 13 }], + output: null, + }, + { + code: ` + declare function foo(...cbs: Array<() => void>): void; + foo( + () => {}, + () => false, + () => 0, + () => '', + ); + `, + errors: [ + { messageId: 'nonVoidReturnInArg', line: 5, column: 17 }, + { messageId: 'nonVoidReturnInArg', line: 6, column: 17 }, + { messageId: 'nonVoidReturnInArg', line: 7, column: 17 }, + ], + output: ` + declare function foo(...cbs: Array<() => void>): void; + foo( + () => {}, + () => {}, + () => {}, + () => {}, + ); + `, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + declare function foo(...cbs: [() => void, () => void, (() => void)?]): void; + foo( + () => {}, + () => Math.random(), + () => (1).toString(), + ); + `, + errors: [ + { messageId: 'nonVoidReturnInArg', line: 5, column: 17 }, + { messageId: 'nonVoidReturnInArg', line: 6, column: 17 }, + ], + output: ` + declare function foo(...cbs: [() => void, () => void, (() => void)?]): void; + foo( + () => {}, + () => { Math.random(); }, + () => { (1).toString(); }, + ); + `, + }, + { + code: ` + document.addEventListener('click', async () => {}); + `, + errors: [{ messageId: 'asyncFuncInArgOverload', line: 2, column: 53 }], + output: ` + document.addEventListener('click', () => {}); + `, + }, + { + code: ` + declare function foo(x: null, cb: () => void): void; + declare function foo(x: unknown, cb: () => any): void; + foo({}, async () => {}); + `, + errors: [{ messageId: 'asyncFuncInArgOverload', line: 4, column: 26 }], + output: ` + declare function foo(x: null, cb: () => void): void; + declare function foo(x: unknown, cb: () => any): void; + foo({}, () => {}); + `, + }, + { + code: ` + const arr = [1, 2]; + arr.forEach(async x => { + console.log(x); + }); + `, + errors: [ + { + messageId: 'asyncNoTryCatchFuncInArg', + line: 3, + column: 29, + suggestions: [ + { + messageId: 'suggestWrapInTryCatch', + output: ` + const arr = [1, 2]; + arr.forEach(async x => { try { + console.log(x); + } catch {} }); + `, + }, + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + const arr = [1, 2]; + arr.forEach(x => void (async () => { + console.log(x); + })()); + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` + [1, 2].forEach(async x => console.log(x)); + `, + errors: [{ messageId: 'nonVoidReturnInArg', line: 2, column: 35 }], + output: ` + [1, 2].forEach(x => void console.log(x)); + `, + }, + { + code: ` + const foo: () => void = () => false; + `, + errors: [{ messageId: 'nonVoidReturnInVar', line: 2, column: 39 }], + output: ` + const foo: () => void = () => {}; + `, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + const foo: () => void = async () => Promise.resolve(true); + `, + errors: [{ messageId: 'nonVoidReturnInVar', line: 2, column: 45 }], + output: ` + const foo: () => void = () => { Promise.resolve(true); }; + `, + }, + { + code: 'const cb: () => void = (): Array => [];', + errors: [{ messageId: 'nonVoidReturnInVar', line: 1, column: 45 }], + output: 'const cb: () => void = (): void => {};', + }, + { + code: ` + const cb: () => void = (): Array => { + return []; + }; + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 2, column: 36 }], + output: ` + const cb: () => void = (): void => { + return []; + }; + `, + }, + { + code: noFormat`const cb: () => void = function*foo() {}`, + errors: [{ messageId: 'genFuncInVar', line: 1, column: 24 }], + output: `const cb: () => void = function foo() {}`, + }, + { + options: [{ allowReturnUndefined: false }], + code: 'const cb: () => void = (): Promise => Promise.resolve(1);', + errors: [{ messageId: 'nonVoidReturnInVar', line: 1, column: 47 }], + output: 'const cb: () => void = (): void => { Promise.resolve(1); };', + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + const cb: () => void = async (): Promise => { + try { + return Promise.resolve(1); + } catch {} + }; + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 2, column: 42 }], + output: ` + const cb: () => void = async (): Promise => { + try { + return Promise.resolve(1); + } catch {} + }; + `, + }, + { + code: 'const cb: () => void = async (): Promise => Promise.resolve(1);', + errors: [{ messageId: 'nonVoidReturnInVar', line: 1, column: 53 }], + output: 'const cb: () => void = (): void => void Promise.resolve(1);', + }, + { + code: ` + const foo: () => void = async () => { + try { + return 1; + } catch {} + }; + `, + errors: [{ messageId: 'nonVoidReturnInVar', line: 4, column: 13 }], + output: ` + const foo: () => void = async () => { + try { + return; + } catch {} + }; + `, + }, + { + code: ` + const foo: () => void = async (): Promise => { + try { + await Promise.resolve(); + } finally { + } + }; + `, + errors: [ + { + messageId: 'asyncNoTryCatchFuncInVar', + line: 2, + column: 57, + suggestions: [ + { + messageId: 'suggestWrapInTryCatch', + output: ` + const foo: () => void = async (): Promise => { try { + try { + await Promise.resolve(); + } finally { + } + } catch {} }; + `, + }, + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + const foo: () => void = (): void => void (async () => { + try { + await Promise.resolve(); + } finally { + } + })(); + `, + }, + ], + }, + ], + output: null, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + const foo: () => void = async () => { + try { + await Promise.resolve(); + } catch (err) { + console.error(err); + } + console.log('ok'); + }; + `, + errors: [ + { + messageId: 'asyncNoTryCatchFuncInVar', + line: 2, + column: 42, + suggestions: [ + { + messageId: 'suggestWrapInTryCatch', + output: ` + const foo: () => void = async () => { try { + try { + await Promise.resolve(); + } catch (err) { + console.error(err); + } + console.log('ok'); + } catch {} }; + `, + }, + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + const foo: () => void = () => { (async () => { + try { + await Promise.resolve(); + } catch (err) { + console.error(err); + } + console.log('ok'); + })(); }; + `, + }, + ], + }, + ], + output: null, + }, + // TODO: Check every union type separately + // { + // code: ` + // declare let foo: (() => void) | (() => boolean); + // foo = () => 1; + // `, + // errors: [{ messageId: 'nonVoidReturnInVar', line: 3, column: 21 }], + // output: ` + // declare let foo: (() => void) | (() => boolean); + // foo = () => {}; + // `, + // }, + { + code: 'const foo: () => void = (): number => {};', + errors: [{ messageId: 'nonVoidFuncInVar', line: 1, column: 29 }], + output: 'const foo: () => void = (): void => {};', + }, + { + code: ` + declare function cb(): boolean; + const foo: () => void = cb; + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 3, column: 33 }], + output: null, + }, + { + options: [{ allowReturnNull: false, allowReturnUndefined: false }], + code: ` + const foo: () => void = function () { + if (maybe) { + return null; + } else { + return void 0; + } + }; + `, + errors: [ + { messageId: 'nonVoidReturnInVar', line: 4, column: 13 }, + { messageId: 'nonVoidReturnInVar', line: 6, column: 13 }, + ], + output: ` + const foo: () => void = function () { + if (maybe) { + return; + } else { + return; + } + }; + `, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + const foo: () => void = function () { + if (maybe) { + console.log('elo'); + return { [1]: Math.random() }; + } + }; + `, + errors: [{ messageId: 'nonVoidReturnInVar', line: 5, column: 13 }], + output: ` + const foo: () => void = function () { + if (maybe) { + console.log('elo'); + ;({ [1]: Math.random() }); return; + } + }; + `, + }, + { + code: ` + const foo: { (arg: number): void; (arg: string): void } = arg => { + console.log('foo'); + switch (typeof arg) { + case 'number': + return 0; + case 'string': + return ''; + } + }; + `, + errors: [ + { messageId: 'nonVoidReturnInVar', line: 6, column: 15 }, + { messageId: 'nonVoidReturnInVar', line: 8, column: 15 }, + ], + output: ` + const foo: { (arg: number): void; (arg: string): void } = arg => { + console.log('foo'); + switch (typeof arg) { + case 'number': + return; + case 'string': + return; + } + }; + `, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + const foo: ((arg: number) => void) | ((arg: string) => void) = async () => { + return 1; + }; + `, + errors: [ + { + messageId: 'asyncNoTryCatchFuncInVar', + line: 2, + column: 81, + suggestions: [ + { + messageId: 'suggestWrapInTryCatch', + output: ` + const foo: ((arg: number) => void) | ((arg: string) => void) = async () => { try { + return 1; + } catch {} }; + `, + }, + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + const foo: ((arg: number) => void) | ((arg: string) => void) = () => { (async () => { + return 1; + })(); }; + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` + type Foo = () => void; + const foo: Foo = cb; + function cb() { + return [1, 2, 3]; + } + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 3, column: 26 }], + output: null, + }, + { + code: ` + interface Foo { + (): void; + } + const foo: Foo = cb; + function cb() { + return { a: 1 }; + } + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 5, column: 26 }], + output: null, + }, + { + code: ` + declare function cb(): unknown; + declare let foo: () => void; + foo = cb; + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 4, column: 15 }], + output: null, + }, + { + code: ` + declare let foo: { arg?: string; cb?: () => void }; + foo.cb = () => { + return 'siema'; + console.log('siema'); + }; + `, + errors: [{ messageId: 'nonVoidReturnInVar', line: 4, column: 11 }], + output: ` + declare let foo: { arg?: string; cb?: () => void }; + foo.cb = () => { + return; + console.log('siema'); + }; + `, + }, + { + code: ` + declare function cb(): unknown; + let foo: (() => void) | null = null; + foo ??= cb; + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 4, column: 17 }], + output: null, + }, + { + code: ` + declare function cb(): unknown; + let foo: (() => void) | boolean = false; + foo ||= cb; + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 4, column: 17 }], + output: null, + }, + { + code: ` + declare function cb(): unknown; + let foo: (() => void) | boolean = false; + foo &&= cb; + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 4, column: 17 }], + output: null, + }, + { + filename: 'react.tsx', + code: ` + declare function Foo(props: { cb: () => void }): unknown; + return 1} />; + `, + errors: [{ messageId: 'nonVoidReturnInAttr', line: 3, column: 31 }], + output: ` + declare function Foo(props: { cb: () => void }): unknown; + return {}} />; + `, + }, + { + filename: 'react.tsx', + options: [{ allowReturnNull: false, allowReturnUndefined: false }], + code: ` + declare function Foo(props: { cb: () => void }): unknown; + declare function getNull(): null; + return ( + { + if (maybe) return Math.random(); + else return getNull(); + }} + /> + ); + `, + errors: [ + { messageId: 'nonVoidReturnInAttr', line: 7, column: 26 }, + { messageId: 'nonVoidReturnInAttr', line: 8, column: 20 }, + ], + output: ` + declare function Foo(props: { cb: () => void }): unknown; + declare function getNull(): null; + return ( + { + if (maybe) { Math.random(); return; } + else { getNull(); return; } + }} + /> + ); + `, + }, + { + filename: 'react.tsx', + code: ` + type Cb = () => void; + declare function Foo(props: { cb: Cb; s: string }): unknown; + return ; + `, + errors: [{ messageId: 'asyncFuncInAttr', line: 4, column: 25 }], + output: ` + type Cb = () => void; + declare function Foo(props: { cb: Cb; s: string }): unknown; + return ; + `, + }, + { + filename: 'react.tsx', + code: ` + type Cb = () => void; + declare function Foo(props: { n: number; cb?: Cb }): unknown; + return ; + `, + errors: [{ messageId: 'genFuncInAttr', line: 4, column: 34 }], + output: ` + type Cb = () => void; + declare function Foo(props: { n: number; cb?: Cb }): unknown; + return ; + `, + }, + { + filename: 'react.tsx', + code: ` + type Cb = ((arg: string) => void) | ((arg: number) => void); + declare function Foo(props: { cb?: Cb }): unknown; + return ( + + ); + `, + errors: [{ messageId: 'genFuncInAttr', line: 6, column: 17 }], + output: null, + }, + { + filename: 'react.tsx', + code: ` + interface Props { + cb: ((arg: unknown) => void) | boolean; + } + declare function Foo(props: Props): unknown; + return x} />; + `, + errors: [{ messageId: 'nonVoidReturnInAttr', line: 6, column: 30 }], + output: ` + interface Props { + cb: ((arg: unknown) => void) | boolean; + } + declare function Foo(props: Props): unknown; + return {}} />; + `, + }, + { + filename: 'react.tsx', + code: ` + interface Props { + children: (arg: unknown) => void; + } + declare function Foo(props: Props): unknown; + declare function cb(): unknown; + return {cb}; + `, + errors: [{ messageId: 'nonVoidFuncInAttr', line: 7, column: 22 }], + output: null, + }, + { + code: ` + declare function foo(cbs: { arg: number; cb: () => void }): void; + foo({ arg: 1, cb: () => 1 }); + `, + errors: [{ messageId: 'nonVoidReturnInProp', line: 3, column: 33 }], + output: ` + declare function foo(cbs: { arg: number; cb: () => void }): void; + foo({ arg: 1, cb: () => {} }); + `, + }, + { + code: ` + declare let foo: { arg?: string; cb: () => void }; + foo = { + cb: () => { + let x = 'siema'; + return x; + }, + }; + `, + errors: [{ messageId: 'nonVoidReturnInProp', line: 6, column: 13 }], + output: ` + declare let foo: { arg?: string; cb: () => void }; + foo = { + cb: () => { + let x = 'siema'; + \ + + }, + }; + `, + }, + { + code: ` + declare let foo: { cb: (n: number) => void }; + foo = { + cb(n) { + return n; + }, + }; + `, + errors: [{ messageId: 'nonVoidReturnInProp', line: 5, column: 13 }], + output: ` + declare let foo: { cb: (n: number) => void }; + foo = { + cb(n) { + \ + + }, + }; + `, + }, + { + code: ` + declare let foo: { 1234: (n: number) => void }; + foo = { + 1234(n) { + return n; + }, + }; + `, + errors: [{ messageId: 'nonVoidReturnInProp', line: 5, column: 13 }], + output: ` + declare let foo: { 1234: (n: number) => void }; + foo = { + 1234(n) { + \ + + }, + }; + `, + }, + { + code: ` + declare let foo: { '1e+21': () => void }; + foo = { + 1_000_000_000_000_000_000_000: () => 1, + }; + `, + errors: [{ messageId: 'nonVoidReturnInProp', line: 4, column: 48 }], + output: ` + declare let foo: { '1e+21': () => void }; + foo = { + 1_000_000_000_000_000_000_000: () => {}, + }; + `, + }, + { + code: ` + declare let foo: { cb: (() => void) | number }; + foo = { + cb: async () => { + if (maybe) { + return 'asd'; + } + }, + }; + `, + errors: [ + { + messageId: 'asyncNoTryCatchFuncInProp', + line: 4, + column: 11, + suggestions: [ + { + messageId: 'suggestWrapInTryCatch', + output: ` + declare let foo: { cb: (() => void) | number }; + foo = { + cb: async () => { try { + if (maybe) { + return 'asd'; + } + } catch {} }, + }; + `, + }, + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + declare let foo: { cb: (() => void) | number }; + foo = { + cb: () => void (async () => { + if (maybe) { + return 'asd'; + } + })(), + }; + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` + declare function cb(): number; + const foo: Record void> = { + cb1: cb, + cb2: cb, + }; + `, + errors: [ + { messageId: 'nonVoidFuncInProp', line: 4, column: 16 }, + { messageId: 'nonVoidFuncInProp', line: 5, column: 16 }, + ], + output: null, + }, + { + code: ` + declare function cb(): number; + const foo: Array<(() => void) | false> = [false, cb, () => cb()]; + `, + errors: [ + { messageId: 'nonVoidFuncInVar', line: 3, column: 58 }, + { messageId: 'nonVoidReturnInVar', line: 3, column: 68 }, + ], + output: ` + declare function cb(): number; + const foo: Array<(() => void) | false> = [false, cb, () => void cb()]; + `, + }, + { + code: ` + declare function cb(): number; + const foo: [string, () => void, (() => void)?] = ['asd', cb]; + `, + errors: [{ messageId: 'nonVoidFuncInVar', line: 3, column: 66 }], + output: null, + }, + { + code: ` + const foo: { cbs: Array<() => void> | null } = { + cbs: [ + function* () { + yield 1; + }, + async () => { + await 1; + }, + null, + ], + }; + `, + errors: [ + { messageId: 'genFuncInVar', line: 4, column: 13 }, + { + messageId: 'asyncNoTryCatchFuncInVar', + line: 7, + column: 22, + suggestions: [ + { + messageId: 'suggestWrapInTryCatch', + output: ` + const foo: { cbs: Array<() => void> | null } = { + cbs: [ + function* () { + yield 1; + }, + async () => { try { + await 1; + } catch {} }, + null, + ], + }; + `, + }, + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + const foo: { cbs: Array<() => void> | null } = { + cbs: [ + function* () { + yield 1; + }, + () => void (async () => { + await 1; + })(), + null, + ], + }; + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` + const foo: { cb: () => void } = class { + static cb = () => ({}); + }; + `, + errors: [{ messageId: 'nonVoidReturnInProp', line: 3, column: 30 }], + output: ` + const foo: { cb: () => void } = class { + static cb = () => {}; + }; + `, + }, + { + code: ` + class Foo { + foo: () => void = () => []; + } + `, + errors: [{ messageId: 'nonVoidReturnInProp', line: 3, column: 35 }], + output: ` + class Foo { + foo: () => void = () => {}; + } + `, + }, + { + code: ` + class Foo { + static foo: () => void = Math.random; + } + `, + errors: [{ messageId: 'nonVoidFuncInProp', line: 3, column: 36 }], + output: null, + }, + { + code: ` + class Foo { + cb = () => {}; + } + class Bar extends Foo { + cb = Math.random; + } + `, + errors: [{ messageId: 'nonVoidFuncInExtMember', line: 6, column: 16 }], + output: null, + }, + // TODO: Check anonymous classes + // { + // code: ` + // class Foo { + // cb() {} + // } + // void class extends Foo { + // cb() { + // return Math.random(); + // } + // }; + // `, + // errors: [{ messageId: 'nonVoidReturnInExtMember', line: 6, column: 16 }], + // output: ` + // class Foo { + // cb() {} + // } + // void class extends Foo { + // cb() { + // Math.random(); + // } + // }; + // `, + // }, + { + code: ` + class Foo { + cb1 = () => {}; + } + class Bar extends Foo { + cb2() {} + } + class Baz extends Bar { + cb1 = () => Math.random(); + cb2() { + return Math.random(); + } + } + `, + errors: [ + { messageId: 'nonVoidReturnInExtMember', line: 9, column: 23 }, + { messageId: 'nonVoidReturnInExtMember', line: 11, column: 13 }, + ], + output: ` + class Foo { + cb1 = () => {}; + } + class Bar extends Foo { + cb2() {} + } + class Baz extends Bar { + cb1 = () => void Math.random(); + cb2() { + Math.random(); + } + } + `, + }, + { + code: ` + declare function f(): Promise; + interface Foo { + cb: () => void; + } + class Bar { + cb = () => {}; + } + class Baz extends Bar implements Foo { + cb: () => void = f; + } + `, + errors: [ + { messageId: 'nonVoidFuncInExtMember', line: 10, column: 28 }, + { messageId: 'nonVoidFuncInImplMember', line: 10, column: 28 }, + { messageId: 'nonVoidFuncInProp', line: 10, column: 28 }, + ], + output: null, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + class Foo { + fn() { + return 'a'; + } + cb() {} + } + class Bar extends Foo { + cb() { + if (maybe) { + return Promise.resolve('siema'); + } else { + return Promise.resolve('nara'); + } + } + } + `, + errors: [ + { messageId: 'nonVoidReturnInExtMember', line: 11, column: 15 }, + { messageId: 'nonVoidReturnInExtMember', line: 13, column: 15 }, + ], + output: ` + class Foo { + fn() { + return 'a'; + } + cb() {} + } + class Bar extends Foo { + cb() { + if (maybe) { + Promise.resolve('siema'); return; + } else { + Promise.resolve('nara'); return; + } + } + } + `, + }, + { + code: ` + abstract class Foo { + abstract cb(): void; + } + class Bar extends Foo { + async cb() {} + } + `, + errors: [{ messageId: 'asyncFuncInExtMember', line: 6, column: 11 }], + output: ` + abstract class Foo { + abstract cb(): void; + } + class Bar extends Foo { + cb() {} + } + `, + }, + { + code: ` + class Foo { + fn() { + return 'a'; + } + cb() {} + } + class Bar extends Foo { + *cb() {} + } + `, + errors: [{ messageId: 'genFuncInExtMember', line: 9, column: 11 }], + output: ` + class Foo { + fn() { + return 'a'; + } + cb() {} + } + class Bar extends Foo { + cb() {} + } + `, + }, + { + code: ` + interface Foo { + cb: () => void; + } + class Bar implements Foo { + cb = Math.random; + } + `, + errors: [{ messageId: 'nonVoidFuncInImplMember', line: 6, column: 16 }], + output: null, + }, + // TODO: Check getters + // { + // code: ` + // interface Foo { + // cb: () => void; + // } + // class Bar implements Foo { + // get cb() { + // return () => 1; + // } + // } + // `, + // errors: [], + // }, + { + code: noFormat` + class Foo { + cb() {} + } + class Bar extends Foo { + async*cb() {} + } + `, + errors: [{ messageId: 'genFuncInExtMember', line: 6, column: 11 }], + output: ` + class Foo { + cb() {} + } + class Bar extends Foo { + async cb() {} + } + `, + }, + { + code: ` + interface Foo { + cb(): void; + } + class Bar implements Foo { + async cb(): Promise { + return Promise.resolve('siema'); + } + } + `, + errors: [ + { + messageId: 'asyncNoTryCatchFuncInImplMember', + line: 6, + column: 11, + suggestions: [ + { + messageId: 'suggestWrapInTryCatch', + output: ` + interface Foo { + cb(): void; + } + class Bar implements Foo { + async cb(): Promise { try { + return Promise.resolve('siema'); + } catch {} } + } + `, + }, + { + messageId: 'suggestWrapInAsyncIIFE', + output: ` + interface Foo { + cb(): void; + } + class Bar implements Foo { + cb(): void { (async () => { + return Promise.resolve('siema'); + })(); } + } + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` + interface Foo { + cb(): void; + } + class Bar implements Foo { + async cb() { + try { + return { a: ['asdf', 1234] }; + } catch { + console.error('error'); + } + } + } + `, + errors: [{ messageId: 'nonVoidReturnInImplMember', line: 8, column: 15 }], + output: ` + interface Foo { + cb(): void; + } + class Bar implements Foo { + async cb() { + try { + return; + } catch { + console.error('error'); + } + } + } + `, + }, + { + code: ` + interface Foo { + cb(): void; + } + class Bar implements Foo { + cb() { + if (maybe) { + return Promise.resolve(1); + } else { + return; + } + } + } + `, + errors: [{ messageId: 'nonVoidReturnInImplMember', line: 8, column: 15 }], + output: ` + interface Foo { + cb(): void; + } + class Bar implements Foo { + cb() { + if (maybe) { + return void Promise.resolve(1); + } else { + return; + } + } + } + `, + }, + { + code: ` + interface Foo1 { + cb1(): void; + } + interface Foo2 { + cb2: () => void; + } + class Bar implements Foo1, Foo2 { + async cb1() {} + async *cb2() {} + } + `, + errors: [ + { messageId: 'asyncFuncInImplMember', line: 9, column: 11 }, + { messageId: 'genFuncInImplMember', line: 10, column: 11 }, + ], + output: ` + interface Foo1 { + cb1(): void; + } + interface Foo2 { + cb2: () => void; + } + class Bar implements Foo1, Foo2 { + cb1() {} + async cb2() {} + } + `, + }, + { + code: ` + interface Foo1 { + cb1(): void; + } + interface Foo2 extends Foo1 { + cb2: () => void; + } + class Bar implements Foo2 { + async cb1() {} + async *cb2() {} + } + `, + errors: [ + { messageId: 'asyncFuncInImplMember', line: 9, column: 11 }, + { messageId: 'genFuncInImplMember', line: 10, column: 11 }, + ], + output: ` + interface Foo1 { + cb1(): void; + } + interface Foo2 extends Foo1 { + cb2: () => void; + } + class Bar implements Foo2 { + cb1() {} + async cb2() {} + } + `, + }, + { + code: ` + declare let foo: () => () => void; + foo = () => () => 1 + 1; + `, + errors: [{ messageId: 'nonVoidReturnInReturn', line: 3, column: 27 }], + output: ` + declare let foo: () => () => void; + foo = () => () => {}; + `, + }, + { + options: [{ allowReturnUndefined: false }], + code: ` + declare let foo: () => () => void; + foo = () => () => Math.random(); + `, + errors: [{ messageId: 'nonVoidReturnInReturn', line: 3, column: 27 }], + output: ` + declare let foo: () => () => void; + foo = () => () => { Math.random(); }; + `, + }, + { + code: ` + declare let foo: () => () => void; + declare const cb: () => null | false; + foo = () => cb; + `, + errors: [{ messageId: 'nonVoidFuncInReturn', line: 4, column: 21 }], + output: null, + }, + { + code: ` + declare let foo: { f(): () => void }; + foo = { + f() { + return () => cb; + }, + }; + function cb() {} + `, + errors: [{ messageId: 'nonVoidReturnInReturn', line: 5, column: 26 }], + output: ` + declare let foo: { f(): () => void }; + foo = { + f() { + return () => {}; + }, + }; + function cb() {} + `, + }, + { + options: [{ allowReturnNull: false }], + code: ` + declare let foo: { f(): () => void }; + foo.f = function () { + return () => { + return null; + }; + }; + `, + errors: [{ messageId: 'nonVoidReturnInReturn', line: 5, column: 13 }], + output: ` + declare let foo: { f(): () => void }; + foo.f = function () { + return () => { + \ + + }; + }; + `, + }, + { + code: ` + declare let foo: () => (() => void) | string; + foo = () => () => { + return 'asd' + 'zxc'; + }; + `, + errors: [{ messageId: 'nonVoidReturnInReturn', line: 4, column: 11 }], + output: ` + declare let foo: () => (() => void) | string; + foo = () => () => { + \ + + }; + `, + }, + { + code: ` + declare function foo(cb: () => () => void): void; + foo(function () { + return async () => {}; + }); + `, + errors: [{ messageId: 'asyncFuncInReturn', line: 4, column: 27 }], + output: ` + declare function foo(cb: () => () => void): void; + foo(function () { + return () => {}; + }); + `, + }, + { + options: [{ allowReturnUndefined: false }], + filename: 'react.tsx', + code: noFormat` + declare function foo(cb: () => () => void): void; + foo(() => () => { + if (n == 1) { + console.log('asd') + return [1].map(x => x) + } + if (n == 2) { + console.log('asd') + return -Math.random() + } + if (n == 3) { + console.log('asd') + return \`x\`.toUpperCase() + } + return {Math.random()} + }); + `, + errors: [ + { messageId: 'nonVoidReturnInReturn', line: 6, column: 13 }, + { messageId: 'nonVoidReturnInReturn', line: 10, column: 13 }, + { messageId: 'nonVoidReturnInReturn', line: 14, column: 13 }, + { messageId: 'nonVoidReturnInReturn', line: 16, column: 11 }, + ], + output: ` + declare function foo(cb: () => () => void): void; + foo(() => () => { + if (n == 1) { + console.log('asd') + ;[1].map(x => x); return; + } + if (n == 2) { + console.log('asd') + ;-Math.random(); return; + } + if (n == 3) { + console.log('asd') + ;\`x\`.toUpperCase(); return; + } + ;{Math.random()}; + }); + `, + }, + { + code: ` + declare function foo(cb: (arg: string) => () => void): void; + declare function foo(cb: (arg: number) => () => boolean): void; + foo((arg: string) => { + return cb; + }); + async function* cb() { + yield true; + } + `, + errors: [{ messageId: 'nonVoidFuncInReturn', line: 5, column: 18 }], + output: null, + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/strict-void-return.shot b/packages/eslint-plugin/tests/schema-snapshots/strict-void-return.shot new file mode 100644 index 000000000000..49bfdf683e9e --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/strict-void-return.shot @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes strict-void-return 1`] = ` +" +# SCHEMA: + +[ + { + "additionalProperties": false, + "properties": { + "allowReturnAny": { + "type": "boolean" + }, + "allowReturnNull": { + "type": "boolean" + }, + "allowReturnPromiseIfTryCatch": { + "type": "boolean" + }, + "allowReturnUndefined": { + "type": "boolean" + }, + "considerBaseClass": { + "type": "boolean" + }, + "considerImplementedInterfaces": { + "type": "boolean" + }, + "considerOtherOverloads": { + "type": "boolean" + } + }, + "type": "object" + } +] + + +# TYPES: + +type Options = [ + { + allowReturnAny?: boolean; + allowReturnNull?: boolean; + allowReturnPromiseIfTryCatch?: boolean; + allowReturnUndefined?: boolean; + considerBaseClass?: boolean; + considerImplementedInterfaces?: boolean; + considerOtherOverloads?: boolean; + }, +]; +" +`; From 5454727cf6ce0fb0defd138d55fd0c22b326d3af Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Aug 2024 14:24:33 +0200 Subject: [PATCH 02/33] chore: generate configs --- packages/typescript-eslint/src/configs/all.ts | 1 + packages/typescript-eslint/src/configs/disable-type-checked.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index 8f78c4e32502..b282382b0328 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -163,6 +163,7 @@ export default ( 'no-return-await': 'off', '@typescript-eslint/return-await': 'error', '@typescript-eslint/strict-boolean-expressions': 'error', + '@typescript-eslint/strict-void-return': 'error', '@typescript-eslint/switch-exhaustiveness-check': 'error', '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/typedef': 'error', diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts index 531593aed938..84a300775476 100644 --- a/packages/typescript-eslint/src/configs/disable-type-checked.ts +++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts @@ -66,6 +66,7 @@ export default ( '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/return-await': 'off', '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/strict-void-return': 'off', '@typescript-eslint/switch-exhaustiveness-check': 'off', '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off', From 38e2d18e1a2021516d461121cb3689ca5412b472 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Aug 2024 14:28:19 +0200 Subject: [PATCH 03/33] better handling of base class/interfaces --- .../src/rules/strict-void-return.ts | 98 +++++-------------- .../src/util/getBaseClassMember.ts | 31 ------ .../src/util/getBaseTypesOfClassMember.ts | 27 +++++ packages/eslint-plugin/src/util/index.ts | 2 +- .../tests/rules/strict-void-return.test.ts | 92 +++++++++-------- 5 files changed, 107 insertions(+), 143 deletions(-) delete mode 100644 packages/eslint-plugin/src/util/getBaseClassMember.ts create mode 100644 packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts diff --git a/packages/eslint-plugin/src/rules/strict-void-return.ts b/packages/eslint-plugin/src/rules/strict-void-return.ts index 4ae6be8edb32..6c7f43bd093f 100644 --- a/packages/eslint-plugin/src/rules/strict-void-return.ts +++ b/packages/eslint-plugin/src/rules/strict-void-return.ts @@ -416,7 +416,7 @@ export default util.createRule({ * we also check against the base class property (when the class extends another class) * and the implemented interfaces (when the class implements an interface). * - * This can produce 3 errors at once. + * This can produce multiple errors at once. */ function checkClassPropertyNode( propNode: TSESTree.PropertyDefinition, @@ -428,12 +428,11 @@ export default util.createRule({ // Check in comparison to the base class property. if (options.considerBaseClass) { - const basePropSymbol = util.getBaseClassMember(propTsNode, checker); - if (basePropSymbol != null && propTsNode.initializer != null) { - const basePropType = checker.getTypeOfSymbolAtLocation( - basePropSymbol, - propTsNode.initializer, - ); + for (const basePropType of util.getBaseTypesOfClassMember( + checker, + propTsNode, + ts.SyntaxKind.ExtendsKeyword, + )) { if (isVoidReturningFunctionType(basePropType)) { reportIfNonVoidFunction(propNode.value, 'ExtMember'); } @@ -442,36 +441,13 @@ export default util.createRule({ // Check in comparison to the implemented interfaces. if (options.considerImplementedInterfaces) { - const classTsNode = propTsNode.parent; - if (classTsNode.heritageClauses != null) { - const propSymbol = checker.getSymbolAtLocation(propTsNode.name); - if (propSymbol != null) { - const valueTsNode = parserServices.esTreeNodeToTSNodeMap.get( - propNode.value, - ); - for (const heritageTsNode of classTsNode.heritageClauses) { - if (heritageTsNode.token !== ts.SyntaxKind.ImplementsKeyword) { - continue; - } - for (const heritageTypeTsNode of heritageTsNode.types) { - const interfaceType = - checker.getTypeAtLocation(heritageTypeTsNode); - const interfacePropSymbol = checker.getPropertyOfType( - interfaceType, - propSymbol.name, - ); - if (interfacePropSymbol == null) { - continue; - } - const interfacePropType = checker.getTypeOfSymbolAtLocation( - interfacePropSymbol, - valueTsNode, - ); - if (isVoidReturningFunctionType(interfacePropType)) { - reportIfNonVoidFunction(propNode.value, 'ImplMember'); - } - } - } + for (const basePropType of util.getBaseTypesOfClassMember( + checker, + propTsNode, + ts.SyntaxKind.ImplementsKeyword, + )) { + if (isVoidReturningFunctionType(basePropType)) { + reportIfNonVoidFunction(propNode.value, 'ImplMember'); } } } @@ -486,7 +462,7 @@ export default util.createRule({ * We check against the base class method (when the class extends another class) * and the implemented interfaces (when the class implements an interface). * - * This can produce 2 errors at once. + * This can produce multiple errors at once. */ function checkClassMethodNode(methodNode: TSESTree.MethodDefinition): void { if ( @@ -505,12 +481,11 @@ export default util.createRule({ // Check in comparison to the base class method. if (options.considerBaseClass) { - const baseMethodSymbol = util.getBaseClassMember(methodTsNode, checker); - if (baseMethodSymbol != null) { - const baseMethodType = checker.getTypeOfSymbolAtLocation( - baseMethodSymbol, - methodTsNode, - ); + for (const baseMethodType of util.getBaseTypesOfClassMember( + checker, + methodTsNode, + ts.SyntaxKind.ExtendsKeyword, + )) { if (isVoidReturningFunctionType(baseMethodType)) { reportIfNonVoidFunction(methodNode.value, 'ExtMember'); } @@ -519,34 +494,13 @@ export default util.createRule({ // Check in comparison to the implemented interfaces. if (options.considerImplementedInterfaces) { - const classTsNode = methodTsNode.parent; - assert(ts.isClassLike(classTsNode)); - if (classTsNode.heritageClauses != null) { - const methodSymbol = checker.getSymbolAtLocation(methodTsNode.name); - if (methodSymbol != null) { - for (const heritageTsNode of classTsNode.heritageClauses) { - if (heritageTsNode.token !== ts.SyntaxKind.ImplementsKeyword) { - continue; - } - for (const heritageTypeTsNode of heritageTsNode.types) { - const interfaceType = - checker.getTypeAtLocation(heritageTypeTsNode); - const interfaceMethodSymbol = checker.getPropertyOfType( - interfaceType, - methodSymbol.name, - ); - if (interfaceMethodSymbol == null) { - continue; - } - const interfaceMethodType = checker.getTypeOfSymbolAtLocation( - interfaceMethodSymbol, - methodTsNode, - ); - if (isVoidReturningFunctionType(interfaceMethodType)) { - reportIfNonVoidFunction(methodNode.value, 'ImplMember'); - } - } - } + for (const baseMethodType of util.getBaseTypesOfClassMember( + checker, + methodTsNode, + ts.SyntaxKind.ImplementsKeyword, + )) { + if (isVoidReturningFunctionType(baseMethodType)) { + reportIfNonVoidFunction(methodNode.value, 'ImplMember'); } } } diff --git a/packages/eslint-plugin/src/util/getBaseClassMember.ts b/packages/eslint-plugin/src/util/getBaseClassMember.ts deleted file mode 100644 index 76519f33bbba..000000000000 --- a/packages/eslint-plugin/src/util/getBaseClassMember.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as ts from 'typescript'; - -/** - * When given a class property or method node, - * if the class extends another class, - * this function returns the corresponding property or method node in the base class. - */ -export function getBaseClassMember( - memberNode: ts.PropertyDeclaration | ts.MethodDeclaration, - checker: ts.TypeChecker, -): ts.Symbol | undefined { - const classNode = memberNode.parent; - const classType = checker.getTypeAtLocation(classNode); - if (!classType.isClassOrInterface()) { - // TODO: anonymous class expressions fail this check - return undefined; - } - const memberNameNode = memberNode.name; - if (ts.isComputedPropertyName(memberNameNode)) { - return undefined; - } - const memberName = memberNameNode.getText(); - const baseTypes = checker.getBaseTypes(classType); - for (const baseType of baseTypes) { - const basePropSymbol = checker.getPropertyOfType(baseType, memberName); - if (basePropSymbol != null) { - return basePropSymbol; - } - } - return undefined; -} diff --git a/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts b/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts new file mode 100644 index 000000000000..ba47d841a874 --- /dev/null +++ b/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts @@ -0,0 +1,27 @@ +import assert from 'assert'; +import * as ts from 'typescript'; + +/** + * Given a member of a class which extends another class or implements an interface, + * returns the corresponding member type for each of the base class/interfaces. + */ +export function getBaseTypesOfClassMember( + checker: ts.TypeChecker, + memberTsNode: ts.PropertyDeclaration | ts.MethodDeclaration, + heritageToken: ts.SyntaxKind.ExtendsKeyword | ts.SyntaxKind.ImplementsKeyword, +): ts.Type[] { + assert(ts.isClassLike(memberTsNode.parent)); + const memberSymbol = checker.getSymbolAtLocation(memberTsNode.name); + if (memberSymbol == null) { + return []; + } + return (memberTsNode.parent.heritageClauses ?? []) + .filter(clauseNode => clauseNode.token === heritageToken) + .flatMap(clauseNode => clauseNode.types) + .map(baseTypeNode => checker.getTypeAtLocation(baseTypeNode)) + .map(baseType => checker.getPropertyOfType(baseType, memberSymbol.name)) + .filter(baseMemberSymbol => baseMemberSymbol != null) + .map(baseMemberSymbol => + checker.getTypeOfSymbolAtLocation(baseMemberSymbol, memberTsNode), + ); +} diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index dfc726175de8..0bf5bca8cc06 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -5,7 +5,7 @@ export * from './astUtils'; export * from './collectUnusedVariables'; export * from './createRule'; export * from './discardReturnValueFix'; -export * from './getBaseClassMember'; +export * from './getBaseTypesOfClassMember'; export * from './getFunctionHeadLoc'; export * from './getOperatorPrecedence'; export * from './getRangeWithParens'; diff --git a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts index b26bbc92488b..ab51567fa242 100644 --- a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts @@ -685,21 +685,6 @@ ruleTester.run('strict-void-return', rule, { } `, }, - { - code: ` - class Foo { - cb() { - console.log('siema'); - } - } - const method = 'cb' as const; - class Bar extends Foo { - [method]() { - return 'nara'; - } - } - `, - }, { code: ` class Foo { @@ -2216,30 +2201,59 @@ ruleTester.run('strict-void-return', rule, { errors: [{ messageId: 'nonVoidFuncInExtMember', line: 6, column: 16 }], output: null, }, - // TODO: Check anonymous classes - // { - // code: ` - // class Foo { - // cb() {} - // } - // void class extends Foo { - // cb() { - // return Math.random(); - // } - // }; - // `, - // errors: [{ messageId: 'nonVoidReturnInExtMember', line: 6, column: 16 }], - // output: ` - // class Foo { - // cb() {} - // } - // void class extends Foo { - // cb() { - // Math.random(); - // } - // }; - // `, - // }, + { + code: ` + class Foo { + cb() { + console.log('siema'); + } + } + const method = 'cb' as const; + class Bar extends Foo { + [method]() { + return 'nara'; + } + } + `, + errors: [{ messageId: 'nonVoidReturnInExtMember', line: 10, column: 13 }], + output: ` + class Foo { + cb() { + console.log('siema'); + } + } + const method = 'cb' as const; + class Bar extends Foo { + [method]() { + \ + + } + } + `, + }, + { + code: ` + class Foo { + cb() {} + } + void class extends Foo { + cb() { + return Math.random(); + } + }; + `, + errors: [{ messageId: 'nonVoidReturnInExtMember', line: 7, column: 13 }], + output: ` + class Foo { + cb() {} + } + void class extends Foo { + cb() { + Math.random(); + } + }; + `, + }, { code: ` class Foo { From cb58ccc464cc97df84d6ef82fb94394c94f3f2d6 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Aug 2024 18:03:11 +0200 Subject: [PATCH 04/33] update docs --- packages/eslint-plugin/docs/rules/strict-void-return.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/strict-void-return.mdx b/packages/eslint-plugin/docs/rules/strict-void-return.mdx index 4db97a1bfab2..5369b1b1f8be 100644 --- a/packages/eslint-plugin/docs/rules/strict-void-return.mdx +++ b/packages/eslint-plugin/docs/rules/strict-void-return.mdx @@ -46,7 +46,8 @@ if (val) console.log(val.toUpperCase()); // ✅ No crash ### Unhandled promises If a promise is returned from a callback that should return void, -it won't be awaited and its rejection will be silently ignored or crash the process depending on runtime. +it probably won't be awaited and its rejection will be silently ignored +or crash the process depending on runtime. @@ -80,11 +81,13 @@ takesCallback(() => { :::info -If you only care about promises, you can use the [`no-misused-promises`](no-misused-promises.mdx) rule instead. +If you only care about promises, +you can use the [`no-misused-promises`](no-misused-promises.mdx) rule instead. ::: :::tip -Use [`no-floating-promises`](no-floating-promises.mdx) to also enforce error handling of non-awaited promises. +Use [`no-floating-promises`](no-floating-promises.mdx) +to also enforce error handling of non-awaited promises in statement positions. ::: ### Ignored generators From 75da13d19feb2f0ab792512d77706a6c96ab839f Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Aug 2024 18:24:28 +0200 Subject: [PATCH 05/33] restructure some code --- .../src/rules/strict-void-return.ts | 223 +++++++++--------- .../tests/rules/strict-void-return.test.ts | 21 +- 2 files changed, 136 insertions(+), 108 deletions(-) diff --git a/packages/eslint-plugin/src/rules/strict-void-return.ts b/packages/eslint-plugin/src/rules/strict-void-return.ts index 6c7f43bd093f..96ce668ecee8 100644 --- a/packages/eslint-plugin/src/rules/strict-void-return.ts +++ b/packages/eslint-plugin/src/rules/strict-void-return.ts @@ -554,7 +554,7 @@ export default util.createRule({ // The provided function is a generator function. // Generator functions are not allowed. - assert(funcNode.body.type === AST_NODE_TYPES.BlockStatement); + assert(funcNode.type === AST_NODE_TYPES.FunctionExpression); if (funcNode.body.body.length === 0) { // Function body is empty. // Fix it by removing the generator star. @@ -581,123 +581,96 @@ export default util.createRule({ ? funcNode.body.body.length === 0 : !ASTUtils.hasSideEffect(funcNode.body, sourceCode) ) { - // Function body is empty or has no side effects. + // This async function is empty or has no side effects. // Fix it by removing the body and the async keyword. return context.report({ loc: util.getFunctionHeadLoc(funcNode, sourceCode), messageId: `asyncFuncIn${msgId}`, - fix: fixer => emptyFuncFix(fixer, funcNode), + fix: fixer => removeFuncBodyFix(fixer, funcNode), }); } - if (funcNode.body.type === AST_NODE_TYPES.BlockStatement) { - // This async function has a block body. - - if (options.allowReturnPromiseIfTryCatch) { - // Async functions are allowed if they are wrapped in a try-catch block. - - if ( - funcNode.body.body.length > 1 || - funcNode.body.body[0].type !== AST_NODE_TYPES.TryStatement || - funcNode.body.body[0].handler == null - ) { - // Function is not wrapped in a try-catch block. - // Suggest wrapping it in a try-catch block in addition to async IIFE. - return context.report({ - loc: util.getFunctionHeadLoc(funcNode, sourceCode), - messageId: `asyncNoTryCatchFuncIn${msgId}`, - suggest: [ - { - messageId: 'suggestWrapInTryCatch', - fix: fixer => wrapFuncInTryCatchFix(fixer, funcNode), - }, - { - messageId: 'suggestWrapInAsyncIIFE', - fix: fixer => wrapFuncInAsyncIIFEFix(fixer, funcNode), - }, - ], - }); - } - } else { - // Async functions are never allowed. - // Suggest wrapping its body in an async IIFE. - return context.report({ - loc: util.getFunctionHeadLoc(funcNode, sourceCode), - messageId: `asyncFuncIn${msgId}`, - suggest: [ - { - messageId: 'suggestWrapInAsyncIIFE', - fix: fixer => wrapFuncInAsyncIIFEFix(fixer, funcNode), - }, - ], - }); - } + if (funcNode.body.type !== AST_NODE_TYPES.BlockStatement) { + // This async function is an arrow function shorthand without braces. + // It's not worth suggesting wrapping a single expression in a try-catch/IIFE. + // Fix it by adding a void operator or braces. + assert(funcNode.type === AST_NODE_TYPES.ArrowFunctionExpression); + return context.report({ + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `asyncFuncIn${msgId}`, + fix: fixer => + options.allowReturnUndefined + ? addVoidToArrowFix(fixer, funcNode) + : addBracesToArrowFix(fixer, funcNode), + }); } - } - - // At this point the function is either: - // a regular function, - // async with block body wrapped in try-catch (if allowed), - // or async arrow shorthand without braces. - if (funcNode.body.type !== AST_NODE_TYPES.BlockStatement) { - // The provided function is an arrow function expression shorthand without braces. - assert(funcNode.type === AST_NODE_TYPES.ArrowFunctionExpression); + // This async function has a block body. - if (!ASTUtils.hasSideEffect(funcNode.body, sourceCode)) { - // Function return value has no side effects. - // Fix it by removing the body and the async keyword. + if (!options.allowReturnPromiseIfTryCatch) { + // Async functions aren't allowed. + // Suggest wrapping its body in an async IIFE. return context.report({ - node: funcNode.body, - messageId: `nonVoidReturnIn${msgId}`, - fix: fixer => emptyFuncFix(fixer, funcNode), + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `asyncFuncIn${msgId}`, + suggest: [ + { + messageId: 'suggestWrapInAsyncIIFE', + fix: fixer => wrapFuncInAsyncIIFEFix(fixer, funcNode), + }, + ], }); } - if (options.allowReturnUndefined) { - // Fix it by adding a void operator. + + // Async functions are allowed if they are wrapped in a try-catch block. + + if ( + funcNode.body.body.length > 1 || + funcNode.body.body[0].type !== AST_NODE_TYPES.TryStatement || + funcNode.body.body[0].handler == null + ) { + // Function is not wrapped in a try-catch block. + // Suggest wrapping it in a try-catch block in addition to async IIFE. return context.report({ - node: funcNode.body, - messageId: `nonVoidReturnIn${msgId}`, - fix: function* (fixer) { - if (funcNode.async) { - yield removeAsyncKeywordFix(fixer, funcNode); - } - if (funcNode.returnType != null) { - yield fixer.replaceText( - funcNode.returnType.typeAnnotation, - 'void', - ); - } - yield util.getWrappingFixer({ - node: funcNode.body, - sourceCode, - wrap: code => `void ${code}`, - })(fixer); - }, + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `asyncNoTryCatchFuncIn${msgId}`, + suggest: [ + { + messageId: 'suggestWrapInTryCatch', + fix: fixer => wrapFuncInTryCatchFix(fixer, funcNode), + }, + { + messageId: 'suggestWrapInAsyncIIFE', + fix: fixer => wrapFuncInAsyncIIFEFix(fixer, funcNode), + }, + ], }); } - // Fix it by adding braces to function body. + } + + // At this point the function is either a regular function, + // or async with block body wrapped in try-catch (if allowed). + + if (funcNode.body.type !== AST_NODE_TYPES.BlockStatement) { + // The provided function is an arrow function shorthand without braces. + assert(funcNode.type === AST_NODE_TYPES.ArrowFunctionExpression); + // Fix it by removing the body or adding a void operator or braces. return context.report({ node: funcNode.body, messageId: `nonVoidReturnIn${msgId}`, - fix: function* (fixer) { - if (funcNode.async) { - yield removeAsyncKeywordFix(fixer, funcNode); - } - if (funcNode.returnType != null) { - yield fixer.replaceText( - funcNode.returnType.typeAnnotation, - 'void', - ); - } - yield util.addBracesToArrowFix(fixer, sourceCode, funcNode); - }, + fix: fixer => + !ASTUtils.hasSideEffect(funcNode.body, sourceCode) + ? removeFuncBodyFix(fixer, funcNode) + : options.allowReturnUndefined + ? addVoidToArrowFix(fixer, funcNode) + : addBracesToArrowFix(fixer, funcNode), }); } // The function is a regular or arrow function with a block body. // Possibly async and wrapped in try-catch if allowed. + // Check return type annotation. if (funcNode.returnType != null) { // The provided function has an explicit return type annotation. const typeAnnotationNode = funcNode.returnType.typeAnnotation; @@ -717,18 +690,17 @@ export default util.createRule({ return context.report({ node: typeAnnotationNode, messageId: `nonVoidFuncIn${msgId}`, - fix: fixer => { - if (funcNode.async) { - return fixer.replaceText(typeAnnotationNode, 'Promise'); - } - return fixer.replaceText(typeAnnotationNode, 'void'); - }, + fix: fixer => + fixer.replaceText( + typeAnnotationNode, + funcNode.async ? 'Promise' : 'void', + ), }); } } - // Iterate over all function's statements recursively. - for (const statement of util.walkStatements([funcNode.body])) { + // Iterate over all function's return statements. + for (const statement of util.walkStatements(funcNode.body.body)) { if ( statement.type !== AST_NODE_TYPES.ReturnStatement || statement.argument == null @@ -745,14 +717,14 @@ export default util.createRule({ continue; } + // This return statement causes the non-void return type. + // Fix it by discarding the return value. const returnKeyword = util.nullThrows( sourceCode.getFirstToken(statement, { filter: token => token.value === 'return', }), util.NullThrowsReasons.MissingToken('return keyword', statement.type), ); - - // This return statement causes the non-void return type. context.report({ node: returnKeyword, messageId: `nonVoidReturnIn${msgId}`, @@ -765,11 +737,13 @@ export default util.createRule({ ), }); } + + // No invalid returns found. The function is allowed. } function removeGeneratorStarFix( fixer: TSESLint.RuleFixer, - funcNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, + funcNode: TSESTree.FunctionExpression, ): TSESLint.RuleFix { const funcHeadNode = funcNode.parent.type === AST_NODE_TYPES.Property || @@ -801,8 +775,11 @@ export default util.createRule({ sourceCode.isSpaceBetween(starToken, afterStarToken) || afterStarToken.type === AST_TOKEN_TYPES.Punctuator ) { + // There is space between tokens or other token is a punctuator. + // No space necessary after removing the star. return fixer.remove(starToken); } + // Replace with space. return fixer.replaceText(starToken, ' '); } @@ -831,7 +808,7 @@ export default util.createRule({ return fixer.removeRange([asyncToken.range[0], afterAsyncToken.range[0]]); } - function* emptyFuncFix( + function* removeFuncBodyFix( fixer: TSESLint.RuleFixer, funcNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, ): Generator { @@ -890,5 +867,41 @@ export default util.createRule({ yield fixer.insertTextAfterRange(bodyRange, ')(); }'); } } + + function* addVoidToArrowFix( + fixer: TSESLint.RuleFixer, + funcNode: TSESTree.ArrowFunctionExpression, + ): Generator { + // Remove async keyword + if (funcNode.async) { + yield removeAsyncKeywordFix(fixer, funcNode); + } + // Replace return type with void + if (funcNode.returnType != null) { + yield fixer.replaceText(funcNode.returnType.typeAnnotation, 'void'); + } + // Add void operator + yield util.getWrappingFixer({ + node: funcNode.body, + sourceCode, + wrap: code => `void ${code}`, + })(fixer); + } + + function* addBracesToArrowFix( + fixer: TSESLint.RuleFixer, + funcNode: TSESTree.ArrowFunctionExpression, + ): Generator { + // Remove async keyword + if (funcNode.async) { + yield removeAsyncKeywordFix(fixer, funcNode); + } + // Replace return type with void + if (funcNode.returnType != null) { + yield fixer.replaceText(funcNode.returnType.typeAnnotation, 'void'); + } + // Add braces + yield util.addBracesToArrowFix(fixer, sourceCode, funcNode); + } }, }); diff --git a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts index ab51567fa242..c2fbe42d57b4 100644 --- a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts @@ -960,6 +960,21 @@ ruleTester.run('strict-void-return', rule, { errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 13 }], output: null, }, + { + code: ` + type AnyFunc = (...args: unknown[]) => unknown; + declare function foo(cb: F): void; + foo(async () => ({})); + foo<() => void>(async () => ({})); + `, + errors: [{ messageId: 'asyncFuncInArg', line: 5, column: 34 }], + output: ` + type AnyFunc = (...args: unknown[]) => unknown; + declare function foo(cb: F): void; + foo(async () => ({})); + foo<() => void>(() => {}); + `, + }, { options: [{ allowReturnUndefined: false }], code: ` @@ -1411,7 +1426,7 @@ ruleTester.run('strict-void-return', rule, { code: ` [1, 2].forEach(async x => console.log(x)); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 2, column: 35 }], + errors: [{ messageId: 'asyncFuncInArg', line: 2, column: 32 }], output: ` [1, 2].forEach(x => void console.log(x)); `, @@ -1430,7 +1445,7 @@ ruleTester.run('strict-void-return', rule, { code: ` const foo: () => void = async () => Promise.resolve(true); `, - errors: [{ messageId: 'nonVoidReturnInVar', line: 2, column: 45 }], + errors: [{ messageId: 'asyncFuncInVar', line: 2, column: 42 }], output: ` const foo: () => void = () => { Promise.resolve(true); }; `, @@ -1484,7 +1499,7 @@ ruleTester.run('strict-void-return', rule, { }, { code: 'const cb: () => void = async (): Promise => Promise.resolve(1);', - errors: [{ messageId: 'nonVoidReturnInVar', line: 1, column: 53 }], + errors: [{ messageId: 'asyncFuncInVar', line: 1, column: 50 }], output: 'const cb: () => void = (): void => void Promise.resolve(1);', }, { From 2d82282a22f4af39b141e85fcb7d8122e71c43b9 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 5 Aug 2024 16:05:30 +0200 Subject: [PATCH 06/33] update to new RuleTester --- .../tests/fixtures/tsconfig.dom.json | 11 +-- .../tests/rules/strict-void-return.test.ts | 81 +++++++++++++++++-- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig.dom.json b/packages/eslint-plugin/tests/fixtures/tsconfig.dom.json index 9eaad1181ee2..68d7b6065ae5 100644 --- a/packages/eslint-plugin/tests/fixtures/tsconfig.dom.json +++ b/packages/eslint-plugin/tests/fixtures/tsconfig.dom.json @@ -1,11 +1,6 @@ { + "extends": "./tsconfig.json", "compilerOptions": { - "jsx": "preserve", - "target": "es5", - "module": "commonjs", - "strict": true, - "esModuleInterop": true, - "lib": ["es2015", "es2017", "esnext", "dom"] - }, - "include": ["file.ts", "react.tsx"] + "lib": ["es2023", "dom"] + } } diff --git a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts index c2fbe42d57b4..576d9f91d8bc 100644 --- a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts @@ -6,10 +6,11 @@ import { getFixturesRootDir } from '../RuleTester'; const rootDir = getFixturesRootDir(); const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - tsconfigRootDir: rootDir, - project: './tsconfig.dom.json', + languageOptions: { + parserOptions: { + tsconfigRootDir: rootDir, + project: './tsconfig.dom.json', + }, }, }); @@ -21,6 +22,23 @@ ruleTester.run('strict-void-return', rule, { foo(() => () => []); `, }, + { + code: ` + declare function foo(cb: () => void): void; + type Void = void; + foo((): Void => { + return; + }); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo((): ReturnType => { + return; + }); + `, + }, { code: ` declare function foo(cb: any): void; @@ -1489,13 +1507,22 @@ ruleTester.run('strict-void-return', rule, { }; `, errors: [{ messageId: 'nonVoidFuncInVar', line: 2, column: 42 }], - output: ` + output: [ + ` const cb: () => void = async (): Promise => { try { return Promise.resolve(1); } catch {} }; `, + ` + const cb: () => void = async (): Promise => { + try { + Promise.resolve(1); return; + } catch {} + }; + `, + ], }, { code: 'const cb: () => void = async (): Promise => Promise.resolve(1);', @@ -2444,7 +2471,8 @@ ruleTester.run('strict-void-return', rule, { } `, errors: [{ messageId: 'genFuncInExtMember', line: 6, column: 11 }], - output: ` + output: [ + ` class Foo { cb() {} } @@ -2452,6 +2480,15 @@ ruleTester.run('strict-void-return', rule, { async cb() {} } `, + ` + class Foo { + cb() {} + } + class Bar extends Foo { + cb() {} + } + `, + ], }, { code: ` @@ -2580,7 +2617,8 @@ ruleTester.run('strict-void-return', rule, { { messageId: 'asyncFuncInImplMember', line: 9, column: 11 }, { messageId: 'genFuncInImplMember', line: 10, column: 11 }, ], - output: ` + output: [ + ` interface Foo1 { cb1(): void; } @@ -2592,6 +2630,19 @@ ruleTester.run('strict-void-return', rule, { async cb2() {} } `, + ` + interface Foo1 { + cb1(): void; + } + interface Foo2 { + cb2: () => void; + } + class Bar implements Foo1, Foo2 { + cb1() {} + cb2() {} + } + `, + ], }, { code: ` @@ -2610,7 +2661,8 @@ ruleTester.run('strict-void-return', rule, { { messageId: 'asyncFuncInImplMember', line: 9, column: 11 }, { messageId: 'genFuncInImplMember', line: 10, column: 11 }, ], - output: ` + output: [ + ` interface Foo1 { cb1(): void; } @@ -2622,6 +2674,19 @@ ruleTester.run('strict-void-return', rule, { async cb2() {} } `, + ` + interface Foo1 { + cb1(): void; + } + interface Foo2 extends Foo1 { + cb2: () => void; + } + class Bar implements Foo2 { + cb1() {} + cb2() {} + } + `, + ], }, { code: ` From 8cea97b42b6dddaf21fffe6a124f8c289e3a1b10 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 9 Aug 2024 14:01:42 +0200 Subject: [PATCH 07/33] more detailed messages --- .../src/rules/strict-void-return.ts | 215 +++-- .../src/util/getBaseTypesOfClassMember.ts | 37 +- .../src/util/getNameFromExpression.ts | 62 ++ packages/eslint-plugin/src/util/index.ts | 1 + .../strict-void-return.shot | 28 +- .../tests/rules/strict-void-return.test.ts | 891 +++++++++++++++--- 6 files changed, 1035 insertions(+), 199 deletions(-) create mode 100644 packages/eslint-plugin/src/util/getNameFromExpression.ts diff --git a/packages/eslint-plugin/src/rules/strict-void-return.ts b/packages/eslint-plugin/src/rules/strict-void-return.ts index 96ce668ecee8..95e92af36eeb 100644 --- a/packages/eslint-plugin/src/rules/strict-void-return.ts +++ b/packages/eslint-plugin/src/rules/strict-void-return.ts @@ -30,7 +30,8 @@ type ErrorPlaceId = | 'Prop' | 'Return' | 'ExtMember' - | 'ImplMember'; + | 'ImplMember' + | 'Other'; type ErrorMessageId = | `nonVoidReturnIn${ErrorPlaceId}` @@ -56,93 +57,103 @@ export default util.createRule({ }, messages: { nonVoidReturnInArg: - 'Value returned in a callback argument where a void callback was expected.', + 'Value returned in a callback argument to `{{funcName}}`, which expects a void callback.', asyncFuncInArg: - 'Async callback passed as an argument where a void callback was expected.', + 'Async callback passed as an argument to `{{funcName}}`, which expects a void callback.', asyncNoTryCatchFuncInArg: - 'Async callback not wrapped with a try-catch block and passed as an argument where a void callback was expected.', + 'Async callback not wrapped with a try-catch block and passed as an argument to `{{funcName}}`, which expects a void callback.', genFuncInArg: - 'Generator callback passed as an argument where a void callback was expected.', + 'Generator callback passed as an argument to `{{funcName}}`, which expects a void callback.', nonVoidFuncInArg: - 'Value-returning callback passed as an argument where a void callback was expected.', + 'Value-returning callback passed as an argument to `{{funcName}}`, which expects a void callback.', nonVoidReturnInArgOverload: - 'Value returned in a callback argument where one of the function signatures suggests it should be a void callback.', + 'Value returned in a callback argument to `{{funcName}}`, whose other overload expects a void callback.', asyncFuncInArgOverload: - 'Async callback passed as an argument where one of the function signatures suggests it should be a void callback.', + 'Async callback passed as an argument to `{{funcName}}`, whose other overload expects a void callback.', asyncNoTryCatchFuncInArgOverload: - 'Async callback not wrapped with a try-catch block and passed as an argument where one of the function signatures suggests it should be a void callback.', + 'Async callback not wrapped with a try-catch block and passed as an argument to `{{funcName}}`, whose other overload expects a void callback.', genFuncInArgOverload: - 'Generator callback passed as an argument where one of the function signatures suggests it should be a void callback.', + 'Generator callback passed as an argument to `{{funcName}}`, whose other overload expects a void callback.', nonVoidFuncInArgOverload: - 'Value-returning callback passed as an argument where one of the function signatures suggests it should be a void callback.', + 'Value-returning callback passed as an argument to `{{funcName}}`, whose other overload expects a void callback.', nonVoidReturnInAttr: - 'Value returned in a callback attribute where a void callback was expected.', + 'Value returned in `{{attrName}}` event handler prop of `{{elemName}}`, which expects a void `{{attrName}}` event handler.', asyncFuncInAttr: - 'Async callback passed as an attribute where a void callback was expected.', + 'Async event handler `{{attrName}}` passed as a prop to `{{elemName}}`, which expects a void `{{attrName}}` event handler.', asyncNoTryCatchFuncInAttr: - 'Async callback not wrapped with a try-catch block and passed as an attribute where a void callback was expected.', + 'Async event handler `{{attrName}}` not wrapped with a try-catch block and passed as a prop to `{{elemName}}`, which expects a void `{{attrName}}` event handler.', genFuncInAttr: - 'Generator callback passed as an attribute where a void callback was expected.', + 'Generator event handler `{{attrName}}` passed as a prop to `{{elemName}}`, which expects a void `{{attrName}}` event handler.', nonVoidFuncInAttr: - 'Value-returning callback passed as an attribute where a void callback was expected.', + 'Value-returning event handler `{{attrName}}` passed as a prop to `{{elemName}}`, which expects a void `{{attrName}}` event handler.', - // Also used for array elements nonVoidReturnInVar: - 'Value returned in a context where a void return was expected.', + 'Value returned in `{{varName}}` function variable, which expects a void function.', asyncFuncInVar: - 'Async function used in a context where a void function is expected.', + 'Async function assigned to `{{varName}}` variable, which expects a void function.', asyncNoTryCatchFuncInVar: - 'Async function not wrapped with a try-catch block and used in a context where a void function is expected.', + 'Async function not wrapped with a try-catch block and assigned to `{{varName}}` variable, which expects a void function.', genFuncInVar: - 'Generator function used in a context where a void function is expected.', + 'Generator function assigned to `{{varName}}` variable, which expects a void function.', nonVoidFuncInVar: - 'Value-returning function used in a context where a void function is expected.', + 'Value-returning function assigned to `{{varName}}` variable, which expects a void function.', nonVoidReturnInProp: - 'Value returned in an object method which must be a void method.', + 'Value returned in `{{propName}}` method of an object, which expects a void `{{propName}}` method.', asyncFuncInProp: - 'Async function provided as an object method which must be a void method.', + 'Async function passed as `{{propName}}` method of an object, which expects a void `{{propName}}` method.', asyncNoTryCatchFuncInProp: - 'Async function not wrapped with a try-catch block and provided as an object method which must be a void method.', + 'Async function not wrapped with a try-catch block and passed as `{{propName}}` method of an object, which expects a void `{{propName}}` method.', genFuncInProp: - 'Generator function provided as an object method which must be a void method.', + 'Generator function passed as `{{propName}}` method of an object, which expects a void `{{propName}}` method.', nonVoidFuncInProp: - 'Value-returning function provided as an object method which must be a void method.', + 'Value-returning function passed as `{{propName}}` method of an object, which expects a void `{{propName}}` method.', nonVoidReturnInReturn: - 'Value returned in a callback returned from a function which must return a void callback', + 'Value returned in a callback returned from a function, which must return a void callback', asyncFuncInReturn: - 'Async callback returned from a function which must return a void callback.', + 'Async callback returned from a function, which must return a void callback.', asyncNoTryCatchFuncInReturn: - 'Async callback not wrapped with a try-catch block and returned from a function which must return a void callback.', + 'Async callback not wrapped with a try-catch block and returned from a function, which must return a void callback.', genFuncInReturn: - 'Generator callback returned from a function which must return a void callback.', + 'Generator callback returned from a function, which must return a void callback.', nonVoidFuncInReturn: - 'Value-returning callback returned from a function which must return a void callback.', + 'Value-returning callback returned from a function, which must return a void callback.', nonVoidReturnInExtMember: - 'Value returned in a method which overrides a void method.', + 'Value returned in `{{memberName}}` method of `{{className}}`, whose base class `{{baseName}}` declares it as a void method.', asyncFuncInExtMember: - 'Overriding a void method with an async method is forbidden.', + 'Async function provided as `{{memberName}}` method of `{{className}}`, whose base class `{{baseName}}` declares it as a void method.', asyncNoTryCatchFuncInExtMember: - 'Overriding a void method with an async method requires wrapping the body in a try-catch block.', + 'Async function not wrapped with a try-catch block and provided as `{{memberName}}` method of `{{className}}`, whose base class `{{baseName}}` declares it as a void method.', genFuncInExtMember: - 'Overriding a void method with a generator method is forbidden.', + 'Generator function provided as `{{memberName}}` method of `{{className}}`, whose base class `{{baseName}}` declares it as a void method.', nonVoidFuncInExtMember: - 'Overriding a void method with a value-returning function is forbidden.', + 'Value-returning function provided as `{{memberName}}` method of `{{className}}`, whose base class `{{baseName}}` declares it as a void method.', nonVoidReturnInImplMember: - 'Value returned in a method which implements a void method.', + 'Value returned in `{{memberName}}` method of `{{className}}`, whose interface `{{baseName}}` declares it as a void method.', asyncFuncInImplMember: - 'Implementing a void method as an async method is forbidden.', + 'Async function provided as `{{memberName}}` method of `{{className}}`, whose interface `{{baseName}}` declares it as a void method.', asyncNoTryCatchFuncInImplMember: - 'Implementing a void method as an async method requires wrapping the body in a try-catch block.', + 'Async function not wrapped with a try-catch block and provided as `{{memberName}}` method of `{{className}}`, whose interface `{{baseName}}` declares it as a void method.', genFuncInImplMember: - 'Implementing a void method as a generator method is forbidden.', + 'Generator function provided as `{{memberName}}` method of `{{className}}`, whose interface `{{baseName}}` declares it as a void method.', nonVoidFuncInImplMember: - 'Implementing a void method as a value-returning function is forbidden.', + 'Value-returning function provided as `{{memberName}}` method of `{{className}}`, whose interface `{{baseName}}` declares it as a void method.', + + nonVoidReturnInOther: + 'Value returned in a context where a void return was expected.', + asyncFuncInOther: + 'Async function used in a context where a void function was expected.', + asyncNoTryCatchFuncInOther: + 'Async function not wrapped with a try-catch block and used in a context where a void function was expected.', + genFuncInOther: + 'Generator function used in a context where a void function was expected.', + nonVoidFuncInOther: + 'Value-returning function used in a context where a void function was expected.', suggestWrapInAsyncIIFE: 'Wrap the function body in an immediately-invoked async function expression.', @@ -186,19 +197,37 @@ export default util.createRule({ ): void => { checkFunctionCallNode(node); }, - JSXExpressionContainer: (node): void => { - if (node.expression.type !== AST_NODE_TYPES.JSXEmptyExpression) { - checkExpressionNode(node.expression, 'Attr'); + JSXAttribute: (node): void => { + if ( + node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && + node.value.expression.type !== AST_NODE_TYPES.JSXEmptyExpression + ) { + const attrName = sourceCode.getText(node.name); + const elemName = sourceCode.getText(node.parent.name); + checkExpressionNode(node.value.expression, 'Attr', { + attrName, + elemName, + }); } }, VariableDeclarator: (node): void => { if (node.init != null) { - checkExpressionNode(node.init, 'Var'); + const varName = util.getNameFromExpression(sourceCode, node.id); + if (varName != null) { + checkExpressionNode(node.init, 'Var', { varName }); + } else { + checkExpressionNode(node.init, 'Other'); + } } }, AssignmentExpression: (node): void => { if (['=', '||=', '&&=', '??='].includes(node.operator)) { - checkExpressionNode(node.right, 'Var'); + const varName = util.getNameFromExpression(sourceCode, node.left); + if (varName != null) { + checkExpressionNode(node.right, 'Var', { varName }); + } else { + checkExpressionNode(node.right, 'Other'); + } } }, ObjectExpression: (node): void => { @@ -214,7 +243,7 @@ export default util.createRule({ elemNode != null && elemNode.type !== AST_NODE_TYPES.SpreadElement ) { - checkExpressionNode(elemNode, 'Var'); + checkExpressionNode(elemNode, 'Other'); } } }, @@ -244,9 +273,11 @@ export default util.createRule({ tsutils.unionTypeParts(signature.getReturnType()), ); return ( + // At least one return type is void returnTypes.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Void), ) && + // The rest are nullish or any returnTypes.every(type => tsutils.isTypeFlagSet( type, @@ -270,13 +301,14 @@ export default util.createRule({ function checkExpressionNode( node: TSESTree.Expression, msgId: ErrorPlaceId, + data?: Record, ): boolean { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); assert(ts.isExpression(tsNode)); const expectedType = checker.getContextualType(tsNode); if (expectedType != null && isVoidReturningFunctionType(expectedType)) { - reportIfNonVoidFunction(node, msgId); + reportIfNonVoidFunction(node, msgId, data); return true; } @@ -300,6 +332,9 @@ export default util.createRule({ function checkFunctionCallNode( callNode: TSESTree.CallExpression | TSESTree.NewExpression, ): void { + const funcName = + util.getNameFromExpression(sourceCode, callNode.callee) ?? 'function'; + const callTsNode = parserServices.esTreeNodeToTSNodeMap.get(callNode); for (const [argIdx, argNode] of callNode.arguments.entries()) { if (argNode.type === AST_NODE_TYPES.SpreadElement) { @@ -307,7 +342,7 @@ export default util.createRule({ } // Check against the contextual type first - if (checkExpressionNode(argNode, 'Arg')) { + if (checkExpressionNode(argNode, 'Arg', { funcName })) { continue; } @@ -349,7 +384,7 @@ export default util.createRule({ ) ) { // We treat this argument as void even though it might be technically any. - reportIfNonVoidFunction(argNode, 'ArgOverload'); + reportIfNonVoidFunction(argNode, 'ArgOverload', { funcName }); } continue; } @@ -371,6 +406,7 @@ export default util.createRule({ ) { return; } + const propName = sourceCode.getText(propNode.key); const propTsNode = parserServices.esTreeNodeToTSNodeMap.get(propNode); if (propTsNode.kind === ts.SyntaxKind.MethodDeclaration) { @@ -400,13 +436,13 @@ export default util.createRule({ propTsNode, ); if (isVoidReturningFunctionType(propExpectedType)) { - reportIfNonVoidFunction(propNode.value, 'Prop'); + reportIfNonVoidFunction(propNode.value, 'Prop', { propName }); } return; } // Object property is a regular property. - checkExpressionNode(propNode.value, 'Prop'); + checkExpressionNode(propNode.value, 'Prop', { propName }); } /** @@ -425,35 +461,53 @@ export default util.createRule({ return; } const propTsNode = parserServices.esTreeNodeToTSNodeMap.get(propNode); + const memberName = sourceCode.getText(propNode.key); + const className = propNode.parent.parent.id?.name ?? 'class'; // Check in comparison to the base class property. if (options.considerBaseClass) { - for (const basePropType of util.getBaseTypesOfClassMember( + for (const { + baseType, + baseMemberType, + } of util.getBaseTypesOfClassMember( checker, propTsNode, ts.SyntaxKind.ExtendsKeyword, )) { - if (isVoidReturningFunctionType(basePropType)) { - reportIfNonVoidFunction(propNode.value, 'ExtMember'); + const baseName = baseType.getSymbol()?.name ?? 'class'; + if (isVoidReturningFunctionType(baseMemberType)) { + reportIfNonVoidFunction(propNode.value, 'ExtMember', { + memberName, + className, + baseName, + }); } } } // Check in comparison to the implemented interfaces. if (options.considerImplementedInterfaces) { - for (const basePropType of util.getBaseTypesOfClassMember( + for (const { + baseType, + baseMemberType, + } of util.getBaseTypesOfClassMember( checker, propTsNode, ts.SyntaxKind.ImplementsKeyword, )) { - if (isVoidReturningFunctionType(basePropType)) { - reportIfNonVoidFunction(propNode.value, 'ImplMember'); + const baseName = baseType.getSymbol()?.name ?? 'interface'; + if (isVoidReturningFunctionType(baseMemberType)) { + reportIfNonVoidFunction(propNode.value, 'ImplMember', { + memberName, + className, + baseName, + }); } } } // Check in comparison to the contextual type. - checkExpressionNode(propNode.value, 'Prop'); + checkExpressionNode(propNode.value, 'Prop', { propName: memberName }); } /** @@ -478,29 +532,47 @@ export default util.createRule({ ) { return; } + const memberName = sourceCode.getText(methodNode.key); + const className = methodNode.parent.parent.id?.name ?? 'class'; // Check in comparison to the base class method. if (options.considerBaseClass) { - for (const baseMethodType of util.getBaseTypesOfClassMember( + for (const { + baseType, + baseMemberType, + } of util.getBaseTypesOfClassMember( checker, methodTsNode, ts.SyntaxKind.ExtendsKeyword, )) { - if (isVoidReturningFunctionType(baseMethodType)) { - reportIfNonVoidFunction(methodNode.value, 'ExtMember'); + const baseName = baseType.getSymbol()?.name ?? 'class'; + if (isVoidReturningFunctionType(baseMemberType)) { + reportIfNonVoidFunction(methodNode.value, 'ExtMember', { + memberName, + className, + baseName, + }); } } } // Check in comparison to the implemented interfaces. if (options.considerImplementedInterfaces) { - for (const baseMethodType of util.getBaseTypesOfClassMember( + for (const { + baseType, + baseMemberType, + } of util.getBaseTypesOfClassMember( checker, methodTsNode, ts.SyntaxKind.ImplementsKeyword, )) { - if (isVoidReturningFunctionType(baseMethodType)) { - reportIfNonVoidFunction(methodNode.value, 'ImplMember'); + const baseName = baseType.getSymbol()?.name ?? 'interface'; + if (isVoidReturningFunctionType(baseMemberType)) { + reportIfNonVoidFunction(methodNode.value, 'ImplMember', { + memberName, + className, + baseName, + }); } } } @@ -512,6 +584,7 @@ export default util.createRule({ function reportIfNonVoidFunction( funcNode: TSESTree.Expression, msgId: ErrorPlaceId, + data?: Record, ): void { const allowedReturnType = ts.TypeFlags.Void | @@ -545,6 +618,7 @@ export default util.createRule({ return context.report({ node: funcNode, messageId: `nonVoidFuncIn${msgId}`, + data, }); } @@ -561,6 +635,7 @@ export default util.createRule({ return context.report({ loc: util.getFunctionHeadLoc(funcNode, sourceCode), messageId: `genFuncIn${msgId}`, + data, fix: fixer => removeGeneratorStarFix(fixer, funcNode), }); } @@ -570,6 +645,7 @@ export default util.createRule({ return context.report({ loc: util.getFunctionHeadLoc(funcNode, sourceCode), messageId: `genFuncIn${msgId}`, + data, }); } @@ -586,6 +662,7 @@ export default util.createRule({ return context.report({ loc: util.getFunctionHeadLoc(funcNode, sourceCode), messageId: `asyncFuncIn${msgId}`, + data, fix: fixer => removeFuncBodyFix(fixer, funcNode), }); } @@ -598,6 +675,7 @@ export default util.createRule({ return context.report({ loc: util.getFunctionHeadLoc(funcNode, sourceCode), messageId: `asyncFuncIn${msgId}`, + data, fix: fixer => options.allowReturnUndefined ? addVoidToArrowFix(fixer, funcNode) @@ -613,6 +691,7 @@ export default util.createRule({ return context.report({ loc: util.getFunctionHeadLoc(funcNode, sourceCode), messageId: `asyncFuncIn${msgId}`, + data, suggest: [ { messageId: 'suggestWrapInAsyncIIFE', @@ -634,6 +713,7 @@ export default util.createRule({ return context.report({ loc: util.getFunctionHeadLoc(funcNode, sourceCode), messageId: `asyncNoTryCatchFuncIn${msgId}`, + data, suggest: [ { messageId: 'suggestWrapInTryCatch', @@ -658,6 +738,7 @@ export default util.createRule({ return context.report({ node: funcNode.body, messageId: `nonVoidReturnIn${msgId}`, + data, fix: fixer => !ASTUtils.hasSideEffect(funcNode.body, sourceCode) ? removeFuncBodyFix(fixer, funcNode) @@ -690,6 +771,7 @@ export default util.createRule({ return context.report({ node: typeAnnotationNode, messageId: `nonVoidFuncIn${msgId}`, + data, fix: fixer => fixer.replaceText( typeAnnotationNode, @@ -728,6 +810,7 @@ export default util.createRule({ context.report({ node: returnKeyword, messageId: `nonVoidReturnIn${msgId}`, + data, fix: fixer => util.discardReturnValueFix( fixer, diff --git a/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts b/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts index ba47d841a874..17fda233b749 100644 --- a/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts +++ b/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts @@ -3,25 +3,36 @@ import * as ts from 'typescript'; /** * Given a member of a class which extends another class or implements an interface, - * returns the corresponding member type for each of the base class/interfaces. + * yields the corresponding member type for each of the base class/interfaces. */ -export function getBaseTypesOfClassMember( +export function* getBaseTypesOfClassMember( checker: ts.TypeChecker, memberTsNode: ts.PropertyDeclaration | ts.MethodDeclaration, heritageToken: ts.SyntaxKind.ExtendsKeyword | ts.SyntaxKind.ImplementsKeyword, -): ts.Type[] { +): Generator<{ baseType: ts.Type; baseMemberType: ts.Type }> { assert(ts.isClassLike(memberTsNode.parent)); const memberSymbol = checker.getSymbolAtLocation(memberTsNode.name); if (memberSymbol == null) { - return []; + return; + } + for (const clauseNode of memberTsNode.parent.heritageClauses ?? []) { + if (clauseNode.token !== heritageToken) { + continue; + } + for (const baseTypeNode of clauseNode.types) { + const baseType = checker.getTypeAtLocation(baseTypeNode); + const baseMemberSymbol = checker.getPropertyOfType( + baseType, + memberSymbol.name, + ); + if (baseMemberSymbol == null) { + continue; + } + const baseMemberType = checker.getTypeOfSymbolAtLocation( + baseMemberSymbol, + memberTsNode, + ); + yield { baseType, baseMemberType }; + } } - return (memberTsNode.parent.heritageClauses ?? []) - .filter(clauseNode => clauseNode.token === heritageToken) - .flatMap(clauseNode => clauseNode.types) - .map(baseTypeNode => checker.getTypeAtLocation(baseTypeNode)) - .map(baseType => checker.getPropertyOfType(baseType, memberSymbol.name)) - .filter(baseMemberSymbol => baseMemberSymbol != null) - .map(baseMemberSymbol => - checker.getTypeOfSymbolAtLocation(baseMemberSymbol, memberTsNode), - ); } diff --git a/packages/eslint-plugin/src/util/getNameFromExpression.ts b/packages/eslint-plugin/src/util/getNameFromExpression.ts new file mode 100644 index 000000000000..8ae35ab7af15 --- /dev/null +++ b/packages/eslint-plugin/src/util/getNameFromExpression.ts @@ -0,0 +1,62 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +/** + * Returns the identifier or chain of identifiers + * that refers to the value of the expression, + * if any. + */ +export function getIdentifierFromExpression( + node: TSESTree.Expression | TSESTree.PrivateIdentifier, +): + | TSESTree.Literal + | TSESTree.Identifier + | TSESTree.PrivateIdentifier + | TSESTree.Super + | TSESTree.MemberExpression + | null { + if ( + node.type === AST_NODE_TYPES.Literal || + node.type === AST_NODE_TYPES.Identifier || + node.type === AST_NODE_TYPES.PrivateIdentifier || + node.type === AST_NODE_TYPES.Super + ) { + return node; + } + if (node.type === AST_NODE_TYPES.MemberExpression) { + const objectNode = getIdentifierFromExpression(node.object); + const propertyNode = getIdentifierFromExpression(node.property); + if (objectNode != null && propertyNode != null) { + return node; + } + return propertyNode; + } + if ( + node.type === AST_NODE_TYPES.ChainExpression || + node.type === AST_NODE_TYPES.TSNonNullExpression + ) { + return getIdentifierFromExpression(node.expression); + } + return null; +} + +/** + * {@link getIdentifierFromExpression} but returns the name as a string. + */ +export function getNameFromExpression( + sourceCode: TSESLint.SourceCode, + node: TSESTree.Expression | TSESTree.PrivateIdentifier | null, +): string | null { + if (node == null) { + return null; + } + const nameNode = getIdentifierFromExpression(node); + if (nameNode == null) { + return null; + } + if (nameNode.type === AST_NODE_TYPES.Identifier) { + // Get without type annotation + return nameNode.name; + } + return sourceCode.getText(nameNode); +} diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 0bf5bca8cc06..fd17db272121 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -7,6 +7,7 @@ export * from './createRule'; export * from './discardReturnValueFix'; export * from './getBaseTypesOfClassMember'; export * from './getFunctionHeadLoc'; +export * from './getNameFromExpression'; export * from './getOperatorPrecedence'; export * from './getRangeWithParens'; export * from './getStaticStringValue'; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot index da2c46ea83e9..0b2f96e0bcff 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot @@ -4,7 +4,7 @@ exports[`Validating rule docs strict-void-return.mdx code examples ESLint output "Incorrect const bad: () => void = () => 2137; - ~~~~ Value returned in a context where a void return was expected. + ~~~~ Value returned in \`bad\` function variable, which expects a void function. const func = Math.random() > 0.1 ? bad : prompt; const val = func(); if (val) console.log(val.toUpperCase()); // ❌ Crash if bad was called @@ -27,7 +27,7 @@ exports[`Validating rule docs strict-void-return.mdx code examples ESLint output declare function takesCallback(cb: () => void): void; takesCallback(async () => { - ~~ Async callback not wrapped with a try-catch block and passed as an argument where a void callback was expected. + ~~ Async callback not wrapped with a try-catch block and passed as an argument to \`takesCallback\`, which expects a void callback. const response = await fetch('https://api.example.com/'); const data = await response.json(); console.log(data); @@ -56,7 +56,7 @@ exports[`Validating rule docs strict-void-return.mdx code examples ESLint output declare function takesCallback(cb: () => void): void; takesCallback(function* () { - ~~~~~~~~~~ Generator callback passed as an argument where a void callback was expected. + ~~~~~~~~~~ Generator callback passed as an argument to \`takesCallback\`, which expects a void callback. console.log('Hello'); yield; console.log('World'); @@ -84,7 +84,7 @@ exports[`Validating rule docs strict-void-return.mdx code examples ESLint output "Incorrect ['Kazik', 'Zenek'].forEach(name => \`Hello, \${name}!\`); - ~~~~~~~~~~~~~~~~~ Value returned in a callback argument where a void callback was expected. + ~~~~~~~~~~~~~~~~~ Value returned in a callback argument to \`forEach\`, which expects a void callback. " `; @@ -103,7 +103,7 @@ Options: { "considerOtherOverloads": true } document.addEventListener('click', () => { return 'Clicked'; - ~~~~~~ Value returned in a callback argument where one of the function signatures suggests it should be a void callback. + ~~~~~~ Value returned in a callback argument to \`document.addEventListener\`, whose other overload expects a void callback. }); " `; @@ -130,7 +130,7 @@ class MyElement extends HTMLElement { click() { super.click(); return 'Clicked'; - ~~~~~~ Value returned in a method which overrides a void method. + ~~~~~~ Value returned in \`click\` method of \`MyElement\`, whose base class \`HTMLElement\` declares it as a void method. } } " @@ -160,7 +160,7 @@ Options: { "considerImplementedInterfaces": true } class FooListener implements EventListenerObject { handleEvent() { return 'Handled'; - ~~~~~~ Value returned in a method which implements a void method. + ~~~~~~ Value returned in \`handleEvent\` method of \`FooListener\`, whose interface \`EventListenerObject\` declares it as a void method. } } " @@ -185,7 +185,7 @@ exports[`Validating rule docs strict-void-return.mdx code examples ESLint output Options: { "allowReturnPromiseIfTryCatch": false } const cb: () => void = async () => { - ~~ Async function used in a context where a void function is expected. + ~~ Async function assigned to \`cb\` variable, which expects a void function. try { const response = await fetch('https://api.example.com/'); const data = await response.json(); @@ -218,11 +218,11 @@ Options: { "allowReturnUndefined": false } let cb: () => void; cb = () => undefined; - ~~~~~~~~~ Value returned in a context where a void return was expected. + ~~~~~~~~~ Value returned in \`cb\` function variable, which expects a void function. cb = () => { return void 0; - ~~~~~~ Value returned in a context where a void return was expected. + ~~~~~~ Value returned in \`cb\` function variable, which expects a void function. }; " `; @@ -248,11 +248,11 @@ Options: { "allowReturnNull": false } let cb: () => void; cb = () => null; - ~~~~ Value returned in a context where a void return was expected. + ~~~~ Value returned in \`cb\` function variable, which expects a void function. cb = () => { return null; - ~~~~~~ Value returned in a context where a void return was expected. + ~~~~~~ Value returned in \`cb\` function variable, which expects a void function. }; " `; @@ -278,11 +278,11 @@ Options: { "allowReturnAny": false } declare function fn(cb: () => void): void; fn(() => JSON.parse('{}')); - ~~~~~~~~~~~~~~~~ Value returned in a callback argument where a void callback was expected. + ~~~~~~~~~~~~~~~~ Value returned in a callback argument to \`fn\`, which expects a void callback. fn(() => { return someUntypedApi(); - ~~~~~~ Value returned in a callback argument where a void callback was expected. + ~~~~~~ Value returned in a callback argument to \`fn\`, which expects a void callback. }); " `; diff --git a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts index 576d9f91d8bc..d42617b58186 100644 --- a/packages/eslint-plugin/tests/rules/strict-void-return.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts @@ -703,6 +703,21 @@ ruleTester.run('strict-void-return', rule, { } `, }, + { + code: ` + class Bar {} + class Foo extends Bar { + foo = () => 1; + } + `, + }, + { + code: ` + class Foo extends Wtf { + foo = () => 1; + } + `, + }, { code: ` class Foo { @@ -911,7 +926,14 @@ ruleTester.run('strict-void-return', rule, { declare function foo(cb: () => void): void; foo(() => false); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 3, column: 19 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 3, + column: 19, + }, + ], output: ` declare function foo(cb: () => void): void; foo(() => {}); @@ -922,7 +944,14 @@ ruleTester.run('strict-void-return', rule, { declare function foo(cb: () => void): void; foo(() => (((true)))); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 3, column: 22 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 3, + column: 22, + }, + ], output: ` declare function foo(cb: () => void): void; foo(() => {}); @@ -937,7 +966,14 @@ ruleTester.run('strict-void-return', rule, { } }); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 5, column: 13 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 5, + column: 13, + }, + ], output: ` declare function foo(cb: () => void): void; foo(() => { @@ -952,7 +988,14 @@ ruleTester.run('strict-void-return', rule, { declare function foo(arg: number, cb: () => void): void; foo(0, () => 0); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 3, column: 22 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 3, + column: 22, + }, + ], output: ` declare function foo(arg: number, cb: () => void): void; foo(0, () => {}); @@ -963,19 +1006,67 @@ ruleTester.run('strict-void-return', rule, { declare function foo(cb?: { (): void }): void; foo(() => () => {}); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 3, column: 19 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 3, + column: 19, + }, + ], output: ` declare function foo(cb?: { (): void }): void; foo(() => {}); `, }, + { + code: ` + declare const obj: { foo(cb: () => void) } | null; + obj?.foo(() => JSON.parse('{}')); + `, + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'obj?.foo' }, + line: 3, + column: 24, + }, + ], + output: ` + declare const obj: { foo(cb: () => void) } | null; + obj?.foo(() => void JSON.parse('{}')); + `, + }, + { + code: ` + ((cb: () => void) => cb())!(() => 1); + `, + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'function' }, + line: 2, + column: 43, + }, + ], + output: ` + ((cb: () => void) => cb())!(() => {}); + `, + }, { code: ` declare function foo(cb: { (): void }): void; declare function cb(): string; foo(cb); `, - errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 13 }], + errors: [ + { + messageId: 'nonVoidFuncInArg', + data: { funcName: 'foo' }, + line: 4, + column: 13, + }, + ], output: null, }, { @@ -985,7 +1076,14 @@ ruleTester.run('strict-void-return', rule, { foo(async () => ({})); foo<() => void>(async () => ({})); `, - errors: [{ messageId: 'asyncFuncInArg', line: 5, column: 34 }], + errors: [ + { + messageId: 'asyncFuncInArg', + data: { funcName: 'foo' }, + line: 5, + column: 34, + }, + ], output: ` type AnyFunc = (...args: unknown[]) => unknown; declare function foo(cb: F): void; @@ -1002,7 +1100,14 @@ ruleTester.run('strict-void-return', rule, { foo(null, () => Math.random()); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 6, column: 25 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 6, + column: 25, + }, + ], output: ` function foo(arg: T, cb: () => T); function foo(arg: null, cb: () => void); @@ -1018,7 +1123,14 @@ ruleTester.run('strict-void-return', rule, { foo(null, async () => {}); `, - errors: [{ messageId: 'asyncFuncInArg', line: 5, column: 28 }], + errors: [ + { + messageId: 'asyncFuncInArg', + data: { funcName: 'foo' }, + line: 5, + column: 28, + }, + ], output: ` declare function foo(arg: T, cb: () => T): void; declare function foo(arg: any, cb: () => void): void; @@ -1037,6 +1149,7 @@ ruleTester.run('strict-void-return', rule, { errors: [ { messageId: 'asyncNoTryCatchFuncInArg', + data: { funcName: 'foo' }, line: 4, column: 22, suggestions: [ @@ -1071,7 +1184,14 @@ ruleTester.run('strict-void-return', rule, { foo(cb); async function cb() {} `, - errors: [{ messageId: 'nonVoidFuncInArg', line: 3, column: 13 }], + errors: [ + { + messageId: 'nonVoidFuncInArg', + data: { funcName: 'foo' }, + line: 3, + column: 13, + }, + ], output: null, }, { @@ -1083,7 +1203,12 @@ ruleTester.run('strict-void-return', rule, { }); `, errors: [ - { messageId: 'nonVoidReturnInArgOverload', line: 5, column: 11 }, + { + messageId: 'nonVoidReturnInArgOverload', + data: { funcName: 'foo' }, + line: 5, + column: 11, + }, ], output: ` declare function foo void>(cb: Cb): void; @@ -1101,16 +1226,30 @@ ruleTester.run('strict-void-return', rule, { foo(cb); } `, - errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 15 }], + errors: [ + { + messageId: 'nonVoidFuncInArg', + data: { funcName: 'foo' }, + line: 4, + column: 15, + }, + ], output: null, }, { code: ` declare function foo(cb: { (): void }): void; const cb = () => dunno; - foo(cb); + foo!(cb); `, - errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 13 }], + errors: [ + { + messageId: 'nonVoidFuncInArg', + data: { funcName: 'foo' }, + line: 4, + column: 14, + }, + ], output: null, }, { @@ -1120,7 +1259,14 @@ ruleTester.run('strict-void-return', rule, { }; foo(false, () => Promise.resolve(undefined)); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 5, column: 26 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 5, + column: 26, + }, + ], output: ` declare const foo: { (arg: boolean, cb: () => void): void; @@ -1138,7 +1284,14 @@ ruleTester.run('strict-void-return', rule, { () => Promise.resolve(1), ); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 7, column: 17 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo.bar' }, + line: 7, + column: 17, + }, + ], output: ` declare const foo: { bar(cb1: () => any, cb2: () => void): void; @@ -1156,7 +1309,14 @@ ruleTester.run('strict-void-return', rule, { }; new Foo(async () => {}); `, - errors: [{ messageId: 'asyncFuncInArg', line: 5, column: 26 }], + errors: [ + { + messageId: 'asyncFuncInArg', + data: { funcName: 'Foo' }, + line: 5, + column: 26, + }, + ], output: ` declare const Foo: { new (cb: () => void): void; @@ -1179,8 +1339,18 @@ ruleTester.run('strict-void-return', rule, { }); `, errors: [ - { messageId: 'nonVoidReturnInArg', line: 6, column: 26 }, - { messageId: 'nonVoidReturnInArg', line: 7, column: 20 }, + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 6, + column: 26, + }, + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 7, + column: 20, + }, ], output: ` declare function foo(cb: () => void): void; @@ -1211,7 +1381,14 @@ ruleTester.run('strict-void-return', rule, { } while (maybe); }); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 8, column: 15 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 8, + column: 15, + }, + ], output: ` declare function foo(cb: () => void): void; foo(() => { @@ -1242,6 +1419,7 @@ ruleTester.run('strict-void-return', rule, { errors: [ { messageId: 'asyncFuncInArg', + data: { funcName: 'foo' }, line: 3, column: 22, suggestions: [ @@ -1271,7 +1449,14 @@ ruleTester.run('strict-void-return', rule, { }; new Foo(() => false); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 6, column: 23 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'Foo' }, + line: 6, + column: 23, + }, + ], output: ` declare const Foo: { new (cb: () => void): void; @@ -1288,7 +1473,14 @@ ruleTester.run('strict-void-return', rule, { }; Foo(() => false); `, - errors: [{ messageId: 'nonVoidReturnInArg', line: 6, column: 19 }], + errors: [ + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'Foo' }, + line: 6, + column: 19, + }, + ], output: ` declare const Foo: { new (cb: () => any): void; @@ -1309,7 +1501,14 @@ ruleTester.run('strict-void-return', rule, { return true; } `, - errors: [{ messageId: 'nonVoidFuncInArg', line: 7, column: 13 }], + errors: [ + { + messageId: 'nonVoidFuncInArg', + data: { funcName: 'foo' }, + line: 7, + column: 13, + }, + ], output: null, }, { @@ -1322,7 +1521,14 @@ ruleTester.run('strict-void-return', rule, { return 1 + 1; } `, - errors: [{ messageId: 'nonVoidFuncInArg', line: 5, column: 13 }], + errors: [ + { + messageId: 'nonVoidFuncInArg', + data: { funcName: 'foo' }, + line: 5, + column: 13, + }, + ], output: null, }, { @@ -1331,7 +1537,14 @@ ruleTester.run('strict-void-return', rule, { declare function cb(): boolean; foo(cb); `, - errors: [{ messageId: 'nonVoidFuncInArg', line: 4, column: 13 }], + errors: [ + { + messageId: 'nonVoidFuncInArg', + data: { funcName: 'foo' }, + line: 4, + column: 13, + }, + ], output: null, }, { @@ -1345,9 +1558,24 @@ ruleTester.run('strict-void-return', rule, { ); `, errors: [ - { messageId: 'nonVoidReturnInArg', line: 5, column: 17 }, - { messageId: 'nonVoidReturnInArg', line: 6, column: 17 }, - { messageId: 'nonVoidReturnInArg', line: 7, column: 17 }, + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 5, + column: 17, + }, + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 6, + column: 17, + }, + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 7, + column: 17, + }, ], output: ` declare function foo(...cbs: Array<() => void>): void; @@ -1370,8 +1598,18 @@ ruleTester.run('strict-void-return', rule, { ); `, errors: [ - { messageId: 'nonVoidReturnInArg', line: 5, column: 17 }, - { messageId: 'nonVoidReturnInArg', line: 6, column: 17 }, + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 5, + column: 17, + }, + { + messageId: 'nonVoidReturnInArg', + data: { funcName: 'foo' }, + line: 6, + column: 17, + }, ], output: ` declare function foo(...cbs: [() => void, () => void, (() => void)?]): void; @@ -1386,7 +1624,14 @@ ruleTester.run('strict-void-return', rule, { code: ` document.addEventListener('click', async () => {}); `, - errors: [{ messageId: 'asyncFuncInArgOverload', line: 2, column: 53 }], + errors: [ + { + messageId: 'asyncFuncInArgOverload', + data: { funcName: 'document.addEventListener' }, + line: 2, + column: 53, + }, + ], output: ` document.addEventListener('click', () => {}); `, @@ -1397,7 +1642,14 @@ ruleTester.run('strict-void-return', rule, { declare function foo(x: unknown, cb: () => any): void; foo({}, async () => {}); `, - errors: [{ messageId: 'asyncFuncInArgOverload', line: 4, column: 26 }], + errors: [ + { + messageId: 'asyncFuncInArgOverload', + data: { funcName: 'foo' }, + line: 4, + column: 26, + }, + ], output: ` declare function foo(x: null, cb: () => void): void; declare function foo(x: unknown, cb: () => any): void; @@ -1414,6 +1666,7 @@ ruleTester.run('strict-void-return', rule, { errors: [ { messageId: 'asyncNoTryCatchFuncInArg', + data: { funcName: 'arr.forEach' }, line: 3, column: 29, suggestions: [ @@ -1444,7 +1697,14 @@ ruleTester.run('strict-void-return', rule, { code: ` [1, 2].forEach(async x => console.log(x)); `, - errors: [{ messageId: 'asyncFuncInArg', line: 2, column: 32 }], + errors: [ + { + messageId: 'asyncFuncInArg', + data: { funcName: 'forEach' }, + line: 2, + column: 32, + }, + ], output: ` [1, 2].forEach(x => void console.log(x)); `, @@ -1453,24 +1713,59 @@ ruleTester.run('strict-void-return', rule, { code: ` const foo: () => void = () => false; `, - errors: [{ messageId: 'nonVoidReturnInVar', line: 2, column: 39 }], + errors: [ + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'foo' }, + line: 2, + column: 39, + }, + ], output: ` const foo: () => void = () => {}; `, }, + { + code: ` + const { name }: () => void = function foo() { + return false; + }; + `, + errors: [{ messageId: 'nonVoidReturnInOther', line: 3, column: 11 }], + output: ` + const { name }: () => void = function foo() { + \ + + }; + `, + }, { options: [{ allowReturnUndefined: false }], code: ` const foo: () => void = async () => Promise.resolve(true); `, - errors: [{ messageId: 'asyncFuncInVar', line: 2, column: 42 }], + errors: [ + { + messageId: 'asyncFuncInVar', + data: { varName: 'foo' }, + line: 2, + column: 42, + }, + ], output: ` const foo: () => void = () => { Promise.resolve(true); }; `, }, { code: 'const cb: () => void = (): Array => [];', - errors: [{ messageId: 'nonVoidReturnInVar', line: 1, column: 45 }], + errors: [ + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'cb' }, + line: 1, + column: 45, + }, + ], output: 'const cb: () => void = (): void => {};', }, { @@ -1479,7 +1774,14 @@ ruleTester.run('strict-void-return', rule, { return []; }; `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 2, column: 36 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'cb' }, + line: 2, + column: 36, + }, + ], output: ` const cb: () => void = (): void => { return []; @@ -1488,13 +1790,27 @@ ruleTester.run('strict-void-return', rule, { }, { code: noFormat`const cb: () => void = function*foo() {}`, - errors: [{ messageId: 'genFuncInVar', line: 1, column: 24 }], + errors: [ + { + messageId: 'genFuncInVar', + data: { varName: 'cb' }, + line: 1, + column: 24, + }, + ], output: `const cb: () => void = function foo() {}`, }, { options: [{ allowReturnUndefined: false }], code: 'const cb: () => void = (): Promise => Promise.resolve(1);', - errors: [{ messageId: 'nonVoidReturnInVar', line: 1, column: 47 }], + errors: [ + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'cb' }, + line: 1, + column: 47, + }, + ], output: 'const cb: () => void = (): void => { Promise.resolve(1); };', }, { @@ -1506,7 +1822,14 @@ ruleTester.run('strict-void-return', rule, { } catch {} }; `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 2, column: 42 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'cb' }, + line: 2, + column: 42, + }, + ], output: [ ` const cb: () => void = async (): Promise => { @@ -1526,7 +1849,14 @@ ruleTester.run('strict-void-return', rule, { }, { code: 'const cb: () => void = async (): Promise => Promise.resolve(1);', - errors: [{ messageId: 'asyncFuncInVar', line: 1, column: 50 }], + errors: [ + { + messageId: 'asyncFuncInVar', + data: { varName: 'cb' }, + line: 1, + column: 50, + }, + ], output: 'const cb: () => void = (): void => void Promise.resolve(1);', }, { @@ -1537,7 +1867,14 @@ ruleTester.run('strict-void-return', rule, { } catch {} }; `, - errors: [{ messageId: 'nonVoidReturnInVar', line: 4, column: 13 }], + errors: [ + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'foo' }, + line: 4, + column: 13, + }, + ], output: ` const foo: () => void = async () => { try { @@ -1558,6 +1895,7 @@ ruleTester.run('strict-void-return', rule, { errors: [ { messageId: 'asyncNoTryCatchFuncInVar', + data: { varName: 'foo' }, line: 2, column: 57, suggestions: [ @@ -1603,6 +1941,7 @@ ruleTester.run('strict-void-return', rule, { errors: [ { messageId: 'asyncNoTryCatchFuncInVar', + data: { varName: 'foo' }, line: 2, column: 42, suggestions: [ @@ -1643,7 +1982,7 @@ ruleTester.run('strict-void-return', rule, { // declare let foo: (() => void) | (() => boolean); // foo = () => 1; // `, - // errors: [{ messageId: 'nonVoidReturnInVar', line: 3, column: 21 }], + // errors: [{ messageId: 'nonVoidReturnInVar', data: {varName: throw}, line: 3, column: 21 }], // output: ` // declare let foo: (() => void) | (() => boolean); // foo = () => {}; @@ -1651,7 +1990,14 @@ ruleTester.run('strict-void-return', rule, { // }, { code: 'const foo: () => void = (): number => {};', - errors: [{ messageId: 'nonVoidFuncInVar', line: 1, column: 29 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'foo' }, + line: 1, + column: 29, + }, + ], output: 'const foo: () => void = (): void => {};', }, { @@ -1659,7 +2005,14 @@ ruleTester.run('strict-void-return', rule, { declare function cb(): boolean; const foo: () => void = cb; `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 3, column: 33 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'foo' }, + line: 3, + column: 33, + }, + ], output: null, }, { @@ -1674,8 +2027,18 @@ ruleTester.run('strict-void-return', rule, { }; `, errors: [ - { messageId: 'nonVoidReturnInVar', line: 4, column: 13 }, - { messageId: 'nonVoidReturnInVar', line: 6, column: 13 }, + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'foo' }, + line: 4, + column: 13, + }, + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'foo' }, + line: 6, + column: 13, + }, ], output: ` const foo: () => void = function () { @@ -1697,7 +2060,14 @@ ruleTester.run('strict-void-return', rule, { } }; `, - errors: [{ messageId: 'nonVoidReturnInVar', line: 5, column: 13 }], + errors: [ + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'foo' }, + line: 5, + column: 13, + }, + ], output: ` const foo: () => void = function () { if (maybe) { @@ -1720,8 +2090,18 @@ ruleTester.run('strict-void-return', rule, { }; `, errors: [ - { messageId: 'nonVoidReturnInVar', line: 6, column: 15 }, - { messageId: 'nonVoidReturnInVar', line: 8, column: 15 }, + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'foo' }, + line: 6, + column: 15, + }, + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'foo' }, + line: 8, + column: 15, + }, ], output: ` const foo: { (arg: number): void; (arg: string): void } = arg => { @@ -1745,6 +2125,7 @@ ruleTester.run('strict-void-return', rule, { errors: [ { messageId: 'asyncNoTryCatchFuncInVar', + data: { varName: 'foo' }, line: 2, column: 81, suggestions: [ @@ -1777,7 +2158,14 @@ ruleTester.run('strict-void-return', rule, { return [1, 2, 3]; } `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 3, column: 26 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'foo' }, + line: 3, + column: 26, + }, + ], output: null, }, { @@ -1790,7 +2178,14 @@ ruleTester.run('strict-void-return', rule, { return { a: 1 }; } `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 5, column: 26 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'foo' }, + line: 5, + column: 26, + }, + ], output: null, }, { @@ -1799,7 +2194,14 @@ ruleTester.run('strict-void-return', rule, { declare let foo: () => void; foo = cb; `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 4, column: 15 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'foo' }, + line: 4, + column: 15, + }, + ], output: null, }, { @@ -1810,7 +2212,14 @@ ruleTester.run('strict-void-return', rule, { console.log('siema'); }; `, - errors: [{ messageId: 'nonVoidReturnInVar', line: 4, column: 11 }], + errors: [ + { + messageId: 'nonVoidReturnInVar', + data: { varName: 'foo.cb' }, + line: 4, + column: 11, + }, + ], output: ` declare let foo: { arg?: string; cb?: () => void }; foo.cb = () => { @@ -1825,7 +2234,14 @@ ruleTester.run('strict-void-return', rule, { let foo: (() => void) | null = null; foo ??= cb; `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 4, column: 17 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'foo' }, + line: 4, + column: 17, + }, + ], output: null, }, { @@ -1834,7 +2250,14 @@ ruleTester.run('strict-void-return', rule, { let foo: (() => void) | boolean = false; foo ||= cb; `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 4, column: 17 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'foo' }, + line: 4, + column: 17, + }, + ], output: null, }, { @@ -1843,7 +2266,14 @@ ruleTester.run('strict-void-return', rule, { let foo: (() => void) | boolean = false; foo &&= cb; `, - errors: [{ messageId: 'nonVoidFuncInVar', line: 4, column: 17 }], + errors: [ + { + messageId: 'nonVoidFuncInVar', + data: { varName: 'foo' }, + line: 4, + column: 17, + }, + ], output: null, }, { @@ -1852,7 +2282,14 @@ ruleTester.run('strict-void-return', rule, { declare function Foo(props: { cb: () => void }): unknown; return 1} />; `, - errors: [{ messageId: 'nonVoidReturnInAttr', line: 3, column: 31 }], + errors: [ + { + messageId: 'nonVoidReturnInAttr', + data: { attrName: 'cb', elemName: 'Foo' }, + line: 3, + column: 31, + }, + ], output: ` declare function Foo(props: { cb: () => void }): unknown; return {}} />; @@ -1874,8 +2311,18 @@ ruleTester.run('strict-void-return', rule, { ); `, errors: [ - { messageId: 'nonVoidReturnInAttr', line: 7, column: 26 }, - { messageId: 'nonVoidReturnInAttr', line: 8, column: 20 }, + { + messageId: 'nonVoidReturnInAttr', + data: { attrName: 'cb', elemName: 'Foo' }, + line: 7, + column: 26, + }, + { + messageId: 'nonVoidReturnInAttr', + data: { attrName: 'cb', elemName: 'Foo' }, + line: 8, + column: 20, + }, ], output: ` declare function Foo(props: { cb: () => void }): unknown; @@ -1897,7 +2344,14 @@ ruleTester.run('strict-void-return', rule, { declare function Foo(props: { cb: Cb; s: string }): unknown; return ; `, - errors: [{ messageId: 'asyncFuncInAttr', line: 4, column: 25 }], + errors: [ + { + messageId: 'asyncFuncInAttr', + data: { attrName: 'cb', elemName: 'Foo' }, + line: 4, + column: 25, + }, + ], output: ` type Cb = () => void; declare function Foo(props: { cb: Cb; s: string }): unknown; @@ -1911,7 +2365,14 @@ ruleTester.run('strict-void-return', rule, { declare function Foo(props: { n: number; cb?: Cb }): unknown; return ; `, - errors: [{ messageId: 'genFuncInAttr', line: 4, column: 34 }], + errors: [ + { + messageId: 'genFuncInAttr', + data: { attrName: 'cb', elemName: 'Foo' }, + line: 4, + column: 34, + }, + ], output: ` type Cb = () => void; declare function Foo(props: { n: number; cb?: Cb }): unknown; @@ -1932,7 +2393,14 @@ ruleTester.run('strict-void-return', rule, { /> ); `, - errors: [{ messageId: 'genFuncInAttr', line: 6, column: 17 }], + errors: [ + { + messageId: 'genFuncInAttr', + data: { attrName: 'cb', elemName: 'Foo' }, + line: 6, + column: 17, + }, + ], output: null, }, { @@ -1944,7 +2412,14 @@ ruleTester.run('strict-void-return', rule, { declare function Foo(props: Props): unknown; return x} />; `, - errors: [{ messageId: 'nonVoidReturnInAttr', line: 6, column: 30 }], + errors: [ + { + messageId: 'nonVoidReturnInAttr', + data: { attrName: 'cb', elemName: 'Foo' }, + line: 6, + column: 30, + }, + ], output: ` interface Props { cb: ((arg: unknown) => void) | boolean; @@ -1956,22 +2431,47 @@ ruleTester.run('strict-void-return', rule, { { filename: 'react.tsx', code: ` - interface Props { - children: (arg: unknown) => void; + type EventHandler = { bivarianceHack(event: E): void }['bivarianceHack']; + interface ButtonProps { + onClick?: EventHandler | undefined; + } + declare function Button(props: ButtonProps): unknown; + function App() { + return