Repository: prettier/prettier (JavaScript)

Issue:
(idempotence violation) Empty line after parenthesized array item
**Prettier 2.7.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBtAOlABFghlgXiwCMAaTHCrMLAelswF0RSQIAHGAS2gGdlQuAE5CIAdwAKwhPxS4ANmNwBPfq2JDcYANZwYAZVwBbOABkuUOMgBmC3nHWade-ey0WA5shhCArg5A4I2I4ABNQsNNcKA9fXA84ADEIISNcGG4Y5BBcXxgIFhAACxgjeQB1Iq54XjcwOH0Zaq4AN2rlbLBeNRALeyEYCU0PNJs7AIArXgAPfU95OABFXwh4Mfl7VjchfuziXBD5QvYhCxhyrlCYIuQADgAGLdF7cs12bJO4fparVgBHFbwIYcWQ5XgAWksYTChSEcABXDhQ3ioyQtg2AXsRi43j8mPmS0BVjR41YMAOFyuNyQACYyZouPJPABhCBGVGBXgAVkKvnsABUDrJ0ZsQC1-ABJKARWD6MCnTgAQWl+hgygW63sAF8tUA)
<!-- prettier-ignore -->
```sh
--parser babel
```

**Input:**
<!-- prettier-ignore -->
```jsx
[
  a = b,
  
  c //
]
```

**Output:**
<!-- prettier-ignore -->
```jsx
[
  (a = b),

  c, //
];

```

**Second Output:**
<!-- prettier-ignore -->
```jsx
[
  (a = b),
  c, //
];

```


Hints:
I haven't been able to replicate this. I'm on macOS and ran a script to compare the output 1000 times, and they were all identical.

it may be difficult to determine the cause without more information

@solarized-fox the issue is to do with the second output not being equal to the first output, which means that idempotency is violated. This is still true for the latest Prettier:

**Prettier 2.8.7**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBtAOlABFghlgXiwCMAaTHCrMLAelswF0RSQIAHGAS2gGdlQuAE5CIAdwAKwhPxS4ANmNwBPfq2JDcYANZwYAZVwBbOABkuUOMgBmC3nHWade-ey0WA5shhCArg5A4I2I4ABNQsNNcKA9fXA84ADEIISNcGG4Y5BBcXxgIFhAACxgjeQB1Iq54XjcwOH0Zaq4AN2rlbLBeNRALeyEYCU0PNJs7AIArXgAPfU95OABFXwh4Mfl7VjchfuziXBD5QvYhCxhyrlCYIuQADgAGLdF7cs12bJO4fparVgBHFbwIYcWQ5XgAWksYTChSEcABXDhQ3ioyQtg2AXsRi43j8mPmS0BVjR41YMAOFyuNyQACYyZouPJPABhCBGVGBXgAVkKvnsABUDrJ0ZsQC1-ABJKARWD6MCnTgAQWl+hgygW63sAF8tUA)
<!-- prettier-ignore -->
```sh
--parser babel
```

**Input:**
<!-- prettier-ignore -->
```jsx
[
  a = b,
  
  c //
]
```

**Output:**
<!-- prettier-ignore -->
```jsx
[
  (a = b),

  c, //
];

```

**Second Output:**
<!-- prettier-ignore -->
```jsx
[
  (a = b),
  c, //
];

```

I checked Prettier `3.0.0-alpha.6` locally, got the same results.
hmm, what OS are you on? I'm running macOS 12.6.x, and this script:

```
echo "[
  a = b,

  c //
]" > foo.js

git checkout 3.0.0-alpha.6
printf "$(yarn prettier --parser babel foo.js)\n"
printf "$(yarn prettier --parser babel foo.js)\n"
```

produced this output on my machine:

```
HEAD is now at 432976de1 Release 3.0.0-alpha.6
[
  (a = b),

  c, //
];
[
  (a = b),

  c, //
];
```

could it be possible the issue is connected to the environment somehow?
I’m on macOS too, yep. Can you try adding `--write` to `prettier` commands and see what happens? By default, Prettier does not save the results of formatting so you get:

```
source → result 1
source → result 1
```

instead of

```
source → result 1
result 1 → result 2
```

If the idempotency principle is followed, we should get:

```
source → result 1
result 1 → result 1
result 1 → result 1
... ∞ ...
```

ohhh I understand, the second output is supposed to use the input of the first. this gives me a few ideas, I’ll see if I can’t track this bug down
I believe I've identified the problem!

