-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): add prefer-function-type rule (#222)
feat(eslint-plugin): add prefer-function-type rule
- Loading branch information
1 parent
317405a
commit b95c4cf
Showing
5 changed files
with
378 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; | ||
} | ||
}; |
Oops, something went wrong.