Movatterモバイル変換


[0]ホーム

URL:


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

Sign up
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Show file tree
Hide file tree
Changes fromall 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 deletionspackages/eslint-plugin/README.md
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionpackages/eslint-plugin/ROADMAP.md
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand 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 deletionspackages/eslint-plugin/docs/rules/prefer-function-type.md
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionspackages/eslint-plugin/lib/rules/prefer-function-type.js
View file
Open in desktop
Original file line numberDiff line numberDiff 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

[8]ページ先頭

©2009-2025 Movatter.jp