Skip to content

Commit

Permalink
feat(eslint-plugin): [consistent-return] add new rule
Browse files Browse the repository at this point in the history
  • Loading branch information
yeonjuan committed Jan 23, 2024
1 parent 920f909 commit 957461e
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 0 deletions.
33 changes: 33 additions & 0 deletions packages/eslint-plugin/docs/rules/consistent-return.md
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;
}
```
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export = {
'@typescript-eslint/class-methods-use-this': 'error',
'@typescript-eslint/consistent-generic-constructors': 'error',
'@typescript-eslint/consistent-indexed-object-style': 'error',
'consistent-return': 'off',
'@typescript-eslint/consistent-return': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/consistent-type-exports': 'error',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/disable-type-checked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export = {
parserOptions: { project: null, program: null },
rules: {
'@typescript-eslint/await-thenable': 'off',
'@typescript-eslint/consistent-return': 'off',
'@typescript-eslint/consistent-type-exports': 'off',
'@typescript-eslint/dot-notation': 'off',
'@typescript-eslint/naming-convention': 'off',
Expand Down
95 changes: 95 additions & 0 deletions packages/eslint-plugin/src/rules/consistent-return.ts
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);
},
};
},
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import commaDangle from './comma-dangle';
import commaSpacing from './comma-spacing';
import consistentGenericConstructors from './consistent-generic-constructors';
import consistentIndexedObjectStyle from './consistent-indexed-object-style';
import consistentReturn from './consistent-return';
import consistentTypeAssertions from './consistent-type-assertions';
import consistentTypeDefinitions from './consistent-type-definitions';
import consistentTypeExports from './consistent-type-exports';
Expand Down Expand Up @@ -153,6 +154,7 @@ export default {
'comma-spacing': commaSpacing,
'consistent-generic-constructors': consistentGenericConstructors,
'consistent-indexed-object-style': consistentIndexedObjectStyle,
'consistent-return': consistentReturn,
'consistent-type-assertions': consistentTypeAssertions,
'consistent-type-definitions': consistentTypeDefinitions,
'consistent-type-exports': consistentTypeExports,
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/util/getESLintCoreRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const isESLintV8 = semver.major(version) >= 8;

interface RuleMap {
/* eslint-disable @typescript-eslint/consistent-type-imports -- more concise to use inline imports */
'consistent-return': typeof import('eslint/lib/rules/consistent-return');
'arrow-parens': typeof import('eslint/lib/rules/arrow-parens');
'block-spacing': typeof import('eslint/lib/rules/block-spacing');
'brace-style': typeof import('eslint/lib/rules/brace-style');
Expand Down
216 changes: 216 additions & 0 deletions packages/eslint-plugin/tests/rules/consistent-return.test.ts
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,
},
],
},
],
});

0 comments on commit 957461e

Please sign in to comment.