-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): [consistent-return] add new rule (#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
- Loading branch information
Showing
9 changed files
with
685 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
--- | ||
description: 'Require `return` statements to either always or never specify values.' | ||
--- | ||
|
||
> 🛑 This file is source code, not the primary documentation location! 🛑 | ||
> | ||
> See **https://typescript-eslint.io/rules/consistent-return** for documentation. | ||
This rule extends the base [`eslint/consistent-return`](https://eslint.org/docs/rules/consistent-return) rule. | ||
This version adds support for functions that return `void` or `Promise<void>`. | ||
|
||
<!--tabs--> | ||
|
||
### ❌ Incorrect | ||
|
||
```ts | ||
function foo(): undefined {} | ||
function bar(flag: boolean): undefined { | ||
if (flag) return foo(); | ||
return; | ||
} | ||
|
||
async function baz(flag: boolean): Promise<undefined> { | ||
if (flag) return; | ||
return foo(); | ||
} | ||
``` | ||
|
||
### ✅ Correct | ||
|
||
```ts | ||
function foo(): void {} | ||
function bar(flag: boolean): void { | ||
if (flag) return foo(); | ||
return; | ||
} | ||
|
||
async function baz(flag: boolean): Promise<void | number> { | ||
if (flag) return 42; | ||
return; | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import type { TSESTree } from '@typescript-eslint/utils'; | ||
import * as tsutils from 'ts-api-utils'; | ||
import * as ts from 'typescript'; | ||
|
||
import type { | ||
InferMessageIdsTypeFromRule, | ||
InferOptionsTypeFromRule, | ||
} from '../util'; | ||
import { createRule, getParserServices, isTypeFlagSet } from '../util'; | ||
import { getESLintCoreRule } from '../util/getESLintCoreRule'; | ||
|
||
const baseRule = getESLintCoreRule('consistent-return'); | ||
|
||
type Options = InferOptionsTypeFromRule<typeof baseRule>; | ||
type MessageIds = InferMessageIdsTypeFromRule<typeof baseRule>; | ||
|
||
type FunctionNode = | ||
| TSESTree.FunctionDeclaration | ||
| TSESTree.FunctionExpression | ||
| TSESTree.ArrowFunctionExpression; | ||
|
||
export default createRule<Options, MessageIds>({ | ||
name: 'consistent-return', | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: | ||
'Require `return` statements to either always or never specify values', | ||
extendsBaseRule: true, | ||
requiresTypeChecking: true, | ||
}, | ||
hasSuggestions: baseRule.meta.hasSuggestions, | ||
schema: baseRule.meta.schema, | ||
messages: baseRule.meta.messages, | ||
}, | ||
defaultOptions: [{ treatUndefinedAsUnspecified: false }], | ||
create(context, [options]) { | ||
const services = getParserServices(context); | ||
const checker = services.program.getTypeChecker(); | ||
const rules = baseRule.create(context); | ||
const functions: FunctionNode[] = []; | ||
const treatUndefinedAsUnspecified = | ||
options?.treatUndefinedAsUnspecified === true; | ||
|
||
function enterFunction(node: FunctionNode): void { | ||
functions.push(node); | ||
} | ||
|
||
function exitFunction(): void { | ||
functions.pop(); | ||
} | ||
|
||
function getCurrentFunction(): FunctionNode | null { | ||
return functions[functions.length - 1] ?? null; | ||
} | ||
|
||
function isPromiseVoid(node: ts.Node, type: ts.Type): boolean { | ||
if ( | ||
tsutils.isThenableType(checker, node, type) && | ||
tsutils.isTypeReference(type) | ||
) { | ||
const awaitedType = type.typeArguments?.[0]; | ||
if (awaitedType) { | ||
if (isTypeFlagSet(awaitedType, ts.TypeFlags.Void)) { | ||
return true; | ||
} | ||
return isPromiseVoid(node, awaitedType); | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
function isReturnVoidOrThenableVoid(node: FunctionNode): boolean { | ||
const functionType = services.getTypeAtLocation(node); | ||
const tsNode = services.esTreeNodeToTSNodeMap.get(node); | ||
const callSignatures = functionType.getCallSignatures(); | ||
|
||
return callSignatures.some(signature => { | ||
const returnType = signature.getReturnType(); | ||
if (node.async) { | ||
return isPromiseVoid(tsNode, returnType); | ||
} | ||
return isTypeFlagSet(returnType, ts.TypeFlags.Void); | ||
}); | ||
} | ||
|
||
return { | ||
...rules, | ||
FunctionDeclaration: enterFunction, | ||
'FunctionDeclaration:exit'(node): void { | ||
exitFunction(); | ||
rules['FunctionDeclaration:exit'](node); | ||
}, | ||
FunctionExpression: enterFunction, | ||
'FunctionExpression:exit'(node): void { | ||
exitFunction(); | ||
rules['FunctionExpression:exit'](node); | ||
}, | ||
ArrowFunctionExpression: enterFunction, | ||
'ArrowFunctionExpression:exit'(node): void { | ||
exitFunction(); | ||
rules['ArrowFunctionExpression:exit'](node); | ||
}, | ||
ReturnStatement(node): void { | ||
const functionNode = getCurrentFunction(); | ||
if ( | ||
!node.argument && | ||
functionNode && | ||
isReturnVoidOrThenableVoid(functionNode) | ||
) { | ||
return; | ||
} | ||
if (treatUndefinedAsUnspecified && node.argument) { | ||
const returnValueType = services.getTypeAtLocation(node.argument); | ||
if (returnValueType.flags === ts.TypeFlags.Undefined) { | ||
rules.ReturnStatement({ | ||
...node, | ||
argument: null, | ||
}); | ||
return; | ||
} | ||
} | ||
|
||
rules.ReturnStatement(node); | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.