Repository: prettier/prettier (JavaScript)

Issue:
Single line comments are merged on trailing trivia
**Prettier 2.1.2**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAzArlMMCW0AEAHgBQCU+wAOlAL74D09+qOATgM4zUDcj+7cSFAAmIADQgIAB1zR2yUAENWrCAHcACsoTyUigDZrFAT3kSARq0VgA1nBgBlKdZxQA5shit0cCXAC25nDCwsEAMoru6IpucABiEKz+ijC47sggiugwEOIgABYw-voA6vk48OzOYHAOOhU4AG4Vxhlg7GYgrgKsMBpWbsnIqAYCEgBW7IQAQla29g6K-nBhrnDDo74gk4QOrm76cACK6BDwG-pjIM4ccKwZ5opB+nlSrK4wJTjCMPnIABwABgkbwgAhKVikGTecB6jXWEgAjqd4P1pLpMuwALRQODBYJ5VhwZFsOD9GJDJAjS5bAT+HCeby0-aHE5ndZUzYSGBPL4-P5IABM3KsOH0+wAwhB-JSQLCAKx5dACAAqT101KujR8AEkRAhHGB3jIAIIiBwwYyHC4CGg0IA)
```sh
--parser babel
```

**Input:**
```jsx
function x() {
} // first
; // second
```

**Output:**
```jsx
function x() {} // first // second

```

**Expected behavior:**
```jsx
function x() {} // first
// second
```

**Details:**

As you can see, the two comments (that should be on their own lines) are merged on a single line. I think this has to do with the `lineSuffix` command being used on two tokens on the same line. When that happens, both the suffixes are appended to the same line.

This is obviously very dangerous, and destroys the semantic meaning of the comment. Not only it makes it much harder to read and reason about (since comments cannot be broken), it also means that it will break any tools/parsers that read the comment contents and expect certain strings to be there/not there, like ignores and other directives.

| - | First | Second |
| --------------- | --------------- | --------------- |
| Before | ` first` | ` second` |
| After | ` first // second` | **removed** |

This does not just affect JavaScript, but it also affects all other languages, as they have no other means AFAICT of fixing this correctly using other available commands.


Hints:
@sosukesuzuki I tracked the issue to this piece of code:

https://github.com/prettier/prettier/blob/74d144989659f0a03922f91c28d1f109cf12a14a/src/document/doc-printer.js#L468-L470

Happy to work on a fix and add tests, but since I'm a first time contributor, some pointers would be appreciated. Can you please let me know:

* Is there a case where `lineSuffix` can be used multiple times on the same line? I cannot find an existing example in a language.
* If not, should an unconditional break always exist after line suffixes?

TIA!
~Actually, this is already fixed. Will post details.~ Sorry, it's not.

I was attempt to fix it in #9772, but didn't finish.  #9736 was blocking it at that point.
> Is there a case where lineSuffix can be used multiple times on the same line?

