Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Add return-undefined rule #2251

Merged
merged 3 commits into from
Mar 20, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/language/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ export function someAncestor(node: ts.Node, predicate: (n: ts.Node) => boolean):
return predicate(node) || (node.parent != null && someAncestor(node.parent, predicate));
}

export function ancestorWhere<T extends ts.Node>(node: ts.Node, predicate: (n: ts.Node) => boolean): ts.Node | undefined {
let cur: ts.Node | undefined = node;
do {
if (predicate(cur)) {
return cur as T;
}
cur = cur.parent;
} while (cur);
return undefined;
}

export function isAssignment(node: ts.Node) {
if (node.kind === ts.SyntaxKind.BinaryExpression) {
const binaryExpression = node as ts.BinaryExpression;
Expand Down
135 changes: 135 additions & 0 deletions src/rules/returnUndefinedRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* @license
* Copyright 2017 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as u from "tsutils";
import * as ts from "typescript";

import * as Lint from "../index";

export class Rule extends Lint.Rules.TypedRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "return-undefined",
description: "Prefer `return;` in void functions and `return undefined;` in value-returning functions.",
optionsDescription: "Not configurable.",
options: null,
optionExamples: ["true"],
type: "style",
typescriptOnly: true,
requiresTypeInfo: true,
};
/* tslint:enable:object-literal-sort-keys */

public static FAILURE_STRING_VALUE_RETURN =
"Value-returning function should use `return undefined;`, not just `return;`.";
public static FAILURE_STRING_VOID_RETURN =
"`void` function should use `return;`, not `return undefined;`.";

public applyWithProgram(sourceFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, (ctx) => walk(ctx, langSvc.getProgram().getTypeChecker()));
}
}

function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker) {
return ts.forEachChild(ctx.sourceFile, cb);
function cb(node: ts.Node): void {
check(node);
return ts.forEachChild(node, cb);
}

function check(node: ts.Node): void {
if (!u.isReturnStatement(node)) {
return;
}

const returnKindFromExpression = !node.expression
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewrite as if statements

? ReturnKind.Void
: u.isIdentifier(node.expression) && node.expression.text === "undefined"
? ReturnKind.Value
: undefined;

if (returnKindFromExpression === undefined) {
return;
}

const functionReturningFrom = Lint.ancestorWhere(node, isFunctionLike)! as FunctionLike;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should handle undefined return value properly. This may crash TSLint later when linting invalid code.

const returnKindFromType = getReturnKind(functionReturningFrom, checker);
if (returnKindFromType !== undefined && returnKindFromType !== returnKindFromExpression) {
ctx.addFailureAtNode(node,
returnKindFromType === ReturnKind.Void ? Rule.FAILURE_STRING_VOID_RETURN : Rule.FAILURE_STRING_VALUE_RETURN);
}
}
}

enum ReturnKind {
Void,
Value,
}

type FunctionLike =
| ts.FunctionDeclaration
| ts.FunctionExpression
| ts.ArrowFunction
| ts.MethodDeclaration
| ts.ConstructorDeclaration
| ts.GetAccessorDeclaration
| ts.SetAccessorDeclaration;

function getReturnKind(node: FunctionLike, checker: ts.TypeChecker): ReturnKind | undefined {
switch (node.kind) {
case ts.SyntaxKind.Constructor:
case ts.SyntaxKind.SetAccessor:
return ReturnKind.Void;
case ts.SyntaxKind.GetAccessor:
return ReturnKind.Value;
default:
}

// Go with an explicit type declaration if possible.
if (node.type) {
return node.type.kind === ts.SyntaxKind.VoidKeyword ? ReturnKind.Void : ReturnKind.Value;
}

const contextualType = node.kind === ts.SyntaxKind.FunctionExpression || node.kind === ts.SyntaxKind.ArrowFunction
? checker.getContextualType(node)
: undefined;

const ty = contextualType || checker.getTypeAtLocation(node);
if (!ty) {
// Type error somewhere. Bail.
return undefined;
}

const sig = checker.getSignaturesOfType(ty, ts.SignatureKind.Call)[0];
const returnType = checker.getReturnTypeOfSignature(sig);
return Lint.isTypeFlagSet(returnType, ts.TypeFlags.Void) ? ReturnKind.Void : ReturnKind.Value;
}

function isFunctionLike(node: ts.Node): node is FunctionLike {
switch (node.kind) {
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.FunctionExpression:
case ts.SyntaxKind.ArrowFunction:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.Constructor:
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.SetAccessor:
return true;
default:
return false;
}
}
28 changes: 28 additions & 0 deletions test/rules/return-undefined/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
function valueWrong(): number | undefined {
return;
~~~~~~~ [UNDEFINED]
}
function valueRight(): number | undefined {
return undefined;
}

function voidWrong(): void {
return undefined;
~~~~~~~~~~~~~~~~~ [VOID]
}
function voidRight(): void {
return;
}

// Infers type from context.
[].forEach(() => {
return undefined;
~~~~~~~~~~~~~~~~~ [VOID]
});
[].map<number | undefined>(() => {
return;
~~~~~~~ [UNDEFINED]
});

[VOID]: `void` function should use `return;`, not `return undefined;`.
[UNDEFINED]: Value-returning function should use `return undefined;`, not just `return;`.
8 changes: 8 additions & 0 deletions test/rules/return-undefined/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"linterOptions": {
"typeCheck": true
},
"rules": {
"return-undefined": true
}
}