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

Add callable-types rule #1891

Merged
merged 1 commit into from Dec 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 12 additions & 3 deletions docs/_data/rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
]
},
"optionExamples": [
"[true, array]",
"[true, generic]",
"[true, array-simple]"
"[true, \"array\"]",
"[true, \"generic\"]",
"[true, \"array-simple\"]"
],
"type": "style",
"typescriptOnly": true
Expand Down Expand Up @@ -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.",
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/array-type/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@
"array-simple"
]
}
---
---
12 changes: 12 additions & 0 deletions docs/rules/callable-types/index.html
Original file line number Diff line number Diff line change
@@ -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'
---
104 changes: 104 additions & 0 deletions src/rules/callableTypesRule.ts
Original file line number Diff line number Diff line change
@@ -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<ts.HeritageClause> | 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;
}
25 changes: 25 additions & 0 deletions test/rules/callable-types/test.ts.lint
Original file line number Diff line number Diff line change
@@ -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): T;
~~~~~~~~~~~~~ [Type literal has only a call signature — use `<T>(t: T) => T` instead.]
}

// Overloads OK
interface K {
(x: number): number;
(x: string): string;
}
5 changes: 5 additions & 0 deletions test/rules/callable-types/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"callable-types": true
}
}