Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(eslint-plugin): [prefer-function-type] apply existing comments to the fixed code #3094

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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