diff --git a/README.md b/README.md index f59ef336363..cfc1f04d38b 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,7 @@ Core Rules Core rules are included in the `tslint` package. +* `adjacent-overload-signatures` enforces function overloads to be consecutive. * `align` enforces vertical alignment. Rule options: * `"parameters"` checks alignment of function parameters. * `"arguments"` checks alignment of function call arguments. diff --git a/src/rules/adjacentOverloadSignaturesRule.ts b/src/rules/adjacentOverloadSignaturesRule.ts new file mode 100644 index 00000000000..2c5b711d467 --- /dev/null +++ b/src/rules/adjacentOverloadSignaturesRule.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2013 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 ts from "typescript"; + +import * as Lint from "../lint"; + +export class Rule extends Lint.Rules.AbstractRule { + + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "adjacent-overload-signatures", + description: "Enforces function overloads to be consecutive.", + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "typescript", + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING_FACTORY = (name: string) => `All '${name}' signatures should be adjacent`; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new AdjacentOverloadSignaturesWalker(sourceFile, this.getOptions())); + } +} + +class AdjacentOverloadSignaturesWalker extends Lint.RuleWalker { + + public visitInterfaceDeclaration(node: ts.InterfaceDeclaration): void { + this.checkNode(node); + super.visitInterfaceDeclaration(node); + } + + public visitTypeLiteral(node: ts.TypeLiteralNode): void { + this.checkNode(node); + super.visitTypeLiteral(node); + } + + public checkNode(node: ts.TypeLiteralNode | ts.InterfaceDeclaration) { + let last: string = undefined; + const seen: { [name: string]: boolean } = {}; + for (const member of node.members) { + if (member.name !== undefined) { + const methodName = getTextOfPropertyName(member.name); + if (methodName !== undefined) { + if (seen[methodName] && last !== methodName) { + this.addFailure(this.createFailure(member.getStart(), member.getWidth(), + Rule.FAILURE_STRING_FACTORY(methodName))); + } + last = methodName; + seen[methodName] = true; + } + } else { + last = undefined; + } + } + } +} + +function isLiteralExpression(node: ts.Node): node is ts.LiteralExpression { + return node.kind === ts.SyntaxKind.StringLiteral || node.kind === ts.SyntaxKind.NumericLiteral; +} + +function getTextOfPropertyName(name: ts.PropertyName): string { + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).text; + case ts.SyntaxKind.ComputedPropertyName: + const { expression } = (name as ts.ComputedPropertyName); + if (isLiteralExpression(expression)) { + return expression.text; + } + default: + if (isLiteralExpression(name)) { + return name.text; + } + } +} diff --git a/src/tsconfig.json b/src/tsconfig.json index 92bbae51d9d..81acfdc5344 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -68,6 +68,7 @@ "language/walker/scopeAwareRuleWalker.ts", "language/walker/skippableTokenAwareRuleWalker.ts", "language/walker/syntaxWalker.ts", + "rules/adjacentOverloadSignaturesRule.ts", "rules/alignRule.ts", "rules/arrowParensRule.ts", "rules/banRule.ts", diff --git a/test/rules/adjacent-overload-signatures/test.ts.lint b/test/rules/adjacent-overload-signatures/test.ts.lint new file mode 100644 index 00000000000..7379405b1b8 --- /dev/null +++ b/test/rules/adjacent-overload-signatures/test.ts.lint @@ -0,0 +1,80 @@ +// good + +interface i1 { + a(); + a(x: number); + b(); + b(x: string); +} + +interface i2 { + a(); + a(x: number); + a(): void; + b(); + b(x: string); +} + +interface i3 { + a(); + "a"(); +} + +interface i4 { + a(); + ["a"](); +} + +interface i5 { + a(): string; + bar: { + a(): number; + } +} + + +// bad + +interface b1 { + a(); + a(x: number); + b(); + b(x: string); + a(x: string); + ~~~~~~~~~~~~~ [All 'a' signatures should be adjacent] +} + +interface b2 { + a(); + a(x: number); + b(); + b(x: string); + a(): void; + ~~~~~~~~~~ [All 'a' signatures should be adjacent] +} + +interface b3 { + a(); + 12(); + "a"(); + ~~~~~~ [All 'a' signatures should be adjacent] + 12(); + ~~~~~ [All '12' signatures should be adjacent] +} + +interface b4 { + a(); + b(): void; + ["a"](v: number): void; + ~~~~~~~~~~~~~~~~~~~~~~~ [All 'a' signatures should be adjacent] +} + +interface b5 { + a(): string; + bar: { + a(): number; + b(); + a(b: number): void; + ~~~~~~~~~~~~~~~~~~~ [All 'a' signatures should be adjacent] + } +} diff --git a/test/rules/adjacent-overload-signatures/tslint.json b/test/rules/adjacent-overload-signatures/tslint.json new file mode 100644 index 00000000000..69957950dd9 --- /dev/null +++ b/test/rules/adjacent-overload-signatures/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "adjacent-overload-signatures": true + } +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 861ab1f57b1..1ba03f0e125 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -64,6 +64,7 @@ "../src/lint.ts", "../src/ruleLoader.ts", "../src/rules.ts", + "../src/rules/adjacentOverloadSignaturesRule.ts", "../src/rules/alignRule.ts", "../src/rules/arrowParensRule.ts", "../src/rules/banRule.ts",