diff --git a/src/printer.mjs b/src/printer.mjs index a9f06c170..346f77945 100644 --- a/src/printer.mjs +++ b/src/printer.mjs @@ -32,6 +32,7 @@ import { hasEmptyBody, isNextLineEmptyAfterNamespace, shouldPrintHardlineBeforeTrailingComma, + isHeredocNode, isDocNode, getAncestorNode, isReferenceLikeNode, @@ -626,7 +627,7 @@ function printArgumentsList(path, options, print, argumentsKey = "arguments") { node.kind )) ? indent([ - lastArg && shouldPrintHardlineBeforeTrailingComma(lastArg) + lastArg && shouldPrintHardlineBeforeTrailingComma(lastArg, options) ? hardline : "", ",", @@ -1648,6 +1649,32 @@ function isStringOnItsOwnLine(node, text) { ); } +// `foo(<<<'SQL' ... SQL)` and `foo("multi-line string")`: a single string-like argument +// stays on the call's line instead of breaking the argument list (PER-CS 4.7: +// one argument spanning several lines does not split the list). A heredoc only +// hugs from PHP 7.3 on, when its closing marker may be followed by `)`. +function shouldHugSingleStringArgument(node, options) { + if (node.arguments.length !== 1) { + return false; + } + + const [argument] = node.arguments; + + return ( + isStringOnItsOwnLine(argument, options.originalText) || + (options.phpVersion >= 7.3 && isHeredocNode(argument)) + ); +} + +function printSingleStringArgument(path, print) { + const [argument] = path.node.arguments; + const printed = path.map(print, "arguments"); + + // PER-CS 10: the heredoc body and closing marker are indented once past the + // statement; a quoted string keeps its literal lines as they are. + return ["(", isHeredocNode(argument) ? indent(printed) : printed, ")"]; +} + function printComposedTypes(path, print, glue) { return group( path.map(({ isFirst }) => (isFirst ? [print()] : [glue, print()]), "types") @@ -2162,17 +2189,9 @@ function printNode(path, options, print) { return node.kind; case "call": { - // Multiline strings as single arguments - if ( - node.arguments.length === 1 && - isStringOnItsOwnLine(node.arguments[0], options.originalText) - ) { - return [ - print("what"), - "(", - join(", ", path.map(print, "arguments")), - ")", - ]; + // Multiline strings and heredocs as single arguments + if (shouldHugSingleStringArgument(node, options)) { + return [print("what"), printSingleStringArgument(path, print)]; } // chain: Call (*LookupNode (Call (*LookupNode (...)))) @@ -2186,19 +2205,16 @@ function printNode(path, options, print) { const isAnonymousClassNode = node.what && node.what.kind === "class" && node.what.isAnonymous; - // Multiline strings as single arguments + // Multiline strings and heredocs as single arguments if ( !isAnonymousClassNode && - node.arguments.length === 1 && - isStringOnItsOwnLine(node.arguments[0], options.originalText) + shouldHugSingleStringArgument(node, options) ) { return [ "new ", ...path.call(printAttrs, "what"), print("what"), - "(", - join(", ", path.map(print, "arguments")), - ")", + printSingleStringArgument(path, print), ]; } @@ -2590,7 +2606,8 @@ function printNode(path, options, print) { ifBreak( !needsForcedTrailingComma && shouldPrintComma(options, 5.0) ? [ - lastElem && shouldPrintHardlineBeforeTrailingComma(lastElem) + lastElem && + shouldPrintHardlineBeforeTrailingComma(lastElem, options) ? hardline : "", ",", @@ -2942,7 +2959,8 @@ function printNode(path, options, print) { node.type === "heredoc" ? linebreak : "", ...path.map(print, "value"), getEncapsedQuotes(node, { opening: false }), - node.type === "heredoc" && docShouldHaveTrailingNewline(path) + node.type === "heredoc" && + docShouldHaveTrailingNewline(path, options) ? hardline : "", ]; @@ -2969,7 +2987,7 @@ function printNode(path, options, print) { join(linebreak, node.value.split("\n")), linebreak, node.label, - docShouldHaveTrailingNewline(path) ? hardline : "", + docShouldHaveTrailingNewline(path, options) ? hardline : "", ]; } case "name": diff --git a/src/util.mjs b/src/util.mjs index 8a4aa944d..fc10679f8 100644 --- a/src/util.mjs +++ b/src/util.mjs @@ -228,7 +228,14 @@ function isDocNode(node) { * Heredoc/Nowdoc nodes need a trailing linebreak if they * appear as function arguments or array elements */ -function docShouldHaveTrailingNewline(path, recurse = 0) { +// Before PHP 7.3 the closing marker of a heredoc or nowdoc had to be alone on +// its line, so anything following it (`,`, `:`, `=>`) needed a line break. +// Flexible heredocs (7.3+) lift that restriction; the marker is then followed +// by the comma, as in PER-CS 10. Two exceptions keep the line break in every +// version: a concatenation operator, which the `bin` printer lays out on its +// own line, and the `:` of a `case`, which php-parser does not accept right +// after a closing marker although PHP does. +function docShouldHaveTrailingNewline(path, options, recurse = 0) { const node = path.getNode(recurse); const parent = path.getNode(recurse + 1); const parentParent = path.getNode(recurse + 2); @@ -237,6 +244,10 @@ function docShouldHaveTrailingNewline(path, recurse = 0) { return false; } + if (options.phpVersion >= 7.3 && !["bin", "case"].includes(parent.kind)) { + return false; + } + if ( (parentParent && ["call", "new", "echo"].includes(parentParent.kind) && @@ -271,7 +282,8 @@ function docShouldHaveTrailingNewline(path, recurse = 0) { if (parent.kind === "bin") { return ( - parent.left === node || docShouldHaveTrailingNewline(path, recurse + 1) + parent.left === node || + docShouldHaveTrailingNewline(path, options, recurse + 1) ); } @@ -319,7 +331,7 @@ function docShouldHaveTrailingNewline(path, recurse = 0) { } if (parent.kind === "retif") { - return docShouldHaveTrailingNewline(path, recurse + 1); + return docShouldHaveTrailingNewline(path, options, recurse + 1); } return false; @@ -564,19 +576,26 @@ function isNextLineEmptyAfterNamespace(text, node) { return hasNewline(text, idx); } -function shouldPrintHardlineBeforeTrailingComma(lastElem) { - if ( - lastElem.kind === "nowdoc" || - (lastElem.kind === "encapsed" && lastElem.type === "heredoc") - ) { +function isHeredocNode(node) { + return ( + node.kind === "nowdoc" || + (node.kind === "encapsed" && node.type === "heredoc") + ); +} + +// Before PHP 7.3 the closing marker of a heredoc or nowdoc had to be alone on +// its line, so a trailing comma after it needed a line of its own. Flexible +// heredocs (7.3+) allow `LABEL,`, which is also what PER-CS 10 shows. +function shouldPrintHardlineBeforeTrailingComma(lastElem, options) { + if (options.phpVersion >= 7.3) { + return false; + } + + if (isHeredocNode(lastElem)) { return true; } - if ( - lastElem.kind === "entry" && - (lastElem.value.kind === "nowdoc" || - (lastElem.value.kind === "encapsed" && lastElem.value.type === "heredoc")) - ) { + if (lastElem.kind === "entry" && isHeredocNode(lastElem.value)) { return true; } @@ -754,6 +773,7 @@ export { hasEmptyBody, isNextLineEmptyAfterNamespace, shouldPrintHardlineBeforeTrailingComma, + isHeredocNode, isDocNode, getAncestorNode, normalizeMagicMethodName, diff --git a/tests/call/__snapshots__/jsfmt.spec.mjs.snap b/tests/call/__snapshots__/jsfmt.spec.mjs.snap index 604148b70..280cce4f2 100644 --- a/tests/call/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/call/__snapshots__/jsfmt.spec.mjs.snap @@ -655,24 +655,18 @@ string $var\`, ); -call( - <<foo)(); diff --git a/tests/comments/__snapshots__/jsfmt.spec.mjs.snap b/tests/comments/__snapshots__/jsfmt.spec.mjs.snap index 5adfe5f8c..0363a369f 100644 --- a/tests/comments/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/comments/__snapshots__/jsfmt.spec.mjs.snap @@ -2760,8 +2760,7 @@ echo // Comment Example of string spanning multiple lines using heredoc syntax. - EOD - , + EOD, $a; ================================================================================ diff --git a/tests/echo/__snapshots__/jsfmt.spec.mjs.snap b/tests/echo/__snapshots__/jsfmt.spec.mjs.snap index 455ac56e9..1a68c61cb 100644 --- a/tests/echo/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/echo/__snapshots__/jsfmt.spec.mjs.snap @@ -427,16 +427,14 @@ echo << "bar", ]; $expected = [ @@ -1739,8 +1731,7 @@ $expected = [ # bar baz - EOT -, + EOT, ]; $expected = [ "foo" => <<<'EOT' @@ -1748,22 +1739,19 @@ $expected = [ # bar baz - EOT - , + EOT, "bar" => <<<'EOT' foo # bar baz - EOT - , + EOT, "baz" => <<<'EOT' foo # bar baz - EOT -, + EOT, ]; $expected = [ "test" => <<<'EOT' @@ -1771,8 +1759,7 @@ $expected = [ # bar baz - EOT - , + EOT, "collection" => [ [ "one" => <<<'EOT' @@ -1780,16 +1767,14 @@ $expected = [ # bar baz - EOT - , + EOT, ], [ "two" => <<<'EOT' foo # bar baz - EOT - , + EOT, ], ], ]; @@ -2057,22 +2042,16 @@ function b() } // Heredoc in function call with correct indentation -printf( - <<addSql(<<<'SQL' + CREATE TABLE t ( + id INT + ) + SQL); +$this->addSql( + <<<'SQL' + SELECT 1 + SQL + , +); +$x = new Exception(<<where(<<<'DQL' + a = 1 + DQL)->andWhere('b = 2'); +$arr = ['k' => <<<'V' + val + V, 'other' => 1]; +$list = [<<<'A' + a + A, <<<'B' + b + B]; + +// A concatenation operand keeps the operator on its own line. +$s = <<<'EOF' + string + EOF . $var; + +=====================================output===================================== +addSql(<<<'SQL' + CREATE TABLE t ( + id INT + ) + SQL); +$this->addSql(<<<'SQL' + SELECT 1 + SQL); +$x = new Exception(<<where( + <<<'DQL' + a = 1 + DQL, +)->andWhere("b = 2"); +$arr = [ + "k" => <<<'V' + val + V, + "other" => 1, +]; +$list = [ + <<<'A' + a + A, + <<<'B' + b + B, +]; + +// A concatenation operand keeps the operator on its own line. +$s = + <<<'EOF' + string + EOF + . $var; + +================================================================================ +`; + +exports[`heredoc-arguments.php 2`] = ` +====================================options===================================== +parsers: ["php"] +phpVersion: "7.2" +printWidth: 80 + | printWidth +=====================================input====================================== +addSql(<<<'SQL' + CREATE TABLE t ( + id INT + ) + SQL); +$this->addSql( + <<<'SQL' + SELECT 1 + SQL + , +); +$x = new Exception(<<where(<<<'DQL' + a = 1 + DQL)->andWhere('b = 2'); +$arr = ['k' => <<<'V' + val + V, 'other' => 1]; +$list = [<<<'A' + a + A, <<<'B' + b + B]; + +// A concatenation operand keeps the operator on its own line. +$s = <<<'EOF' + string + EOF . $var; + +=====================================output===================================== +addSql( + <<<'SQL' +CREATE TABLE t ( + id INT +) +SQL +); +$this->addSql( + <<<'SQL' +SELECT 1 +SQL +); +$x = new Exception( + <<where( + <<<'DQL' +a = 1 +DQL +)->andWhere("b = 2"); +$arr = [ + "k" => <<<'V' +val +V + , + "other" => 1, +]; +$list = [ + <<<'A' +a +A + , + <<<'B' +b +B +, +]; + +// A concatenation operand keeps the operator on its own line. +$s = + <<<'EOF' +string +EOF + . $var; + +================================================================================ +`; diff --git a/tests/heredoc-arguments/heredoc-arguments.php b/tests/heredoc-arguments/heredoc-arguments.php new file mode 100644 index 000000000..1f5ffb9c2 --- /dev/null +++ b/tests/heredoc-arguments/heredoc-arguments.php @@ -0,0 +1,48 @@ +addSql(<<<'SQL' + CREATE TABLE t ( + id INT + ) + SQL); +$this->addSql( + <<<'SQL' + SELECT 1 + SQL + , +); +$x = new Exception(<<where(<<<'DQL' + a = 1 + DQL)->andWhere('b = 2'); +$arr = ['k' => <<<'V' + val + V, 'other' => 1]; +$list = [<<<'A' + a + A, <<<'B' + b + B]; + +// A concatenation operand keeps the operator on its own line. +$s = <<<'EOF' + string + EOF . $var; diff --git a/tests/heredoc-arguments/jsfmt.spec.mjs b/tests/heredoc-arguments/jsfmt.spec.mjs new file mode 100644 index 000000000..880ef9cea --- /dev/null +++ b/tests/heredoc-arguments/jsfmt.spec.mjs @@ -0,0 +1,2 @@ +run_spec(import.meta, ["php"], { phpVersion: "8.4" }); +run_spec(import.meta, ["php"], { phpVersion: "7.2" }); diff --git a/tests/new/__snapshots__/jsfmt.spec.mjs.snap b/tests/new/__snapshots__/jsfmt.spec.mjs.snap index 177c147da..c45ab94f0 100644 --- a/tests/new/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/new/__snapshots__/jsfmt.spec.mjs.snap @@ -254,8 +254,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOD - , + EOD, $arg, ); @@ -265,8 +264,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOD - , + EOD, ); $var = new Foo( @@ -275,8 +273,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOD - , + EOD, $arg, ); @@ -285,8 +282,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOF - , + EOF, $arg, ); @@ -296,8 +292,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOF - , + EOF, ); $var = new Foo( @@ -306,8 +301,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOF - , + EOF, $arg, ); @@ -690,8 +684,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOD - , + EOD, $arg, ); @@ -701,8 +694,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOD - , + EOD, ); $var = new Foo( @@ -711,8 +703,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOD - , + EOD, $arg, ); @@ -721,8 +712,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOF - , + EOF, $arg, ); @@ -732,8 +722,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOF - , + EOF, ); $var = new Foo( @@ -742,8 +731,7 @@ $var = new Foo( Example of string spanning multiple lines using nowdoc syntax. - EOF - , + EOF, $arg, ); diff --git a/tests/nowdoc/__snapshots__/jsfmt.spec.mjs.snap b/tests/nowdoc/__snapshots__/jsfmt.spec.mjs.snap index 3a9942819..015619f53 100644 --- a/tests/nowdoc/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/nowdoc/__snapshots__/jsfmt.spec.mjs.snap @@ -3073,8 +3073,7 @@ EOL; $str = [ <<<'EOF' foo - EOF - , + EOF, "bar", ]; @@ -3082,30 +3081,24 @@ $str = [ "bar", <<<'EOF' foo - EOF -, + EOF, ]; -$str = sprintf( - <<<'EOF' +$str = sprintf(<<<'EOF' foo - EOF - , -); + EOF); $str = sprintf( <<<'EOF' foo - EOF - , + EOF, true, ); $str = sprintf( << $a, ]; @@ -3641,26 +3609,21 @@ switch ($i) { TXT; } -$var->call( - <<<'TXT' +$var->call(<<<'TXT' string - TXT - , -); + TXT); $var->call( $var, <<<'TXT' string - TXT - , + TXT, ); $var->call( <<<'TXT' string - TXT - , + TXT, $var, ); @@ -3668,8 +3631,7 @@ $var->call( $var, <<<'TXT' string - TXT - , + TXT, $var, ); @@ -3755,36 +3717,27 @@ $var = EOD; call( - call( - <<<'EOD' + call(<<<'EOD' string string string - EOD - , - ), + EOD), ); call( - $var->call( - <<<'EOD' + $var->call(<<<'EOD' string string string - EOD - , - ), + EOD), ); call( - Foo::call( - <<<'EOD' + Foo::call(<<<'EOD' string string string - EOD - , - ), + EOD), ); for ( @@ -3826,8 +3779,7 @@ for ( string string string - EOD - , + EOD, $i = 0; $i < 10; $i++ @@ -3863,8 +3815,7 @@ for ( string string string - EOD - , + EOD, $i < 10; $i++ ) { @@ -3888,8 +3839,7 @@ for ( string string string - EOD - , + EOD, $i++ ) { } @@ -3925,26 +3875,21 @@ for ( ) { } -call( - <<<'TXT' +call(<<<'TXT' string - TXT - , -); + TXT); call( $var, <<<'TXT' string - TXT - , + TXT, ); call( <<<'TXT' string - TXT - , + TXT, $var, ); @@ -3952,8 +3897,7 @@ call( $var, <<<'TXT' string - TXT - , + TXT, $var, ); @@ -4046,13 +3990,11 @@ call( string string EOD - : <<<'EOD' string string string - EOD - , + EOD, $var, ); @@ -4064,13 +4006,11 @@ call( string string EOD - : <<<'EOD' string string string - EOD - , + EOD, $var, ); @@ -4079,13 +4019,11 @@ $array = [ string string string - EOD - => <<<'EOD' + EOD => <<<'EOD' string string string - EOD -, + EOD, ]; $php73FlexibleNowdoc = <<<'EOD' @@ -4105,8 +4043,7 @@ EOD; ? "foo" : <<<'EOD' string - EOD - , + EOD, "bar", ]; @@ -4117,8 +4054,7 @@ EOD; ? "bar" : <<<'EOD' string - EOD - , + EOD, "bar", ]; @@ -4126,8 +4062,7 @@ EOD; $var ?? <<<'EOD' string - EOD - , + EOD, "bar", ]; @@ -4136,8 +4071,7 @@ EOD; ($b ?? <<<'EOD' string - EOD - ), + EOD), "bar", ]; diff --git a/tests/trailing_commas/__snapshots__/jsfmt.spec.mjs.snap b/tests/trailing_commas/__snapshots__/jsfmt.spec.mjs.snap index c570a1a63..eb7e4daff 100644 --- a/tests/trailing_commas/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/trailing_commas/__snapshots__/jsfmt.spec.mjs.snap @@ -1571,8 +1571,7 @@ $expected = [ # bar baz - EOT -, + EOT, ]; $expected = [ "foo", @@ -1581,8 +1580,7 @@ $expected = [ # bar baz - EOT -, + EOT, ]; $expected = [ <<<'EOT' @@ -1590,8 +1588,7 @@ $expected = [ # bar baz - EOT - , + EOT, "bar", ]; @@ -1602,8 +1599,7 @@ $expected = [ # bar baz - EOT -, + EOT, ]; $expected = [ "foo", @@ -1612,8 +1608,7 @@ $expected = [ # bar baz - EOT -, + EOT, ]; $expected = [ << <<<'EOT' @@ -1652,8 +1644,7 @@ $expected = [ # bar baz - EOT - , + EOT, "bar", ]; @@ -1664,8 +1655,7 @@ $expected = [ # bar baz - EOT -, + EOT, ]; $expected = [ "foo", @@ -1674,8 +1664,7 @@ $expected = [ # bar baz - EOT -, + EOT, ]; $expected = [ "key" => << [ [ "one" => <<<'EOT' @@ -1704,16 +1691,14 @@ $expected = [ # bar baz - EOT - , + EOT, ], [ "two" => <<<'EOT' foo # bar baz - EOT - , + EOT, ], ], ]; @@ -1725,8 +1710,7 @@ $expected = [ # bar baz - EOT - , + EOT, "collection" => [ [ "one" => << <<foo. Now, I am printing some {$foo->bar[1]}. This should not print a capital 'A': \\x41 - EOT - , -); + EOT); -call( - <<foo. Now, I am printing some {$foo->bar[1]}. This should not print a capital 'A': \\x41 - EOT - , -); + EOT); $sel = $this->connections