Skip to content

Commit d7d4eeb

Browse files
authored
fix(eslint-plugin): [explicit-module-boundary-types] don't check returned functions if parent function has return type (typescript-eslint#2084)
1 parent e383691 commit d7d4eeb

File tree

3 files changed

+268
-34
lines changed

3 files changed

+268
-34
lines changed

packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
checkFunctionExpressionReturnType,
99
checkFunctionReturnType,
1010
isTypedFunctionExpression,
11+
ancestorHasReturnType,
1112
} from '../util/explicitReturnTypeUtils';
1213

1314
type Options = [
@@ -74,6 +75,10 @@ export default util.createRule<Options, MessageIds>({
7475
create(context, [options]) {
7576
const sourceCode = context.getSourceCode();
7677

78+
/**
79+
* Steps up the nodes parents to check if any ancestor nodes are export
80+
* declarations.
81+
*/
7782
function isUnexported(node: TSESTree.Node | undefined): boolean {
7883
let isReturnedValue = false;
7984
while (node) {
@@ -111,6 +116,9 @@ export default util.createRule<Options, MessageIds>({
111116
return true;
112117
}
113118

119+
/**
120+
* Returns true when the argument node is not typed.
121+
*/
114122
function isArgumentUntyped(node: TSESTree.Identifier): boolean {
115123
return (
116124
!node.typeAnnotation ||
@@ -265,7 +273,7 @@ export default util.createRule<Options, MessageIds>({
265273
}
266274

267275
/**
268-
* Checks if a function expression follow the rule
276+
* Checks if a function expression follow the rule.
269277
*/
270278
function checkFunctionExpression(
271279
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
@@ -280,7 +288,8 @@ export default util.createRule<Options, MessageIds>({
280288

281289
if (
282290
isAllowedName(node.parent) ||
283-
isTypedFunctionExpression(node, options)
291+
isTypedFunctionExpression(node, options) ||
292+
ancestorHasReturnType(node.parent, options)
284293
) {
285294
return;
286295
}
@@ -300,7 +309,10 @@ export default util.createRule<Options, MessageIds>({
300309
* Checks if a function follow the rule
301310
*/
302311
function checkFunction(node: TSESTree.FunctionDeclaration): void {
303-
if (isAllowedName(node.parent)) {
312+
if (
313+
isAllowedName(node.parent) ||
314+
ancestorHasReturnType(node.parent, options)
315+
) {
304316
return;
305317
}
306318

packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts

Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -240,31 +240,8 @@ interface Options {
240240
}
241241

242242
/**
243-
* Checks if a function declaration/expression has a return type.
243+
* True when the provided function expression is typed.
244244
*/
245-
function checkFunctionReturnType(
246-
node:
247-
| TSESTree.ArrowFunctionExpression
248-
| TSESTree.FunctionDeclaration
249-
| TSESTree.FunctionExpression,
250-
options: Options,
251-
sourceCode: TSESLint.SourceCode,
252-
report: (loc: TSESTree.SourceLocation) => void,
253-
): void {
254-
if (
255-
options.allowHigherOrderFunctions &&
256-
doesImmediatelyReturnFunctionExpression(node)
257-
) {
258-
return;
259-
}
260-
261-
if (node.returnType || isConstructor(node.parent) || isSetter(node.parent)) {
262-
return;
263-
}
264-
265-
report(getReporLoc(node, sourceCode));
266-
}
267-
268245
function isTypedFunctionExpression(
269246
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
270247
options: Options,
@@ -286,16 +263,15 @@ function isTypedFunctionExpression(
286263
}
287264

288265
/**
289-
* Checks if a function declaration/expression has a return type.
266+
* Check whether the function expression return type is either typed or valid
267+
* with the provided options.
290268
*/
291-
function checkFunctionExpressionReturnType(
269+
function isValidFunctionExpressionReturnType(
292270
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
293271
options: Options,
294-
sourceCode: TSESLint.SourceCode,
295-
report: (loc: TSESTree.SourceLocation) => void,
296-
): void {
272+
): boolean {
297273
if (isTypedFunctionExpression(node, options)) {
298-
return;
274+
return true;
299275
}
300276

301277
const parent = nullThrows(node.parent, NullThrowsReasons.MissingParent);
@@ -306,7 +282,7 @@ function checkFunctionExpressionReturnType(
306282
parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration &&
307283
parent.type !== AST_NODE_TYPES.ClassProperty
308284
) {
309-
return;
285+
return true;
310286
}
311287

312288
// https://github.com/typescript-eslint/typescript-eslint/issues/653
@@ -315,14 +291,112 @@ function checkFunctionExpressionReturnType(
315291
node.type === AST_NODE_TYPES.ArrowFunctionExpression &&
316292
returnsConstAssertionDirectly(node)
317293
) {
294+
return true;
295+
}
296+
297+
return false;
298+
}
299+
300+
/**
301+
* Check that the function expression or declaration is valid.
302+
*/
303+
function isValidFunctionReturnType(
304+
node:
305+
| TSESTree.ArrowFunctionExpression
306+
| TSESTree.FunctionDeclaration
307+
| TSESTree.FunctionExpression,
308+
options: Options,
309+
isParentCheck = false,
310+
): boolean {
311+
if (
312+
!isParentCheck &&
313+
options.allowHigherOrderFunctions &&
314+
doesImmediatelyReturnFunctionExpression(node)
315+
) {
316+
return true;
317+
}
318+
319+
if (node.returnType || isConstructor(node.parent) || isSetter(node.parent)) {
320+
return true;
321+
}
322+
323+
return false;
324+
}
325+
326+
/**
327+
* Checks if a function declaration/expression has a return type.
328+
*/
329+
function checkFunctionReturnType(
330+
node:
331+
| TSESTree.ArrowFunctionExpression
332+
| TSESTree.FunctionDeclaration
333+
| TSESTree.FunctionExpression,
334+
options: Options,
335+
sourceCode: TSESLint.SourceCode,
336+
report: (loc: TSESTree.SourceLocation) => void,
337+
): void {
338+
if (isValidFunctionReturnType(node, options)) {
339+
return;
340+
}
341+
342+
report(getReporLoc(node, sourceCode));
343+
}
344+
345+
/**
346+
* Checks if a function declaration/expression has a return type.
347+
*/
348+
function checkFunctionExpressionReturnType(
349+
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
350+
options: Options,
351+
sourceCode: TSESLint.SourceCode,
352+
report: (loc: TSESTree.SourceLocation) => void,
353+
): void {
354+
if (isValidFunctionExpressionReturnType(node, options)) {
318355
return;
319356
}
320357

321358
checkFunctionReturnType(node, options, sourceCode, report);
322359
}
323360

361+
/**
362+
* Check whether any ancestor of the provided node has a valid return type, with
363+
* the given options.
364+
*/
365+
function ancestorHasReturnType(
366+
ancestor: TSESTree.Node | undefined,
367+
options: Options,
368+
): boolean {
369+
// Exit early if this ancestor is not a ReturnStatement.
370+
if (ancestor?.type !== AST_NODE_TYPES.ReturnStatement) {
371+
return false;
372+
}
373+
374+
// This boolean tells the `isValidFunctionReturnType` that it is being called
375+
// by an ancestor check.
376+
const isParentCheck = true;
377+
378+
while (ancestor) {
379+
switch (ancestor.type) {
380+
case AST_NODE_TYPES.ArrowFunctionExpression:
381+
case AST_NODE_TYPES.FunctionExpression:
382+
return (
383+
isValidFunctionExpressionReturnType(ancestor, options) ||
384+
isValidFunctionReturnType(ancestor, options, isParentCheck)
385+
);
386+
case AST_NODE_TYPES.FunctionDeclaration:
387+
return isValidFunctionReturnType(ancestor, options, isParentCheck);
388+
}
389+
390+
ancestor = ancestor.parent;
391+
}
392+
393+
/* istanbul ignore next */
394+
return false;
395+
}
396+
324397
export {
325398
checkFunctionReturnType,
326399
checkFunctionExpressionReturnType,
327400
isTypedFunctionExpression,
401+
ancestorHasReturnType,
328402
};

packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,89 @@ export default { Foo };
437437
`,
438438
options: [{ shouldTrackReferences: true }],
439439
},
440+
{
441+
code: `
442+
export function foo(): (n: number) => string {
443+
return n => String(n);
444+
}
445+
`,
446+
},
447+
{
448+
code: `
449+
export const foo = (a: string): ((n: number) => string) => {
450+
return function (n) {
451+
return String(n);
452+
};
453+
};
454+
`,
455+
},
456+
{
457+
code: `
458+
export function a(): void {
459+
function b() {}
460+
const x = () => {};
461+
(function () {});
462+
463+
function c() {
464+
return () => {};
465+
}
466+
467+
return;
468+
}
469+
`,
470+
},
471+
{
472+
code: `
473+
export function a(): void {
474+
function b() {
475+
function c() {}
476+
}
477+
const x = () => {
478+
return () => 100;
479+
};
480+
(function () {
481+
(function () {});
482+
});
483+
484+
function c() {
485+
return () => {
486+
(function () {});
487+
};
488+
}
489+
490+
return;
491+
}
492+
`,
493+
},
494+
{
495+
code: `
496+
export function a() {
497+
return function b(): () => void {
498+
return function c() {};
499+
};
500+
}
501+
`,
502+
options: [{ allowHigherOrderFunctions: true }],
503+
},
504+
{
505+
code: `
506+
export var arrowFn = () => (): void => {};
507+
`,
508+
},
509+
{
510+
code: `
511+
export function fn() {
512+
return function (): void {};
513+
}
514+
`,
515+
},
516+
{
517+
code: `
518+
export function foo(outer: string) {
519+
return function (inner: string): void {};
520+
}
521+
`,
522+
},
440523
],
441524
invalid: [
442525
{
@@ -1280,5 +1363,70 @@ export default test;
12801363
},
12811364
],
12821365
},
1366+
{
1367+
code: `
1368+
export const foo = () => (a: string): ((n: number) => string) => {
1369+
return function (n) {
1370+
return String(n);
1371+
};
1372+
};
1373+
`,
1374+
options: [{ allowHigherOrderFunctions: false }],
1375+
errors: [
1376+
{
1377+
messageId: 'missingReturnType',
1378+
line: 2,
1379+
column: 20,
1380+
},
1381+
],
1382+
},
1383+
{
1384+
code: `
1385+
export var arrowFn = () => () => {};
1386+
`,
1387+
options: [{ allowHigherOrderFunctions: true }],
1388+
errors: [
1389+
{
1390+
messageId: 'missingReturnType',
1391+
line: 2,
1392+
column: 28,
1393+
},
1394+
],
1395+
},
1396+
{
1397+
code: `
1398+
export function fn() {
1399+
return function () {};
1400+
}
1401+
`,
1402+
options: [{ allowHigherOrderFunctions: true }],
1403+
errors: [
1404+
{
1405+
messageId: 'missingReturnType',
1406+
line: 3,
1407+
column: 10,
1408+
},
1409+
],
1410+
},
1411+
{
1412+
code: `
1413+
export function foo(outer) {
1414+
return function (inner): void {};
1415+
}
1416+
`,
1417+
options: [{ allowHigherOrderFunctions: true }],
1418+
errors: [
1419+
{
1420+
messageId: 'missingArgType',
1421+
line: 2,
1422+
column: 8,
1423+
},
1424+
{
1425+
messageId: 'missingArgType',
1426+
line: 3,
1427+
column: 10,
1428+
},
1429+
],
1430+
},
12831431
],
12841432
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy