diff --git a/src/configs/all.ts b/src/configs/all.ts index c32ce31fc70..c5f95af52ee 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -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 diff --git a/src/rules/useDefaultTypeParameterRule.ts b/src/rules/useDefaultTypeParameterRule.ts new file mode 100644 index 00000000000..d5e0455582f --- /dev/null +++ b/src/rules/useDefaultTypeParameterRule.ts @@ -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, 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; +} diff --git a/test/rules/use-default-type-parameter/test.ts.fix b/test/rules/use-default-type-parameter/test.ts.fix new file mode 100644 index 00000000000..8930a90507a --- /dev/null +++ b/test/rules/use-default-type-parameter/test.ts.fix @@ -0,0 +1,18 @@ + +function f() {} +f(); +f(); + +function g() {} +g(); +g(); // Must specify 1st type parameter if 2nd is different than default. + + +class C {} +function h(c: C) {} +new C(); +class D extends C {} + +interface I {} +class Impl implements I {} + diff --git a/test/rules/use-default-type-parameter/test.ts.lint b/test/rules/use-default-type-parameter/test.ts.lint new file mode 100644 index 00000000000..516caf71c5b --- /dev/null +++ b/test/rules/use-default-type-parameter/test.ts.lint @@ -0,0 +1,26 @@ +[typescript]: >= 2.3.0 + +function f() {} +f(); + ~~~~~~ [0] +f(); + +function g() {} +g(); + ~~~~~~ [0] +g(); // Must specify 1st type parameter if 2nd is different than default. + + +class C {} +function h(c: C) {} + ~~~~~~ [0] +new C(); + ~~~~~~ [0] +class D extends C {} + ~~~~~~ [0] + +interface I {} +class Impl implements I {} + ~~~~~~ [0] + +[0]: This is the default value for this type parameter, so it can be omitted. diff --git a/test/rules/use-default-type-parameter/tsconfig.json b/test/rules/use-default-type-parameter/tsconfig.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/test/rules/use-default-type-parameter/tsconfig.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/rules/use-default-type-parameter/tslint.json b/test/rules/use-default-type-parameter/tslint.json new file mode 100644 index 00000000000..d5a2fb118d1 --- /dev/null +++ b/test/rules/use-default-type-parameter/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "use-default-type-parameter": true + } +}