Repository: prettier/prettier (JavaScript)

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)).



Hints:

Patch:
diff --git a/src/document/printer.js b/src/document/printer.js
--- 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
--- 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
--- 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
--- /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);
+  }
+});