Repository: jlongster/prettier (js)

Issue:
Ternary comments are not stable
```js
var inspect = (4 === util.inspect.length ?
  // node <= 0.8.x
  function (v, colors) {
    return util.inspect(v, void 0, void 0, colors);
  } :
  // node > 0.8.x
  function (v, colors) {
    return util.inspect(v, { colors: colors });
  }
);
```

turns into

```js
var inspect = 4 === util.inspect.length
  ? // node <= 0.8.x
    (function(v, colors) {
      return util.inspect(v, void 0, void 0, colors);
    })
  : // node > 0.8.x
    (function(v, colors) {
      return util.inspect(v, { colors: colors });
    });
```

https://jlongster.github.io/prettier/#%7B%22content%22%3A%22var%20inspect%20%3D%20(4%20%3D%3D%3D%20util.inspect.length%20%3F%5Cn%20%20%2F%2F%20node%20%3C%3D%200.8.x%5Cn%20%20function%20(v%2C%20colors)%20%7B%5Cn%20%20%20%20return%20util.inspect(v%2C%20void%200%2C%20void%200%2C%20colors)%3B%5Cn%20%20%7D%20%3A%5Cn%20%20%2F%2F%20node%20%3E%200.8.x%5Cn%20%20function%20(v%2C%20colors)%20%7B%5Cn%20%20%20%20return%20util.inspect(v%2C%20%7B%20colors%3A%20colors%20%7D)%3B%5Cn%20%20%7D%5Cn)%3B%5Cn%5Cn%22%2C%22options%22%3A%7B%22printWidth%22%3A80%2C%22tabWidth%22%3A2%2C%22singleQuote%22%3Afalse%2C%22trailingComma%22%3Afalse%2C%22bracketSpacing%22%3Atrue%2C%22doc%22%3Afalse%7D%7D

which then turns into

```js
var inspect = 4 === util.inspect.length // node <= 0.8.x
  ? (function(v, colors) {
      return util.inspect(v, void 0, void 0, colors);
    }) // node > 0.8.x
  : (function(v, colors) {
      return util.inspect(v, { colors: colors });
    });
```

https://jlongster.github.io/prettier/#%7B%22content%22%3A%22var%20inspect%20%3D%204%20%3D%3D%3D%20util.inspect.length%5Cn%20%20%3F%20%2F%2F%20node%20%3C%3D%200.8.x%5Cn%20%20%20%20(function(v%2C%20colors)%20%7B%5Cn%20%20%20%20%20%20return%20util.inspect(v%2C%20void%200%2C%20void%200%2C%20colors)%3B%5Cn%20%20%20%20%7D)%5Cn%20%20%3A%20%2F%2F%20node%20%3E%200.8.x%5Cn%20%20%20%20(function(v%2C%20colors)%20%7B%5Cn%20%20%20%20%20%20return%20util.inspect(v%2C%20%7B%20colors%3A%20colors%20%7D)%3B%5Cn%20%20%20%20%7D)%3B%5Cn%22%2C%22options%22%3A%7B%22printWidth%22%3A80%2C%22tabWidth%22%3A2%2C%22singleQuote%22%3Afalse%2C%22trailingComma%22%3Afalse%2C%22bracketSpacing%22%3Atrue%2C%22doc%22%3Afalse%7D%7D

Pull request:
Stabilize comments inside of ternaries
Now, all the variations of comments in ternaries are printed the same way.

Fixes #617

Patch:
diff --git a/editors/jetbrains/README.md b/editors/jetbrains/README.md
index f75eddde7..1a7ae61f4 100644
--- a/editors/jetbrains/README.md
+++ b/editors/jetbrains/README.md
@@ -14,6 +14,6 @@ If on the other hand you have `prettier` installed locally, replace the **Progra
 
 ### Configure Keymap
 
