-
-
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
- Loading branch information
Showing
9 changed files
with
396 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,33 @@ | ||
--- | ||
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 type. | ||
|
||
<!--tabs--> | ||
|
||
#### ❌ Incorrect | ||
|
||
```ts | ||
function foo(): undefined {} | ||
function bar(flag: boolean): undefined { | ||
if (flag) return foo(); | ||
return; | ||
} | ||
``` | ||
|
||
### ✅ Correct | ||
|
||
```ts | ||
/* eslint @typescript-eslint/consistent-return: "error" */ | ||
function foo(): void {} | ||
function bar(flag: boolean): void { | ||
if (flag) return foo(); | ||
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,95 @@ | ||
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: [], | ||
create(context) { | ||
const services = getParserServices(context); | ||
const checker = services.program.getTypeChecker(); | ||
const rules = baseRule.create(context); | ||
|
||
let functionNode: FunctionNode | null; | ||
|
||
function setFunctionNode(node: FunctionNode): void { | ||
functionNode = node; | ||
} | ||
|
||
function isReturnPromiseVoid( | ||
node: FunctionNode, | ||
signature: ts.Signature, | ||
): boolean { | ||
const tsNode = services.esTreeNodeToTSNodeMap.get(node); | ||
const returnType = signature.getReturnType(); | ||
if ( | ||
tsutils.isThenableType(checker, tsNode, returnType) && | ||
tsutils.isTypeReference(returnType) | ||
) { | ||
const typeArgs = returnType.typeArguments; | ||
const hasVoid = !!typeArgs?.some(typeArg => | ||
isTypeFlagSet(typeArg, ts.TypeFlags.Void), | ||
); | ||
return hasVoid; | ||
} | ||
return false; | ||
} | ||
|
||
function isReturnVoidOrThenableVoid(node: FunctionNode): boolean { | ||
const functionType = services.getTypeAtLocation(node); | ||
const callSignatures = functionType.getCallSignatures(); | ||
|
||
return callSignatures.some(signature => { | ||
if (node.async) { | ||
return isReturnPromiseVoid(node, signature); | ||
} | ||
const returnType = signature.getReturnType(); | ||
return isTypeFlagSet(returnType, ts.TypeFlags.Void); | ||
}); | ||
} | ||
|
||
return { | ||
...rules, | ||
FunctionDeclaration: setFunctionNode, | ||
FunctionExpression: setFunctionNode, | ||
ArrowFunctionExpression: setFunctionNode, | ||
ReturnStatement(node): void { | ||
if (!node.argument && functionNode) { | ||
if (isReturnVoidOrThenableVoid(functionNode)) { | ||
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
216 changes: 216 additions & 0 deletions
216
packages/eslint-plugin/tests/rules/consistent-return.test.ts
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,216 @@ | ||
import { RuleTester } from '@typescript-eslint/rule-tester'; | ||
import { AST_NODE_TYPES } from '@typescript-eslint/utils'; | ||
|
||
import rule from '../../src/rules/consistent-return'; | ||
import { getFixturesRootDir } from '../RuleTester'; | ||
|
||
const rootDir = getFixturesRootDir(); | ||
const ruleTester = new RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
parserOptions: { | ||
ecmaVersion: 2021, | ||
tsconfigRootDir: rootDir, | ||
project: './tsconfig.json', | ||
}, | ||
}); | ||
|
||
ruleTester.run('consistent-return', rule, { | ||
valid: [ | ||
// base rule | ||
` | ||
function foo() { | ||
return; | ||
} | ||
`, | ||
` | ||
class A { | ||
foo() { | ||
if (a) return true; | ||
return false; | ||
} | ||
} | ||
`, | ||
{ | ||
code: ` | ||
const foo = (flag: boolean) => { | ||
if (flag) return; | ||
else return undefined; | ||
}; | ||
`, | ||
options: [{ treatUndefinedAsUnspecified: true }], | ||
}, | ||
` | ||
function foo(flag: boolean): number { | ||
if (flag) { | ||
return 1; | ||
} else { | ||
return 2; | ||
} | ||
} | ||
`, | ||
// void | ||
` | ||
declare function bar(): void; | ||
function foo(flag: boolean): void { | ||
if (flag) { | ||
return bar(); | ||
} | ||
return; | ||
} | ||
`, | ||
` | ||
declare function bar(): void; | ||
const foo = (flag: boolean): void => { | ||
if (flag) { | ||
return; | ||
} | ||
return bar(); | ||
}; | ||
`, | ||
` | ||
function foo(): boolean; | ||
function foo(flag: boolean): void; | ||
function foo(flag?: boolean): boolean | void { | ||
if (flag) { | ||
return; | ||
} | ||
return true; | ||
} | ||
`, | ||
` | ||
declare function bar(): void; | ||
async function foo(flag?: boolean): Promise<void> { | ||
if (flag) { | ||
return bar(); | ||
} | ||
return; | ||
} | ||
`, | ||
` | ||
type PromiseVoidNumber = Promise<void | number>; | ||
declare function bar(): void; | ||
async function foo(flag?: boolean): PromiseVoidNumber { | ||
if (flag) { | ||
return bar(); | ||
} | ||
return; | ||
} | ||
`, | ||
` | ||
class Foo { | ||
baz(): void {} | ||
bar(flag: boolean): void { | ||
if (flag) return baz(); | ||
return; | ||
} | ||
} | ||
`, | ||
` | ||
class Foo { | ||
baz(): void {} | ||
async bar(flag: boolean): Promise<void> { | ||
if (flag) return baz(); | ||
return; | ||
} | ||
} | ||
`, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
function foo(flag: boolean): any { | ||
if (flag) return true; | ||
else return; | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'missingReturnValue', | ||
data: { name: "Function 'foo'" }, | ||
type: AST_NODE_TYPES.ReturnStatement, | ||
line: 4, | ||
column: 16, | ||
endLine: 4, | ||
endColumn: 23, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
function bar(): undefined {} | ||
function foo(flag: boolean): undefined { | ||
if (flag) return bar(); | ||
return; | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'missingReturnValue', | ||
data: { name: "Function 'foo'" }, | ||
type: AST_NODE_TYPES.ReturnStatement, | ||
line: 5, | ||
column: 11, | ||
endLine: 5, | ||
endColumn: 18, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
function foo(flag: boolean): Promise<void> { | ||
if (flag) return Promise.resolve(void 0); | ||
else return; | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'missingReturnValue', | ||
data: { name: "Function 'foo'" }, | ||
type: AST_NODE_TYPES.ReturnStatement, | ||
line: 4, | ||
column: 16, | ||
endLine: 4, | ||
endColumn: 23, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
async function foo(flag: boolean): Promise<string> { | ||
if (flag) return; | ||
else return 'value'; | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'unexpectedReturnValue', | ||
data: { name: "Async function 'foo'" }, | ||
type: AST_NODE_TYPES.ReturnStatement, | ||
line: 4, | ||
column: 16, | ||
endLine: 4, | ||
endColumn: 31, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
function foo(flag: boolean): Promise<string | undefined> { | ||
if (flag) return; | ||
else return 'value'; | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'unexpectedReturnValue', | ||
data: { name: "Function 'foo'" }, | ||
type: AST_NODE_TYPES.ReturnStatement, | ||
line: 4, | ||
column: 16, | ||
endLine: 4, | ||
endColumn: 31, | ||
}, | ||
], | ||
}, | ||
], | ||
}); |
Oops, something went wrong.