The Ruby plugin uses `lineSuffix` to support heredoc strings (https://github.com/prettier/plugin-ruby/pull/681):
```ruby
a(b(c(<<~HERE, <<~DOC)))
  this is the content of the first string
HERE
  this is the content of the second string
DOC
```
@fisker thanks for pointing out.
Do you plan on rebasing/merging #9772 since #9736 is now merged?
Going over your fix, it seems that this will also fix it for other languages as well.
I'm a little busy recently, and seems #9772 breaking some cases.

You can work on it if you like.
@fisker pulled the fix down and started going through the code. IIUC, I don't believe we are talking about the same issue. Your PR handles **multiple multi-line comments that should be on the same line**. This issue describes **single line comments that should NOT be on the same line**.
Just ignore my PR.

Patch:
diff --git a/src/language-js/print/comment.js b/src/language-js/print/comment.js
--- a/src/language-js/print/comment.js
+++ b/src/language-js/print/comment.js
@@ -1,4 +1,3 @@
-import { hasNewline } from "../../common/util.js";
 import { join, hardline } from "../../document/builders.js";
 import { replaceEndOfLine } from "../../document/utils.js";
 
@@ -18,19 +17,7 @@ function printComment(commentPath, options) {
 
   if (isBlockComment(comment)) {
     if (isIndentableBlockComment(comment)) {
-      const printed = printIndentableBlockComment(comment);
-      // We need to prevent an edge case of a previous trailing comment
-      // printed as a `lineSuffix` which causes the comments to be
-      // interleaved. See https://github.com/prettier/prettier/issues/4412
-      if (
-        comment.trailing &&
-        !hasNewline(options.originalText, locStart(comment), {
-          backwards: true,
-        })
-      ) {
-        return [hardline, printed];
-      }
-      return printed;
+      return printIndentableBlockComment(comment);
     }
 
     const commentEnd = locEnd(comment);
diff --git a/src/main/comments.js b/src/main/comments.js
--- a/src/main/comments.js
+++ b/src/main/comments.js
@@ -461,14 +461,17 @@ function printLeadingComment(path, options) {
   return parts;
 }
 
-function printTrailingComment(path, options) {
+function printTrailingComment(path, options, previousComment) {
   const comment = path.getValue();
   const printed = printComment(path, options);
 
   const { printer, originalText, locStart } = options;
-  const isBlock = printer.isBlockComment && printer.isBlockComment(comment);
+  const isBlock = printer.isBlockComment?.(comment);
 
-  if (hasNewline(originalText, locStart(comment), { backwards: true })) {
+  if (
+    (previousComment?.hasLineSuffix && !previousComment?.isBlock) ||
+    hasNewline(originalText, locStart(comment), { backwards: true })
+  ) {
     // This allows comments at the end of nested structures:
     // {
     //   x: 1,
@@ -487,17 +490,22 @@ function printTrailingComment(path, options) {
       locStart
     );
 
-    return lineSuffix([hardline, isLineBeforeEmpty ? hardline : "", printed]);
+    return {
+      doc: lineSuffix([hardline, isLineBeforeEmpty ? hardline : "", printed]),
+      isBlock,
+      hasLineSuffix: true,
+    };
   }
 
-  let parts = [" ", printed];
-
-  // Trailing block comments never need a newline
-  if (!isBlock) {
-    parts = [lineSuffix(parts), breakParent];
+  if (!isBlock || previousComment?.hasLineSuffix) {
+    return {
+      doc: [lineSuffix([" ", printed]), breakParent],
+      isBlock,
+      hasLineSuffix: true,
+    };
   }
 
-  return parts;
+  return { doc: [" ", printed], isBlock, hasLineSuffix: false };
 }
 
 function printDanglingComments(path, options, sameIndent, filter) {
@@ -544,6 +552,7 @@ function printCommentsSeparately(path, options, ignored) {
 
   const leadingParts = [];
   const trailingParts = [];
+  let printedTrailingComment;
   path.each(() => {
     const comment = path.getValue();
     if (ignored && ignored.has(comment)) {
@@ -554,7 +563,12 @@ function printCommentsSeparately(path, options, ignored) {
     if (leading) {
       leadingParts.push(printLeadingComment(path, options));
     } else if (trailing) {
-      trailingParts.push(printTrailingComment(path, options));
+      printedTrailingComment = printTrailingComment(
+        path,
+        options,
+        printedTrailingComment
+      );
+      trailingParts.push(printedTrailingComment.doc);
     }
   }, "comments");
 


Test patch:
diff --git a/tests/format/js/comments/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/comments/__snapshots__/jsfmt.spec.js.snap
--- a/tests/format/js/comments/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/js/comments/__snapshots__/jsfmt.spec.js.snap
@@ -1444,6 +1444,101 @@ const test = "💖";
 ================================================================================
 `;
 
+exports[`empty-statements.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+a; /* a */ // b
+; /* c */
+
+foo; // first
+;// second
+;// third
+
+function x() {
+} // first
+; // second
+
+a = (
+  b // 1
+  + // 2
+  c // 3
+  + // 4
+  d // 5
+  + /* 6 */
+  e // 7
+);
+
+=====================================output=====================================
+a /* a */ // b
+/* c */
+foo // first
+// second
+// third
+function x() {} // first
+// second
+a =
+  b + // 1
+  // 2
+  c + // 3
+  // 4
+  d + // 5
+  /* 6 */
+  e // 7
+
+================================================================================
+`;
+
+exports[`empty-statements.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+a; /* a */ // b
+; /* c */
+
+foo; // first
+;// second
+;// third
+
+function x() {
+} // first
+; // second
+
+a = (
+  b // 1
+  + // 2
+  c // 3
+  + // 4
+  d // 5
+  + /* 6 */
+  e // 7
+);
+
+=====================================output=====================================
+a; /* a */ // b
+/* c */
+foo; // first
+// second
+// third
+function x() {} // first
+// second
+a =
+  b + // 1
+  // 2
+  c + // 3
+  // 4
+  d + // 5
+  /* 6 */
+  e; // 7
+
+================================================================================
+`;
+
 exports[`export.js - {"semi":false} format 1`] = `
 ====================================options=====================================
 parsers: ["babel", "flow", "typescript"]
@@ -3189,8 +3284,7 @@ onClick={() => {}}>
 
 ;<div>
   {
-    a
-    /* comment
+    a /* comment
      */
   }
 </div>
@@ -3430,8 +3524,7 @@ onClick={() => {}}>
 
 <div>
   {
-    a
-    /* comment
+    a /* comment
      */
   }
 </div>;
@@ -3924,8 +4017,7 @@ a
 b
 
 a /*
-1*/ /*2*/
-/*3
+1*/ /*2*/ /*3
  */
 b
 
@@ -4116,8 +4208,7 @@ a;
 b;
 
 a; /*
-1*/ /*2*/
-/*3
+1*/ /*2*/ /*3
  */
 b;
 
@@ -5489,11 +5580,9 @@ const CONNECTION_STATUS = (exports.CONNECTION_STATUS = {
   NOT_CONNECTED: Object.freeze({ kind: "NOT_CONNECTED" }),
 })
 
-/* A comment */
-/**
+/* A comment */ /**
  * A type that can be written to a buffer.
- */
-/**
+ */ /**
  * Describes the connection status of a ReactiveSocket/DuplexConnection.
  * - NOT_CONNECTED: no connection established or pending.
  * - CONNECTING: when \`connect()\` has been called but a connection is not yet
@@ -5501,12 +5590,10 @@ const CONNECTION_STATUS = (exports.CONNECTION_STATUS = {
  * - CONNECTED: when a connection is established.
  * - CLOSED: when the connection has been explicitly closed via \`close()\`.
  * - ERROR: when the connection has been closed for any other reason.
- */
-/**
+ */ /**
  * A contract providing different interaction models per the [ReactiveSocket protocol]
  * (https://github.com/ReactiveSocket/reactivesocket/blob/master/Protocol.md).
- */
-/**
+ */ /**
  * A single unit of data exchanged between the peers of a \`ReactiveSocket\`.
  */
 
@@ -5550,11 +5637,9 @@ const CONNECTION_STATUS = (exports.CONNECTION_STATUS = {
   NOT_CONNECTED: Object.freeze({ kind: "NOT_CONNECTED" }),
 });
 
-/* A comment */
-/**
+/* A comment */ /**
  * A type that can be written to a buffer.
- */
-/**
+ */ /**
  * Describes the connection status of a ReactiveSocket/DuplexConnection.
  * - NOT_CONNECTED: no connection established or pending.
  * - CONNECTING: when \`connect()\` has been called but a connection is not yet
@@ -5562,12 +5647,10 @@ const CONNECTION_STATUS = (exports.CONNECTION_STATUS = {
  * - CONNECTED: when a connection is established.
  * - CLOSED: when the connection has been explicitly closed via \`close()\`.
  * - ERROR: when the connection has been closed for any other reason.
- */
-/**
+ */ /**
  * A contract providing different interaction models per the [ReactiveSocket protocol]
  * (https://github.com/ReactiveSocket/reactivesocket/blob/master/Protocol.md).
- */
-/**
+ */ /**
  * A single unit of data exchanged between the peers of a \`ReactiveSocket\`.
  */
 
diff --git a/tests/format/js/comments/empty-statements.js b/tests/format/js/comments/empty-statements.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/comments/empty-statements.js
@@ -0,0 +1,20 @@
+a; /* a */ // b
+; /* c */
+
+foo; // first
+;// second
+;// third
+
+function x() {
+} // first
+; // second
+
+a = (
+  b // 1
+  + // 2
+  c // 3
+  + // 4
+  d // 5
+  + /* 6 */
+  e // 7
+);