Skip to content

Commit

Permalink
fix(eslint-plugin): [explicit-function-return-type] improved checking…
Browse files Browse the repository at this point in the history
… for allowHigherOrderFunctions option
  • Loading branch information
yeonjuan committed Feb 19, 2024
1 parent 18c3216 commit 9394cb9
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 56 deletions.
101 changes: 68 additions & 33 deletions packages/eslint-plugin/src/rules/explicit-function-return-type.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -98,6 +99,18 @@ export default createRule<Options, MessageIds>({
},
],
create(context, [options]) {
const functionInfo: FunctionInfo[] = [];

function pushFunctionInfo(): void {
functionInfo.push({
returns: [],
});
}

function popFunctionInfo(): FunctionInfo {
return functionInfo.pop()!;
}

function isAllowedFunction(
node:
| TSESTree.ArrowFunctionExpression
Expand Down Expand Up @@ -168,56 +181,78 @@ export default createRule<Options, MessageIds>({
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;
}
if (options.allowTypedFunctionExpressions && node.returnType) {
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);
},
};
},
});
53 changes: 30 additions & 23 deletions 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';
Expand All @@ -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.
* ```
Expand Down Expand Up @@ -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),
);
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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);
}

/**
Expand Down
Expand Up @@ -267,6 +267,17 @@ const myObj = {
},
{
code: `
() => {
const foo = 'foo';
return function (): string {
return foo;
};
};
`,
options: [{ allowHigherOrderFunctions: true }],
},
{
code: `
function fn() {
return (): void => {};
}
Expand All @@ -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 {};
}
`,
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 9394cb9

Please sign in to comment.