Skip to content

Commit

Permalink
fix(eslint-plugin): [prefer-function-type] apply existing comments to…
Browse files Browse the repository at this point in the history
… the fixed code (#3094)
  • Loading branch information
arminyahya committed Feb 28, 2021
1 parent ae14bf5 commit c32f803
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 58 deletions.
142 changes: 85 additions & 57 deletions 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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
},
});
}
}
Expand Down
111 changes: 110 additions & 1 deletion packages/eslint-plugin/tests/rules/prefer-function-type.test.ts
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down

0 comments on commit c32f803

Please sign in to comment.