From 639df14025f0ea50dd48d906b4a780fb6c436cb1 Mon Sep 17 00:00:00 2001 From: armin Date: Mon, 22 Feb 2021 17:58:58 +0330 Subject: [PATCH] fix(eslint-plugin): [prefer-function-type] apply existing comments to the fixed code --- .../src/rules/prefer-function-type.ts | 142 +++++++++++------- .../tests/rules/prefer-function-type.test.ts | 111 +++++++++++++- 2 files changed, 195 insertions(+), 58 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-function-type.ts b/packages/eslint-plugin/src/rules/prefer-function-type.ts index f6aad463930e..761e8bc98eb9 100644 --- a/packages/eslint-plugin/src/rules/prefer-function-type.ts +++ b/packages/eslint-plugin/src/rules/prefer-function-type.ts @@ -1,6 +1,7 @@ import { AST_NODE_TYPES, AST_TOKEN_TYPES, + TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; @@ -69,46 +70,6 @@ export default util.createRule({ } } - /** - * @param call The call signature causing the diagnostic - * @param parent The parent of the call - * @returns The suggestion to report - */ - function renderSuggestion( - call: - | TSESTree.TSCallSignatureDeclaration - | TSESTree.TSConstructSignatureDeclaration, - parent: TSESTree.Node, - ): string { - 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, - )}`; - - const lastChar = suggestion.endsWith(';') ? ';' : ''; - if (lastChar) { - suggestion = suggestion.slice(0, -1); - } - if (shouldWrapSuggestion(parent.parent)) { - suggestion = `(${suggestion})`; - } - if (parent.type === AST_NODE_TYPES.TSInterfaceDeclaration) { - if (typeof parent.typeParameters !== 'undefined') { - return `type ${sourceCode - .getText() - .slice( - parent.id.range[0], - parent.typeParameters.range[1], - )} = ${suggestion}${lastChar}`; - } - return `type ${parent.id.name} = ${suggestion}${lastChar}`; - } - return suggestion; - } - /** * @param member The TypeElement being checked * @param node The parent of member being checked @@ -140,30 +101,97 @@ export default util.createRule({ }); return; } - const suggestion = renderSuggestion(member, node); - const fixStart = - node.type === AST_NODE_TYPES.TSTypeLiteral - ? node.range[0] - : sourceCode - .getTokens(node) - .filter( - token => - token.type === AST_TOKEN_TYPES.Keyword && - token.value === 'interface', - )[0].range[0]; + const fixable = + node.parent && + node.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration; context.report({ node: member, messageId: 'functionTypeOverCallableType', data: { literalOrInterface: phrases[node.type], }, - fix(fixer) { - return fixer.replaceTextRange( - [fixStart, node.range[1]], - suggestion, - ); - }, + fix: fixable + ? null + : (fixer): TSESLint.RuleFix[] => { + const fixes: TSESLint.RuleFix[] = []; + const start = member.range[0]; + const colonPos = member.returnType!.range[0] - start; + const text = sourceCode.getText().slice(start, member.range[1]); + const comments = sourceCode + .getCommentsBefore(member) + .concat(sourceCode.getCommentsAfter(member)); + let suggestion = `${text.slice(0, colonPos)} =>${text.slice( + colonPos + 1, + )}`; + const lastChar = suggestion.endsWith(';') ? ';' : ''; + if (lastChar) { + suggestion = suggestion.slice(0, -1); + } + if (shouldWrapSuggestion(node.parent)) { + suggestion = `(${suggestion})`; + } + + if (node.type === AST_NODE_TYPES.TSInterfaceDeclaration) { + if (typeof node.typeParameters !== 'undefined') { + suggestion = `type ${sourceCode + .getText() + .slice( + node.id.range[0], + node.typeParameters.range[1], + )} = ${suggestion}${lastChar}`; + } else { + suggestion = `type ${node.id.name} = ${suggestion}${lastChar}`; + } + } + + const isParentExported = + node.parent && + node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration; + + if ( + node.type === AST_NODE_TYPES.TSInterfaceDeclaration && + isParentExported + ) { + const commentsText = comments.reduce((text, comment) => { + return ( + text + + (comment.type === AST_TOKEN_TYPES.Line + ? `//${comment.value}` + : `/*${comment.value}*/`) + + '\n' + ); + }, ''); + // comments should move before export and not between export and interface declaration + fixes.push( + fixer.insertTextBefore( + node.parent as TSESTree.Node | TSESTree.Token, + commentsText, + ), + ); + } else { + comments.forEach(comment => { + let commentText = + comment.type === AST_TOKEN_TYPES.Line + ? `//${comment.value}` + : `/*${comment.value}*/`; + const isCommentOnTheSameLine = + comment.loc.start.line === member.loc.start.line; + if (!isCommentOnTheSameLine) { + commentText += '\n'; + } else { + commentText += ' '; + } + suggestion = commentText + suggestion; + }); + } + + const fixStart = node.range[0]; + fixes.push( + fixer.replaceTextRange([fixStart, node.range[1]], suggestion), + ); + return fixes; + }, }); } } 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 71c2557fda44..51aa85234890 100644 --- a/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts @@ -65,6 +65,114 @@ interface Foo { type Foo = () => string; `, }, + // https://github.com/typescript-eslint/typescript-eslint/issues/3004 + { + code: ` +export default interface Foo { + /** comment */ + (): string; +} + `, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: AST_NODE_TYPES.TSCallSignatureDeclaration, + data: { + literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration], + }, + }, + ], + output: ` +export default interface Foo { + /** comment */ + (): string; +} + `, + }, + { + code: ` +interface Foo { + // comment + (): string; +} + `, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: AST_NODE_TYPES.TSCallSignatureDeclaration, + data: { + literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration], + }, + }, + ], + output: ` +// comment +type Foo = () => string; + `, + }, + { + code: ` +export interface Foo { + /** comment */ + (): string; +} + `, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: AST_NODE_TYPES.TSCallSignatureDeclaration, + data: { + literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration], + }, + }, + ], + output: ` +/** comment */ +export type Foo = () => string; + `, + }, + { + code: ` +export interface Foo { + // comment + (): string; +} + `, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: AST_NODE_TYPES.TSCallSignatureDeclaration, + data: { + literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration], + }, + }, + ], + output: ` +// comment +export type Foo = () => string; + `, + }, + { + code: ` +function foo(bar: { /* comment */ (s: string): number } | undefined): number { + return bar('hello'); +} + `, + errors: [ + { + messageId: 'functionTypeOverCallableType', + type: AST_NODE_TYPES.TSCallSignatureDeclaration, + data: { + literalOrInterface: phrases[AST_NODE_TYPES.TSTypeLiteral], + }, + }, + ], + output: ` +function foo(bar: /* comment */ ((s: string) => number) | undefined): number { + return bar('hello'); +} + `, + }, { code: ` type Foo = { @@ -234,8 +342,8 @@ interface Foo { }, { code: ` +// isn't actually valid ts but want to not give message saying it refers to Foo. interface Foo { - // isn't actually valid ts but want to not give message saying it refers to Foo. (): { a: { nested: this; @@ -257,6 +365,7 @@ interface Foo { }, ], output: noFormat` +// isn't actually valid ts but want to not give message saying it refers to Foo. type Foo = () => { a: { nested: this;