diff --git a/packages/eslint-plugin/src/rules/prefer-function-type.ts b/packages/eslint-plugin/src/rules/prefer-function-type.ts index dae4fae32956..9aa95d9597e8 100644 --- a/packages/eslint-plugin/src/rules/prefer-function-type.ts +++ b/packages/eslint-plugin/src/rules/prefer-function-type.ts @@ -5,6 +5,13 @@ import { } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; +const possibleReturnThisTypeHolders = new Set([ + AST_NODE_TYPES.TSTypeReference, + AST_NODE_TYPES.TSThisType, + AST_NODE_TYPES.TSFunctionType, + AST_NODE_TYPES.TSTypeAnnotation, +]); + export default util.createRule({ name: 'prefer-function-type', meta: { @@ -26,6 +33,35 @@ export default util.createRule({ create(context) { const sourceCode = context.getSourceCode(); + /** + * Get the range of the `this` which is being used as a type annotation else returns null + * @param node The node being checked + * @returns {Array | null} the range or null if no `this` type annotation are found + */ + function getReturnType( + node: TSESTree.TSTypeAnnotation, + ): Array | null { + if (!possibleReturnThisTypeHolders.has(node.type)) { + return null; + } + + if (node.type === AST_NODE_TYPES.TSThisType) { + return node.range; + } + + if (node.type === AST_NODE_TYPES.TSTypeAnnotation) { + if (node.typeAnnotation.type === AST_NODE_TYPES.TSThisType) { + return node.typeAnnotation.range; + } + } + + if (node.type === AST_NODE_TYPES.TSFunctionType) { + return getReturnType(node?.returnType); + } + + return null; + } + /** * Checks if there the interface has exactly one supertype that isn't named 'Function' * @param node The node being checked @@ -76,11 +112,32 @@ export default util.createRule({ const start = call.range[0]; const colonPos = call.returnType!.range[0] - start; const text = sourceCode.getText().slice(start, call.range[1]); + const returnType = getReturnType(call.returnType?.typeAnnotation); + let lhs = text.slice(0, colonPos); + let rhs = text.slice(colonPos + 1); - let suggestion = `${text.slice(0, colonPos)} =>${text.slice( - colonPos + 1, - )}`; + if (returnType !== null && returnType.length === 2) { + if ( + sourceCode.getText().slice(returnType[0], returnType[1]) === 'this' + ) { + // safe to change + rhs = rhs.replace('this', parent.id.name); + } + } + + if (call.params.length > 0) { + call.params.forEach(param => { + if ( + param.typeAnnotation?.typeAnnotation?.type === + AST_NODE_TYPES.TSThisType + ) { + // replacing each first occurance of `this` + lhs = lhs.replace('this', parent.id.name); + } + }); + } + let suggestion = `${lhs} =>${rhs}`; if (shouldWrapSuggestion(parent.parent)) { suggestion = `(${suggestion})`; } diff --git a/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts b/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts index b4c6b4a3de85..20d4b9a8a595 100644 --- a/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts @@ -38,6 +38,7 @@ interface Foo { interface Bar extends Function, Foo { (): void; }`, + 'interface Interface {(arg: this): this, [key : number] : string}', ], invalid: [ @@ -129,5 +130,65 @@ interface Foo { output: ` type Foo = (bar: T) => string;`, }, + { + code: 'interface Foo { (): () => this; }', + errors: [ + { + messageId: 'functionTypeOverCallableType', + tyoe: AST_NODE_TYPES.TSCallSignatureDeclaration, + }, + ], + output: `type Foo = () => () => Foo;`, + }, + { + code: 'interface Foo { (): () => string; }', + errors: [ + { + messageId: 'functionTypeOverCallableType', + tyoe: AST_NODE_TYPES.TSCallSignatureDeclaration, + }, + ], + output: `type Foo = () => () => string;`, + }, + { + code: 'interface Interface {(): this}', + errors: [ + { + messageId: 'functionTypeOverCallableType', + tyoe: AST_NODE_TYPES.TSCallSignatureDeclaration, + }, + ], + output: `type Interface = () => Interface`, + }, + { + code: 'interface Interface {(): Object}', + errors: [ + { + messageId: 'functionTypeOverCallableType', + tyoe: AST_NODE_TYPES.TSCallSignatureDeclaration, + }, + ], + output: `type Foo = () => () => this;`, + }, + { + code: 'interface Interface {() : string}', + errors: [ + { + messageId: 'functionTypeOverCallableType', + tyoe: AST_NODE_TYPES.TSCallSignatureDeclaration, + }, + ], + output: `type Interface = () => string`, + }, + { + code: 'interface Interface {(arg: this): this}', + errors: [ + { + messageId: 'functionTypeOverCallableType', + tyoe: AST_NODE_TYPES.TSCallSignatureDeclaration, + }, + ], + output: `type Interface = (arg: Interface) => Interface`, + }, ], });