diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 5030ca291751..daacab20afdb 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -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: | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index 86f0224f0af5..9d4c2ef5a9f1 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -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] | @@ -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 diff --git a/packages/eslint-plugin/docs/rules/prefer-function-type.md b/packages/eslint-plugin/docs/rules/prefer-function-type.md new file mode 100644 index 000000000000..7cbf2be0480d --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-function-type.md @@ -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/) diff --git a/packages/eslint-plugin/lib/rules/prefer-function-type.js b/packages/eslint-plugin/lib/rules/prefer-function-type.js new file mode 100644 index 000000000000..f591179932fb --- /dev/null +++ b/packages/eslint-plugin/lib/rules/prefer-function-type.js @@ -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); + } + }; + } +}; diff --git a/packages/eslint-plugin/tests/lib/rules/prefer-function-type.js b/packages/eslint-plugin/tests/lib/rules/prefer-function-type.js new file mode 100644 index 000000000000..d0587afcaf7c --- /dev/null +++ b/packages/eslint-plugin/tests/lib/rules/prefer-function-type.js @@ -0,0 +1,147 @@ +/** + * @fileoverview Use function types instead of interfaces with call signatures + * @author Benjamin Lichtman + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/prefer-function-type'), + RuleTester = require('eslint').RuleTester; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const parserOptions = { + ecmaVersion: 2015 +}; +var ruleTester = new RuleTester({ + parserOptions, + parser: '@typescript-eslint/parser' +}); +ruleTester.run('prefer-function-type', rule, { + valid: [ + ` +interface Foo { + (): void; + bar: number; +}`, + ` +type Foo = { + (): void; + bar: number; +}`, + ` +function foo(bar: { (): string, baz: number }): string { + return bar(); +}`, + ` +interface Foo { + bar: string; +} +interface Bar extends Foo { + (): void; +}`, + ` +interface Foo { + bar: string; +} +interface Bar extends Function, Foo { + (): void; +}` + ], + + invalid: [ + { + code: ` +interface Foo { + (): string; +}`, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: 'TSCallSignatureDeclaration' + } + ], + output: ` +type Foo = () => string;` + }, + { + code: ` +type Foo = { + (): string; +}`, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: 'TSCallSignatureDeclaration' + } + ], + output: ` +type Foo = () => string` + }, + { + code: ` +function foo(bar: { (s: string): number }): number { + return bar("hello"); +}`, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: 'TSCallSignatureDeclaration' + } + ], + output: ` +function foo(bar: (s: string) => number): number { + return bar("hello"); +}` + }, + { + code: ` +function foo(bar: { (s: string): number } | undefined): number { + return bar("hello"); +}`, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: 'TSCallSignatureDeclaration' + } + ], + output: ` +function foo(bar: ((s: string) => number) | undefined): number { + return bar("hello"); +}` + }, + { + code: ` +interface Foo extends Function { + (): void; +}`, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: 'TSCallSignatureDeclaration' + } + ], + output: ` +type Foo = () => void;` + }, + { + code: ` +interface Foo { + (bar: T): string; +}`, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: 'TSCallSignatureDeclaration' + } + ], + output: ` +type Foo = (bar: T) => string;` + } + ] +}); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy