Repository: prettier/prettier (JavaScript)

Issue:
Comment in `IfStatement` not printed.
**Prettier 3.0.0**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAzArlMMCW0AEAhgBQCU+wAOlPvjqvsYeQG6EBO+Y+AvPgIz4A9ACouEALYSEMfADZC+EUIDc1WnAA2AZzj52cGOnY0ATGqgBfEABoQEAA65o25KA7sIAdwAKHBK4ohJpehACernYARuyEYADWhgDKhNIAMjhQcMiowbrRsQnJDnGZAObIMOzocHa6EjiV1bUg2uWacACK6BDwOXktAFbaAB5J7V09fUi5Oi0AjlNwPp4OgSCE2gC0WXAAJvu2IFWEOJrlAMKSEoTIG5qaR21QZR0AgjBVOFHo8D5w7AyWX6czsAAsYBJNAB1ME4eDaEpgOBJALwnAseFhO5gbSREAsGoASSgB1gSTA7BwTjepKSMDCHRB+RADk8umhsQcdzZcF07BY2TsmX5MBWhDKN2ZLRK7H5dyihCiWiObMyMGhOD2MDByAAHAAGOwGRY4AziyW3GYDOwwJWa7W6pCmOzoXQAFSVgVmLLgEmVewOezShBe6AlcAAYhB2DdPuU7oRfhAQJZLEA)
<!-- prettier-ignore -->
```sh
--parser babel
```

**Input:**
<!-- prettier-ignore -->
```jsx
function a() {
  if (a) var c = 1 /* comment 6a */;
  else return 2;
}
```

**Output:**
<!-- prettier-ignore -->
```jsx
Error: Comment "comment 6a" was not printed. Please report this error!
    at pu (https://prettier.io/lib/standalone.js:27:5829)
    at Ze (https://prettier.io/lib/standalone.js:27:7320)
    at async Ou (https://prettier.io/lib/standalone.js:27:11026)
    at async Rr (https://prettier.io/lib/standalone.js:32:581)
    at async formatCode (https://prettier.io/worker.js:170:12)
    at async handleFormatMessage (https://prettier.io/worker.js:82:24)
    at async self.onmessage (https://prettier.io/worker.js:37:14)
```

**Expected behavior:**

See https://github.com/prettier/prettier/pull/15076#discussion_r1259242307
fix: Print dangling comments for `break|continue|debugger`
## Description

<!-- Please provide a brief summary of your changes: -->

Some comments after `break|continue|debugger` cannot be printed.

Fixes #12655

## Checklist

<!-- Please ensure you’ve done all of these things (if applicable). -->
<!-- You can replace the `[ ]` with `[x]` to mark each task as done. -->

