diff --git a/packages/eslint-plugin/src/rules/method-signature-style.ts b/packages/eslint-plugin/src/rules/method-signature-style.ts index c4e76b0593a4..8480ecb98eba 100644 --- a/packages/eslint-plugin/src/rules/method-signature-style.ts +++ b/packages/eslint-plugin/src/rules/method-signature-style.ts @@ -4,6 +4,7 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, + forEachChildESTree, isClosingParenToken, isCommaToken, isOpeningParenToken, @@ -131,6 +132,7 @@ export default createRule({ return; } + const skipFix = returnTypeReferencesThisType(methodNode.returnType); const parent = methodNode.parent; const members = parent.type === AST_NODE_TYPES.TSInterfaceBody @@ -156,39 +158,41 @@ export default createRule({ context.report({ node: methodNode, messageId: 'errorMethod', - *fix(fixer) { - const methodNodes = [ - methodNode, - ...duplicatedKeyMethodNodes, - ].sort((a, b) => (a.range[0] < b.range[0] ? -1 : 1)); - const typeString = methodNodes - .map(node => { - const params = getMethodParams(node); - const returnType = getMethodReturnType(node); - return `(${params} => ${returnType})`; - }) - .join(' & '); - const key = getMethodKey(methodNode); - const delimiter = getDelimiter(methodNode); - yield fixer.replaceText( - methodNode, - `${key}: ${typeString}${delimiter}`, - ); - for (const node of duplicatedKeyMethodNodes) { - const lastToken = context.sourceCode.getLastToken(node); - if (lastToken) { - const nextToken = - context.sourceCode.getTokenAfter(lastToken); - if (nextToken) { - yield fixer.remove(node); - yield fixer.replaceTextRange( - [lastToken.range[1], nextToken.range[0]], - '', - ); + fix: skipFix + ? undefined + : function* fix(fixer) { + const methodNodes = [ + methodNode, + ...duplicatedKeyMethodNodes, + ].sort((a, b) => (a.range[0] < b.range[0] ? -1 : 1)); + const typeString = methodNodes + .map(node => { + const params = getMethodParams(node); + const returnType = getMethodReturnType(node); + return `(${params} => ${returnType})`; + }) + .join(' & '); + const key = getMethodKey(methodNode); + const delimiter = getDelimiter(methodNode); + yield fixer.replaceText( + methodNode, + `${key}: ${typeString}${delimiter}`, + ); + for (const node of duplicatedKeyMethodNodes) { + const lastToken = context.sourceCode.getLastToken(node); + if (lastToken) { + const nextToken = + context.sourceCode.getTokenAfter(lastToken); + if (nextToken) { + yield fixer.remove(node); + yield fixer.replaceTextRange( + [lastToken.range[1], nextToken.range[0]], + '', + ); + } + } } - } - } - }, + }, }); } return; @@ -203,16 +207,18 @@ export default createRule({ context.report({ node: methodNode, messageId: 'errorMethod', - fix: fixer => { - const key = getMethodKey(methodNode); - const params = getMethodParams(methodNode); - const returnType = getMethodReturnType(methodNode); - const delimiter = getDelimiter(methodNode); - return fixer.replaceText( - methodNode, - `${key}: ${params} => ${returnType}${delimiter}`, - ); - }, + fix: skipFix + ? undefined + : fixer => { + const key = getMethodKey(methodNode); + const params = getMethodParams(methodNode); + const returnType = getMethodReturnType(methodNode); + const delimiter = getDelimiter(methodNode); + return fixer.replaceText( + methodNode, + `${key}: ${params} => ${returnType}${delimiter}`, + ); + }, }); } }, @@ -243,3 +249,15 @@ export default createRule({ }; }, }); + +function returnTypeReferencesThisType( + node: TSESTree.TSTypeAnnotation | undefined, +) { + return ( + node && + forEachChildESTree( + node.typeAnnotation, + child => child.type === AST_NODE_TYPES.TSThisType, + ) + ); +} diff --git a/packages/eslint-plugin/src/util/astUtils.ts b/packages/eslint-plugin/src/util/astUtils.ts index 1d8500052634..10c9b75e9947 100644 --- a/packages/eslint-plugin/src/util/astUtils.ts +++ b/packages/eslint-plugin/src/util/astUtils.ts @@ -1,5 +1,6 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { visitorKeys } from '@typescript-eslint/visitor-keys'; import * as ts from 'typescript'; import { escapeRegExp } from './escapeRegExp'; @@ -79,3 +80,58 @@ export function forEachReturnStatement( return undefined; } } + +function isESTreeNodeLike(node: unknown): node is TSESTree.Node { + return ( + typeof node === 'object' && + node != null && + 'type' in node && + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + typeof (node as any).type === 'string' + ); +} + +/** + * Rough equivalent to ts.forEachChild for ESTree nodes. + * It returns the first truthy value returned by the callback, if any. + */ +export function forEachChildESTree( + node: TSESTree.Node, + callback: (child: TSESTree.Node) => false | Result | null | undefined, +): Result | undefined { + function visit(currentNode: TSESTree.Node): Result | undefined { + const result = callback(currentNode); + if (result) { + return result; + } + + const currentKeys = visitorKeys[currentNode.type]; + if (!currentKeys) { + return undefined; + } + + for (const key of currentKeys) { + const currentProperty = currentNode[key as keyof typeof currentNode]; + + if (Array.isArray(currentProperty)) { + for (const child of currentProperty) { + if (isESTreeNodeLike(child)) { + const result = visit(child); + if (result) { + return result; + } + } + } + } else if (isESTreeNodeLike(currentProperty)) { + const result = visit(currentProperty); + if (result) { + return result; + } + } + } + + return undefined; + } + + return visit(node); +} diff --git a/packages/eslint-plugin/tests/rules/method-signature-style.test.ts b/packages/eslint-plugin/tests/rules/method-signature-style.test.ts index 9a7ab2b345c2..a747c6f8aa99 100644 --- a/packages/eslint-plugin/tests/rules/method-signature-style.test.ts +++ b/packages/eslint-plugin/tests/rules/method-signature-style.test.ts @@ -670,5 +670,80 @@ interface MyInterface { } `, }, + { + code: ` +interface Test { + f(value: number): this; +} + `, + errors: [ + { + line: 3, + messageId: 'errorMethod', + }, + ], + output: null, + }, + { + code: ` +interface Test { + foo(): this; + foo(): Promise; +} + `, + errors: [ + { + line: 3, + messageId: 'errorMethod', + }, + { + line: 4, + messageId: 'errorMethod', + }, + ], + output: null, + }, + { + code: ` +interface Test { + f(value: number): this | undefined; +} + `, + errors: [ + { + line: 3, + messageId: 'errorMethod', + }, + ], + output: null, + }, + { + code: ` +interface Test { + f(value: number): Promise; +} + `, + errors: [ + { + line: 3, + messageId: 'errorMethod', + }, + ], + output: null, + }, + { + code: ` +interface Test { + f(value: number): Promise; +} + `, + errors: [ + { + line: 3, + messageId: 'errorMethod', + }, + ], + output: null, + }, ], });