Repository: prettier/prettier (JavaScript)
Date: 2022-08-19 08:46:18+00:00
Task type: Bug Fix

Issue:
Modification of nested arrays inside format string
<!--

BEFORE SUBMITTING AN ISSUE:

1.  Search for your issue on GitHub: https://github.com/prettier/prettier/issues
    A large number of opened issues are duplicates of existing issues.
    If someone has already opened an issue for what you are experiencing,
    you do not need to open a new issue — please add a 👍 reaction to the
    existing issue instead.

2.  We get a lot of requests for adding options, but Prettier is
    built on the principle of being opinionated about code formatting.
    This means we add options only in the case of strict technical necessity.
    Find out more: https://prettier.io/docs/en/option-philosophy.html

Don't fill the form below manually! Let a program create a report for you:

1. Go to https://prettier.io/playground
2. Paste your code and set options
3. Press the "Report issue" button in the lower right

-->

**Prettier 3.3.3**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAPABAXnQAwCTADahAjADToBMAuhYQMwUAs11AvjiGSBAA4wBLaAGdkoAIYAnSRADuABSkJRKcQBtZ4gJ6juAI0niwAazgwAyuIC2cADICocZADN1wuPsMmz53kYcA5sgwkgCuHiDuVgLBYRFwqLxwkgI2sOoAKslQUgJwKq5q7tzCgWpwAIqhEPAubhEAVsKo5mWV1bVIhcUgAI4dcPIyvCog4sIAtI5wACazXCAh4gJqgQDCEFZW4shjamoLpVAB5QCCMCECeqHw8sn2jnVFEQAWMFZqAOovAvDCfmA4OZlL8BAA3X5aXZgYS6EBg8IASSgc1g5jAKX4pxR5hgWnKTx6vBk7k+hl4u2J+WSYKc3Ac7kkMCG4gC20JET8kkZuz04j0cAO3GJDhgnwEMxgL2QAA4AAzcSRwfoCJUstk7Lr1bgwfniyXSpCUbihdwZfkFbUgOBWAUzOYzWziY6hVlwABiEEk2wugV24huEBAbDYQA)

```sh
# Options (if any):
None
```

**Input:**

```jsx
x = `${[[1, 2], [3, 4]]}`
````

**Output:**

```jsx
x = `${[
  [2],
  [4],
]}`;
```

**Expected output:**

```jsx
x = `${[
  [1, 2],
  [3, 4],
]}`;
```

**Why?**

Prettier is changing the actual array contents, which it should not. Seems like it always takes the last element of each nested array.

I can work around it by pulling the array out into a separate variable ([playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBDATugBAXiwbXwEYAaLAJgF0z8BmMgFksoB0oAPXLAAwBJgM6AL7cQJEBAAOMAJbQAzslCCIAdwAKGBIpSoANqtQBPReIBG6VGADWcGAGVUAWzgAZGVDjIAZvvlxzSxs7e0krDwBzZBh0AFcAkH8nGWi4hLh2STh0GRdYfQAVbKgMGTgdXz1-cXlIvTgARViIeB8-BIAreXZ7Osbm1qRK6pAARwG4dXQpHTR5AFpPOAATFbEQGNQZPUiAYQgnJ1RkND09ddqoCPqAQRgYmTNY+HVs9082qoSACxgnPQA6t8ZPB5GEwHB7NoQTIAG4gownMDyUwgWHxACSUFWsHsYBy0hu2PsMCM9U+I0k038AMskhOVPK2VhXnEHn86BgU1QESOFISYXQHJOZlQZjg53EVI8MABMmWMG+yAAHAAGcToODjGSa7m845DdriGBiuUKpVIcjiWL+ApiipGkBwJzi5arZauVBXWI8uAAMQg6CO90iJ1QzwgICEQiAA)).



Issue discussion:

Solution patch:
diff --git a/changelog_unreleased/api/13315.md b/changelog_unreleased/api/13315.md
new file mode 100644
index 000000000000..cc593ea5cff3
--- /dev/null
+++ b/changelog_unreleased/api/13315.md
@@ -0,0 +1,3 @@
+#### Stop doc mutation in `prettier.doc.printDocToString` (#13315 by @fisker)
+
+For performance reason, `prettier.doc.printDocToString` used to mutate `.parts` of the [`fill`](https://github.com/prettier/prettier/blob/main/commands.md#fill) command during print. It was converted to a [pure function](https://en.wikipedia.org/wiki/Pure_function) to ensure output correctness.
diff --git a/changelog_unreleased/javascript/13315-2.md b/changelog_unreleased/javascript/13315-2.md
new file mode 100644
index 000000000000..3cfa3aff9459
--- /dev/null
+++ b/changelog_unreleased/javascript/13315-2.md
@@ -0,0 +1,19 @@
+#### Fix template literal print with array (#13315 by @fisker, @syi0808)
+
+<!-- prettier-ignore -->
+```jsx
+// Input
+const string = `${[[1, 2], [3, 4]]}`
+
+// Prettier stable
+const string = `${[
+  [1, 2],
+  [3, 4],
+]}`;
+
+// Prettier main
+const string = `${[
+  [1, 2],
+  [3, 4],
+]}`;
+```
diff --git a/src/document/printer.js b/src/document/printer.js
index 552b44c39108..00449808006e 100644
--- a/src/document/printer.js
+++ b/src/document/printer.js
@@ -1,6 +1,6 @@
 import { convertEndOfLineToChars } from "../common/end-of-line.js";
 import getStringWidth from "../utils/get-string-width.js";
-import { fill, hardlineWithoutBreakParent, indent } from "./builders.js";
+import { hardlineWithoutBreakParent, indent } from "./builders.js";
 import {
   DOC_TYPE_ALIGN,
   DOC_TYPE_ARRAY,
@@ -32,6 +32,8 @@ const MODE_FLAT = Symbol("MODE_FLAT");
 
 const CURSOR_PLACEHOLDER = Symbol("cursor");
 
+const IS_MUTABLE_FILL = Symbol("IS_MUTABLE_FILL");
+
 function rootIndent() {
   return { value: "", length: 0, queue: [] };
 }
@@ -503,16 +505,26 @@ function printDocToString(doc, options) {
           break;
         }
 
+        const secondContent = parts[2];
+
         // At this point we've handled the first pair (context, separator)
-        // and will create a new fill doc for the rest of the content.
-        // Ideally we wouldn't mutate the array here but copying all the
-        // elements to a new array would make this algorithm quadratic,
+        // and will create a new *mutable* fill doc for the rest of the content.
+        // Copying all the elements to a new array would make this algorithm quadratic,
         // which is unusable for large arrays (e.g. large texts in JSX).
-        parts.splice(0, 2);
-        /** @type {Command} */
-        const remainingCmd = { ind, mode, doc: fill(parts) };
+        // https://github.com/prettier/prettier/issues/3263#issuecomment-344275152
+        let remainingDoc = doc;
+        if (doc[IS_MUTABLE_FILL]) {
+          parts.splice(0, 2);
+        } else {
+          remainingDoc = {
+            ...doc,
+            parts: parts.slice(2),
+            [IS_MUTABLE_FILL]: true,
+          };
+        }
 
-        const secondContent = parts[0];
+        /** @type {Command} */
+        const remainingCmd = { ind, mode, doc: remainingDoc };
 
         /** @type {Command} */
         const firstAndSecondContentFlatCmd = {


Test patch:
diff --git a/tests/format/js/template-literals/__snapshots__/format.test.js.snap b/tests/format/js/template-literals/__snapshots__/format.test.js.snap
index b635df2fd63b..d4470d98754e 100644
--- a/tests/format/js/template-literals/__snapshots__/format.test.js.snap
+++ b/tests/format/js/template-literals/__snapshots__/format.test.js.snap
@@ -164,6 +164,8 @@ descirbe('something', () => {
 
 throw new Error(\`pretty-format: Option "theme" has a key "\${key}" whose value "\${value}" is undefined in ansi-styles.\`,)
 
+a = \`\${[[1, 2, 3], [4, 5, 6]]}\`
+
 =====================================output=====================================
 const long1 = \`long \${
   a.b //comment
@@ -224,6 +226,11 @@ throw new Error(
   \`pretty-format: Option "theme" has a key "\${key}" whose value "\${value}" is undefined in ansi-styles.\`,
 );
 
+a = \`\${[
+  [1, 2, 3],
+  [4, 5, 6],
+]}\`;
+
 ================================================================================
 `;
 
