Repository: prettier/prettier (JavaScript)

Issue:
Nestled JSDoc comments are still broken after #9672
**Prettier 2.6.2**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEAzCFMC8mAOiAEICGATqQNzFQMD0AVCw5i5gALwC2ABwA2FeJgAqHLtwHUKfTMHEBPAXAC+mGKrhSesqvMXjNANwpCArrqiceVODEtVbwAGoXr6qU1btb0gZGSjqa2mp63A5OLooeVho+DNiWUGAwAJbQmOYJABThcAA0OZ5wAJSKHJgZ2Jh51ADmlnwIMGgAdEIIjTAAFoQERABMlcDVmJjRzrZQcADumADC0GDRcPHWBToludbl9LaYmnBCaHBVR5PTsXOLm3DbagfV3lBvIEUgEAKZ6MigahUCDzAAK1AQaGQIAs8woyihXwARoYwABrRwAZXkcAAMhk5shsBZzsjURiYJjZGACY1kDAqNYvnA+Ei4AATdkc3EUKDNCiNOAAMQgVD4okyfOhFEsMAgnxAfRgfCEAHU+hl4GhqXBMZDNRlTJrlNCwGhESACecqDBQYZGuKiSTiiAAFZoAAemNp3QAipYIPAnWcXQZrdCkRQ2UIFQIqASYKqMuz+sgABwABi+cYg51VhgE0LjcGtpjgCoAjgH4HaflCUBQ0ABaOYcjkKhxVjIOO0Cx1IYkhr7nPgZemMl1oH1wf2B8sD51fGBRpMpvrIYZLwwZIS0lZ8fsgEsAVgVlnO4ij9cHpJApmsAEkoFzYJi1hlfgBBZ+Y7TdYPnOo6hAA)
<!-- prettier-ignore -->
```sh
--parser babel
```

**Input:**
<!-- prettier-ignore -->
```jsx
const foo = "Bar";

/**
 * @template T
 * @param {Type} type
 * @param {T} value
 * @return {Value}
 *//**
 * @param {Type} type
 * @return {Value}
 */
function value(type, value) {
  if (arguments.length === 2) {
    return new ConcreteValue(type, value);
  } else {
    return new Value(type);
  }
}

```

**Output:**
<!-- prettier-ignore -->
```jsx
const foo = "Bar";

/**
 * @template T
 * @param {Type} type
 * @param {T} value
 * @return {Value}
 */ /**
 * @param {Type} type
 * @return {Value}
 */
function value(type, value) {
  if (arguments.length === 2) {
    return new ConcreteValue(type, value);
  } else {
    return new Value(type);
  }
}

```

**Expected behavior:**

As reported in #7724, JSDoc function overloads require two comment blocks "nestled" together.  When that issue was reported, "nestled" comments would have a blank line inserted between them, breaking the JSDoc parser.  That issue was closed via #9672.  Now, instead of inserting a blank line, Prettier inserts a single space, turning `*//**` into `*/ /**`.  This still breaks the JSDoc parser.  The sequence must be exactly `*//**`, and it must not be modified by Prettier at all.

/cc @fisker, who wrote the linked PR, and @grassator who reported the previous issue.


Hints:

