Skip to content

Commit

Permalink
Support CRLF newlines in pretty printer
Browse files Browse the repository at this point in the history
Can be enabled using the "newlines" option.
  • Loading branch information
nikic committed May 21, 2023
1 parent ad8daa1 commit d43edfb
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 18 deletions.
16 changes: 8 additions & 8 deletions lib/PhpParser/PrettyPrinter/Standard.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,12 @@ protected function pScalar_String(Scalar\String_ $node): string {
$label = $node->getAttribute('docLabel');
if ($label && !$this->containsEndLabel($node->value, $label)) {
if ($node->value === '') {
return "<<<'$label'\n$label" . $this->docStringEndToken;
return "<<<'$label'{$this->newline}$label{$this->docStringEndToken}";
}

// Make sure trailing \r is not combined with following \n into CRLF.
if ($node->value[strlen($node->value) - 1] !== "\r") {
return "<<<'$label'\n$node->value\n$label"
return "<<<'$label'{$this->newline}{$node->value}{$this->newline}$label"
. $this->docStringEndToken;
}
}
Expand All @@ -152,10 +152,10 @@ protected function pScalar_String(Scalar\String_ $node): string {
$escaped = $this->escapeString($node->value, null);
if ($label && !$this->containsEndLabel($escaped, $label)) {
if ($escaped === '') {
return "<<<$label\n$label" . $this->docStringEndToken;
return "<<<$label{$this->newline}$label{$this->docStringEndToken}";
}

return "<<<$label\n" . $escaped . "\n$label"
return "<<<$label{$this->newline}$escaped{$this->newline}$label"
. $this->docStringEndToken;
}
/* break missing intentionally */
Expand All @@ -174,11 +174,11 @@ protected function pScalar_InterpolatedString(Scalar\InterpolatedString $node):
&& $node->parts[0] instanceof Node\InterpolatedStringPart
&& $node->parts[0]->value === ''
) {
return "<<<$label\n$label" . $this->docStringEndToken;
return "<<<$label{$this->newline}$label{$this->docStringEndToken}";
}

return "<<<$label\n" . $this->pEncapsList($node->parts, null) . "\n$label"
. $this->docStringEndToken;
return "<<<$label{$this->newline}" . $this->pEncapsList($node->parts, null)
. "{$this->newline}$label{$this->docStringEndToken}";
}
}
return '"' . $this->pEncapsList($node->parts, '"') . '"';
Expand Down Expand Up @@ -993,7 +993,7 @@ protected function pStmt_Unset(Stmt\Unset_ $node): string {
}

protected function pStmt_InlineHTML(Stmt\InlineHTML $node): string {
$newline = $node->getAttribute('hasLeadingNewline', true) ? "\n" : '';
$newline = $node->getAttribute('hasLeadingNewline', true) ? $this->newline : '';
return '?>' . $newline . $node->value . '<?php ';
}

