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): new rule method-signature-style (#1685)
- Loading branch information
Showing
7 changed files
with
337 additions
and
13 deletions.
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
83 changes: 83 additions & 0 deletions
83
packages/eslint-plugin/docs/rules/method-signature-style.md
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,83 @@ | ||
# Enforces using a particular method signature syntax. (`method-signature-style`) | ||
|
||
There are two ways to define an object/interface function property. | ||
|
||
```ts | ||
// method shorthand syntax | ||
interface T1 { | ||
func(arg: string): number; | ||
} | ||
|
||
// regular property with function type | ||
interface T2 { | ||
func: (arg: string) => number; | ||
} | ||
``` | ||
|
||
A good practice is to use the TypeScript's `strict` option (which implies `strictFunctionTypes`) which enables correct typechecking for function properties only (method signatures get old behavior). | ||
|
||
TypeScript FAQ: | ||
|
||
> A method and a function property of the same type behave differently. | ||
> Methods are always bivariant in their argument, while function properties are contravariant in their argument under `strictFunctionTypes`. | ||
See the reasoning behind that in the [TypeScript PR for the compiler option](https://github.com/microsoft/TypeScript/pull/18654). | ||
|
||
## Options | ||
|
||
This rule accepts one string option: | ||
|
||
- `"property"`: Enforce using property signature for functions. Use this to enforce maximum correctness together with TypeScript's strict mode. | ||
- `"method"`: Enforce using method signature for functions. Use this if you aren't using TypeScript's strict mode and prefer this style. | ||
|
||
The default is `"property"`. | ||
|
||
## Rule Details | ||
|
||
Examples of **incorrect** code with `property` option. | ||
|
||
```ts | ||
interface T1 { | ||
func(arg: string): number; | ||
} | ||
type T2 = { | ||
func(arg: boolean): void; | ||
}; | ||
``` | ||
|
||
Examples of **correct** code with `property` option. | ||
|
||
```ts | ||
interface T1 { | ||
func: (arg: string) => number; | ||
} | ||
type T2 = { | ||
func: (arg: boolean) => void; | ||
}; | ||
``` | ||
|
||
Examples of **incorrect** code with `method` option. | ||
|
||
```ts | ||
interface T1 { | ||
func: (arg: string) => number; | ||
} | ||
type T2 = { | ||
func: (arg: boolean) => void; | ||
}; | ||
``` | ||
|
||
Examples of **correct** code with `method` option. | ||
|
||
```ts | ||
interface T1 { | ||
func(arg: string): number; | ||
} | ||
type T2 = { | ||
func(arg: boolean): void; | ||
}; | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
If you don't want to enforce a particular style for object/interface function types, and/or if you don't use `strictFunctionTypes`, then you don't need this rule. |
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
124 changes: 124 additions & 0 deletions
124
packages/eslint-plugin/src/rules/method-signature-style.ts
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,124 @@ | ||
import { | ||
AST_NODE_TYPES, | ||
TSESTree, | ||
} from '@typescript-eslint/experimental-utils'; | ||
import * as util from '../util'; | ||
|
||
export type Options = ['property' | 'method']; | ||
|
||
export type MessageId = 'errorMethod' | 'errorProperty'; | ||
|
||
export default util.createRule<Options, MessageId>({ | ||
name: 'method-signature-style', | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Enforces using a particular method signature syntax.', | ||
category: 'Best Practices', | ||
recommended: false, | ||
}, | ||
fixable: 'code', | ||
messages: { | ||
errorMethod: | ||
'Shorthand method signature is forbidden. Use a function property instead.', | ||
errorProperty: | ||
'Function property signature is forbidden. Use a method shorthand instead.', | ||
}, | ||
schema: [ | ||
{ | ||
enum: ['property', 'method'], | ||
}, | ||
], | ||
}, | ||
defaultOptions: ['property'], | ||
|
||
create(context, [mode]) { | ||
const sourceCode = context.getSourceCode(); | ||
|
||
function getMethodKey( | ||
node: TSESTree.TSMethodSignature | TSESTree.TSPropertySignature, | ||
): string { | ||
let key = sourceCode.getText(node.key); | ||
if (node.computed) { | ||
key = `[${key}]`; | ||
} | ||
if (node.optional) { | ||
key = `${key}?`; | ||
} | ||
if (node.readonly) { | ||
key = `readonly ${key}`; | ||
} | ||
return key; | ||
} | ||
|
||
function getMethodParams( | ||
node: TSESTree.TSMethodSignature | TSESTree.TSFunctionType, | ||
): string { | ||
let params = '()'; | ||
if (node.params.length > 0) { | ||
params = sourceCode.text.substring( | ||
sourceCode.getTokenBefore(node.params[0])!.range[0], | ||
sourceCode.getTokenAfter(node.params[node.params.length - 1])! | ||
.range[1], | ||
); | ||
} | ||
if (node.typeParameters != null) { | ||
const typeParams = sourceCode.getText(node.typeParameters); | ||
params = `${typeParams}${params}`; | ||
} | ||
return params; | ||
} | ||
|
||
function getMethodReturnType( | ||
node: TSESTree.TSMethodSignature | TSESTree.TSFunctionType, | ||
): string { | ||
return sourceCode.getText(node.returnType!.typeAnnotation); | ||
} | ||
|
||
return { | ||
TSMethodSignature(methodNode): void { | ||
if (mode === 'method') { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node: methodNode, | ||
messageId: 'errorMethod', | ||
fix: fixer => { | ||
const key = getMethodKey(methodNode); | ||
const params = getMethodParams(methodNode); | ||
const returnType = getMethodReturnType(methodNode); | ||
return fixer.replaceText( | ||
methodNode, | ||
`${key}: ${params} => ${returnType}`, | ||
); | ||
}, | ||
}); | ||
}, | ||
TSPropertySignature(propertyNode): void { | ||
const typeNode = propertyNode.typeAnnotation?.typeAnnotation; | ||
if (typeNode?.type !== AST_NODE_TYPES.TSFunctionType) { | ||
return; | ||
} | ||
|
||
if (mode === 'property') { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node: propertyNode, | ||
messageId: 'errorProperty', | ||
fix: fixer => { | ||
const key = getMethodKey(propertyNode); | ||
const params = getMethodParams(typeNode); | ||
const returnType = getMethodReturnType(typeNode); | ||
return fixer.replaceText( | ||
propertyNode, | ||
`${key}${params}: ${returnType}`, | ||
); | ||
}, | ||
}); | ||
}, | ||
}; | ||
}, | ||
}); |
Oops, something went wrong.