Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat(eslint-plugin): add prefer-function-type rule #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
1 change: 1 addition & 0 deletions 1 packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | |
| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | |
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements (`no-var-requires` from TSLint) | :heavy_check_mark: | |
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures (`callable-types` from TSLint) | | :wrench: |
| [`@typescript-eslint/prefer-interface`](./docs/rules/prefer-interface.md) | Prefer an interface declaration over a type literal (type T = { ... }) (`interface-over-type-literal` from TSLint) | :heavy_check_mark: | :wrench: |
| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules. (`no-internal-module` from TSLint) | :heavy_check_mark: | :wrench: |
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | :heavy_check_mark: | |
Expand Down
3 changes: 2 additions & 1 deletion 3 packages/eslint-plugin/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
| [`arrow-parens`] | 🌟 | [`arrow-parens`][arrow-parens] |
| [`arrow-return-shorthand`] | 🌟 | [`arrow-body-style`][arrow-body-style] |
| [`binary-expression-operand-order`] | 🌟 | [`yoda`][yoda] |
| [`callable-types`] | 🛑 | N/A |
| [`callable-types`] | | [`@typescript-eslint/prefer-function-type`] |
| [`class-name`] | ✅ | [`@typescript-eslint/class-name-casing`] |
| [`comment-format`] | 🌟 | [`capitalized-comments`][capitalized-comments] & [`spaced-comment`][spaced-comment] |
| [`completed-docs`] | 🔌 | [`eslint-plugin-jsdoc`][plugin:jsdoc] |
Expand Down Expand Up @@ -587,6 +587,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
[`@typescript-eslint/member-delimiter-style`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
[`@typescript-eslint/prefer-interface`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-interface.md
[`@typescript-eslint/no-array-constructor`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-array-constructor.md
[`@typescript-eslint/prefer-function-type`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-function-type.md
[`@typescript-eslint/no-for-in-array`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-for-in-array.md

<!-- eslint-plugin-import -->
Expand Down
57 changes: 57 additions & 0 deletions 57 packages/eslint-plugin/docs/rules/prefer-function-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Use function types instead of interfaces with call signatures (prefer-function-type)

## Rule Details

This rule suggests using a function type instead of an interface or object type literal with a single call signature.

Examples of **incorrect** code for this rule:

```ts
interface Foo {
(): string;
}
```

```ts
function foo(bar: { (): number }): number {
return bar();
}
```

```ts
interface Foo extends Function {
(): void;
}
```

Examples of **correct** code for this rule:

```ts
interface Foo {
(): void;
bar: number;
}
```

```ts
function foo(bar: { (): string; baz: number }): string {
return bar();
}
```

```ts
interface Foo {
bar: string;
}
interface Bar extends Foo {
(): void;
}
```

## When Not To Use It

If you specifically want to use an interface or type literal with a single call signature for stylistic reasons, you can disable this rule.

## Further Reading

- TSLint: [`callable-types`](https://palantir.github.io/tslint/rules/callable-types/)
171 changes: 171 additions & 0 deletions 171 packages/eslint-plugin/lib/rules/prefer-function-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* @fileoverview Use function types instead of interfaces with call signatures
* @author Benjamin Lichtman
*/
'use strict';
const util = require('../util');

/**
* @typedef {import("eslint").Rule.RuleModule} RuleModule
* @typedef {import("estree").Node} ESTreeNode
*/

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/**
* @type {RuleModule}
*/
module.exports = {
meta: {
docs: {
description:
'Use function types instead of interfaces with call signatures',
category: 'TypeScript',
recommended: false,
extraDescription: [util.tslintRule('prefer-function-type')],
url: util.metaDocsUrl('prefer-function-type')
},
fixable: 'code',
messages: {
functionTypeOverCallableType:
"{{ type }} has only a call signature - use '{{ sigSuggestion }}' instead."
},
schema: [],
type: 'suggestion'
},

create(context) {
const sourceCode = context.getSourceCode();

//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------

/**
* Checks if there is no supertype or if the supertype is 'Function'
* @param {ESTreeNode} node The node being checked
* @returns {boolean} Returns true iff there is no supertype or if the supertype is 'Function'
*/
function noSupertype(node) {
if (!node.extends || node.extends.length === 0) {
return true;
}
if (node.extends.length !== 1) {
return false;
}
const expr = node.extends[0].expression;

return expr.type === 'Identifier' && expr.name === 'Function';
}

/**
* @param {ESTreeNode} parent The parent of the call signature causing the diagnostic
* @returns {boolean} true iff the parent node needs to be wrapped for readability
*/
function shouldWrapSuggestion(parent) {
switch (parent.type) {
case 'TSUnionType':
case 'TSIntersectionType':
case 'TSArrayType':
return true;
default:
return false;
}
}

/**
* @param {ESTreeNode} call The call signature causing the diagnostic
* @param {ESTreeNode} parent The parent of the call
* @returns {string} The suggestion to report
*/
function renderSuggestion(call, parent) {
const start = call.range[0];
const colonPos = call.returnType.range[0] - start;
const text = sourceCode.getText().slice(start, call.range[1]);

let suggestion = `${text.slice(0, colonPos)} =>${text.slice(
colonPos + 1
)}`;

if (shouldWrapSuggestion(parent.parent)) {
suggestion = `(${suggestion})`;
}
if (parent.type === 'TSInterfaceDeclaration') {
if (typeof parent.typeParameters !== 'undefined') {
return `type ${sourceCode
.getText()
.slice(
parent.id.range[0],
parent.typeParameters.range[1]
)} = ${suggestion}`;
}
return `type ${parent.id.name} = ${suggestion}`;
}
return suggestion.endsWith(';') ? suggestion.slice(0, -1) : suggestion;
}

/**
* @param {ESTreeNode} member The TypeElement being checked
* @param {ESTreeNode} node The parent of member being checked
* @returns {void}
*/
function checkMember(member, node) {
if (
(member.type === 'TSCallSignatureDeclaration' ||
member.type === 'TSConstructSignatureDeclaration') &&
typeof member.returnType !== 'undefined'
) {
const suggestion = renderSuggestion(member, node);
const fixStart =
node.type === 'TSTypeLiteral'
? node.range[0]
: sourceCode
.getTokens(node)
.filter(
token =>
token.type === 'Keyword' && token.value === 'interface'
)[0].range[0];

context.report({
node: member,
messageId: 'functionTypeOverCallableType',
data: {
type: node.type === 'TSTypeLiteral' ? 'Type literal' : 'Interface',
sigSuggestion: suggestion
},
fix(fixer) {
return fixer.replaceTextRange(
[fixStart, node.range[1]],
suggestion
);
}
});
}
}

//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------

return {
/**
* @param {TSInterfaceDeclaration} node The node being checked
* @returns {void}
*/
TSInterfaceDeclaration(node) {
if (noSupertype(node) && node.body.body.length === 1) {
checkMember(node.body.body[0], node);
}
},
/**
* @param {TSTypeLiteral} node The node being checked
* @returns {void}
*/
'TSTypeLiteral[members.length = 1]'(node) {
checkMember(node.members[0], node);
}
};
}
};
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.