8000 feat(eslint-plugin): [consistent-return] add new rule (#8289) · yeonjuan/typescript-eslint@46cef96 · GitHub
[go: up one dir, main page]

Skip to content

Commit 46cef96

Browse files
authored
feat(eslint-plugin): [consistent-return] add new rule (typescript-eslint#8289)
* feat(eslint-plugin): [consistent-return] add new rule * apply reviews * fix docs * fix order * docs: remove eslint comment * handle treatUndefinedAsUnspecified option * fix rule in rulemap order * add test case * fix docs * add test case * fix default options * refactor * refactor * fix * handle nested promise * fix lint error
1 parent bba28a9 commit 46cef96

File tree

9 files changed

+685
-0
lines changed

9 files changed

+685
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
description: 'Require `return` statements to either always or never specify values.'
3+
---
4+
5+
> 🛑 This file is source code, not the primary documentation location! 🛑
6+
>
7+
> See **https://typescript-eslint.io/rules/consistent-return** for documentation.
8+
9+
This rule extends the base [`eslint/consistent-return`](https://eslint.org/docs/rules/consistent-return) rule.
10+
This version adds support for functions that return `void` or `Promise<void>`.
11+
12+
<!--tabs-->
13+
14+
### ❌ Incorrect
15+
16+
```ts
17+
function foo(): undefined {}
18+
function bar(flag: boolean): undefined {
19+
if (flag) return foo();
20+
return;
21+
}
22+
23+
async function baz(flag: boolean): Promise<undefined> {
24+
if (flag) return;
25+
return foo();
26+
}
27+
```
28+
29+
### ✅ Correct
30+
31+
```ts
32+
function foo(): void {}
33+
function bar(flag: boolean): void {
34+
if (flag) return foo();
35+
return;
36+
}
37+
38+
async function baz(flag: boolean): Promise<void | number> {
39+
if (flag) return 42;
40+
return;
41+
}
42+
```

packages/eslint-plugin/src/configs/all.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export = {
2121
'@typescript-eslint/class-methods-use-this': 'error',
2222
'@typescript-eslint/consistent-generic-constructors': 'error',
2323
'@typescript-eslint/consistent-indexed-object-style': 'error',
24+
'consistent-return': 'off',
25+
'@typescript-eslint/consistent-return': 'error',
2426
'@typescript-eslint/consistent-type-assertions': 'error',
2527
'@typescript-eslint/consistent-type-definitions': 'error',
2628
'@typescript-eslint/consistent-type-exports': 'error',

packages/eslint-plugin/src/configs/disable-type-checked.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export = {
1111
parserOptions: { project: false, program: null },
1212
rules: {
1313
'@typescript-eslint/await-thenable': 'off',
14+
'@typescript-eslint/consistent-return': 'off',
1415
'@typescript-eslint/consistent-type-exports': 'off',
1516
'@typescript-eslint/dot-notation': 'off',
1617
'@typescript-eslint/naming-convention': 'off',
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import * as tsutils from 'ts-api-utils';
3+
import * as ts from 'typescript';
4+
5+
import type {
6+
InferMessageIdsTypeFromRule,
7+
InferOptionsTypeFromRule,
8+
} from '../util';
9+
import { createRule, getParserServices, isTypeFlagSet } from '../util';
10+
import { getESLintCoreRule } from '../util/getESLintCoreRule';
11+
12+
const baseRule = getESLintCoreRule('consistent-return');
13+
14+
type Options = InferOptionsTypeFromRule<typeof baseRule>;
15+
type MessageIds = InferMessageIdsTypeFromRule<typeof baseRule>;
16+
17+
type FunctionNode =
18+
| TSESTree.FunctionDeclaration
19+
| TSESTree.FunctionExpression
20+
| TSESTree.ArrowFunctionExpression;
21+
22+
export default createRule<Options, MessageIds>({
23+
name: 'consistent-return',
24+
meta: {
25+
type: 'suggestion',
26+
docs: {
27+
description:
28+
'Require `return` statements to either always or never specify values',
29+
extendsBaseRule: true,
30+
requiresTypeChecking: true,
31+
},
32+
hasSuggestions: baseRule.meta.hasSuggestions,
33+
schema: baseRule.meta.schema,
34+
messages: baseRule.meta.messages,
35+
},
36+
defaultOptions: [{ treatUndefinedAsUnspecified: false }],
37+
create(context, [options]) {
38+
const services = getParserServices(context);
39+
const checker = services.program.getTypeChecker();
40+
const rules = baseRule.create(context);
41+
const functions: FunctionNode[] = [];
42+
const treatUndefinedAsUnspecified =
43+
options?.treatUndefinedAsUnspecified === true;
44+
45+
function enterFunction(node: FunctionNode): void {
46+
functions.push(node);
47+
}
48+
49+
function exitFunction(): void {
50+
functions.pop();
51+
}
52+
53+
function getCurrentFunction(): FunctionNode | null {
54+
return functions[functions.length - 1] ?? null;
55+
}
56+
57+
function isPromiseVoid(node: ts.Node, type: ts.Type): boolean {
58+
if (
59+
tsutils.isThenableType(checker, node, type) &&
60+
tsutils.isTypeReference(type)
61+
) {
62+
const awaitedType = type.typeArguments?.[0];
63+
if (awaitedType) {
64+
if (isTypeFlagSet(awaitedType, ts.TypeFlags.Void)) {
65+
return true;
66+
}
67+
return isPromiseVoid(node, awaitedType);
68+
}
69+
}
70+
return false;
71+
}
72+
73+
function isReturnVoidOrThenableVoid(node: FunctionNode): boolean {
74+
const functionType = services.getTypeAtLocation(node);
75+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
76+
const callSignatures = functionType.getCallSignatures();
77+
78+
return callSignatures.some(signature => {
79+
const returnType = signature.getReturnType();
80+
if (node.async) {
81+
return isPromiseVoid(tsNode, returnType);
82+
}
83+
return isTypeFlagSet(returnType, ts.TypeFlags.Void);
84+
});
85+
}
86+
87+
return {
88+
...rules,
89+
FunctionDeclaration: enterFunction,
90+
'FunctionDeclaration:exit'(node): void {
91+
exitFunction();
92+
rules['FunctionDeclaration:exit'](node);
93+
},
94+
FunctionExpression: enterFunction,
95+
'FunctionExpression:exit'(node): void {
96+
exitFunction();
97+
rules['FunctionExpression:exit'](node);
98+
},
99+
ArrowFunctionExpression: enterFunction,
100+
'ArrowFunctionExpression:exit'(node): void {
101+
exitFunction();
102+
rules['ArrowFunctionExpression:exit'](node);
103+
},
104+
ReturnStatement(node): void {
105+
const functionNode = getCurrentFunction();
106+
if (
107+
!node.argument &&
108+
functionNode &&
109+
isReturnVoidOrThenableVoid(functionNode)
110+
) {
111+
return;
112+
}
113+
if (treatUndefinedAsUnspecified && node.argument) {
114+
const returnValueType = services.getTypeAtLocation(node.argument);
115+
if (returnValueType.flags === ts.TypeFlags.Undefined) {
116+
rules.ReturnStatement({
117+
...node,
118+
argument: null,
119+
});
120+
return;
121+
}
122+
}
123+
124+
rules.ReturnStatement(node);
125+
},
126+
};
127+
},
128+
});

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import commaDangle from './comma-dangle';
1414
import commaSpacing from './comma-spacing';
1515
import consistentGenericConstructors from './consistent-generic-constructors';
1616
import consistentIndexedObjectStyle from './consistent-indexed-object-style';
17+
import consistentReturn from './consistent-return';
1718
import consistentTypeAssertions from './consistent-type-assertions';
1819
import consistentTypeDefinitions from './consistent-type-definitions';
1920
import consistentTypeExports from './consistent-type-exports';
@@ -156,6 +157,7 @@ export default {
156157
'comma-spacing': commaSpacing,
157158
'consistent-generic-constructors': consistentGenericConstructors,
158159
'consistent-indexed-object-style': consistentIndexedObjectStyle,
160+
'consistent-return': consistentReturn,
159161
'consistent-type-assertions': consistentTypeAssertions,
160162
'consistent-type-definitions': consistentTypeDefinitions,
161163
'consistent-type-exports': consistentTypeExports,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface RuleMap {
77
'block-spacing': typeof import('eslint/lib/rules/block-spacing');
88
'brace-style': typeof import('eslint/lib/rules/brace-style');
99
'comma-dangle': typeof import('eslint/lib/rules/comma-dangle');
10+
'consistent-return': typeof import('eslint/lib/rules/consistent-return');
1011
'dot-notation': typeof import('eslint/lib/rules/dot-notation');
1112
indent: typeof import('eslint/lib/rules/indent');
1213
'init-declarations': typeof import('eslint/lib/rules/init-declarations');

0 commit comments

Comments
 (0)
0