Expand Down
34 changes: 24 additions & 10 deletions lib/PhpParser/PrettyPrinterAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter {

/** @var int Current indentation level. */
protected $indentLevel;
/** @var string Newline style. Does not include current indentation. */
protected $newline;
/** @var string Newline including current indentation. */
protected $nl;
/** @var string|null Token placed at end of doc string to ensure it is followed by a newline.
Expand Down Expand Up @@ -165,14 +167,23 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter {
* array() vs []). It is safe to pretty-print an AST for a newer
* PHP version while specifying an older target (but the result will
* of course not be compatible with the older version in that case).
* * string $newline: The newline style to use. Should be "\n" (default) or "\r\n".
* * bool $shortArraySyntax: Whether to use [] instead of array() as the default array
* syntax, if the node does not specify a format. Defaults to whether
* the phpVersion support short array syntax.
*
* @param array{phpVersion?: PhpVersion, shortArraySyntax?: bool} $options Dictionary of formatting options
* @param array{
* phpVersion?: PhpVersion, newline?: string, shortArraySyntax?: bool
* } $options Dictionary of formatting options
*/
public function __construct(array $options = []) {
$this->phpVersion = $options['phpVersion'] ?? PhpVersion::fromComponents(7, 1);

$this->newline = $options['newline'] ?? "\n";
if ($this->newline !== "\n" && $this->newline != "\r\n") {
throw new \LogicException('Option "newline" must be one of "\n" or "\r\n"');
}

$this->shortArraySyntax =
$options['shortArraySyntax'] ?? $this->phpVersion->supportsShortArraySyntax();
$this->docStringEndToken =
Expand All @@ -184,7 +195,7 @@ public function __construct(array $options = []) {
*/
protected function resetState(): void {
$this->indentLevel = 0;
$this->nl = "\n";
$this->nl = $this->newline;
$this->origTokens = null;
}

Expand All @@ -195,7 +206,7 @@ protected function resetState(): void {
*/
protected function setIndentLevel(int $level): void {
$this->indentLevel = $level;
$this->nl = "\n" . \str_repeat(' ', $level);
$this->nl = $this->newline . \str_repeat(' ', $level);
}

/**
Expand All @@ -212,7 +223,7 @@ protected function indent(): void {
protected function outdent(): void {
assert($this->indentLevel >= 4);
$this->indentLevel -= 4;
$this->nl = "\n" . str_repeat(' ', $this->indentLevel);
$this->nl = $this->newline . str_repeat(' ', $this->indentLevel);
}

/**
Expand Down Expand Up @@ -250,13 +261,13 @@ public function prettyPrintExpr(Expr $node): string {
*/
public function prettyPrintFile(array $stmts): string {
if (!$stmts) {
return "<?php\n\n";
return "<?php" . $this->newline . $this->newline;
}

$p = "<?php\n\n" . $this->prettyPrint($stmts);
$p = "<?php" . $this->newline . $this->newline . $this->prettyPrint($stmts);

if ($stmts[0] instanceof Stmt\InlineHTML) {
$p = preg_replace('/^<\?php\s+\?>\n?/', '', $p);
$p = preg_replace('/^<\?php\s+\?>\r?\n?/', '', $p);
}
if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) {
$p = preg_replace('/<\?php$/', '', rtrim($p));
Expand Down Expand Up @@ -290,8 +301,11 @@ protected function preprocessNodes(array $nodes): void {
protected function handleMagicTokens(string $str): string {
if ($this->docStringEndToken !== null) {
// Replace doc-string-end tokens with nothing or a newline
$str = str_replace($this->docStringEndToken . ";\n", ";\n", $str);
$str = str_replace($this->docStringEndToken, "\n", $str);
$str = str_replace(
$this->docStringEndToken . ';' . $this->newline,
';' . $this->newline,
$str);
$str = str_replace($this->docStringEndToken, $this->newline, $str);
}

return $str;
Expand Down Expand Up @@ -537,7 +551,7 @@ public function printFormatPreserving(array $stmts, array $origStmts, array $ori
} else {
// Fallback
// TODO Add <?php properly
$result = "<?php\n" . $this->pStmts($stmts, false);
$result = "<?php" . $this->newline . $this->pStmts($stmts, false);
}

return $this->handleMagicTokens($result);
Expand Down
45 changes: 45 additions & 0 deletions test/PhpParser/PrettyPrinterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,49 @@ public function provideTestRoundTripPrint() {
$this->getTests(__DIR__ . '/../code/parser', 'test')
);
}

public function testWindowsNewline() {
$prettyPrinter = new Standard(['newline' => "\r\n"]);
$stmts = [
new Stmt\If_(new Int_(1), [
'stmts' => [
new Stmt\Echo_([new String_('Hello')]),
new Stmt\Echo_([new String_('World')]),
],
]),
];
$code = $prettyPrinter->prettyPrint($stmts);
$this->assertSame("if (1) {\r\n echo 'Hello';\r\n echo 'World';\r\n}", $code);
$code = $prettyPrinter->prettyPrintFile($stmts);
$this->assertSame("<?php\r\n\r\nif (1) {\r\n echo 'Hello';\r\n echo 'World';\r\n}", $code);

$stmts = [new Stmt\InlineHTML('Hello world')];
$code = $prettyPrinter->prettyPrintFile($stmts);
$this->assertSame("Hello world", $code);

$stmts = [
new Stmt\Expression(new String_('Test', [
'kind' => String_::KIND_NOWDOC,
'docLabel' => 'STR'
])),
new Stmt\Expression(new String_('Test 2', [
'kind' => String_::KIND_HEREDOC,
'docLabel' => 'STR'
])),
new Stmt\Expression(new InterpolatedString([new InterpolatedStringPart('Test 3')], [
'kind' => String_::KIND_HEREDOC,
'docLabel' => 'STR'
])),
];
$code = $prettyPrinter->prettyPrint($stmts);
$this->assertSame(
"<<<'STR'\r\nTest\r\nSTR;\r\n<<<STR\r\nTest 2\r\nSTR;\r\n<<<STR\r\nTest 3\r\nSTR\r\n;",
$code);
}

public function testInvalidNewline() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Option "newline" must be one of "\n" or "\r\n"');
new PrettyPrinter\Standard(['newline' => 'foo']);
}
}

0 comments on commit d43edfb

Please sign in to comment.