Skip to content

Commit c389688

Browse files
committed
feat(comments): preserve a comment after an opening brace in a function body
test(comments): add more tests refactor(comments): refactor the methods preserving comments Signed-off-by: Kristinita <Kristinita@users.noreply.github.com>
1 parent 983a613 commit c389688

2 files changed

Lines changed: 152 additions & 37 deletions

File tree

src/writer.ts

Lines changed: 88 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,32 @@ export class Writer {
1717

1818
getBodySource({ body }: AnyFunction): string {
1919
if (this.options.returnStyle !== 'explicit' && this.guard.isBlockStatementWithSingleReturn(body)) {
20-
const returnValue = body.body[0].argument;
21-
const source = this.sourceCode.getText(returnValue);
22-
return returnValue.type === AST_NODE_TYPES.ObjectExpression ? `(${source})` : source;
20+
const firstStatementInFunction = body.body[0];
21+
const firstStatementValue = firstStatementInFunction.argument;
22+
23+
// Get a comment after an opening brace and before a first statement in a function
24+
const commentBeforeFirstStatement = this.sourceCode.getCommentsBefore(firstStatementInFunction)[0];
25+
26+
const bodySource = this.sourceCode.getText(firstStatementValue);
27+
const wrappedBodySource =
28+
firstStatementValue.type === AST_NODE_TYPES.ObjectExpression ? `(${bodySource})` : bodySource;
29+
30+
// Get a text of a comment after an opening brace in a function if this comment exists
31+
if (!commentBeforeFirstStatement) {
32+
return wrappedBodySource;
33+
}
34+
const commentBeforeFirstStatementText = this.sourceCode.getText(commentBeforeFirstStatement);
35+
36+
/* Return a function body with a comment before it.
37+
38+
This method adds line breaks before and after a comment. Reasons:
39+
40+
1. If a comment is single-line, a new line after this comment is required, because
41+
a comment in the same line as a first statement of a function is incorrect syntax.
42+
43+
2. A new line before a comment added, because otherwise JavaScript beautifiers
44+
may prettify a comment not in the best way. */
45+
return `\n${commentBeforeFirstStatementText}\n${wrappedBodySource}`;
2346
}
2447
if (this.guard.hasImplicitReturn(body) && this.options.returnStyle !== 'implicit') {
2548
return `{ return ${this.sourceCode.getText(body)} }`;
@@ -29,19 +52,19 @@ export class Writer {
2952

3053
getParamsSource(params: TSESTree.Parameter[]): string[] {
3154
return params.map((param) => {
32-
// Get parameter value
33-
const paramText = this.sourceCode.getText(param);
55+
// Get a parameter value
56+
const parameterText = this.sourceCode.getText(param);
3457

35-
// Get a comment before parameter if exists
36-
const commentsBefore = this.sourceCode.getCommentsBefore(param);
37-
const beforeText = commentsBefore.length > 0 ? this.sourceCode.getText(commentsBefore[0]) : '';
58+
// Get a comment before a parameter if exists
59+
const commentBeforeParameter = this.sourceCode.getCommentsBefore(param)[0];
60+
const commentBeforeParameterText = commentBeforeParameter ? this.sourceCode.getText(commentBeforeParameter) : '';
3861

39-
// Get a comment after parameter if exists
40-
const commentsAfter = this.sourceCode.getCommentsAfter(param);
41-
const afterText = commentsAfter.length > 0 ? this.sourceCode.getText(commentsAfter[0]) : '';
62+
// Get a comment after a parameter if exists
63+
const commentAfterParameter = this.sourceCode.getCommentsAfter(param)[0];
64+
const commentAfterParameterText = commentAfterParameter ? this.sourceCode.getText(commentAfterParameter) : '';
4265

43-
// Combine all parts
44-
return beforeText + paramText + afterText;
66+
// Return a parameter with comments before and after it
67+
return `${commentBeforeParameterText}${parameterText}${commentAfterParameterText}`;
4568
});
4669
}
4770

@@ -70,34 +93,67 @@ export class Writer {
7093
const RETURN_TYPE = fn.returnType ? fn.returnType : '';
7194
const PARAMS = fn.params.join(', ');
7295

73-
// Preserve a comment after a return type annotation
74-
const commentInside = this.sourceCode.getCommentsInside(node);
75-
let middleComment = '';
96+
/* Get all comments of a function:
7697
77-
if (commentInside.length > 0) {
78-
// Find a comment between a return type and a function body
79-
const returnTypeComment = commentInside.find((candidateComment) => {
80-
// Get the start position of a candidate comment
81-
const commentStart = candidateComment.range[0];
98+
1. Comments after parameters
99+
2. Comments between parameters and a function body
100+
3. Comments inside a function body */
101+
const allFunctionComments = this.sourceCode.getCommentsInside(node);
82102

83-
/* If a return type exists, use its end position.
84-
Otherwise, use the end of the last function parameter */
85-
const returnTypeEnd = node.returnType ? node.returnType.range[1] : node.params[node.params.length - 1].range[1];
103+
let preservedCommentText = '';
86104

87-
// Get the start position of a function body
88-
const bodyStart = node.body.range[0];
105+
// Checking if comments exists instead of using the “find()” method everywhere for improving performance
106+
if (allFunctionComments[0]) {
107+
/* Find a comment between parameters and a function body.
89108
90-
// Keep a comment between a return type and a function body
91-
return commentStart > returnTypeEnd && commentStart < bodyStart;
109+
It’s possible to get the value of the “commentBetweenParametersAndFunctionBodyText”
110+
use more simple way, but if a JavaScript function contain not solely a comment
111+
between parameters and a function body, eslint-plugin-prefer-arrow-functions may work incorrectly.
112+
Therefore, I use the method “getCommentsInside(node)”. */
113+
const commentBetweenParametersAndFunctionBody = allFunctionComments.find((candidateComment) => {
114+
// Get the start position of a candidate comment
115+
const candidateCommentStartPosition = candidateComment.range[0];
116+
117+
// Get the position of the closing parenthesis after parameters if parameters exist or no
118+
const parametersClosingParenthesisPosition = node.params[0]
119+
? /* Use the old “length()” property instead of the modern method “at()”,
120+
because in TypeScript “at()” usage isn’t simple:
121+
https://github.com/microsoft/TypeScript/issues/57224 */
122+
node.params[node.params.length - 1].range[1] + 1
123+
: node.range[0] + 1;
124+
125+
// Get the position of the opening brace of a function body
126+
const functionBodyOpeningBracePosition = node.body.range[0];
127+
128+
// Keep a comment between parameters and a function body if exists
129+
return (
130+
candidateCommentStartPosition > parametersClosingParenthesisPosition &&
131+
candidateCommentStartPosition < functionBodyOpeningBracePosition
132+
);
92133
});
93134

94-
// If a return type comment exists, get its text
95-
if (returnTypeComment) {
96-
middleComment = this.sourceCode.getText(returnTypeComment);
135+
/* If a comment between parameters and a function body exists,
136+
get its text and convert it to a single-line comment if it’s a multiline */
137+
if (commentBetweenParametersAndFunctionBody) {
138+
let commentBetweenParametersAndFunctionBodyText = this.sourceCode.getText(
139+
commentBetweenParametersAndFunctionBody,
140+
);
141+
142+
// Check if a comment is multiline
143+
if (commentBetweenParametersAndFunctionBodyText.includes('\n')) {
144+
/* Convert a multiline comment with line breaks to a single-line (replace line breaks with spaces),
145+
because JavaScript doesn’t support a multiline comment before a fat arrow. */
146+
commentBetweenParametersAndFunctionBodyText = commentBetweenParametersAndFunctionBodyText
147+
.split(/\r?\n/)
148+
.map((commentLine) => commentLine.trim())
149+
.join(' ');
150+
}
151+
152+
preservedCommentText = commentBetweenParametersAndFunctionBodyText;
97153
}
98154
}
99155

100-
const arrowFunction = `${ASYNC}${GENERIC}(${PARAMS})${RETURN_TYPE}${middleComment} => ${BODY}`;
156+
const arrowFunction = `${ASYNC}${GENERIC}(${PARAMS})${preservedCommentText}${RETURN_TYPE} => ${BODY}`;
101157

102158
// Check if parentheses are needed due to operator precedence
103159
if (this.needsParentheses(node)) {

test/scenarios.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,6 @@ export const validWhenSingleReturnOnly = [
7777
];
7878

7979
export const invalidAndHasSingleReturn = [
80-
// Test for comment preservation
81-
{
82-
code: 'var foo = function(bar/*: string */)/*: string */ { return `${bar}`; };',
83-
output: 'var foo = (bar/*: string */)/*: string */ => `${bar}`;',
84-
},
8580
// ES6 classes & functions declared in object literals
8681
{
8782
code: 'class MyClass { render(a, b) { return 3; } }',
@@ -306,6 +301,70 @@ export const invalidAndHasSingleReturn = [
306301
code: 'export { foo }; export async function bar() { return false; }',
307302
output: 'export { foo }; export const bar = async () => false;',
308303
},
304+
305+
/* ##########################
306+
# Preserving comments tests #
307+
########################## */
308+
309+
// Test for a comment after a parameter
310+
{
311+
code: 'var foo = function(a/*: string */) { return "bar"; }',
312+
output: 'var foo = (a/*: string */) => "bar"',
313+
},
314+
315+
// Test for comments after parameters
316+
{
317+
code: 'var foo = function(a/*: string */, b/*: number */) { return "bar"; }',
318+
output: 'var foo = (a/*: string */, b/*: number */) => "bar"',
319+
},
320+
321+
// Test for a comment between a closing parenthesis of parameters and an opening brace of a function body
322+
{
323+
code: 'var foo = function() /*: boolean */ { return "bar"; }',
324+
output: 'var foo = ()/*: boolean */ => "bar"',
325+
},
326+
327+
// Test for a comment with line breaks between a closing parenthesis of parameters and an opening brace of a function body
328+
{
329+
code: 'var foo = function() /*:\n\tbar\n\tbaz */ { return "qux"; }',
330+
output: 'var foo = ()/*: bar baz */ => "qux"',
331+
},
332+
333+
// Test for a comment after a parameter and a comment between parameters and a function body
334+
{
335+
code: 'var foo = function(a/*: boolean */) /*: string */ { return "bar"; }',
336+
output: 'var foo = (a/*: boolean */)/*: string */ => "bar"',
337+
},
338+
339+
// Test for comments after parameters and a comment between parameters and a function body
340+
{
341+
code: 'var foo = function(a/*: boolean */, b/*: number */) /*: string */ { return "bar"; }',
342+
output: 'var foo = (a/*: boolean */, b/*: number */)/*: string */ => "bar"',
343+
},
344+
345+
// Test for a single-line comment after an opening brace of a function body
346+
{
347+
code: 'var foo = function() { // bar\n\treturn "baz";}',
348+
output: 'var foo = () => \n// bar\n"baz"',
349+
},
350+
351+
// Test for a multiline comment after an opening brace of a function body
352+
{
353+
code: 'var foo = function() { /* bar */ return "baz";}',
354+
output: 'var foo = () => \n/* bar */\n"baz"',
355+
},
356+
357+
// Test for a multiline comment with line breaks after an opening brace of a function body
358+
{
359+
code: 'var foo = function() {\n\t/* bar\n\tbaz */\n\treturn "qux";}',
360+
output: 'var foo = () => \n/* bar\n\tbaz */\n"qux"',
361+
},
362+
363+
// Test for a comment between parameters and a function body and a comment after an opening brace of a function body
364+
{
365+
code: 'var foo = function() /*: string */ { // bar\n\treturn "baz";}',
366+
output: 'var foo = ()/*: string */ => \n// bar\n"baz"',
367+
},
309368
];
310369

311370
export const invalidAndHasSingleReturnWithMultipleMatches = [

0 commit comments

Comments
 (0)