Skip to content

Commit 50a493e

Browse files
SvishJamesHenry
authored andcommitted
feat(eslint-plugin): [explicit-function-return-type] allowHigherOrderFunctions (typescript-eslint#193) (typescript-eslint#538)
1 parent f354a3d commit 50a493e

File tree

3 files changed

+275
-3
lines changed

3 files changed

+275
-3
lines changed

packages/eslint-plugin/docs/rules/explicit-function-return-type.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,16 @@ The rule accepts an options object with the following properties:
6565
type Options = {
6666
// if true, only functions which are part of a declaration will be checked
6767
allowExpressions?: boolean;
68-
// if true, type annotations are also allowed on the variable of a function expression rather than on the function directly.
68+
// if true, type annotations are also allowed on the variable of a function expression rather than on the function directly
6969
allowTypedFunctionExpressions?: boolean;
70+
// if true, functions immediately returning another function expression will not be checked
71+
allowHigherOrderFunctions?: boolean;
7072
};
7173

7274
const defaults = {
7375
allowExpressions: false,
7476
allowTypedFunctionExpressions: false,
77+
allowHigherOrderFunctions: false,
7578
};
7679
```
7780

@@ -121,7 +124,7 @@ let funcExpr: FuncType = function() {
121124
};
122125

123126
let asTyped = (() => '') as () => string;
124-
let caasTyped = <() => string>(() => '');
127+
let castTyped = <() => string>(() => '');
125128

126129
interface ObjectType {
127130
foo(): number;
@@ -137,6 +140,32 @@ let objectPropCast = <ObjectType>{
137140
};
138141
```
139142

143+
### allowHigherOrderFunctions
144+
145+
Examples of **incorrect** code for this rule with `{ allowHigherOrderFunctions: true }`:
146+
147+
```ts
148+
var arrowFn = (x: number) => (y: number) => x + y;
149+
150+
function fn(x: number) {
151+
return function(y: number) {
152+
return x + y;
153+
};
154+
}
155+
```
156+
157+
Examples of **correct** code for this rule with `{ allowHigherOrderFunctions: true }`:
158+
159+
```ts
160+
var arrowFn = (x: number) => (y: number): number => x + y;
161+
162+
function fn(x: number) {
163+
return function(y: number): number {
164+
return x + y;
165+
};
166+
}
167+
```
168+
140169
## When Not To Use It
141170

142171
If you don't wish to prevent calling code from using function return values in unexpected ways, then

packages/eslint-plugin/src/rules/explicit-function-return-type.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type Options = [
88
{
99
allowExpressions?: boolean;
1010
allowTypedFunctionExpressions?: boolean;
11+
allowHigherOrderFunctions?: boolean;
1112
}
1213
];
1314
type MessageIds = 'missingReturnType';
@@ -35,6 +36,9 @@ export default util.createRule<Options, MessageIds>({
3536
allowTypedFunctionExpressions: {
3637
type: 'boolean',
3738
},
39+
allowHigherOrderFunctions: {
40+
type: 'boolean',
41+
},
3842
},
3943
additionalProperties: false,
4044
},
@@ -44,6 +48,7 @@ export default util.createRule<Options, MessageIds>({
4448
{
4549
allowExpressions: false,
4650
allowTypedFunctionExpressions: false,
51+
allowHigherOrderFunctions: false,
4752
},
4853
],
4954
create(context, [options]) {
@@ -138,6 +143,50 @@ export default util.createRule<Options, MessageIds>({
138143
);
139144
}
140145

146+
/**
147+
* Checks if a function belongs to:
148+
* `() => () => ...`
149+
* `() => function () { ... }`
150+
* `() => { return () => ... }`
151+
* `() => { return function () { ... } }`
152+
* `function fn() { return () => ... }`
153+
* `function fn() { return function() { ... } }`
154+
*/
155+
function doesImmediatelyReturnFunctionExpression({
156+
body,
157+
}:
158+
| TSESTree.ArrowFunctionExpression
159+
| TSESTree.FunctionDeclaration
160+
| TSESTree.FunctionExpression): boolean {
161+
// Should always have a body; really checking just in case
162+
/* istanbul ignore if */ if (!body) {
163+
return false;
164+
}
165+
166+
// Check if body is a block with a single statement
167+
if (
168+
body.type === AST_NODE_TYPES.BlockStatement &&
169+
body.body.length === 1
170+
) {
171+
const [statement] = body.body;
172+
173+
// Check if that statement is a return statement with an argument
174+
if (
175+
statement.type === AST_NODE_TYPES.ReturnStatement &&
176+
!!statement.argument
177+
) {
178+
// If so, check that returned argument as body
179+
body = statement.argument;
180+
}
181+
}
182+
183+
// Check if the body being returned is a function expression
184+
return (
185+
body.type === AST_NODE_TYPES.ArrowFunctionExpression ||
186+
body.type === AST_NODE_TYPES.FunctionExpression
187+
);
188+
}
189+
141190
/**
142191
* Checks if a function declaration/expression has a return type.
143192
*/
@@ -147,6 +196,13 @@ export default util.createRule<Options, MessageIds>({
147196
| TSESTree.FunctionDeclaration
148197
| TSESTree.FunctionExpression,
149198
): void {
199+
if (
200+
options.allowHigherOrderFunctions &&
201+
doesImmediatelyReturnFunctionExpression(node)
202+
) {
203+
return;
204+
}
205+
150206
if (
151207
node.returnType ||
152208
isConstructor(node.parent) ||
@@ -169,7 +225,8 @@ export default util.createRule<Options, MessageIds>({
169225
function checkFunctionExpressionReturnType(
170226
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
171227
): void {
172-
if (node.parent) {
228+
// Should always have a parent; checking just in case
229+
/* istanbul ignore else */ if (node.parent) {
173230
if (options.allowTypedFunctionExpressions) {
174231
if (
175232
isTypeCast(node.parent) ||

packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,71 @@ const myObj = {
180180
};
181181
`,
182182
},
183+
{
184+
filename: 'test.ts',
185+
code: `
186+
() => (): void => {};
187+
`,
188+
options: [{ allowHigherOrderFunctions: true }],
189+
},
190+
{
191+
filename: 'test.ts',
192+
code: `
193+
() => function (): void {};
194+
`,
195+
options: [{ allowHigherOrderFunctions: true }],
196+
},
197+
{
198+
filename: 'test.ts',
199+
code: `
200+
() => { return (): void => {} };
201+
`,
202+
options: [{ allowHigherOrderFunctions: true }],
203+
},
204+
{
205+
filename: 'test.ts',
206+
code: `
207+
() => { return function (): void {} };
208+
`,
209+
options: [{ allowHigherOrderFunctions: true }],
210+
},
211+
{
212+
filename: 'test.ts',
213+
code: `
214+
function fn() { return (): void => {} };
215+
`,
216+
options: [{ allowHigherOrderFunctions: true }],
217+
},
218+
{
219+
filename: 'test.ts',
220+
code: `
221+
function fn() { return function (): void {} };
222+
`,
223+
options: [{ allowHigherOrderFunctions: true }],
224+
},
225+
{
226+
filename: 'test.ts',
227+
code: `
228+
function FunctionDeclaration() {
229+
return function FunctionExpression_Within_FunctionDeclaration() {
230+
return function FunctionExpression_Within_FunctionExpression() {
231+
return () => { // ArrowFunctionExpression_Within_FunctionExpression
232+
return () => // ArrowFunctionExpression_Within_ArrowFunctionExpression
233+
(): number => 1 // ArrowFunctionExpression_Within_ArrowFunctionExpression_WithNoBody
234+
}
235+
}
236+
}
237+
}
238+
`,
239+
options: [{ allowHigherOrderFunctions: true }],
240+
},
241+
{
242+
filename: 'test.ts',
243+
code: `
244+
() => () => { return (): void => { return; } };
245+
`,
246+
options: [{ allowHigherOrderFunctions: true }],
247+
},
183248
],
184249
invalid: [
185250
{
@@ -364,5 +429,126 @@ const x: Foo = {
364429
},
365430
],
366431
},
432+
{
433+
filename: 'test.ts',
434+
code: `
435+
() => () => {};
436+
`,
437+
options: [{ allowHigherOrderFunctions: true }],
438+
errors: [
439+
{
440+
messageId: 'missingReturnType',
441+
line: 2,
442+
column: 7,
443+
},
444+
],
445+
},
446+
{
447+
filename: 'test.ts',
448+
code: `
449+
() => function () {};
450+
`,
451+
options: [{ allowHigherOrderFunctions: true }],
452+
errors: [
453+
{
454+
messageId: 'missingReturnType',
455+
line: 2,
456+
column: 7,
457+
},
458+
],
459+
},
460+
{
461+
filename: 'test.ts',
462+
code: `
463+
() => { return () => {} };
464+
`,
465+
options: [{ allowHigherOrderFunctions: true }],
466+
errors: [
467+
{
468+
messageId: 'missingReturnType',
469+
line: 2,
470+
column: 16,
471+
},
472+
],
473+
},
474+
{
475+
filename: 'test.ts',
476+
code: `
477+
() => { return function () {} };
478+
`,
479+
options: [{ allowHigherOrderFunctions: true }],
480+
errors: [
481+
{
482+
messageId: 'missingReturnType',
483+
line: 2,
484+
column: 16,
485+
},
486+
],
487+
},
488+
{
489+
filename: 'test.ts',
490+
code: `
491+
function fn() { return () => {} };
492+
`,
493+
options: [{ allowHigherOrderFunctions: true }],
494+
errors: [
495+
{
496+
messageId: 'missingReturnType',
497+
line: 2,
498+
column: 24,
499+
},
500+
],
501+
},
502+
{
503+
filename: 'test.ts',
504+
code: `
505+
function fn() { return function () {} };
506+
`,
507+
options: [{ allowHigherOrderFunctions: true }],
508+
errors: [
509+
{
510+
messageId: 'missingReturnType',
511+
line: 2,
512+
column: 24,
513+
},
514+
],
515+
},
516+
{
517+
filename: 'test.ts',
518+
code: `
519+
function FunctionDeclaration() {
520+
return function FunctionExpression_Within_FunctionDeclaration() {
521+
return function FunctionExpression_Within_FunctionExpression() {
522+
return () => { // ArrowFunctionExpression_Within_FunctionExpression
523+
return () => // ArrowFunctionExpression_Within_ArrowFunctionExpression
524+
() => 1 // ArrowFunctionExpression_Within_ArrowFunctionExpression_WithNoBody
525+
}
526+
}
527+
}
528+
}
529+
`,
530+
options: [{ allowHigherOrderFunctions: true }],
531+
errors: [
532+
{
533+
messageId: 'missingReturnType',
534+
line: 7,
535+
column: 11,
536+
},
537+
],
538+
},
539+
{
540+
filename: 'test.ts',
541+
code: `
542+
() => () => { return () => { return; } };
543+
`,
544+
options: [{ allowHigherOrderFunctions: true }],
545+
errors: [
546+
{
547+
messageId: 'missingReturnType',
548+
line: 2,
549+
column: 22,
550+
},
551+
],
552+
},
367553
],
368554
});

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