-Now when you setup **External Tool** I guess you what to add hotkey for it. Go to *File | Settings | Keymap* for Windows and Linux *WebStorm | Preferences | Keymap* and type external tool name in search box.
+Now when you setup **External Tool** I guess you want to add hotkey for it. Go to *File | Settings | Keymap* for Windows and Linux *WebStorm | Preferences | Keymap* and type external tool name in search box.
 
 See [this documentation](https://www.jetbrains.com/help/webstorm/configuring-keyboard-shortcuts.html) about configuring keyboard shortcuts.
diff --git a/src/comments.js b/src/comments.js
index d97dae762..695326cde 100644
--- a/src/comments.js
+++ b/src/comments.js
@@ -157,9 +157,11 @@ function attach(comments, ast, text) {
         addDanglingComment(ast, comment);
       }
     } else if (util.hasNewline(text, locEnd(comment))) {
-      // There is content before this comment on the same line, but
-      // none after it, so prefer a trailing comment of the previous node.
-      if (precedingNode) {
+      if (handleConditionalExpressionComments(enclosingNode, followingNode, comment)) {
+        // We're good
+      } else if (precedingNode) {
+        // There is content before this comment on the same line, but
+        // none after it, so prefer a trailing comment of the previous node.
         addTrailingComment(precedingNode, comment);
       } else if (followingNode) {
         addLeadingComment(followingNode, comment);
@@ -170,12 +172,14 @@ function attach(comments, ast, text) {
         addDanglingComment(ast, comment);
       }
     } else {
-      // Otherwise, text exists both before and after the comment on
-      // the same line. If there is both a preceding and following
-      // node, use a tie-breaking algorithm to determine if it should
-      // be attached to the next or previous node. In the last case,
-      // simply attach the right node;
-      if (precedingNode && followingNode) {
+      if (handleIfStatementComments(enclosingNode, followingNode, comment)) {
+        // We're good
+      } else if (precedingNode && followingNode) {
+        // Otherwise, text exists both before and after the comment on
+        // the same line. If there is both a preceding and following
+        // node, use a tie-breaking algorithm to determine if it should
+        // be attached to the next or previous node. In the last case,
+        // simply attach the right node;
         const tieCount = tiesToBreak.length;
         if (tieCount > 0) {
           var lastTie = tiesToBreak[tieCount - 1];
@@ -367,6 +371,14 @@ function handleMemberExpressionComment(enclosingNode, followingNode, comment) {
   return false;
 }
 
+function handleConditionalExpressionComments(enclosingNode, followingNode, comment) {
+  if (enclosingNode && enclosingNode.type === 'ConditionalExpression' && followingNode) {
+    addLeadingComment(followingNode, comment);
+    return true;
+  }
+  return false;
+}
+
 function printComment(commentPath) {
   const comment = commentPath.getValue();
 
diff --git a/src/doc-debug.js b/src/doc-debug.js
index efcf9d20d..e1fc15671 100644
--- a/src/doc-debug.js
+++ b/src/doc-debug.js
@@ -92,6 +92,12 @@ function printDoc(doc) {
       printDoc(doc.contents) +
       ")";
   }
+
+  if (doc.type === "line-suffix") {
+    return "lineSuffix(" + printDoc(doc.contents) + ")";
+  }
+
+  throw new Error('Unknown doc type ' + doc.type);
 }
 
 module.exports = {
diff --git a/src/printer.js b/src/printer.js
index e78ec9718..81ccc2ed5 100644
--- a/src/printer.js
+++ b/src/printer.js
@@ -684,13 +684,13 @@ function genericPrintNoParens(path, options, print) {
           parts.push(concat([": ", printedValue]));
         } else {
           parts.push(
-            concat([
+            group(concat([
               ":",
               ifBreak(" (", " "),
               indent(options.tabWidth, concat([softline, printedValue])),
               softline,
               ifBreak(")")
-            ])
+            ]))
           );
         }
       }
@@ -712,7 +712,14 @@ function genericPrintNoParens(path, options, print) {
     case "ArrayPattern":
       if (n.elements.length === 0) {
         parts.push(
-          concat(["[", comments.printDanglingComments(path, options), "]"])
+          group(
+            concat([
+              "[",
+              comments.printDanglingComments(path, options),
+              softline,
+              "]"
+            ])
+          )
         );
       } else {
         const lastElem = util.getLast(n.elements);
@@ -2224,22 +2231,23 @@ function printMemberChain(path, options, print) {
   // node is just an identifier with the name starting with a capital
   // letter or just a sequence of _$. The rationale is that they are
   // likely to be factories.
-  if (
+  const shouldMerge =
     groups[0].length === 1 &&
-      groups[0][0].node.type === "Identifier" &&
-      groups[0][0].node.name.match(/(^[A-Z])|^[_$]+$/) &&
-      groups.length >= 2
-  ) {
-    // Push all the values of groups[0] at the beginning of groups[1]
-    [].unshift.apply(groups[1], groups[0]);
-    // Remove groups[0]
-    groups.splice(0, 1);
-  }
+    groups[0][0].node.type === "Identifier" &&
+    groups[0][0].node.name.match(/(^[A-Z])|^[_$]+$/) &&
+    groups.length >= 2;
 
   function printGroup(printedGroup) {
     return concat(printedGroup.map(tuple => tuple.printed));
   }
 
+  function printIndentedGroup(groups, lineType) {
+    return indent(
+      options.tabWidth,
+      group(concat([lineType, join(lineType, groups.map(printGroup))]))
+    );
+  }
+
   const printedGroups = groups.map(printGroup);
 
   const oneLine = concat(printedGroups);
@@ -2252,10 +2260,8 @@ function printMemberChain(path, options, print) {
 
   const expanded = concat([
     printGroup(groups[0]),
-    indent(
-      options.tabWidth,
-      group(concat([hardline, join(hardline, groups.slice(1).map(printGroup))]))
-    )
+    shouldMerge ? printIndentedGroup(groups.slice(1, 2), softline) : "",
+    printIndentedGroup(groups.slice(shouldMerge ? 2 : 1), hardline)
   ]);
 
   // If any group but the last one has a hard line, we want to force expand


Test patch:
diff --git a/tests/comments/__snapshots__/jsfmt.spec.js.snap b/tests/comments/__snapshots__/jsfmt.spec.js.snap
index 063b75f2d..b6c036234 100644
--- a/tests/comments/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/comments/__snapshots__/jsfmt.spec.js.snap
@@ -74,6 +74,32 @@ declare class Foo extends Qux<string> {/* dangling */}
 "
 `;
 
+exports[`test dangling_array.js 1`] = `
+"expect(() => {}).toTriggerReadyStateChanges([
+  // Nothing.
+]);
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+expect(() => {}).toTriggerReadyStateChanges(
+  [
+    // Nothing.
+  ]
+);
+"
+`;
+
+exports[`test dangling_array.js 2`] = `
+"expect(() => {}).toTriggerReadyStateChanges([
+  // Nothing.
+]);
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+expect(() => {}).toTriggerReadyStateChanges(
+  [
+    // Nothing.
+  ]
+);
+"
+`;
+
 exports[`test first-line.js 1`] = `
 "a // comment
 b
diff --git a/tests/comments/dangling_array.js b/tests/comments/dangling_array.js
new file mode 100644
index 000000000..599897d57
--- /dev/null
+++ b/tests/comments/dangling_array.js
@@ -0,0 +1,3 @@
+expect(() => {}).toTriggerReadyStateChanges([
+  // Nothing.
+]);
diff --git a/tests/conditional/__snapshots__/jsfmt.spec.js.snap b/tests/conditional/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000..688b10956
--- /dev/null
+++ b/tests/conditional/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,71 @@
+exports[`test comments.js 1`] = `
+"var inspect = 4 === util.inspect.length
+  ? // node <= 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, void 0, void 0, colors);
+    })
+  : // node > 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, { colors: colors });
+    });
+
+var inspect = 4 === util.inspect.length
+  ? // node <= 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, void 0, void 0, colors);
+    })
+  : // node > 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, { colors: colors });
+    });
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths
+  // Making sure that the publicPath goes back to to build folder.
+  ? { publicPath: Array(cssFilename.split(\'/\').length).join(\'../\') } :
+  {};
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths
+  ? // Making sure that the publicPath goes back to to build folder.
+    { publicPath: Array(cssFilename.split(\"/\").length).join(\"../\") }
+  : {};
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths // Making sure that the publicPath goes back to to build folder.
+  ? { publicPath: Array(cssFilename.split(\"/\").length).join(\"../\") }
+  : {};
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+var inspect = 4 === util.inspect.length
+  ? // node <= 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, void 0, void 0, colors);
+    })
+  : // node > 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, { colors: colors });
+    });
+
+var inspect = 4 === util.inspect.length
+  ? // node <= 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, void 0, void 0, colors);
+    })
+  : // node > 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, { colors: colors });
+    });
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths
+  ? // Making sure that the publicPath goes back to to build folder.
+    { publicPath: Array(cssFilename.split(\"/\").length).join(\"../\") }
+  : {};
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths
+  ? // Making sure that the publicPath goes back to to build folder.
+    { publicPath: Array(cssFilename.split(\"/\").length).join(\"../\") }
+  : {};
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths
+  ? // Making sure that the publicPath goes back to to build folder.
+    { publicPath: Array(cssFilename.split(\"/\").length).join(\"../\") }
+  : {};
+"
+`;
diff --git a/tests/conditional/comments.js b/tests/conditional/comments.js
new file mode 100644
index 000000000..78e9a024f
--- /dev/null
+++ b/tests/conditional/comments.js
@@ -0,0 +1,33 @@
+var inspect = 4 === util.inspect.length
+  ? // node <= 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, void 0, void 0, colors);
+    })
+  : // node > 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, { colors: colors });
+    });
+
+var inspect = 4 === util.inspect.length
+  ? // node <= 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, void 0, void 0, colors);
+    })
+  : // node > 0.8.x
+    (function(v, colors) {
+      return util.inspect(v, { colors: colors });
+    });
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths
+  // Making sure that the publicPath goes back to to build folder.
+  ? { publicPath: Array(cssFilename.split('/').length).join('../') } :
+  {};
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths
+  ? // Making sure that the publicPath goes back to to build folder.
+    { publicPath: Array(cssFilename.split("/").length).join("../") }
+  : {};
+
+const extractTextPluginOptions = shouldUseRelativeAssetPaths // Making sure that the publicPath goes back to to build folder.
+  ? { publicPath: Array(cssFilename.split("/").length).join("../") }
+  : {};
diff --git a/tests/conditional/jsfmt.spec.js b/tests/conditional/jsfmt.spec.js
new file mode 100644
index 000000000..989047bcc
--- /dev/null
+++ b/tests/conditional/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(__dirname);
diff --git a/tests/if_comments/__snapshots__/jsfmt.spec.js.snap b/tests/if_comments/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000..569510378
--- /dev/null
+++ b/tests/if_comments/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,73 @@
+exports[`test if_comments.js 1`] = `
+"async function f() {
+  if (untrackedChoice === 0) /* Cancel */ {
+    return null;
+  } else if (untrackedChoice === 1) /* Add */ {
+    await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) /* Allow Untracked */ {
+    allowUntracked = true;
+  }
+}
+
+async function f() {
+  if (untrackedChoice === 0)
+    /* Cancel */ {
+      return null;
+    }
+  else if (untrackedChoice === 1)
+    /* Add */ {
+      await repository.addAll(Array.from(untrackedChanges.keys()));
+      shouldAmend = true;
+    }
+  else if (untrackedChoice === 2)
+    /* Allow Untracked */ {
+      allowUntracked = true;
+    }
+}
+
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
+
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
+
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
+"
+`;
diff --git a/tests/if_comments/if_comments.js b/tests/if_comments/if_comments.js
new file mode 100644
index 000000000..2b3552999
--- /dev/null
+++ b/tests/if_comments/if_comments.js
@@ -0,0 +1,37 @@
+async function f() {
+  if (untrackedChoice === 0) /* Cancel */ {
+    return null;
+  } else if (untrackedChoice === 1) /* Add */ {
+    await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) /* Allow Untracked */ {
+    allowUntracked = true;
+  }
+}
+
+async function f() {
+  if (untrackedChoice === 0)
+    /* Cancel */ {
+      return null;
+    }
+  else if (untrackedChoice === 1)
+    /* Add */ {
+      await repository.addAll(Array.from(untrackedChanges.keys()));
+      shouldAmend = true;
+    }
+  else if (untrackedChoice === 2)
+    /* Allow Untracked */ {
+      allowUntracked = true;
+    }
+}
+
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
diff --git a/tests/if_comments/jsfmt.spec.js b/tests/if_comments/jsfmt.spec.js
new file mode 100644
index 000000000..989047bcc
--- /dev/null
+++ b/tests/if_comments/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(__dirname);
diff --git a/tests/method-chain/__snapshots__/jsfmt.spec.js.snap b/tests/method-chain/__snapshots__/jsfmt.spec.js.snap
index 86427b531..daa999035 100644
--- a/tests/method-chain/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/method-chain/__snapshots__/jsfmt.spec.js.snap
@@ -115,6 +115,41 @@ it(\"should group messages with same created time\", () => {
 "
 `;
 
+exports[`test first_long.js 1`] = `
+"export default function theFunction(action$, store) {
+  return action$.ofType(THE_ACTION).switchMap(action => Observable
+    .webSocket({
+      url: THE_URL,
+      more: stuff(),
+      evenMore: stuff({
+        value1: true,
+        value2: false,
+        value3: false
+      })
+    })
+    .filter(data => theFilter(data))
+    .map(({ theType, ...data }) => theMap(theType, data))
+    .retryWhen(errors => errors));
+}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+export default function theFunction(action$, store) {
+  return action$.ofType(THE_ACTION).switchMap(action => Observable
+    .webSocket({
+      url: THE_URL,
+      more: stuff(),
+      evenMore: stuff({
+        value1: true,
+        value2: false,
+        value3: false
+      })
+    })
+    .filter(data => theFilter(data))
+    .map(({ theType, ...data }) => theMap(theType, data))
+    .retryWhen(errors => errors));
+}
+"
+`;
+
 exports[`test multiple-members.js 1`] = `
 "if (testConfig.ENABLE_ONLINE_TESTS === \"true\") {
   describe(\"POST /users/me/pet\", function() {
diff --git a/tests/method-chain/first_long.js b/tests/method-chain/first_long.js
new file mode 100644
index 000000000..ced9a2035
--- /dev/null
+++ b/tests/method-chain/first_long.js
@@ -0,0 +1,15 @@
+export default function theFunction(action$, store) {
+  return action$.ofType(THE_ACTION).switchMap(action => Observable
+    .webSocket({
+      url: THE_URL,
+      more: stuff(),
+      evenMore: stuff({
+        value1: true,
+        value2: false,
+        value3: false
+      })
+    })
+    .filter(data => theFilter(data))
+    .map(({ theType, ...data }) => theMap(theType, data))
+    .retryWhen(errors => errors));
+}
diff --git a/tests/object-prop-break-in/__snapshots__/jsfmt.spec.js.snap b/tests/object-prop-break-in/__snapshots__/jsfmt.spec.js.snap
index 1bc4417f6..cb925d852 100644
--- a/tests/object-prop-break-in/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/object-prop-break-in/__snapshots__/jsfmt.spec.js.snap
@@ -1,3 +1,20 @@
+exports[`test comment.js 1`] = `
+"function foo() {
+  return {
+    // this comment causes the problem
+    bar: baz() + 1
+  };
+}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+function foo() {
+  return {
+    // this comment causes the problem
+    bar: baz() + 1
+  };
+}
+"
+`;
+
 exports[`test test.js 1`] = `
 "const a = classnames({
   \"some-prop\": this.state.longLongLongLongLongLongLongLongLongTooLongProp
diff --git a/tests/object-prop-break-in/comment.js b/tests/object-prop-break-in/comment.js
new file mode 100644
index 000000000..470a85819
--- /dev/null
+++ b/tests/object-prop-break-in/comment.js
@@ -0,0 +1,6 @@
+function foo() {
+  return {
+    // this comment causes the problem
+    bar: baz() + 1
+  };
+}