diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index 52dc08dfcef6..95c4df77af35 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -311,4 +311,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..ac2904259cf2 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/strict-void-return.mdx @@ -0,0 +1,418 @@ +--- +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 + +### Return type unsafety + +Passing a value-returning function in a place expecting a void function can be unsound. +TypeScript generally treats the `void` type as though it has the same runtime behavior as `undefined`, +so this pattern will cause a mismatch between the runtime behavior and the types. + +```ts +// TypeScript errors on overtly wrong ways of populating the `void` type... +function returnsVoid(): void { + return 1234; // TS Error: Type 'number' is not assignable to type 'void'. +} + +// ...but allows more subtle ones +const returnsVoid: () => void = () => 1234; + +// Likewise, TypeScript errors on overtly wrong usages of `void` as a runtime value... +declare const v: void; +if (v) { + // TS Error: An expression of type 'void' cannot be tested for truthiness. + // ... do something +} + +// ...but silently treats `void` as `undefined` in more subtle scenarios +declare const voidOrString: void | string; +if (voidOrString) { + // voidOrString is narrowed to string in this branch, so this is allowed. + console.log(voidOrString.toUpperCase()); +} +``` + +Between these two behaviors, examples like the following will throw at runtime, despite not reporting a type error: + + + + +```ts +const getNothing: () => void = () => 2137; +const getString: () => string = () => 'Hello'; +const maybeString = Math.random() > 0.1 ? getNothing() : getString(); +if (maybeString) console.log(maybeString.toUpperCase()); // ❌ Crash if getNothing was called +``` + + + + +```ts +const getNothing: () => void = () => {}; +const getString: () => string = () => 'Hello'; +const maybeString = Math.random() > 0.1 ? getNothing() : getString(); +if (maybeString) console.log(maybeString.toUpperCase()); // ✅ No crash +``` + + + + +### Unhandled returned promises + +If a callback is meant to return void, values returned from functions are likely ignored. +Ignoring a returned Promise means any Promise 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 in statement positions. +::: + +### Ignored returned 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 often is an indication of incorrect assumptions about APIs. +Those incorrect assumptions can often lead to bugs. + +The following `forEach` loop is a common mistake: its author likely either meant to add `console.log` or meant to use `.map` instead. + + + + +```ts +['Kazik', 'Zenek'].forEach(name => `Hello, ${name}!`); +``` + + + + +```ts +['Kazik', 'Zenek'].forEach(name => console.log(`Hello, ${name}!`)); +``` + + + + +### Void context examples + +Every expression has a contextual (expected) type computed by the TypeScript type system. +This rule will check expressions where the contextual type is a function returning void. + +The following expressions are checked: + +- Function arguments +- JSX attribute values +- Right-hand side of assignments +- Return values +- Object property values +- Array elements +- Class property values + + + + +```tsx +declare function takesCallback(cb: () => void): void; +declare function TakesCallback(props: { cb: () => void }): string; +declare let callback: () => void; +declare let returnsCallback: () => () => void; +declare let callbackObj: Record void>; +declare let callbackArr: (() => void)[]; + +takesCallback(() => 'Hello'); + 'Hello'} />; +callback = () => 'Hello'; +returnsCallback = () => { + return () => 'Hello'; +}; +callbackObj = { + hello: () => 'Hello', +}; +callbackArr = [() => 'Hello']; +``` + + + + +```tsx +declare function takesCallback(cb: () => void): void; +declare function TakesCallback(props: { cb: () => void }): string; +declare let callback: () => void; +declare let returnsCallback: () => () => void; +declare let callbackObj: Record void>; +declare let callbackArr: (() => void)[]; + +takesCallback(() => console.log('Hello')); + console.log('Hello')} />; +callback = () => console.log('Hello'); +returnsCallback = () => { + return () => console.log('Hello'); +}; +callbackObj = { + hello: () => console.log('Hello'), +}; +callbackArr = [() => console.log('Hello')]; +``` + + + + +### Void context from overloads + +This rule treats an any-returning callback argument as a void context, +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. + + + + +```ts +/// + +document.addEventListener('click', () => { + return 'Clicked'; +}); +``` + + + + +```ts +/// + +document.addEventListener('click', () => { + console.log('Clicked'); +}); +``` + + + + +### Void context from base classes + +This rule enforces class methods which override a void method to also be void. + + + + +```ts +class Foo { + cb() { + console.log('foo'); + } +} + +class Bar extends Foo { + cb() { + super.cb(); + return 'bar'; + } +} +``` + + + + +```ts +class Foo { + cb() { + console.log('foo'); + } +} + +class Bar extends Foo { + cb() { + super.cb(); + console.log('bar'); + } +} +``` + + + + +### Void context from implemented interfaces + +This rule enforces class methods which implement a void method to also be void. + + + + +```ts +interface Foo { + cb(): void; +} + +class Bar implements Foo { + cb() { + return 'cb'; + } +} +``` + + + + +```ts +interface Foo { + cb(): void; +} + +class Bar implements Foo { + cb() { + console.log('cb'); + } +} +``` + + + + +## Options + +### `allowReturnAny` + +{/* insert option description */} + +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 + +Some projects are architected so that values returned from synchronous void functions are generally safe. +If you only want to check for misused voids with asynchronous functions then 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 generally 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 may prefer to avoid 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/eslintrc/all.ts b/packages/eslint-plugin/src/configs/eslintrc/all.ts index 2764e4f92a74..c42fce0eb4c7 100644 --- a/packages/eslint-plugin/src/configs/eslintrc/all.ts +++ b/packages/eslint-plugin/src/configs/eslintrc/all.ts @@ -154,6 +154,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/unbound-method': 'error', diff --git a/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts b/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts index 9dd6c95c929e..22b2fa5b69b1 100644 --- a/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts @@ -66,6 +66,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/configs/flat/all.ts b/packages/eslint-plugin/src/configs/flat/all.ts index 37f6b8647ddc..b7b5a24970c4 100644 --- a/packages/eslint-plugin/src/configs/flat/all.ts +++ b/packages/eslint-plugin/src/configs/flat/all.ts @@ -168,6 +168,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/unbound-method': 'error', diff --git a/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts b/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts index 15c5bb0e3dcf..63e5e73d6f9b 100644 --- a/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts @@ -73,6 +73,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', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 2a82c7e18c6b..56d00109bb3c 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -125,6 +125,7 @@ import restrictTemplateExpressions from './restrict-template-expressions'; import returnAwait from './return-await'; import sortTypeConstituents from './sort-type-constituents'; 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 typedef from './typedef'; @@ -259,6 +260,7 @@ const rules = { 'return-await': returnAwait, 'sort-type-constituents': sortTypeConstituents, 'strict-boolean-expressions': strictBooleanExpressions, + 'strict-void-return': strictVoidReturn, 'switch-exhaustiveness-check': switchExhaustivenessCheck, 'triple-slash-reference': tripleSlashReference, typedef, 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..7d4e8f8168d5 --- /dev/null +++ b/packages/eslint-plugin/src/rules/strict-void-return.ts @@ -0,0 +1,428 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import * as util from '../util'; + +type Options = [ + { + allowReturnAny?: boolean; + }, +]; + +type MessageId = `asyncFunc` | `nonVoidFunc` | `nonVoidReturn`; + +export default util.createRule({ + name: 'strict-void-return', + meta: { + type: 'problem', + docs: { + description: + 'Disallow passing a value-returning function in a position accepting a void function', + requiresTypeChecking: true, + }, + messages: { + asyncFunc: + 'Async function used in a context where a void function is expected.', + nonVoidFunc: + 'Value-returning function used in a context where a void function is expected.', + nonVoidReturn: + 'Value returned in a context where a void return is expected.', + }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + allowReturnAny: { + type: 'boolean', + description: + 'Whether to allow functions returning `any` to be used in place expecting a `void` function.', + }, + }, + }, + ], + }, + defaultOptions: [ + { + allowReturnAny: false, + }, + ], + + create(context, [options]) { + const sourceCode = context.sourceCode; + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + return { + ArrayExpression: (node): void => { + for (const elemNode of node.elements) { + if ( + elemNode != null && + elemNode.type !== AST_NODE_TYPES.SpreadElement + ) { + checkExpressionNode(elemNode); + } + } + }, + ArrowFunctionExpression: (node): void => { + if (node.body.type !== AST_NODE_TYPES.BlockStatement) { + checkExpressionNode(node.body); + } + }, + AssignmentExpression: (node): void => { + checkExpressionNode(node.right); // should ignore operators like `+=` or `-=` automatically + }, + 'CallExpression, NewExpression': checkFunctionCallNode, + JSXAttribute: (node): void => { + if ( + node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && + node.value.expression.type !== AST_NODE_TYPES.JSXEmptyExpression + ) { + checkExpressionNode(node.value.expression); + } + }, + MethodDefinition: checkClassMethodNode, + ObjectExpression: (node): void => { + for (const propNode of node.properties) { + if (propNode.type !== AST_NODE_TYPES.SpreadElement) { + checkObjectPropertyNode(propNode); + } + } + }, + PropertyDefinition: checkClassPropertyNode, + ReturnStatement: (node): void => { + if (node.argument != null) { + checkExpressionNode(node.argument); + } + }, + VariableDeclarator: (node): void => { + if (node.init != null) { + checkExpressionNode(node.init); + } + }, + }; + + function isVoidReturningFunctionType(type: ts.Type): boolean { + const returnTypes = tsutils + .getCallSignaturesOfType(type) + .flatMap(signature => + tsutils.unionConstituents(signature.getReturnType()), + ); + return ( + returnTypes.length > 0 && + returnTypes.every(type => + tsutils.isTypeFlagSet(type, ts.TypeFlags.Void), + ) + ); + } + + /** + * 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): boolean { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + node, + ) as ts.Expression; + const expectedType = checker.getContextualType(tsNode); + + if (expectedType != null && isVoidReturningFunctionType(expectedType)) { + reportIfNonVoidFunction(node); + 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); + + const funcType = checker.getTypeAtLocation(callTsNode.expression); + const funcSignatures = tsutils + .unionConstituents(funcType) + .flatMap(type => + ts.isCallExpression(callTsNode) + ? type.getCallSignatures() + : type.getConstructSignatures(), + ); + + 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)) { + continue; + } + + // Check against the types from all of the call signatures + const argExpectedReturnTypes = funcSignatures + .map(s => s.parameters[argIdx]) + .filter(Boolean) + .map(param => + checker.getTypeOfSymbolAtLocation(param, callTsNode.expression), + ) + .flatMap(paramType => tsutils.unionConstituents(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); + } + } + } + + /** + * Finds errors in an object property. + * + * Object properties require different logic + * when the property is a method shorthand. + */ + function checkObjectPropertyNode(propNode: TSESTree.Property): void { + const valueNode = propNode.value as TSESTree.Expression; + 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 as ts.ObjectLiteralExpression; + 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(valueNode); + } + return; + } + + // Object property is a regular property. + checkExpressionNode(valueNode); + } + + /** + * 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). + */ + function checkClassPropertyNode( + propNode: TSESTree.PropertyDefinition, + ): void { + if (propNode.value == null) { + return; + } + + // Check in comparison to the base types. + for (const { baseMemberType } of util.getBaseTypesOfClassMember( + parserServices, + propNode, + )) { + if (isVoidReturningFunctionType(baseMemberType)) { + reportIfNonVoidFunction(propNode.value); + return; // Report at most one error. + } + } + + // Check in comparison to the contextual type. + checkExpressionNode(propNode.value); + } + + /** + * 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). + */ + function checkClassMethodNode(methodNode: TSESTree.MethodDefinition): void { + if ( + methodNode.value.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression + ) { + return; + } + + // Check in comparison to the base types. + for (const { baseMemberType } of util.getBaseTypesOfClassMember( + parserServices, + methodNode, + )) { + if (isVoidReturningFunctionType(baseMemberType)) { + reportIfNonVoidFunction(methodNode.value); + return; // Report at most one error. + } + } + } + + /** + * Reports an error if the provided node is not allowed in a void function context. + */ + function reportIfNonVoidFunction(funcNode: TSESTree.Expression): void { + const allowedReturnType = + ts.TypeFlags.Void | + ts.TypeFlags.Never | + ts.TypeFlags.Undefined | + (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.unionConstituents(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: `nonVoidFunc`, + }); + } + + // The provided function is a function literal. + + if (funcNode.generator) { + // The provided function is a generator function. + // Generator functions are not allowed. + return context.report({ + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `nonVoidFunc`, + }); + } + + if (funcNode.async) { + // The provided function is an async function. + // Async functions aren't allowed. + return context.report({ + loc: util.getFunctionHeadLoc(funcNode, sourceCode), + messageId: `asyncFunc`, + }); + } + + if (funcNode.body.type !== AST_NODE_TYPES.BlockStatement) { + // The provided function is an arrow function shorthand without braces. + return context.report({ + node: funcNode.body, + messageId: `nonVoidReturn`, + }); + } + + // The function is a regular or arrow function with a block body. + + // Check return type annotation. + 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) { + // The explicit return type is not `void`. + return context.report({ + node: typeAnnotationNode, + messageId: `nonVoidFunc`, + }); + } + } + + // 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 + ) { + // 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; + } + + // This return statement causes the non-void return type. + const returnKeyword = util.nullThrows( + sourceCode.getFirstToken(statement, { + filter: token => token.value === 'return', + }), + util.NullThrowsReasons.MissingToken('return keyword', statement.type), + ); + context.report({ + node: returnKeyword, + messageId: `nonVoidReturn`, + }); + } + + // No invalid returns found. The function is allowed. + } + }, +}); diff --git a/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts b/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts new file mode 100644 index 000000000000..def2ec1622e7 --- /dev/null +++ b/packages/eslint-plugin/src/util/getBaseTypesOfClassMember.ts @@ -0,0 +1,47 @@ +import type { + TSESTree, + ParserServicesWithTypeInformation, +} from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; + +/** + * Given a member of a class which extends another class or implements an interface, + * yields the corresponding member type for each of the base class/interfaces. + */ +export function* getBaseTypesOfClassMember( + services: ParserServicesWithTypeInformation, + memberNode: TSESTree.MethodDefinition | TSESTree.PropertyDefinition, +): Generator<{ + baseType: ts.Type; + baseMemberType: ts.Type; + heritageToken: ts.SyntaxKind.ExtendsKeyword | ts.SyntaxKind.ImplementsKeyword; +}> { + const memberTsNode = services.esTreeNodeToTSNodeMap.get(memberNode); + if (memberTsNode.name == null) { + return; + } + const checker = services.program.getTypeChecker(); + const memberSymbol = checker.getSymbolAtLocation(memberTsNode.name); + if (memberSymbol == null) { + return; + } + const classNode = memberTsNode.parent as ts.ClassLikeDeclaration; + for (const clauseNode of classNode.heritageClauses ?? []) { + 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, + ); + const heritageToken = clauseNode.token; + yield { baseMemberType, baseType, heritageToken }; + } + } +} diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 035fca0cec4a..1eb78e3f885c 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -3,6 +3,7 @@ import { ESLintUtils } from '@typescript-eslint/utils'; export * from './astUtils'; export * from './collectUnusedVariables'; export * from './createRule'; +export * from './getBaseTypesOfClassMember'; export * from './getFixOrSuggest'; export * from './getFunctionHeadLoc'; export * from './getOperatorPrecedence'; @@ -29,6 +30,7 @@ export * from './getValueOfLiteralType'; export * from './isHigherPrecedenceThanAwait'; export * from './skipChainExpression'; export * from './truthinessUtils'; +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..8bc80153a75c --- /dev/null +++ b/packages/eslint-plugin/src/util/walkStatements.ts @@ -0,0 +1,58 @@ +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: + case AST_NODE_TYPES.LabeledStatement: { + 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; + } + 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..ed147791a399 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-void-return.shot @@ -0,0 +1,213 @@ +Incorrect + +const getNothing: () => void = () => 2137; + ~~~~ Value returned in a context where a void return is expected. +const getString: () => string = () => 'Hello'; +const maybeString = Math.random() > 0.1 ? getNothing() : getString(); +if (maybeString) console.log(maybeString.toUpperCase()); // ❌ Crash if getNothing was called + +Correct + +const getNothing: () => void = () => {}; +const getString: () => string = () => 'Hello'; +const maybeString = Math.random() > 0.1 ? getNothing() : getString(); +if (maybeString) console.log(maybeString.toUpperCase()); // ✅ No crash + +Incorrect + +declare function takesCallback(cb: () => void): void; + +takesCallback(async () => { + ~~ Async function used in a context where a void function is expected. + const response = await fetch('https://api.example.com/'); + const data = await response.json(); + console.log(data); +}); + +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); +}); + +Incorrect + +declare function takesCallback(cb: () => void): void; + +takesCallback(function* () { + ~~~~~~~~~~ Value-returning function used in a context where a void function is expected. + console.log('Hello'); + yield; + console.log('World'); +}); + +Correct + +declare function takesCallback(cb: () => void): void; + +takesCallback(() => { + function* gen() { + console.log('Hello'); + yield; + console.log('World'); + } + for (const _ of gen()); +}); + +Incorrect + +['Kazik', 'Zenek'].forEach(name => `Hello, ${name}!`); + ~~~~~~~~~~~~~~~~~ Value returned in a context where a void return is expected. + +Correct + +['Kazik', 'Zenek'].forEach(name => console.log(`Hello, ${name}!`)); + +Incorrect + +declare function takesCallback(cb: () => void): void; +declare function TakesCallback(props: { cb: () => void }): string; +declare let callback: () => void; +declare let returnsCallback: () => () => void; +declare let callbackObj: Record void>; +declare let callbackArr: (() => void)[]; + +takesCallback(() => 'Hello'); + ~~~~~~~ Value returned in a context where a void return is expected. + 'Hello'} />; + ~~~~~~~ Value returned in a context where a void return is expected. +callback = () => 'Hello'; + ~~~~~~~ Value returned in a context where a void return is expected. +returnsCallback = () => { + return () => 'Hello'; + ~~~~~~~ Value returned in a context where a void return is expected. +}; +callbackObj = { + hello: () => 'Hello', + ~~~~~~~ Value returned in a context where a void return is expected. +}; +callbackArr = [() => 'Hello']; + ~~~~~~~ Value returned in a context where a void return is expected. + +Correct + +declare function takesCallback(cb: () => void): void; +declare function TakesCallback(props: { cb: () => void }): string; +declare let callback: () => void; +declare let returnsCallback: () => () => void; +declare let callbackObj: Record void>; +declare let callbackArr: (() => void)[]; + +takesCallback(() => console.log('Hello')); + console.log('Hello')} />; +callback = () => console.log('Hello'); +returnsCallback = () => { + return () => console.log('Hello'); +}; +callbackObj = { + hello: () => console.log('Hello'), +}; +callbackArr = [() => console.log('Hello')]; + +Incorrect + +/// + +document.addEventListener('click', () => { + return 'Clicked'; + ~~~~~~ Value returned in a context where a void return is expected. +}); + +Correct + +/// + +document.addEventListener('click', () => { + console.log('Clicked'); +}); + +Incorrect + +class Foo { + cb() { + console.log('foo'); + } +} + +class Bar extends Foo { + cb() { + super.cb(); + return 'bar'; + ~~~~~~ Value returned in a context where a void return is expected. + } +} + +Correct + +class Foo { + cb() { + console.log('foo'); + } +} + +class Bar extends Foo { + cb() { + super.cb(); + console.log('bar'); + } +} + +Incorrect + +interface Foo { + cb(): void; +} + +class Bar implements Foo { + cb() { + return 'cb'; + ~~~~~~ Value returned in a context where a void return is expected. + } +} + +Correct + +interface Foo { + cb(): void; +} + +class Bar implements Foo { + cb() { + console.log('cb'); + } +} + +Incorrect +Options: { "allowReturnAny": false } + +declare function fn(cb: () => void): void; + +fn(() => JSON.parse('{}')); + ~~~~~~~~~~~~~~~~ Value returned in a context where a void return is expected. + +fn(() => { + return someUntypedApi(); + ~~~~~~ Value returned in a context where a void return is expected. +}); + +Correct +Options: { "allowReturnAny": false } + +declare function fn(cb: () => void): void; + +fn(() => void JSON.parse('{}')); + +fn(() => { + someUntypedApi(); +}); 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..01d6fcf16943 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/strict-void-return.test.ts @@ -0,0 +1,2740 @@ +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({ + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }, + }, +}); + +ruleTester.run('strict-void-return', rule, { + valid: [ + { + code: ` + declare function foo(cb: {}): void; + 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; + foo(() => () => []); + `, + }, + { + code: ` + declare class Foo { + constructor(cb: unknown): void; + } + new Foo(() => ({})); + `, + }, + { + code: ` + declare function foo(cb: () => {}): void; + foo(() => 1 as any); + `, + options: [{ allowReturnAny: true }], + }, + { + 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: ` + 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 () => {}); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(() => 1 as any); + `, + options: [{ allowReturnAny: true }], + }, + { + 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() {} + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(() => undefined); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(function () { + return; + }); + `, + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(function () { + return void 0; + }); + `, + }, + { + 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( + () => {}, + () => void 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 () => {}, + () => void 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 void 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 let foo: () => void; + foo += () => 1; + `, + }, + { + 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; + `, + }, + { + code: ` + declare function Foo(props: { cb: () => void }): unknown; + return {}} />; + `, + filename: 'react.tsx', + }, + { + code: ` + declare function Foo(props: { cb: () => void }): unknown; + return ; + `, + filename: 'react.tsx', + }, + { + code: ` + declare function Foo(props: { cb: () => void }): unknown; + return ; + `, + 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}; + `, + filename: 'react.tsx', + }, + { + code: ` + declare function foo(cbs: { arg: number; cb: () => void }): void; + foo({ arg: 1, cb: () => undefined }); + `, + }, + { + 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; + }, + }; + `, + options: [{ allowReturnAny: true }], + }, + { + 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(): string; + const foo: Record void> = { + ...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 Bar { + foo() {} + } + class Foo extends Bar { + foo(); + } + `, + }, + { + code: ` + interface Bar { + foo(): void; + } + class Foo implements Bar { + get foo() { + return new Date(); + } + set foo() { + return new Date('wtf'); + } + } + `, + }, + { + code: ` + class Foo { + foo: () => void = () => undefined; + } + `, + }, + { + code: ` + class Bar {} + class Foo extends Bar { + foo = () => 1; + } + `, + }, + { + code: ` + class Foo extends Wtf { + foo = () => 1; + } + `, + }, + { + code: ` + class Foo extends Wtf { + [unknown] = () => 1; + } + `, + }, + { + 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() {} + } + `, + }, + { + code: ` + declare let foo: () => () => void; + foo = () => () => {}; + `, + }, + { + code: ` + declare let foo: { f(): () => void }; + foo = { + f() { + return () => undefined; + }, + }; + 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(() => null); + `, + errors: [ + { + column: 19, + line: 3, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: noFormat` + declare function foo(cb: () => void): void; + foo(() => (((true)))); + `, + errors: [ + { + column: 22, + line: 3, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: noFormat` + declare function foo(cb: () => void): void; + foo(() => { + if (maybe) { + return (((1) + 1)); + } + }); + `, + errors: [ + { + column: 13, + line: 5, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(arg: number, cb: () => void): void; + foo(0, () => 0); + `, + errors: [ + { + column: 22, + line: 3, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(cb?: { (): void }): void; + foo(() => () => {}); + `, + errors: [ + { + column: 19, + line: 3, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare const obj: { foo(cb: () => void) } | null; + obj?.foo(() => JSON.parse('{}')); + `, + errors: [ + { + column: 24, + line: 3, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + ((cb: () => void) => cb())!(() => 1); + `, + errors: [ + { + column: 43, + line: 2, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(cb: { (): void }): void; + declare function cb(): string; + foo(cb); + `, + errors: [ + { + column: 13, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + type AnyFunc = (...args: unknown[]) => unknown; + declare function foo(cb: F): void; + foo(async () => ({})); + foo<() => void>(async () => ({})); + `, + errors: [ + { + column: 34, + line: 5, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + function foo(arg: T, cb: () => T); + function foo(arg: null, cb: () => void); + function foo(arg: any, cb: () => any) {} + + foo(null, () => Math.random()); + `, + errors: [ + { + column: 25, + line: 6, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(arg: T, cb: () => T): void; + declare function foo(arg: any, cb: () => void): void; + + foo(null, async () => {}); + `, + errors: [ + { + column: 28, + line: 5, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + declare function foo(cb: () => void): void; + declare function foo(cb: () => any): void; + foo(async () => { + return Math.random(); + }); + `, + errors: [ + { + column: 22, + line: 4, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + declare function foo(cb: { (): void }): void; + foo(cb); + async function cb() {} + `, + errors: [ + { + column: 13, + line: 3, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function foo void>(cb: Cb): void; + foo(() => { + console.log('a'); + return 1; + }); + `, + errors: [ + { + column: 11, + line: 5, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(cb: () => void): void; + function bar number>(cb: Cb) { + foo(cb); + } + `, + errors: [ + { + column: 15, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function foo(cb: { (): void }): void; + const cb = () => dunno; + foo!(cb); + `, + errors: [ + { + column: 14, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare const foo: { + (arg: boolean, cb: () => void): void; + }; + foo(false, () => Promise.resolve(undefined)); + `, + errors: [ + { + column: 26, + line: 5, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare const foo: { + bar(cb1: () => any, cb2: () => void): void; + }; + foo.bar( + () => Promise.resolve(1), + () => Promise.resolve(1), + ); + `, + errors: [ + { + column: 17, + line: 7, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare const Foo: { + new (cb: () => void): void; + }; + new Foo(async () => {}); + `, + errors: [ + { + column: 26, + line: 5, + messageId: 'asyncFunc', + }, + ], + }, + { + 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: [ + { + column: 26, + line: 6, + messageId: 'nonVoidReturn', + }, + { + column: 20, + line: 7, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(() => { + do { + try { + throw 1; + } catch (e) { + return null; + } finally { + console.log('finally'); + } + } while (maybe); + }); + `, + errors: [ + { + column: 15, + line: 8, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(cb: () => void): void; + foo(async () => { + try { + await Promise.resolve(); + } catch { + console.error('fail'); + } + }); + `, + errors: [ + { + column: 22, + line: 3, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + declare const Foo: { + new (cb: () => void): void; + (cb: () => unknown): void; + }; + new Foo(() => false); + `, + errors: [ + { + column: 23, + line: 6, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare const Foo: { + new (cb: () => any): void; + (cb: () => void): void; + }; + Foo(() => false); + `, + errors: [ + { + column: 19, + line: 6, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + interface Cb { + (arg: string): void; + (arg: number): void; + } + declare function foo(cb: Cb): void; + foo(cb); + function cb() { + return true; + } + `, + errors: [ + { + column: 13, + line: 7, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function foo( + cb: ((arg: number) => void) | ((arg: string) => void), + ): void; + foo(cb); + function cb() { + return 1 + 1; + } + `, + errors: [ + { + column: 13, + line: 5, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function foo(cb: (() => void) | null): void; + declare function cb(): boolean; + foo(cb); + `, + errors: [ + { + column: 13, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function foo(...cbs: Array<() => void>): void; + foo( + () => {}, + () => false, + () => 0, + () => '', + ); + `, + errors: [ + { + column: 17, + line: 5, + messageId: 'nonVoidReturn', + }, + { + column: 17, + line: 6, + messageId: 'nonVoidReturn', + }, + { + column: 17, + line: 7, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(...cbs: [() => void, () => void, (() => void)?]): void; + foo( + () => {}, + () => Math.random(), + () => (1).toString(), + ); + `, + errors: [ + { + column: 17, + line: 5, + messageId: 'nonVoidReturn', + }, + { + column: 17, + line: 6, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + interface Ev {} + interface EvMap { + DOMContentLoaded: Ev; + } + type EvListOrEvListObj = EvList | EvListObj; + interface EvList { + (evt: Event): void; + } + interface EvListObj { + handleEvent(object: Ev): void; + } + interface Win { + addEventListener( + type: K, + listener: (ev: EvMap[K]) => any, + ): void; + addEventListener(type: string, listener: EvListOrEvListObj): void; + } + declare const win: Win; + win.addEventListener('DOMContentLoaded', ev => ev); + win.addEventListener('custom', ev => ev); + `, + errors: [ + { + column: 56, + line: 21, + messageId: 'nonVoidReturn', + }, + { + column: 46, + line: 22, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function foo(x: null, cb: () => void): void; + declare function foo(x: unknown, cb: () => any): void; + foo({}, async () => {}); + `, + errors: [ + { + column: 26, + line: 4, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + const arr = [1, 2]; + arr.forEach(async x => { + console.log(x); + }); + `, + errors: [ + { + column: 29, + line: 3, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + [1, 2].forEach(async x => console.log(x)); + `, + errors: [ + { + column: 32, + line: 2, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + const foo: () => void = () => false; + `, + errors: [ + { + column: 39, + line: 2, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + const { name }: () => void = function foo() { + return false; + }; + `, + errors: [{ column: 11, line: 3, messageId: 'nonVoidReturn' }], + }, + { + code: ` + declare const foo: Record void>; + foo['a' + 'b'] = () => true; + `, + errors: [{ column: 32, line: 3, messageId: 'nonVoidReturn' }], + }, + { + code: ` + const foo: () => void = async () => Promise.resolve(true); + `, + errors: [ + { + column: 42, + line: 2, + messageId: 'asyncFunc', + }, + ], + }, + { + code: 'const cb: () => void = (): Array => [];', + errors: [ + { + column: 45, + line: 1, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + const cb: () => void = (): Array => { + return []; + }; + `, + errors: [ + { + column: 36, + line: 2, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: noFormat`const cb: () => void = function*foo() {}`, + errors: [ + { + column: 24, + line: 1, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: 'const cb: () => void = (): Promise => Promise.resolve(1);', + errors: [ + { + column: 47, + line: 1, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + const cb: () => void = async (): Promise => { + try { + return Promise.resolve(1); + } catch {} + }; + `, + errors: [ + { + column: 58, + line: 2, + messageId: 'asyncFunc', + }, + ], + }, + { + code: 'const cb: () => void = async (): Promise => Promise.resolve(1);', + errors: [ + { + column: 50, + line: 1, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + const foo: () => void = async () => { + try { + return 1; + } catch {} + }; + `, + errors: [ + { + column: 42, + line: 2, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + const foo: () => void = async (): Promise => { + try { + await Promise.resolve(); + } finally { + } + }; + `, + errors: [ + { + column: 57, + line: 2, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + const foo: () => void = async () => { + try { + await Promise.resolve(); + } catch (err) { + console.error(err); + } + console.log('ok'); + }; + `, + errors: [ + { + column: 42, + line: 2, + messageId: 'asyncFunc', + }, + ], + }, + { + code: 'const foo: () => void = (): number => {};', + errors: [ + { + column: 29, + line: 1, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function cb(): boolean; + const foo: () => void = cb; + `, + errors: [ + { + column: 33, + line: 3, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + const foo: () => void = function () { + if (maybe) { + return null; + } else { + return null; + } + }; + `, + errors: [ + { + column: 13, + line: 4, + messageId: 'nonVoidReturn', + }, + { + column: 13, + line: 6, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + const foo: () => void = function () { + if (maybe) { + console.log('elo'); + return { [1]: Math.random() }; + } + }; + `, + errors: [ + { + column: 13, + line: 5, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + const foo: { (arg: number): void; (arg: string): void } = arg => { + console.log('foo'); + switch (typeof arg) { + case 'number': + return 0; + case 'string': + return ''; + } + }; + `, + errors: [ + { + column: 15, + line: 6, + messageId: 'nonVoidReturn', + }, + { + column: 15, + line: 8, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + const foo: ((arg: number) => void) | ((arg: string) => void) = async () => { + return 1; + }; + `, + errors: [ + { + column: 81, + line: 2, + messageId: 'asyncFunc', + }, + ], + }, + { + code: ` + type Foo = () => void; + const foo: Foo = cb; + function cb() { + return [1, 2, 3]; + } + `, + errors: [ + { + column: 26, + line: 3, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + interface Foo { + (): void; + } + const foo: Foo = cb; + function cb() { + return { a: 1 }; + } + `, + errors: [ + { + column: 26, + line: 5, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function cb(): unknown; + declare let foo: () => void; + foo = cb; + `, + errors: [ + { + column: 15, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare let foo: { arg?: string; cb?: () => void }; + foo.cb = () => { + return 'siema'; + console.log('siema'); + }; + `, + errors: [ + { + column: 11, + line: 4, + messageId: 'nonVoidReturn', + }, + ], + }, + { + code: ` + declare function cb(): unknown; + let foo: (() => void) | null = null; + foo ??= cb; + `, + errors: [ + { + column: 17, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function cb(): unknown; + let foo: (() => void) | boolean = false; + foo ||= cb; + `, + errors: [ + { + column: 17, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function cb(): unknown; + let foo: (() => void) | boolean = false; + foo &&= cb; + `, + errors: [ + { + column: 17, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + }, + { + code: ` + declare function Foo(props: { cb: () => void }): unknown; + return 1} />; + `, + errors: [ + { + column: 31, + line: 3, + messageId: 'nonVoidReturn', + }, + ], + filename: 'react.tsx', + }, + { + code: ` + declare function Foo(props: { cb: () => void }): unknown; + declare function getNull(): null; + return ( + { + if (maybe) return Math.random(); + else return getNull(); + }} + /> + ); + `, + errors: [ + { + column: 26, + line: 7, + messageId: 'nonVoidReturn', + }, + { + column: 20, + line: 8, + messageId: 'nonVoidReturn', + }, + ], + filename: 'react.tsx', + }, + { + code: ` + type Cb = () => void; + declare function Foo(props: { cb: Cb; s: string }): unknown; + return ; + `, + errors: [ + { + column: 25, + line: 4, + messageId: 'asyncFunc', + }, + ], + filename: 'react.tsx', + }, + { + code: ` + type Cb = () => void; + declare function Foo(props: { n: number; cb?: Cb }): unknown; + return ; + `, + errors: [ + { + column: 34, + line: 4, + messageId: 'nonVoidFunc', + }, + ], + filename: 'react.tsx', + }, + { + code: ` + type Cb = ((arg: string) => void) | ((arg: number) => void); + declare function Foo(props: { cb?: Cb }): unknown; + return ( + + ); + `, + errors: [ + { + column: 17, + line: 6, + messageId: 'nonVoidFunc', + }, + ], + filename: 'react.tsx', + }, + { + code: ` + interface Props { + cb: ((arg: unknown) => void) | boolean; + } + declare function Foo(props: Props): unknown; + return x} />; + `, + errors: [ + { + column: 30, + line: 6, + messageId: 'nonVoidReturn', + }, + ], + filename: 'react.tsx', + }, + { + code: ` + type EventHandler = { bivarianceHack(event: E): void }['bivarianceHack']; + interface ButtonProps { + onClick?: EventHandler | undefined; + } + declare function Button(props: ButtonProps): unknown; + function App() { + return