From b5ca90eb708492c2d0c8ae4cc18d9cf2b09a3c35 Mon Sep 17 00:00:00 2001 From: Wleeaf <179125857+wleeaf@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:17:40 +0000 Subject: [PATCH] fix: keep trailing whitespace in heredoc and nowdoc bodies Flexible heredocs and nowdocs (PHP >= 7.3) are re-indented with `hardline`, and the doc printer trims trailing spaces and tabs before every `hardline`. That whitespace is part of the string, so formatting changed the value: `foo ` became `foo`, and a whitespace-only line became empty. After a line that ends in whitespace, use a `literalline` under `markAsRoot` instead, so the next line keeps the same indentation and nothing is trimmed. Other lines still use `hardline`, so output is unchanged for heredocs without trailing whitespace. --- src/printer.mjs | 46 ++++++---- .../__snapshots__/jsfmt.spec.mjs.snap | 90 +++++++++++++++++++ .../encapsed/heredoc-trailing-whitespace.php | 16 ++++ .../nowdoc/__snapshots__/jsfmt.spec.mjs.snap | 82 +++++++++++++++++ tests/nowdoc/trailing-whitespace.php | 14 +++ 5 files changed, 231 insertions(+), 17 deletions(-) create mode 100644 tests/encapsed/heredoc-trailing-whitespace.php create mode 100644 tests/nowdoc/trailing-whitespace.php diff --git a/src/printer.mjs b/src/printer.mjs index a9f06c170..4645c6bc8 100644 --- a/src/printer.mjs +++ b/src/printer.mjs @@ -55,6 +55,7 @@ const { literalline, align, dedentToRoot, + markAsRoot, } = doc.builders; const { willBreak } = doc.utils; const { @@ -866,6 +867,21 @@ function printLookupNodes(path, options, print) { } } +/** + * Join the body lines of a flexible heredoc or nowdoc. `hardline` trims + * trailing whitespace, which is part of the string, so a line ending in + * whitespace is followed by a `literalline` that keeps the indentation. + */ +function joinHeredocLines(lines) { + return markAsRoot( + lines.flatMap((line, index) => + index === 0 + ? [line] + : [/[\t ]$/.test(lines[index - 1]) ? literalline : hardline, line] + ) + ); +} + function getEncapsedQuotes(node, { opening = true } = {}) { if (node.type === "heredoc") { return opening ? `<<<${node.label}` : node.label; @@ -2866,25 +2882,23 @@ function printNode(path, options, print) { const parentParent = path.grandparent; let closingTagIndentation = 0; const flexible = options.phpVersion >= 7.3; - let linebreak = literalline; if (parentParent.type === "heredoc") { - linebreak = flexible ? hardline : literalline; const lines = parentParent.raw.split("\n"); closingTagIndentation = lines[lines.length - 1].search(/\S/); if (closingTagIndentation === -1) { closingTagIndentation = lines[lines.length - 2].search(/\S/); } } - return join( - linebreak, - node.raw - .split("\n") - .map((s, i) => - i > 0 || node.loc.start.column === 0 - ? s.substring(closingTagIndentation) - : s - ) - ); + const valueLines = node.raw + .split("\n") + .map((s, i) => + i > 0 || node.loc.start.column === 0 + ? s.substring(closingTagIndentation) + : s + ); + return parentParent.type === "heredoc" && flexible + ? joinHeredocLines(valueLines) + : join(literalline, valueLines); } const quote = useDoubleQuote(node, options) ? '"' : "'"; @@ -2960,15 +2974,13 @@ function printNode(path, options, print) { return node.value; case "nowdoc": { const flexible = options.phpVersion >= 7.3; - const linebreak = flexible ? hardline : literalline; + const lines = [...node.value.split("\n"), node.label]; return [ "<<<'", node.label, "'", - linebreak, - join(linebreak, node.value.split("\n")), - linebreak, - node.label, + flexible ? hardline : literalline, + flexible ? joinHeredocLines(lines) : join(literalline, lines), docShouldHaveTrailingNewline(path) ? hardline : "", ]; } diff --git a/tests/encapsed/__snapshots__/jsfmt.spec.mjs.snap b/tests/encapsed/__snapshots__/jsfmt.spec.mjs.snap index 0b8acbec2..bca3eb23f 100644 --- a/tests/encapsed/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/encapsed/__snapshots__/jsfmt.spec.mjs.snap @@ -2077,6 +2077,96 @@ printf( ================================================================================ `; +exports[`heredoc-trailing-whitespace.php 1`] = ` +====================================options===================================== +parsers: ["php"] +phpVersion: "7.0" +printWidth: 80 + | printWidth +=====================================input====================================== +