the node representing `a = b,` in the first input has a range ending on the character `,` while the node representing `(a = b),` has a range ending on the character `)` for some reason. the code tries to skip to the end of the line by passing over the characters `,; \t` of which `)` is not one

it seems to me that the problem is with the range assigned to the node in the first place. for what it's worth, the actual location where the newline is omitted on the second output is [line 181 in array.js](https://github.com/prettier/prettier/blob/b232005022b063a7a3b8da2c87b94281a70188bb/src/language-js/print/array.js#L181), where a ternary evaluates differently between the two inputs

incidentally since the problem seems to lie simply with the parens, I'm fairly confident that this issue can be boiled down to "these two statements should format the same":

```
_ = [
  a = b,

  c //
]

_ = [
  (a = b),

  c, //
];
```

I'll try and see if I can find a solution but the only part of the codebase I've really read so far is language-js/print so if the problem lies outside of that area (as I suspect it might) this might take me a bit
okay after looking into it it appears that parens are just not (or not always) part of the character range covered by their respective node. this means that the last node on a line could be followed by a ")". so it seems the correct thing to do is just modify skipToLineEnd to also ignore ")".

yay one character diff, I'll get a PR ready!
@solarized-fox  there will be comments, more parens

```js
[
  (((((
    a = b/* comment */))/* comment */))),
  
  c //
]
```
@fisker adding that to the tests, although it seems the code already accounts for that. it's just the parens that trip it up, apparently

Patch:
diff --git a/src/language-js/print/array.js b/src/language-js/print/array.js
--- a/src/language-js/print/array.js
+++ b/src/language-js/print/array.js
@@ -9,17 +9,19 @@ import {
   fill,
 } from "../../document/builders.js";
 import hasNewline from "../../utils/has-newline.js";
+import isNextLineEmptyAfterIndex from "../../utils/is-next-line-empty.js";
+import skipInlineComment from "../../utils/skip-inline-comment.js";
+import skipTrailingComment from "../../utils/skip-trailing-comment.js";
 import {
   shouldPrintComma,
   hasComment,
   CommentCheckFlags,
-  isNextLineEmpty,
   isNumericLiteral,
   isSignedNumericLiteral,
   isArrayOrTupleExpression,
   isObjectOrRecordExpression,
 } from "../utils/index.js";
-import { locStart } from "../loc.js";
+import { locStart, locEnd } from "../loc.js";
 
 import { printOptionalToken } from "./misc.js";
 import { printTypeAnnotationProperty } from "./type-annotation.js";
@@ -170,6 +172,18 @@ function isConciselyPrintedArray(node, options) {
   );
 }
 
+function isLineAfterElementEmpty({ node }, { originalText: text }) {
+  const skipComment = (idx) =>
+    skipInlineComment(text, skipTrailingComment(text, idx));
+
+  const skipToComma = (currentIdx) =>
+    text[currentIdx] === ","
+      ? currentIdx
+      : skipToComma(skipComment(currentIdx + 1));
+
+  return isNextLineEmptyAfterIndex(text, skipToComma(locEnd(node)));
+}
+
 function printArrayElements(path, options, elementsProperty, print) {
   const parts = [];
 
@@ -180,7 +194,7 @@ function printArrayElements(path, options, elementsProperty, print) {
       parts.push([
         ",",
         line,
-        node && isNextLineEmpty(node, options) ? softline : "",
+        node && isLineAfterElementEmpty(path, options) ? softline : "",
       ]);
     }
   }, elementsProperty);
