diff --git a/changelog_unreleased/api/13315.md b/changelog_unreleased/api/13315.md new file mode 100644 index 000000000..cc593ea5c --- /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 000000000..3cfa3aff9 --- /dev/null +++ b/changelog_unreleased/javascript/13315-2.md @@ -0,0 +1,19 @@ +#### Fix template literal print with array (#13315 by @fisker, @syi0808) + + +```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 552b44c39..004498080 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; } - // 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, - // 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) }; + const secondContent = parts[2]; - const secondContent = parts[0]; + // At this point we've handled the first pair (context, separator) + // 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). + // 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, + }; + } + + /** @type {Command} */ + const remainingCmd = { ind, mode, doc: remainingDoc }; /** @type {Command} */ const firstAndSecondContentFlatCmd = { 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 b635df2fd..d4470d987 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 c68f1176a..c2f98b36b 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 000000000..bf4dbf81d --- /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); + } +});