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

Commit

Permalink
Add use-default-type-parameter rule (#2253)
Browse files Browse the repository at this point in the history
  • Loading branch information
andy-hanson authored and adidahiya committed May 25, 2017
1 parent a70ad66 commit 3088a2d
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export const rules = {
"strict-type-predicates": true,
"switch-default": true,
"triple-equals": true,
"use-default-type-parameter": true,
"use-isnan": true,

// Maintainability
Expand Down
124 changes: 124 additions & 0 deletions src/rules/useDefaultTypeParameterRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* @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 { isClassLikeDeclaration, isInterfaceDeclaration, isTypeAliasDeclaration } from "tsutils";
import * as ts from "typescript";
import * as Lint from "../index";
import { find } from "../utils";

export class Rule extends Lint.Rules.TypedRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "use-default-type-parameter",
description: "Warns if an explicitly specified type argument is the default for that type parameter.",
optionsDescription: "Not configurable.",
options: null,
optionExamples: ["true"],
type: "functionality",
typescriptOnly: true,
requiresTypeInfo: true,
};
/* tslint:enable:object-literal-sort-keys */

public static FAILURE_STRING = "This is the default value for this type parameter, so it can be omitted.";

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

interface ArgsAndParams {
typeArguments: ts.TypeNode[];
typeParameters: ts.TypeParameterDeclaration[];
}

function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker): void {
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
const argsAndParams = getArgsAndParameters(node, checker);
if (argsAndParams !== undefined) {
checkArgsAndParameters(node, argsAndParams);
}
return ts.forEachChild(node, cb);
});

function checkArgsAndParameters(node: ts.Node, { typeArguments, typeParameters }: ArgsAndParams): void {
// Just check the last one. Must specify previous type parameters if the last one is specified.
const i = typeArguments.length - 1;
const arg = typeArguments[i];
const param = typeParameters[i];
// TODO: would like checker.areTypesEquivalent. https://github.com/Microsoft/TypeScript/issues/13502
if (param.default !== undefined && param.default.getText() === arg.getText()) {
ctx.addFailureAtNode(arg, Rule.FAILURE_STRING, createFix());
}

function createFix(): Lint.Fix {
if (i === 0) {
const lt = Lint.childOfKind(node, ts.SyntaxKind.LessThanToken)!;
const gt = Lint.childOfKind(node, ts.SyntaxKind.GreaterThanToken)!;
return Lint.Replacement.deleteFromTo(lt.getStart(), gt.getEnd());
} else {
return Lint.Replacement.deleteFromTo(typeArguments[i - 1].getEnd(), arg.getEnd());
}
}
}
}

function getArgsAndParameters(node: ts.Node, checker: ts.TypeChecker): ArgsAndParams | undefined {
switch (node.kind) {
case ts.SyntaxKind.CallExpression:
case ts.SyntaxKind.NewExpression:
case ts.SyntaxKind.TypeReference:
case ts.SyntaxKind.ExpressionWithTypeArguments:
const decl = node as ts.CallExpression | ts.NewExpression | ts.TypeReferenceNode | ts.ExpressionWithTypeArguments;
const { typeArguments } = decl;
if (typeArguments === undefined) {
return undefined;
}
const typeParameters = decl.kind === ts.SyntaxKind.TypeReference
? typeParamsFromType(decl.typeName, checker)
: decl.kind === ts.SyntaxKind.ExpressionWithTypeArguments
? typeParamsFromType(decl.expression, checker)
: typeParamsFromCall(node as ts.CallExpression | ts.NewExpression, checker);
return typeParameters === undefined ? undefined : { typeArguments, typeParameters };
default:
return undefined;
}
}

function typeParamsFromCall(node: ts.CallLikeExpression, checker: ts.TypeChecker): ts.TypeParameterDeclaration[] | undefined {
const sig = checker.getResolvedSignature(node);
const sigDecl = sig === undefined ? undefined : sig.getDeclaration();
if (sigDecl === undefined) {
return node.kind === ts.SyntaxKind.NewExpression ? typeParamsFromType(node.expression, checker) : undefined;
}

return sigDecl.typeParameters === undefined ? undefined : sigDecl.typeParameters;
}

function typeParamsFromType(type: ts.EntityName | ts.Expression, checker: ts.TypeChecker): ts.TypeParameterDeclaration[] | undefined {
const sym = getAliasedSymbol(checker.getSymbolAtLocation(type), checker);
if (sym === undefined || sym.declarations === undefined) {
return undefined;
}

return find(sym.declarations, (decl) =>
isClassLikeDeclaration(decl) || isTypeAliasDeclaration(decl) || isInterfaceDeclaration(decl) ? decl.typeParameters : undefined);
}

function getAliasedSymbol(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol {
return Lint.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
}
18 changes: 18 additions & 0 deletions test/rules/use-default-type-parameter/test.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

function f<T = number>() {}
f();
f<string>();

function g<T = number, U = string>() {}
g<string>();
g<number, number>(); // Must specify 1st type parameter if 2nd is different than default.


class C<T = number> {}
function h(c: C) {}
new C();
class D extends C {}

interface I<T = number> {}
class Impl implements I {}

26 changes: 26 additions & 0 deletions test/rules/use-default-type-parameter/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[typescript]: >= 2.3.0

function f<T = number>() {}
f<number>();
~~~~~~ [0]
f<string>();

function g<T = number, U = string>() {}
g<string, string>();
~~~~~~ [0]
g<number, number>(); // Must specify 1st type parameter if 2nd is different than default.


class C<T = number> {}
function h(c: C<number>) {}
~~~~~~ [0]
new C<number>();
~~~~~~ [0]
class D extends C<number> {}
~~~~~~ [0]

interface I<T = number> {}
class Impl implements I<number> {}
~~~~~~ [0]

[0]: This is the default value for this type parameter, so it can be omitted.
1 change: 1 addition & 0 deletions test/rules/use-default-type-parameter/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
5 changes: 5 additions & 0 deletions test/rules/use-default-type-parameter/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"use-default-type-parameter": true
}
}

0 comments on commit 3088a2d

Please sign in to comment.