diff --git a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts index d135f4c1a0d3..cba0a6dc712b 100644 --- a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts +++ b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts @@ -2,6 +2,7 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; +import type { FunctionInfo } from '../util/explicitReturnTypeUtils'; import { ancestorHasReturnType, checkFunctionReturnType, @@ -98,6 +99,18 @@ export default createRule({ }, ], create(context, [options]) { + const functionInfo: FunctionInfo[] = []; + + function pushFunctionInfo(): void { + functionInfo.push({ + returns: [], + }); + } + + function popFunctionInfo(): FunctionInfo { + return functionInfo.pop()!; + } + function isAllowedFunction( node: | TSESTree.ArrowFunctionExpression @@ -168,41 +181,54 @@ export default createRule({ return node.parent.type === AST_NODE_TYPES.CallExpression; } - return { - 'ArrowFunctionExpression, FunctionExpression'( - node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, - ): void { - if ( - options.allowConciseArrowFunctionExpressionsStartingWithVoid && - node.type === AST_NODE_TYPES.ArrowFunctionExpression && - node.expression && - node.body.type === AST_NODE_TYPES.UnaryExpression && - node.body.operator === 'void' - ) { - return; - } + function exitFunctionExpression( + node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + ): void { + const functionInfo = popFunctionInfo(); - if (isAllowedFunction(node)) { - return; - } + if ( + options.allowConciseArrowFunctionExpressionsStartingWithVoid && + node.type === AST_NODE_TYPES.ArrowFunctionExpression && + node.expression && + node.body.type === AST_NODE_TYPES.UnaryExpression && + node.body.operator === 'void' + ) { + return; + } - if ( - options.allowTypedFunctionExpressions && - (isValidFunctionExpressionReturnType(node, options) || - ancestorHasReturnType(node)) - ) { - return; - } + if (isAllowedFunction(node)) { + return; + } + + if ( + options.allowTypedFunctionExpressions && + (isValidFunctionExpressionReturnType(node, options) || + ancestorHasReturnType(node)) + ) { + return; + } - checkFunctionReturnType(node, options, context.sourceCode, loc => + checkFunctionReturnType( + node, + options, + context.sourceCode, + functionInfo, + loc => context.report({ node, loc, messageId: 'missingReturnType', }), - ); - }, - FunctionDeclaration(node): void { + ); + } + + return { + 'ArrowFunctionExpression, FunctionExpression': pushFunctionInfo, + 'ArrowFunctionExpression:exit': exitFunctionExpression, + 'FunctionExpression:exit': exitFunctionExpression, + FunctionDeclaration: pushFunctionInfo, + 'FunctionDeclaration:exit'(node): void { + const functionInfo = popFunctionInfo(); if (isAllowedFunction(node)) { return; } @@ -210,14 +236,23 @@ export default createRule({ return; } - checkFunctionReturnType(node, options, context.sourceCode, loc => - context.report({ - node, - loc, - messageId: 'missingReturnType', - }), + checkFunctionReturnType( + node, + options, + context.sourceCode, + functionInfo, + loc => + context.report({ + node, + loc, + messageId: 'missingReturnType', + }), ); }, + ReturnStatement(node): void { + const current = functionInfo[functionInfo.length - 1]; + current.returns.push(node); + }, }; }, }); diff --git a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts index 512ff62202b9..8e12485edcb4 100644 --- a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts +++ b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts @@ -1,5 +1,9 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'; +import { + AST_NODE_TYPES, + ASTUtils, + ESLintUtils, +} from '@typescript-eslint/utils'; import { isConstructor, isSetter, isTypeAssertion } from './astUtils'; import { getFunctionHeadLoc } from './getFunctionHeadLoc'; @@ -9,6 +13,10 @@ type FunctionExpression = | TSESTree.FunctionExpression; type FunctionNode = FunctionExpression | TSESTree.FunctionDeclaration; +export interface FunctionInfo { + returns: TSESTree.ReturnStatement[]; +} + /** * Checks if a node is a variable declarator with a type annotation. * ``` @@ -133,27 +141,23 @@ function isPropertyOfObjectWithType( * function fn() { return function() { ... } } * ``` */ -function doesImmediatelyReturnFunctionExpression({ - body, -}: FunctionNode): boolean { - // Check if body is a block with a single statement - if (body.type === AST_NODE_TYPES.BlockStatement && body.body.length === 1) { - const [statement] = body.body; - - // Check if that statement is a return statement with an argument - if ( - statement.type === AST_NODE_TYPES.ReturnStatement && - !!statement.argument - ) { - // If so, check that returned argument as body - body = statement.argument; - } +function doesImmediatelyReturnFunctionExpression( + node: FunctionNode, + functionInfo: FunctionInfo, +): boolean { + if ( + node.type === AST_NODE_TYPES.ArrowFunctionExpression && + ASTUtils.isFunction(node.body) + ) { + return true; } - // Check if the body being returned is a function expression - return ( - body.type === AST_NODE_TYPES.ArrowFunctionExpression || - body.type === AST_NODE_TYPES.FunctionExpression + if (functionInfo.returns.length === 0) { + return false; + } + + return functionInfo.returns.every( + node => node.argument && ASTUtils.isFunction(node.argument), ); } @@ -253,10 +257,11 @@ function isValidFunctionExpressionReturnType( function isValidFunctionReturnType( node: FunctionNode, options: Options, + functionInfo: FunctionInfo, ): boolean { if ( options.allowHigherOrderFunctions && - doesImmediatelyReturnFunctionExpression(node) + doesImmediatelyReturnFunctionExpression(node, functionInfo) ) { return true; } @@ -275,9 +280,10 @@ function checkFunctionReturnType( node: FunctionNode, options: Options, sourceCode: TSESLint.SourceCode, + functionInfo: FunctionInfo, report: (loc: TSESTree.SourceLocation) => void, ): void { - if (isValidFunctionReturnType(node, options)) { + if (isValidFunctionReturnType(node, options, functionInfo)) { return; } @@ -291,13 +297,14 @@ function checkFunctionExpressionReturnType( node: FunctionExpression, options: Options, sourceCode: TSESLint.SourceCode, + functionInfo: FunctionInfo, report: (loc: TSESTree.SourceLocation) => void, ): void { if (isValidFunctionExpressionReturnType(node, options)) { return; } - checkFunctionReturnType(node, options, sourceCode, report); + checkFunctionReturnType(node, options, sourceCode, functionInfo, report); } /** diff --git a/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts b/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts index 8b0a05d6b5b4..994262a88319 100644 --- a/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts +++ b/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts @@ -267,6 +267,17 @@ const myObj = { }, { code: ` +() => { + const foo = 'foo'; + return function (): string { + return foo; + }; +}; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + code: ` function fn() { return (): void => {}; } @@ -276,6 +287,31 @@ function fn() { { code: ` function fn() { + return function (): void {}; +} + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + code: ` +function fn() { + const bar = () => (): number => 1; + return function (): void {}; +} + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + code: ` +function fn(arg: boolean) { + if (arg) { + return () => (): number => 1; + } else { + return function (): string { + return 'foo'; + }; + } + return function (): void {}; } `, @@ -1230,6 +1266,25 @@ function fn() { }, { code: ` +function fn() { + const bar = () => (): number => 1; + const baz = () => () => 'baz'; + return function (): void {}; +} + `, + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 24, + endColumn: 26, + }, + ], + }, + { + code: ` function FunctionDeclaration() { return function FunctionExpression_Within_FunctionDeclaration() { return function FunctionExpression_Within_FunctionExpression() {