diff --git a/tests/format/js/template-literals/expressions.js b/tests/format/js/template-literals/expressions.js
index c68f1176a1f3..c2f98b36b4cb 100644
--- a/tests/format/js/template-literals/expressions.js
+++ b/tests/format/js/template-literals/expressions.js
@@ -44,3 +44,5 @@ descirbe('something', () => {
 })
 
 throw new Error(`pretty-format: Option "theme" has a key "${key}" whose value "${value}" is undefined in ansi-styles.`,)
+
+a = `${[[1, 2, 3], [4, 5, 6]]}`
diff --git a/tests/unit/doc-printer.js b/tests/unit/doc-printer.js
new file mode 100644
index 000000000000..bf4dbf81d86d
--- /dev/null
+++ b/tests/unit/doc-printer.js
@@ -0,0 +1,45 @@
+// TODO: Find a better way to test the performance
+import { fill, join, line } from "../../src/document/builders.js";
+import { printDocToString } from "../../src/document/printer.js";
+
+test("`printDocToString` should not manipulate docs", () => {
+  const printOptions = { printWidth: 40, tabWidth: 2 };
+  const doc = fill(
+    join(
+      line,
+      Array.from({ length: 255 }, (_, index) => String(index + 1)),
+    ),
+  );
+
+  expect(doc.parts.length).toBe(255 + 254);
+
+  const { formatted: firstPrint } = printDocToString(doc, printOptions);
+
+  expect(doc.parts.length).toBe(255 + 254);
+
+  const { formatted: secondPrint } = printDocToString(doc, printOptions);
+
+  expect(firstPrint).toBe(secondPrint);
+
+  {
+    // About 1000 lines, #3263
+    const WORD = "word";
+    const hugeParts = join(
+      line,
+      Array.from(
+        { length: 1000 * Math.ceil(printOptions.printWidth / WORD.length) },
+        () => WORD,
+      ),
+    );
+    const orignalLength = hugeParts.length;
+
+    const startTime = performance.now();
+    const { formatted } = printDocToString(fill(hugeParts), printOptions);
+    const endTime = performance.now();
+    expect(hugeParts.length).toBe(orignalLength);
+
+    const lines = formatted.split("\n");
+    expect(lines.length).toBeGreaterThan(1000);
+    expect(endTime - startTime).toBeLessThan(1000);
+  }
+});