From e50c67b7a94d2d028f8792f462e31ba4ed9f2680 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Sat, 21 Sep 2024 18:54:50 +0200 Subject: [PATCH] Add basic support for tab indentation Add a new "indent" option for the pretty printer, which can be use to control the indentation width, or switch it to use tabs. Tab width is currenlty hardcoded to 4, but also shouldn't matter much. Possibly the formatting-preserving printer should auto-detect the indentation in the future. --- doc/component/Pretty_printing.markdown | 1 + lib/PhpParser/Internal/TokenStream.php | 17 +++-- lib/PhpParser/PrettyPrinterAbstract.php | 43 ++++++++++--- test/PhpParser/PrettyPrinterTest.php | 10 ++- test/code/formatPreservation/indent.test | 37 +++++++++++ test/code/prettyPrinter/indent.test | 82 ++++++++++++++++++++++++ 6 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 test/code/formatPreservation/indent.test create mode 100644 test/code/prettyPrinter/indent.test diff --git a/doc/component/Pretty_printing.markdown b/doc/component/Pretty_printing.markdown index dcc55b6a3f..f7b9501609 100644 --- a/doc/component/Pretty_printing.markdown +++ b/doc/component/Pretty_printing.markdown @@ -37,6 +37,7 @@ integer should be printed as decimal, hexadecimal, etc). Additionally, it suppor * `phpVersion` (defaults to 7.4) allows opting into formatting that is not supported by older PHP versions. * `newline` (defaults to `"\n"`) can be set to `"\r\n"` in order to produce Windows newlines. +* `indent` (defaults to four spaces `" "`) can be set to any number of spaces or a single tab. * `shortArraySyntax` determines the used array syntax if the `kind` attribute is not set. This is a legacy option, and `phpVersion` should be used to control this behavior instead. diff --git a/lib/PhpParser/Internal/TokenStream.php b/lib/PhpParser/Internal/TokenStream.php index c02844ac75..cdbe2bdcc9 100644 --- a/lib/PhpParser/Internal/TokenStream.php +++ b/lib/PhpParser/Internal/TokenStream.php @@ -20,9 +20,9 @@ class TokenStream { * * @param Token[] $tokens Tokens in PhpToken::tokenize() format */ - public function __construct(array $tokens) { + public function __construct(array $tokens, int $tabWidth) { $this->tokens = $tokens; - $this->indentMap = $this->calcIndentMap(); + $this->indentMap = $this->calcIndentMap($tabWidth); } /** @@ -248,7 +248,7 @@ public function getTokenCode(int $from, int $to, int $indent): string { * * @return int[] Token position to indentation map */ - private function calcIndentMap(): array { + private function calcIndentMap(int $tabWidth): array { $indentMap = []; $indent = 0; foreach ($this->tokens as $i => $token) { @@ -258,11 +258,11 @@ private function calcIndentMap(): array { $content = $token->text; $newlinePos = \strrpos($content, "\n"); if (false !== $newlinePos) { - $indent = \strlen($content) - $newlinePos - 1; + $indent = $this->getIndent(\substr($content, $newlinePos + 1), $tabWidth); } elseif ($i === 1 && $this->tokens[0]->id === \T_OPEN_TAG && $this->tokens[0]->text[\strlen($this->tokens[0]->text) - 1] === "\n") { // Special case: Newline at the end of opening tag followed by whitespace. - $indent = \strlen($content); + $indent = $this->getIndent($content, $tabWidth); } } } @@ -272,4 +272,11 @@ private function calcIndentMap(): array { return $indentMap; } + + private function getIndent(string $ws, int $tabWidth): int { + $spaces = \substr_count($ws, " "); + $tabs = \substr_count($ws, "\t"); + assert(\strlen($ws) === $spaces + $tabs); + return $spaces + $tabs * $tabWidth; + } } diff --git a/lib/PhpParser/PrettyPrinterAbstract.php b/lib/PhpParser/PrettyPrinterAbstract.php index 1367a4bfee..4941be95f4 100644 --- a/lib/PhpParser/PrettyPrinterAbstract.php +++ b/lib/PhpParser/PrettyPrinterAbstract.php @@ -106,6 +106,15 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter { /** @var int Current indentation level. */ protected int $indentLevel; + /** @var string String for single level of indentation */ + private string $indent; + /** @var int Width in spaces to indent by. */ + private int $indentWidth; + /** @var bool Whether to use tab indentation. */ + private bool $useTabs; + /** @var int Width in spaces of one tab. */ + private int $tabWidth = 4; + /** @var string Newline style. Does not include current indentation. */ protected string $newline; /** @var string Newline including current indentation. */ @@ -170,12 +179,14 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter { * 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". + * * string $indent: The indentation to use. Should either be all spaces or a single + * tab. Defaults to four spaces (" "). * * 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, newline?: string, shortArraySyntax?: bool + * phpVersion?: PhpVersion, newline?: string, indent?: string, shortArraySyntax?: bool * } $options Dictionary of formatting options */ public function __construct(array $options = []) { @@ -190,6 +201,17 @@ public function __construct(array $options = []) { $options['shortArraySyntax'] ?? $this->phpVersion->supportsShortArraySyntax(); $this->docStringEndToken = $this->phpVersion->supportsFlexibleHeredoc() ? null : '_DOC_STRING_END_' . mt_rand(); + + $this->indent = $indent = $options['indent'] ?? ' '; + if ($indent === "\t") { + $this->useTabs = true; + $this->indentWidth = $this->tabWidth; + } elseif ($indent === \str_repeat(' ', \strlen($indent))) { + $this->useTabs = false; + $this->indentWidth = \strlen($indent); + } else { + throw new \LogicException('Option "indent" must either be all spaces or a single tab'); + } } /** @@ -208,24 +230,29 @@ protected function resetState(): void { */ protected function setIndentLevel(int $level): void { $this->indentLevel = $level; - $this->nl = $this->newline . \str_repeat(' ', $level); + if ($this->useTabs) { + $tabs = \intdiv($level, $this->tabWidth); + $spaces = $level % $this->tabWidth; + $this->nl = $this->newline . \str_repeat("\t", $tabs) . \str_repeat(' ', $spaces); + } else { + $this->nl = $this->newline . \str_repeat(' ', $level); + } } /** * Increase indentation level. */ protected function indent(): void { - $this->indentLevel += 4; - $this->nl .= ' '; + $this->indentLevel += $this->indentWidth; + $this->nl .= $this->indent; } /** * Decrease indentation level. */ protected function outdent(): void { - assert($this->indentLevel >= 4); - $this->indentLevel -= 4; - $this->nl = $this->newline . str_repeat(' ', $this->indentLevel); + assert($this->indentLevel >= $this->indentWidth); + $this->setIndentLevel($this->indentLevel - $this->indentWidth); } /** @@ -537,7 +564,7 @@ public function printFormatPreserving(array $stmts, array $origStmts, array $ori $this->initializeModifierChangeMap(); $this->resetState(); - $this->origTokens = new TokenStream($origTokens); + $this->origTokens = new TokenStream($origTokens, $this->tabWidth); $this->preprocessNodes($stmts); diff --git a/test/PhpParser/PrettyPrinterTest.php b/test/PhpParser/PrettyPrinterTest.php index 5b650f1373..8e0e472179 100644 --- a/test/PhpParser/PrettyPrinterTest.php +++ b/test/PhpParser/PrettyPrinterTest.php @@ -17,11 +17,13 @@ class PrettyPrinterTest extends CodeTestAbstract { private function createParserAndPrinter(array $options): array { $parserVersion = $options['parserVersion'] ?? $options['version'] ?? null; $printerVersion = $options['version'] ?? null; + $indent = isset($options['indent']) ? json_decode($options['indent']) : null; $factory = new ParserFactory(); $parser = $factory->createForVersion($parserVersion !== null ? PhpVersion::fromString($parserVersion) : PhpVersion::getNewestSupported()); $prettyPrinter = new Standard([ - 'phpVersion' => $printerVersion !== null ? PhpVersion::fromString($printerVersion) : null + 'phpVersion' => $printerVersion !== null ? PhpVersion::fromString($printerVersion) : null, + 'indent' => $indent, ]); return [$parser, $prettyPrinter]; } @@ -297,4 +299,10 @@ public function testInvalidNewline(): void { $this->expectExceptionMessage('Option "newline" must be one of "\n" or "\r\n"'); new PrettyPrinter\Standard(['newline' => 'foo']); } + + public function testInvalidIndent(): void { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Option "indent" must either be all spaces or a single tab'); + new PrettyPrinter\Standard(['indent' => "\t "]); + } } diff --git a/test/code/formatPreservation/indent.test b/test/code/formatPreservation/indent.test new file mode 100644 index 0000000000..c8113a07dd --- /dev/null +++ b/test/code/formatPreservation/indent.test @@ -0,0 +1,37 @@ +Indentation +----- + $stmts]); +----- +!!indent=" " + $stmts]); +----- +!!indent="\t" +stmts[] = new Stmt\Expression(new Expr\Variable('y')); +----- +!!indent="\t" +