Patch:
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -321,6 +321,7 @@ module.exports = {
     },
     {
       files: ["src/language-js/**/*.js"],
+      excludedFiles: ["src/language-js/parse/postprocess/*.js"],
       rules: {
         "prettier-internal-rules/no-node-comments": [
           "error",
@@ -329,7 +330,6 @@ module.exports = {
             functions: ["hasComment", "getComments"],
           },
           "src/language-js/pragma.js",
-          "src/language-js/parse/postprocess/*.js",
           "src/language-js/parse/babel.js",
           "src/language-js/parse/meriyah.js",
           "src/language-js/parse/json.js",
diff --git a/src/language-js/parse/postprocess/index.js b/src/language-js/parse/postprocess/index.js
--- a/src/language-js/parse/postprocess/index.js
+++ b/src/language-js/parse/postprocess/index.js
@@ -2,6 +2,9 @@ import { locStart, locEnd } from "../../loc.js";
 import isTsKeywordType from "../../utils/is-ts-keyword-type.js";
 import isTypeCastComment from "../../utils/is-type-cast-comment.js";
 import getLast from "../../../utils/get-last.js";
+import isNonEmptyArray from "../../../utils/is-non-empty-array.js";
+import isBlockComment from "../../utils/is-block-comment.js";
+import isIndentableBlockComment from "../../utils/is-indentable-block-comment.js";
 import visitNode from "./visit-node.js";
 import { throwErrorForInvalidNodes } from "./typescript.js";
 import throwSyntaxError from "./throw-ts-syntax-error.js";
@@ -154,6 +157,25 @@ function postprocess(ast, options) {
     }
   });
 
+  if (isNonEmptyArray(ast.comments)) {
+    let followingComment = getLast(ast.comments);
+    for (let i = ast.comments.length - 2; i >= 0; i--) {
+      const comment = ast.comments[i];
+      if (
+        locEnd(comment) === locStart(followingComment) &&
+        isBlockComment(comment) &&
+        isBlockComment(followingComment) &&
+        isIndentableBlockComment(comment) &&
+        isIndentableBlockComment(followingComment)
+      ) {
+        ast.comments.splice(i + 1, 1);
+        comment.value += "*//*" + followingComment.value;
+        comment.range = [locStart(comment), locEnd(followingComment)];
+      }
+      followingComment = comment;
+    }
+  }
+
   return ast;
 
   /**
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
@@ -4,6 +4,7 @@ import { replaceEndOfLine } from "../../document/utils.js";
 import { isLineComment } from "../utils/index.js";
 import { locStart, locEnd } from "../loc.js";
 import isBlockComment from "../utils/is-block-comment.js";
+import isIndentableBlockComment from "../utils/is-indentable-block-comment.js";
 
 function printComment(commentPath, options) {
   const comment = commentPath.getValue();
@@ -34,15 +35,6 @@ function printComment(commentPath, options) {
   throw new Error("Not a comment: " + JSON.stringify(comment));
 }
 
-function isIndentableBlockComment(comment) {
-  // If the comment has multiple lines and every line starts with a star
-  // we can fix the indentation of each line. The stars in the `/*` and
-  // `*/` delimiters are not included in the comment value, so add them
-  // back first.
-  const lines = `*${comment.value}*`.split("\n");
-  return lines.length > 1 && lines.every((line) => line.trim()[0] === "*");
-}
-
 function printIndentableBlockComment(comment) {
   const lines = comment.value.split("\n");
 
@@ -60,4 +52,4 @@ function printIndentableBlockComment(comment) {
   ];
 }
 
-export { printComment };
+export { printComment, isIndentableBlockComment };
diff --git a/src/language-js/utils/is-indentable-block-comment.js b/src/language-js/utils/is-indentable-block-comment.js
new file mode 100644
--- /dev/null
+++ b/src/language-js/utils/is-indentable-block-comment.js
@@ -0,0 +1,10 @@
+function isIndentableBlockComment(comment) {
+  // If the comment has multiple lines and every line starts with a star
+  // we can fix the indentation of each line. The stars in the `/*` and
+  // `*/` delimiters are not included in the comment value, so add them
+  // back first.
+  const lines = `*${comment.value}*`.split("\n");
+  return lines.length > 1 && lines.every((line) => line.trimStart()[0] === "*");
+}
+
+export default isIndentableBlockComment;


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
@@ -3144,6 +3144,230 @@ function HelloWorld() {
 ================================================================================
 `;
 
+exports[`jsdoc-nestled.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+const issues = {
+  see: "#7724 and #12653"
+  /** Trailing comment 1 (not nestled as both comments should be multiline for that) *//**
+  * Trailing comment 2
+  */
+};
+
+/**
+ * @template T
+ * @param {Type} type
+ * @param {T} value
+ * @return {Value}
+ *//**
+ * @param {Type} type
+ * @return {Value}
+ */
+function value(type, value) {
+  if (arguments.length === 2) {
+    return new ConcreteValue(type, value);
+  } else {
+    return new Value(type);
+  }
+}
+
+/** Trailing nestled comment 1
+ *//** Trailing nestled comment 2
+ *//** Trailing nestled comment 3
+ */
+
+=====================================output=====================================
+const issues = {
+  see: "#7724 and #12653",
+  /** Trailing comment 1 (not nestled as both comments should be multiline for that) */ /**
+   * Trailing comment 2
+   */
+}
+
+/**
+ * @template T
+ * @param {Type} type
+ * @param {T} value
+ * @return {Value}
+ *//**
+ * @param {Type} type
+ * @return {Value}
+ */
+function value(type, value) {
+  if (arguments.length === 2) {
+    return new ConcreteValue(type, value)
+  } else {
+    return new Value(type)
+  }
+}
+
+/** Trailing nestled comment 1
+ *//** Trailing nestled comment 2
+ *//** Trailing nestled comment 3
+ */
+
+================================================================================
+`;
+
+exports[`jsdoc-nestled.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+const issues = {
+  see: "#7724 and #12653"
+  /** Trailing comment 1 (not nestled as both comments should be multiline for that) *//**
+  * Trailing comment 2
+  */
+};
+
+/**
+ * @template T
+ * @param {Type} type
+ * @param {T} value
+ * @return {Value}
+ *//**
+ * @param {Type} type
+ * @return {Value}
+ */
+function value(type, value) {
+  if (arguments.length === 2) {
+    return new ConcreteValue(type, value);
+  } else {
+    return new Value(type);
+  }
+}
+
+/** Trailing nestled comment 1
+ *//** Trailing nestled comment 2
+ *//** Trailing nestled comment 3
+ */
+
+=====================================output=====================================
+const issues = {
+  see: "#7724 and #12653",
+  /** Trailing comment 1 (not nestled as both comments should be multiline for that) */ /**
+   * Trailing comment 2
+   */
+};
+
+/**
+ * @template T
+ * @param {Type} type
+ * @param {T} value
+ * @return {Value}
+ *//**
+ * @param {Type} type
+ * @return {Value}
+ */
+function value(type, value) {
+  if (arguments.length === 2) {
+    return new ConcreteValue(type, value);
+  } else {
+    return new Value(type);
+  }
+}
+
+/** Trailing nestled comment 1
+ *//** Trailing nestled comment 2
+ *//** Trailing nestled comment 3
+ */
+
+================================================================================
+`;
+
+exports[`jsdoc-nestled-dangling.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+{{{{{{{
+o={
+  /**
+   * A
+   *//**
+   * B
+   */
+
+}
+}}}}}}}
+
+=====================================output=====================================
+{
+  {
+    {
+      {
+        {
+          {
+            {
+              o = {
+                /**
+                 * A
+                 *//**
+                 * B
+                 */
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+================================================================================
+`;
+
+exports[`jsdoc-nestled-dangling.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+{{{{{{{
+o={
+  /**
+   * A
+   *//**
+   * B
+   */
+
+}
+}}}}}}}
+
+=====================================output=====================================
+{
+  {
+    {
+      {
+        {
+          {
+            {
+              o = {
+                /**
+                 * A
+                 *//**
+                 * B
+                 */
+              };
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+================================================================================
+`;
+
 exports[`jsx.js - {"semi":false} format 1`] = `
 ====================================options=====================================
 parsers: ["babel", "flow", "typescript"]
diff --git a/tests/format/js/comments/jsdoc-nestled-dangling.js b/tests/format/js/comments/jsdoc-nestled-dangling.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/comments/jsdoc-nestled-dangling.js
@@ -0,0 +1,10 @@
+{{{{{{{
+o={
+  /**
+   * A
+   *//**
+   * B
+   */
+
+}
+}}}}}}}
diff --git a/tests/format/js/comments/issue-7724.js b/tests/format/js/comments/jsdoc-nestled.js
similarity index 50%
rename from tests/format/js/comments/issue-7724.js
rename to tests/format/js/comments/jsdoc-nestled.js
--- a/tests/format/js/comments/issue-7724.js
+++ b/tests/format/js/comments/jsdoc-nestled.js
@@ -1,4 +1,9 @@
-const foo = "Bar";
+const issues = {
+  see: "#7724 and #12653"
+  /** Trailing comment 1 (not nestled as both comments should be multiline for that) *//**
+  * Trailing comment 2
+  */
+};
 
 /**
  * @template T
@@ -15,4 +20,9 @@ function value(type, value) {
   } else {
     return new Value(type);
   }
-}
\ No newline at end of file
+}
+
+/** Trailing nestled comment 1
+ *//** Trailing nestled comment 2
+ *//** Trailing nestled comment 3
+ */