diff --git a/docs/_data/rules.json b/docs/_data/rules.json index c5b67f588ce..3e152844946 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -48,9 +48,9 @@ ] }, "optionExamples": [ - "[true, array]", - "[true, generic]", - "[true, array-simple]" + "[true, \"array\"]", + "[true, \"generic\"]", + "[true, \"array-simple\"]" ], "type": "style", "typescriptOnly": true @@ -88,6 +88,15 @@ "type": "functionality", "typescriptOnly": false }, + { + "ruleName": "callable-types", + "description": "An interface or literal type with just a call signature can be written as a function type.", + "rationale": "style", + "optionsDescription": "Not configurable.", + "options": null, + "type": "style", + "typescriptOnly": true + }, { "ruleName": "class-name", "description": "Enforces PascalCased class and interface names.", diff --git a/docs/rules/array-type/index.html b/docs/rules/array-type/index.html index 4595a595fd1..cd36ec3c628 100644 --- a/docs/rules/array-type/index.html +++ b/docs/rules/array-type/index.html @@ -31,4 +31,4 @@ "array-simple" ] } ---- +--- \ No newline at end of file diff --git a/docs/rules/callable-types/index.html b/docs/rules/callable-types/index.html new file mode 100644 index 00000000000..4c65f626683 --- /dev/null +++ b/docs/rules/callable-types/index.html @@ -0,0 +1,12 @@ +--- +ruleName: callable-types +description: An interface or literal type with just a call signature can be written as a function type. +rationale: style +optionsDescription: Not configurable. +options: null +type: style +typescriptOnly: true +layout: rule +title: 'Rule: callable-types' +optionsJSON: 'null' +--- \ No newline at end of file diff --git a/src/rules/callableTypesRule.ts b/src/rules/callableTypesRule.ts new file mode 100644 index 00000000000..06243bf1623 --- /dev/null +++ b/src/rules/callableTypesRule.ts @@ -0,0 +1,104 @@ +/** + * @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 "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "callable-types", + description: "An interface or literal type with just a call signature can be written as a function type.", + rationale: "style", + optionsDescription: "Not configurable.", + options: null, + type: "style", + typescriptOnly: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static failureStringForInterface(name: string, sigSuggestion: string): string { + return `Interface has only a call signature — use \`type ${name} = ${sigSuggestion}\` instead.`; + } + + public static failureStringForTypeLiteral(sigSuggestion: string): string { + return `Type literal has only a call signature — use \`${sigSuggestion}\` instead.`; + } + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + public visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { + if (noSupertype(node.heritageClauses)) { + this.check(node); + } + super.visitInterfaceDeclaration(node); + } + + public visitTypeLiteral(node: ts.TypeLiteralNode) { + this.check(node); + super.visitTypeLiteral(node); + } + + private check(node: ts.InterfaceDeclaration | ts.TypeLiteralNode) { + if (node.members.length === 1 && node.members[0].kind === ts.SyntaxKind.CallSignature) { + const call = node.members[0] as ts.CallSignatureDeclaration; + if (!call.type) { + // Bad parse + return; + } + + const suggestion = renderSuggestion(call); + if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { + this.addFailureAtNode(node.name, Rule.failureStringForInterface(node.name.getText(), suggestion)); + } else { + this.addFailureAtNode(call, Rule.failureStringForTypeLiteral(suggestion)); + } + } + } +} + +/** True if there is no supertype or if the supertype is `Function`. */ +function noSupertype(heritageClauses: ts.NodeArray | undefined): boolean { + if (!heritageClauses) { + return true; + } + + if (heritageClauses.length === 1) { + const expr = heritageClauses[0].types![0].expression; + if (expr.kind === ts.SyntaxKind.Identifier && (expr as ts.Identifier).text === "Function") { + return true; + } + } + + return false; +} + +function renderSuggestion(call: ts.CallSignatureDeclaration): string { + const typeParameters = call.typeParameters && call.typeParameters.map((p) => p.getText()).join(", "); + const parameters = call.parameters.map((p) => p.getText()).join(", "); + const returnType = call.type.getText(); + let res = `(${parameters}) => ${returnType}`; + if (typeParameters) { + res = `<${typeParameters}>${res}`; + } + return res; +} diff --git a/test/rules/callable-types/test.ts.lint b/test/rules/callable-types/test.ts.lint new file mode 100644 index 00000000000..ef3ee6525e3 --- /dev/null +++ b/test/rules/callable-types/test.ts.lint @@ -0,0 +1,25 @@ +interface I { + ~ [Interface has only a call signature — use `type I = () => void` instead.] + (): void; +} + +interface J extends Function { + ~ [Interface has only a call signature — use `type J = () => void` instead.] + (): void; +} + +type T = { + (): void; + ~~~~~~~~~ [Type literal has only a call signature — use `() => void` instead.] +} + +type U = { + (t: T): T; + ~~~~~~~~~~~~~ [Type literal has only a call signature — use `(t: T) => T` instead.] +} + +// Overloads OK +interface K { + (x: number): number; + (x: string): string; +} diff --git a/test/rules/callable-types/tslint.json b/test/rules/callable-types/tslint.json new file mode 100644 index 00000000000..d24a06daec4 --- /dev/null +++ b/test/rules/callable-types/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "callable-types": true + } +}