- [x] I’ve added tests to confirm my change works.
- [ ] (If changing the API or CLI) I’ve documented the changes I’ve made (in the `docs/` directory).
- [x] (If the change is user-facing) I’ve added my changes to `changelog_unreleased/*/XXXX.md` file following `changelog_unreleased/TEMPLATE.md`.
- [x] I’ve read the [contributing guidelines](https://github.com/prettier/prettier/blob/main/CONTRIBUTING.md).

<!-- Please DO NOT remove the playground link -->

**✨[Try the playground for this PR](https://prettier.io/playground-redirect)✨**



Hints:
Same problem with version 3.0.0 in the following **TypeScript** snippet:
```
for (const division of collidingDivisions) {
	if (!division.properties.canRemove) {
		if (division.canContainSingleParagraph(startParagraph)) continue; // selection starting in r/o div: always OK to delete
		else if (endParagraph !== division.endParagraph && division.canContainSingleParagraph(endParagraph)) continue; // selection ending in r/o div: OK to delete IF it is NOT last paragraph (which would get absorbed)
		else { // selection over whole r/o div OR ending in last paragraph (which would get absorbed) -> FAIL
			console.warn('deleteSelectedTextAndJoinRemainingNodes(): cannot delete because of must-keep division: ' + division.properties.name);
			return { success: false, reason: DeleteAndJoinErrorReason.READONLY_DIVISION };
		}
	}
}
```

**Error:**
```
[error] EditableArticle.ts: Error: Comment "selection starting in r/o div: always OK to delete" was not printed. Please report this error!
[error]     at ensureAllCommentsPrinted
[error]     at printAstToDoc
[error]     at async coreFormat
[error]     at async formatWithCursor
[error]     at async formatFiles
[error]     at async main
[error]     at async Module.run
```
This is duplicated to #15470
Sorry I mistook
Unstable

**Prettier pr-13677**
[Playground link](https://deploy-preview-13677--prettier.netlify.app/playground/#N4Igxg9gdgLgprEAuEAzCAnABACgNx4CUWwAOlOVlgJaq4wYCucxARhnAIYDWWA9HywwAFtQDOWSAFspCGAHIJcAB7j4UMHCwB3agBs9Wdl17UY5PJSxw9Yregg5ClilAC+IADQgIABxjU0GLIoJwYGBDaAAphCMEonHranACewd7snGDccDAAypyyADLUUHDIqIl2GRhZOfm+WaUA5sgMzN5wUqxwACa9fUWcUM2MnM1wAGKYUpwwASPIIJyMMBBeIMIwUnoA6qLwYo2aeXFm1ABuZilLYGLpIKV2GDBRtc2zFVVw3gBWYso8i09HAAIqMCDwL62H4gRoYZ5LVicHp6Da+DClGC7ai9ETIAAcAAZvBiIHZdrVfEsMXBnhdyt4AI4Q+BvPzxZZiAC0ZT6fQ2HBZ1A4b3GnyQlRh3jsUmobSYsLEwLBrPKku+3hgKJxeOEyAATFravoWgBhCAyThLRJo7yMOwAFRR8Sl1RAF2YAEkoANYHkwJj-ABBX15GApEHQuxuNxAA)
<!-- prettier-ignore -->
```sh
--parser babel
```

**Input:**
<!-- prettier-ignore -->
```jsx
for (;;) {

  if (true) break // this comment's existence will break it
;
  else foo();

}
```

**Output:**
<!-- prettier-ignore -->
```jsx
for (;;) {
  if (true)
    break; // this comment's existence will break it
  else foo();
}

```

**Second Output:**
<!-- prettier-ignore -->
```jsx
for (;;) {
  if (true) break; // this comment's existence will break it
  else foo();
}

```
[repl](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAzCAnABACgNx4CUWwAOlOVlgJaq4wYCucxARhnAIYDWWA9HywwAFtQDOWSAFspCGAHIJcAB7j4UMHCwB3agBs9Wdl17UY5PJSxw9Yregg5ClilAC+IADQgIABxjU0GLIoJwYGBDaAAphCMEonHranACewd7snGDccDAAypyyADLUUHDIqIl2GRhZOfm+WaUA5sgMzN5wUqxwACa9fUWcUM2MnM1wAGKYUpwwASPIIJyMMBBeIMIwUnoA6qLwYo2aeXFm1ABuZilLYGLpIKV2GDBRtc2zFVVw3gBWYso8i09HAAIqMCDwL62H4gRoYZ5LVicHp6Da+DClGC7ai9ETIAAcAAZvBiIHZdrVfEsMXBnhdyt4AI4Q+BvPzxZZiAC0ZT6fQ2HBZ1A4b3GnyQlRh3jsUmobSYsLEwLBrPKku+3hgKJxeOEyAATFravoWgBhCAyThLOkAVg2jDsABUUfEpdUQBdmABJKADWB5MCY-wAQT9eRgKRB0LsbjcQA)

Should have nothing to do with this PR, as is the current main branch.
I don't understand why only node18 fails.😕
> I don't understand why only node18 fails.😕

`FULL_TEST=1 yarn test ...`
Seems you fixed a  idempotence issue, remove this line https://github.com/prettier/prettier/blob/f38111fec6c35b513370832a84bdac8b5663763d/tests/config/format-test.js#L42
Yeah, I improved the error message a little bit.
Can you add a changelog?
Does fixing idempotency need to be stated in the changelog? That seems like the title is a bit too long.
You can use two changelog entry, name `13677.md` and `13677-2.md` 
Something seems not right here. These comments should be attached as trailing to those statements, not as dangling. Did you try to fix it that way @liuxingbaoyu ?
No.
babel has it as a trailing comment, but espree doesn't.
https://astexplorer.net/#/gist/134357615990b3d06424d14bf13c2831/07bcca7af325512345159821b3e8b0e7e937bc64
And I tested in prettier, same result even with babel, so I'm not sure if this is intentional and worried that it affects more, I didn't touch it.
He is talking about our own comment attach logic https://github.com/prettier/prettier/blob/next/src/main/comments.js
I still don't understand the comment logic here, but I can only do so much. 😅
@liuxingbaoyu You correctly identified the problematic place in the code. But it needs a fix for a more general case as that logic (added in #12017) can add dangling comments to _any_ kind of statement (not only `break/continue/debugger`) whereas only a few statements support them. The idea with adding dangling comments with a marker is valid, but they should've been added to `IfStatement`.
I don't understand what needs to be done, what is this comment expected as? Dangling Comments of `ifStatement`? But this behavior appears to be intentional in https://github.com/prettier/prettier/pull/12017.
Also it looks like the way in https://github.com/prettier/prettier/pull/12017/files#diff-0aaa38223318fb8c3d0133b3b8d8f4cbc48d37209f26f3b818398d1ccc28994aR184-R217 is similar to my original commit.

Sorry, I don't have a full solution in mind. I'm just pointing out that the bug isn't specific to `break/continue/debugger` (examples: [one](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBLAZgAgBTohASkzwCcdVMBeTAJgG5MKA+TABgdQFpOjIoBnCABs4AOiEQA5tlQEGAennF8AHShwh-OJgBGAQxLY5akABoQEAA4xU0fslAGSEAO4AFAwnso9Ql3oBPe3MdEj0wAGs4GABlPQBbOAAZVHVkdF8tELDI6JjLcNTJZBgSAFc4czh4nTgAEzr6pL0oSTK9STgAMQgSeL0YG1bkED0ymAgzEAALGHihAHVp1Hh+ArA4GK8V1AA3FYCRsH5gtAE4Ehg3MMl+9MzKkAArfgAPGKKRAEUyiHh7zSPAokLQkEb6WpCKaWEipGALVB1GDTZAADlY5hhEC0CzClhGMLgoN2cCmAEdfvBrlZvKN+Jx1PV6lMSHAKahWdcOnckBlAeYtPFUCVyo9+J84D8-qTeQ9zDA9DoEUiUUgaPKwqghEUAMIQeI8kBEgCsUzKWgAKorvHysiBdhUAJJQRqwGJgWHWACCLpiMACIgBWgAvsGgA), [two](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBLAZgAgBTohASkwBMJNhNIoBnCAGzgDo6IBzbVIgX0wHcALVAw4BaEUQD0EzHggAdKHDrU4mAEYBDAE7YCAbhAAaEBAAOMVNGrJQ2rRF4AFbQmsoNdXhoCe142q0NMABrOBgAZQ0AWzgAGVRFZHQPFX9AkLDw0yCE1mQYLQBXOGM4KLU4YmJK2I0oVkKNVjgAMQgtKI0YC3rkEA1CmAgjEH4YKLoAdUF4amywOHDXVAsANxXvPrBqPzQaOC0YR0DWTqSUkpAAK2oAD3DchgBFQoh4c+VL7K0VLT7NCp0EamLQJGCTVDEGD8ZAADgADMYQRAVJNAqY+iC4L9VnARgBHV7wY5mNz9agiRSVSojLRwQmoOnHJpnJDJT7GFRRVD5IqXaiPOAvN54tkXYwwDRqCFQmFIABMEsCQlyAGEIFFWSBsQBWEaFFQAFSlbnZqRAq2KAEkoNVYOEwKDzABBW3hGDeBgfFRcLhAA)). I'll look into it if I have time.

Patch:
diff --git a/src/language-js/comments/handle-comments.js b/src/language-js/comments/handle-comments.js
--- a/src/language-js/comments/handle-comments.js
+++ b/src/language-js/comments/handle-comments.js
@@ -21,7 +21,6 @@ import {
   isObjectProperty,
   isPrettierIgnoreComment,
   isUnionType,
-  markerForIfWithoutBlockAndSameLineComment,
 } from "../utils/index.js";
 import isBlockComment from "../utils/is-block-comment.js";
 import isTypeCastComment from "../utils/is-type-cast-comment.js";
@@ -202,7 +201,7 @@ function handleIfStatementComments({
       addTrailingComment(precedingNode, comment);
     } else {
       const isSingleLineComment =
-        comment.type === "SingleLine" ||
+        isLineComment(comment) ||
         comment.loc.start.line === comment.loc.end.line;
       const isSameLineComment =
         comment.loc.start.line === precedingNode.loc.start.line;
@@ -211,13 +210,7 @@ function handleIfStatementComments({
         //   if (cond1) expr1; // comment A
         //   else if (cond2) expr2; // comment A
         //   else expr3;
-        addDanglingComment(
-          precedingNode,
-          comment,
-          precedingNode.type === "ExpressionStatement"
-            ? markerForIfWithoutBlockAndSameLineComment
-            : undefined,
-        );
+        addTrailingComment(precedingNode, comment);
       } else {
         addDanglingComment(enclosingNode, comment);
       }
diff --git a/src/language-js/print/estree.js b/src/language-js/print/estree.js
--- a/src/language-js/print/estree.js
+++ b/src/language-js/print/estree.js
@@ -371,12 +371,12 @@ function printEstree(path, options, print, args) {
         adjustClause(node.body, print("body")),
       ]);
     case "IfStatement": {
-      const con = adjustClause(node.consequent, print("consequent"));
+      const consequent = adjustClause(node.consequent, print("consequent"));
       const opening = group([
         "if (",
         group([indent([softline, print("test")]), softline]),
         ")",
-        con,
+        consequent,
       ]);
 
       parts.push(opening);
diff --git a/src/language-js/print/expression-statement.js b/src/language-js/print/expression-statement.js
--- a/src/language-js/print/expression-statement.js
+++ b/src/language-js/print/expression-statement.js
@@ -1,9 +1,3 @@
-import { printDanglingComments } from "../../main/comments/print.js";
-import {
-  CommentCheckFlags,
-  hasComment,
-  markerForIfWithoutBlockAndSameLineComment,
-} from "../utils/index.js";
 import {
   isSingleJsxExpressionStatementInMarkdown,
   isSingleVueEventBindingExpressionStatement,
@@ -23,21 +17,6 @@ function printExpressionStatement(path, options, print) {
     parts.push(";");
   }
 
-  if (
-    hasComment(
-      path.node,
-      CommentCheckFlags.Dangling,
-      ({ marker }) => marker === markerForIfWithoutBlockAndSameLineComment,
-    )
-  ) {
-    parts.push(
-      " ",
-      printDanglingComments(path, options, {
-        marker: markerForIfWithoutBlockAndSameLineComment,
-      }),
-    );
-  }
-
   return parts;
 }
 
diff --git a/src/language-js/utils/index.js b/src/language-js/utils/index.js
--- a/src/language-js/utils/index.js
+++ b/src/language-js/utils/index.js
@@ -1197,13 +1197,6 @@ function isObjectProperty(node) {
   );
 }
 
-/**
- * This is used as a marker for dangling comments.
- */
-const markerForIfWithoutBlockAndSameLineComment = Symbol(
-  "ifWithoutBlockAndSameLineComment",
-);
-
 const isBinaryCastExpression = createTypeCheckFunction([
   // TS
   "TSAsExpression",
@@ -1284,7 +1277,6 @@ export {
   isUnionType,
   iterateCallArgumentsPath,
   iterateFunctionParametersPath,
-  markerForIfWithoutBlockAndSameLineComment,
   needsHardlineAfterDanglingComment,
   rawText,
   shouldFlatten,


Test patch:
diff --git a/tests/config/format-test.js b/tests/config/format-test.js
--- a/tests/config/format-test.js
+++ b/tests/config/format-test.js
@@ -46,6 +46,7 @@ const unstableTests = new Map(
     "jsx/comments/in-attributes.js",
     ["js/ignore/semi/asi.js", (options) => options.semi === false],
     "typescript/union/consistent-with-flow/single-type.ts",
+    "js/if/non-block.js",
   ].map((fixture) => {
     const [file, isUnstable = () => true] = Array.isArray(fixture)
       ? fixture
diff --git a/tests/format/js/if/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/if/__snapshots__/jsfmt.spec.js.snap
--- a/tests/format/js/if/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/js/if/__snapshots__/jsfmt.spec.js.snap
@@ -123,25 +123,28 @@ function a() {
   if (a) throw e; /* comment 6d */
   else return 2;
 
-  // TODO[@fisker]: fix this
-  // if (a) var a = 1; /* comment 6e */
-  // else return 2;
+  if (a) var a = 1; /* comment 6e */
+  else return 2;
 
   if (a) if (b); /* comment 6f */
   else return 2;
 }
 
 =====================================output=====================================
-if (a === 0) doSomething(); // comment A1
-else if (a === 1) doSomethingElse(); // comment B1
+if (a === 0)
+  doSomething(); // comment A1
+else if (a === 1)
+  doSomethingElse(); // comment B1
 else if (a === 2) doSomethingElse(); // comment C1
 
 if (a === 0) doSomething(); /* comment A2 */
 else if (a === 1) doSomethingElse(); /* comment B2 */
 else if (a === 2) doSomethingElse(); /* comment C2 */
 
-if (a === 0) expr; // comment A3
-else if (a === 1) expr; // comment B3
+if (a === 0)
+  expr; // comment A3
+else if (a === 1)
+  expr; // comment B3
 else if (a === 2) expr; // comment C3
 
 if (a === 0) expr; /* comment A4 */
@@ -156,18 +159,17 @@ else if (a === 2)
   looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong; // comment C5
 
 function a() {
-  if (a) return /* comment 6a */;
+  if (a) return; /* comment 6a */
   else return 2;
 
-  if (a) return 1 /* comment 6b */;
+  if (a) return 1; /* comment 6b */
   else return 2;
 
-  if (a) throw e /* comment 6d */;
+  if (a) throw e; /* comment 6d */
   else return 2;
 
-  // TODO[@fisker]: fix this
-  // if (a) var a = 1; /* comment 6e */
-  // else return 2;
+  if (a) var a = 1; /* comment 6e */
+  else return 2;
 
   if (a)
     if (b /* comment 6f */);
@@ -294,6 +296,71 @@ async function f5() {
 ================================================================================
 `;
 
+exports[`issue-15168.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+for (const division of collidingDivisions) {
+	if (!division.properties.canRemove) {
+		if (division.canContainSingleParagraph(startParagraph)) continue; // selection starting in r/o div: always OK to delete
+		else if (endParagraph !== division.endParagraph && division.canContainSingleParagraph(endParagraph)) continue; // selection ending in r/o div: OK to delete IF it is NOT last paragraph (which would get absorbed)
+		else { // selection over whole r/o div OR ending in last paragraph (which would get absorbed) -> FAIL
+			console.warn('deleteSelectedTextAndJoinRemainingNodes(): cannot delete because of must-keep division: ' + division.properties.name);
+		}
+	}
+}
+
+=====================================output=====================================
+for (const division of collidingDivisions) {
+  if (!division.properties.canRemove) {
+    if (division.canContainSingleParagraph(startParagraph))
+      continue; // selection starting in r/o div: always OK to delete
+    else if (
+      endParagraph !== division.endParagraph &&
+      division.canContainSingleParagraph(endParagraph)
+    )
+      continue; // selection ending in r/o div: OK to delete IF it is NOT last paragraph (which would get absorbed)
+    else {
+      // selection over whole r/o div OR ending in last paragraph (which would get absorbed) -> FAIL
+      console.warn(
+        "deleteSelectedTextAndJoinRemainingNodes(): cannot delete because of must-keep division: " +
+          division.properties.name,
+      );
+    }
+  }
+}
+
+================================================================================
+`;
+
+exports[`non-block.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+if (foo) for (i = 2; i > 0; i--) console.log(i); // foo
+else bar();
+
+if (foo) do { console.log(i) } while(i--) // foo
+else bar();
+
+=====================================output=====================================
+if (foo)
+  for (i = 2; i > 0; i--) console.log(i); // foo
+else bar();
+
+if (foo)
+  do {
+    console.log(i);
+  } while (i--); // foo
+else bar();
+
+================================================================================
+`;
+
 exports[`trailing_comment.js format 1`] = `
 ====================================options=====================================
 parsers: ["babel", "flow", "typescript"]
diff --git a/tests/format/js/if/expr_and_same_line_comments.js b/tests/format/js/if/expr_and_same_line_comments.js
--- a/tests/format/js/if/expr_and_same_line_comments.js
+++ b/tests/format/js/if/expr_and_same_line_comments.js
@@ -28,9 +28,8 @@ function a() {
   if (a) throw e; /* comment 6d */
   else return 2;
 
-  // TODO[@fisker]: fix this
-  // if (a) var a = 1; /* comment 6e */
-  // else return 2;
+  if (a) var a = 1; /* comment 6e */
+  else return 2;
 
   if (a) if (b); /* comment 6f */
   else return 2;
diff --git a/tests/format/js/if/issue-15168.js b/tests/format/js/if/issue-15168.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/if/issue-15168.js
@@ -0,0 +1,9 @@
+for (const division of collidingDivisions) {
+	if (!division.properties.canRemove) {
+		if (division.canContainSingleParagraph(startParagraph)) continue; // selection starting in r/o div: always OK to delete
+		else if (endParagraph !== division.endParagraph && division.canContainSingleParagraph(endParagraph)) continue; // selection ending in r/o div: OK to delete IF it is NOT last paragraph (which would get absorbed)
+		else { // selection over whole r/o div OR ending in last paragraph (which would get absorbed) -> FAIL
+			console.warn('deleteSelectedTextAndJoinRemainingNodes(): cannot delete because of must-keep division: ' + division.properties.name);
+		}
+	}
+}
diff --git a/tests/format/js/if/non-block.js b/tests/format/js/if/non-block.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/if/non-block.js
@@ -0,0 +1,5 @@
+if (foo) for (i = 2; i > 0; i--) console.log(i); // foo
+else bar();
+
+if (foo) do { console.log(i) } while(i--) // foo
+else bar();