From 58ff4642ddf00babbdfb816262bf93b3e10085f0 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Fri, 2 Jan 2026 13:41:28 +0800 Subject: [PATCH] Add `PrintResult` for doc printer (#18372) --- benchmarks/print-doc-to-string.js | 52 +++++++++++++--------- benchmarks/utilities.js | 2 +- src/document/printer/print-result.js | 64 ++++++++++++++++++++++++++++ src/document/printer/printer.js | 62 ++++++--------------------- 4 files changed, 110 insertions(+), 70 deletions(-) create mode 100644 src/document/printer/print-result.js diff --git a/benchmarks/print-doc-to-string.js b/benchmarks/print-doc-to-string.js index 99a38f408..2967b516f 100644 --- a/benchmarks/print-doc-to-string.js +++ b/benchmarks/print-doc-to-string.js @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import fs from "node:fs/promises"; +import prettyBytes from "pretty-bytes"; import * as prettierProduction from "../node_modules/prettier/index.mjs"; import * as prettierDevelopment from "../src/index.js"; import { runBenchmark } from "./utilities.js"; @@ -7,27 +8,36 @@ import { runBenchmark } from "./utilities.js"; assert.notEqual(prettierProduction.version, prettierDevelopment.version); const text = await fs.readFile(new URL(import.meta.url), "utf8"); -// @ts-expect-error -- No types -const doc = await prettierDevelopment.__debug.printToDoc(text, { - parser: "babel", - cursorOffset: text.indexOf("cursorOffset"), -}); -const run = (prettier) => - prettier.doc.printer.printDocToString(doc, { printWidth: 80, tabWidth: 2 }); +for (const size of [1, 1e1, 1e2, 1e3]) { + const doc = await Promise.all( + Array.from({ length: size }, (_, index) => + // @ts-expect-error -- No types + prettierDevelopment.__debug.printToDoc(text, { + parser: "babel", + cursorOffset: index === 0 ? text.indexOf("cursorOffset") : undefined, + }), + ), + ); -const expected = await run(prettierProduction); + const run = (prettier) => + prettier.doc.printer.printDocToString(doc, { printWidth: 80, tabWidth: 2 }); -await runBenchmark( - { - name: "printDocToString", - assert: (result) => assert.deepEqual(result, expected), - }, - [ - { name: "Development", prettier: prettierDevelopment }, - { name: "Production", prettier: prettierProduction }, - ].map(({ name, prettier }) => ({ - name, - implementation: () => run(prettier), - })), -); + const expected = await run(prettierProduction); + + await runBenchmark( + { + name: `printDocToString (${size}, ${prettyBytes(text.length * size)})`, + assert: (result) => assert.deepEqual(result, expected), + warmupIterations: 1, + iterations: size > 5e3 ? 1 : size > 1e2 ? 2 : undefined, + }, + [ + { name: "Development", prettier: prettierDevelopment }, + { name: "Production", prettier: prettierProduction }, + ].map(({ name, prettier }) => ({ + name, + implementation: () => run(prettier), + })), + ); +} diff --git a/benchmarks/utilities.js b/benchmarks/utilities.js index e54f20687..294835b24 100644 --- a/benchmarks/utilities.js +++ b/benchmarks/utilities.js @@ -28,7 +28,7 @@ async function runBenchmark(options, implementations) { const { assert, ...benchOptions } = options; - const bench = new Bench(benchOptions); + const bench = new Bench({ throws: true, ...benchOptions }); let error; diff --git a/src/document/printer/print-result.js b/src/document/printer/print-result.js new file mode 100644 index 000000000..d2bc956f5 --- /dev/null +++ b/src/document/printer/print-result.js @@ -0,0 +1,64 @@ +import { trimIndentation } from "./trim-indentation.js"; + +class printResult { + /** @type {string[]} */ + #settledTexts = []; + #unsettledText = ""; + #settledTextLength = 0; + /** @type {number[]} */ + #settledPositions = []; + /** @type {number[]} */ + #unsettledPositions = []; + + #settle() { + const text = this.#unsettledText; + + if (text !== "") { + this.#settledTexts.push(text); + this.#settledTextLength += text.length; + this.#unsettledText = ""; + } + + for (const position of this.#unsettledPositions) { + this.#settledPositions.push(Math.min(position, this.#settledTextLength)); + } + this.#unsettledPositions.length = 0; + } + + markPosition() { + if (this.#settledPositions.length + this.#unsettledPositions.length >= 2) { + throw new Error("There are too many 'cursor' in doc."); + } + + this.#unsettledPositions.push( + this.#settledTextLength + this.#unsettledText.length, + ); + } + + /** + @param {string} text + */ + write(text) { + this.#unsettledText += text; + } + + trim() { + const { text: trimmed, count } = trimIndentation(this.#unsettledText); + + this.#unsettledText = trimmed; + this.#settle(); + + return count; + } + + finish() { + this.#settle(); + + return { + text: this.#settledTexts.join(""), + positions: this.#settledPositions, + }; + } +} + +export default printResult; diff --git a/src/document/printer/printer.js b/src/document/printer/printer.js index 01f77099e..0c3236797 100644 --- a/src/document/printer/printer.js +++ b/src/document/printer/printer.js @@ -22,6 +22,7 @@ import { import { getDocType, propagateBreaks } from "../utilities/index.js"; import InvalidDocError from "../utilities/invalid-doc-error.js"; import { makeAlign, makeIndent, ROOT_INDENT } from "./indent.js"; +import PrintResult from "./print-result.js"; import { trimIndentation } from "./trim-indentation.js"; /** @@ -187,16 +188,11 @@ function printDocToString(doc, options) { // commands to the array instead of recursively calling `print`. /** @type Command[] */ const commands = [{ indent: ROOT_INDENT, mode: MODE_BREAK, doc }]; - let output = ""; let shouldRemeasure = false; /** @type Command[] */ const lineSuffix = []; - const cursorPositions = []; - /** @type string[] */ - const settledOutput = []; - const settledCursorPositions = []; - let settledTextLength = 0; + const result = new PrintResult(); propagateBreaks(doc); @@ -208,7 +204,7 @@ function printDocToString(doc, options) { newLine !== "\n" ? doc.replaceAll("\n", newLine) : doc; // Plugins may print single string, should skip measure the width if (formatted) { - output += formatted; + result.write(formatted); if (commands.length > 0) { position += getStringWidth(formatted); } @@ -223,10 +219,7 @@ function printDocToString(doc, options) { break; case DOC_TYPE_CURSOR: - if (cursorPositions.length >= 2) { - throw new Error("There are too many 'cursor' in doc."); - } - cursorPositions.push(settledTextLength + output.length); + result.markPosition(); break; case DOC_TYPE_INDENT: @@ -246,7 +239,7 @@ function printDocToString(doc, options) { break; case DOC_TYPE_TRIM: - trim(); + position -= result.trim(); break; case DOC_TYPE_GROUP: @@ -481,8 +474,7 @@ function printDocToString(doc, options) { case MODE_FLAT: if (!doc.hard) { if (!doc.soft) { - output += " "; - + result.write(" "); position += 1; } @@ -506,17 +498,17 @@ function printDocToString(doc, options) { } if (doc.literal) { - output += newLine; + result.write(newLine); position = 0; if (indent.root) { if (indent.root.value) { - output += indent.root.value; + result.write(indent.root.value); } position = indent.root.length; } } else { - trim(); - output += newLine + indent.value; + result.trim(); + result.write(newLine + indent.value); position = indent.length; } break; @@ -543,10 +535,9 @@ function printDocToString(doc, options) { } } - const formatted = settledOutput.join("") + output; - const finalCursorPositions = [...settledCursorPositions, ...cursorPositions]; + const { text: formatted, positions: cursorPositions } = result.finish(); - if (finalCursorPositions.length !== 2) { + if (cursorPositions.length !== 2) { // If the doc contained ONE cursor command, // instead of the expected zero or two. If the doc being printed was // returned by printAstToDoc, then the only ways this can have happened @@ -575,37 +566,12 @@ function printDocToString(doc, options) { return { formatted }; } - const cursorNodeStart = finalCursorPositions[0]; + const [cursorNodeStart, cursorNodeEnd] = cursorPositions; return { formatted, cursorNodeStart, - cursorNodeText: formatted.slice( - cursorNodeStart, - finalCursorPositions.at(-1), - ), + cursorNodeText: formatted.slice(cursorNodeStart, cursorNodeEnd), }; - - function trim() { - const { text: trimmed, count } = trimIndentation(output); - - if (trimmed) { - settledOutput.push(trimmed); - settledTextLength += trimmed.length; - } - - output = ""; - position -= count; - - if (cursorPositions.length > 0) { - settledCursorPositions.push( - ...cursorPositions.map((position) => - Math.min(position, settledTextLength), - ), - ); - - cursorPositions.length = 0; - } - } } export { printDocToString };