@@ -191,12 +205,12 @@ function printArrayElements(path, options, elementsProperty, print) {
 function printArrayElementsConcisely(path, options, print, trailingComma) {
   const parts = [];
 
-  path.each(({ node, isLast, next }) => {
+  path.each(({ isLast, next }) => {
     parts.push([print(), isLast ? trailingComma : ","]);
 
     if (!isLast) {
       parts.push(
-        isNextLineEmpty(node, options)
+        isLineAfterElementEmpty(path, options)
           ? [hardline, hardline]
           : hasComment(next, CommentCheckFlags.Leading | CommentCheckFlags.Line)
           ? hardline


Test patch:
diff --git a/tests/format/js/arrays/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/arrays/__snapshots__/jsfmt.spec.js.snap
--- a/tests/format/js/arrays/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/js/arrays/__snapshots__/jsfmt.spec.js.snap
@@ -844,6 +844,87 @@ a = [
 
 ]
 
+b = [
+  100,
+
+  (200)
+  ,
+
+  300
+  ,
+
+  1
+  ,
+  2, 3
+]
+
+c = [
+  "apple",
+  "banana",
+  "blueberry",
+
+  "red",
+  "blue"
+  ,
+  "yellow",
+
+  "broccoli",
+  "celery",
+  "lettuce"
+  ,
+
+  "green"
+  ,
+  "green",
+  "green",
+
+  //an egg
+  "egg",
+  //a bigger egg
+  "big egg"
+  //the biggest egg
+  ,
+  "huge egg"
+  ,
+
+  //not an egg
+  "lasagna"
+
+]
+
+_ = [
+  a,
+
+  b //
+]
+
+_ = [
+  (a),
+
+  b, //
+];
+
+_ = [
+  (((((
+    a = b/* comment */))/* comment */))),
+
+  c //
+]
+
+_ = [
+  (((((
+    (a = b)/* comment */))/* comment */))),
+
+  c //
+]
+
+_ = [
+  (a=b
+
+  ),
+  b, //
+];
+
 =====================================output=====================================
 a = [
   1, 2,
@@ -853,6 +934,73 @@ a = [
   4,
 ];
 
+b = [
+  100,
+
+  200,
+
+  300,
+
+  1, 2, 3,
+];
+
+c = [
+  "apple",
+  "banana",
+  "blueberry",
+
+  "red",
+  "blue",
+  "yellow",
+
+  "broccoli",
+  "celery",
+  "lettuce",
+
+  "green",
+  "green",
+  "green",
+
+  //an egg
+  "egg",
+  //a bigger egg
+  "big egg",
+  //the biggest egg
+  "huge egg",
+
+  //not an egg
+  "lasagna",
+];
+
+_ = [
+  a,
+
+  b, //
+];
+
+_ = [
+  a,
+
+  b, //
+];
+
+_ = [
+  (a = b) /* comment */ /* comment */,
+
+  c, //
+];
+
+_ = [
+  (a = b) /* comment */ /* comment */,
+
+  c, //
+];
+
+_ = [
+  (a = b),
+  b, //
+];
+
 ================================================================================
 `;
 
diff --git a/tests/format/js/arrays/preserve_empty_lines.js b/tests/format/js/arrays/preserve_empty_lines.js
--- a/tests/format/js/arrays/preserve_empty_lines.js
+++ b/tests/format/js/arrays/preserve_empty_lines.js
@@ -10,3 +10,84 @@ a = [
 
 
 ]
+
+b = [
+  100,
+
+  (200)
+  ,
+
+  300
+  ,
+
+  1
+  ,
+  2, 3
+]
+
+c = [
+  "apple",
+  "banana",
+  "blueberry",
+
+  "red",
+  "blue"
+  ,
+  "yellow",
+
+  "broccoli",
+  "celery",
+  "lettuce"
+  ,
+
+  "green"
+  ,
+  "green",
+  "green",
+
+  //an egg
+  "egg",
+  //a bigger egg
+  "big egg"
+  //the biggest egg
+  ,
+  "huge egg"
+  ,
+
+  //not an egg
+  "lasagna"
+
+]
+
+_ = [
+  a,
+
+  b //
+]
+
+_ = [
+  (a),
+
+  b, //
+];
+
+_ = [
+  (((((
+    a = b/* comment */))/* comment */))),
+
+  c //
+]
+
+_ = [
+  (((((
+    (a = b)/* comment */))/* comment */))),
+
+  c //
+]
+
+_ = [
+  (a=b
+
+  ),
+  b, //
+];
diff --git a/tests/format/json/json/__snapshots__/jsfmt.spec.js.snap b/tests/format/json/json/__snapshots__/jsfmt.spec.js.snap
--- a/tests/format/json/json/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/json/json/__snapshots__/jsfmt.spec.js.snap
@@ -1565,6 +1565,7 @@ trailingComma: "all"
   0.5,
   98.6,
   99.44,
+
   1066,
   1e1,
   0.1e1,
@@ -1695,6 +1696,7 @@ trailingComma: "all"
   0.5,
   98.6,
   99.44,
+
   1066,
   1e1,
   0.1e1,
@@ -1825,6 +1827,7 @@ trailingComma: "es5"
   0.5,
   98.6,
   99.44,
+
   1066,
   1e1,
   0.1e1,
@@ -1955,6 +1958,7 @@ trailingComma: "es5"
   0.5,
   98.6,
   99.44,
+
   1066,
   1e1,
   0.1e1,