Skip to content

Commit

Permalink
Add basic support for tab indentation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nikic committed Sep 21, 2024
1 parent 26a0197 commit e50c67b
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 14 deletions.
1 change: 1 addition & 0 deletions doc/component/Pretty_printing.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 12 additions & 5 deletions lib/PhpParser/Internal/TokenStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
}
}
Expand All @@ -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;
}
}
43 changes: 35 additions & 8 deletions lib/PhpParser/PrettyPrinterAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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 = []) {
Expand All @@ -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');
}
}

/**
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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);

Expand Down
10 changes: 9 additions & 1 deletion test/PhpParser/PrettyPrinterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down Expand Up @@ -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 "]);
}
}
37 changes: 37 additions & 0 deletions test/code/formatPreservation/indent.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Indentation
-----
<?php
$x;
-----
$stmts[0] = new Stmt\If_(new Expr\Variable('a'), ['stmts' => $stmts]);
-----
!!indent=" "
<?php
if ($a) {
$x;
}
-----
<?php
$x;
-----
$stmts[0] = new Stmt\If_(new Expr\Variable('a'), ['stmts' => $stmts]);
-----
!!indent="\t"
<?php
if ($a) {
@@{"\t"}@@$x;
}
-----
<?php
if ($a) {
@@{"\t"}@@$x;
}
-----
$stmts[0]->stmts[] = new Stmt\Expression(new Expr\Variable('y'));
-----
!!indent="\t"
<?php
if ($a) {
@@{"\t"}@@$x;
@@{"\t"}@@$y;
}
82 changes: 82 additions & 0 deletions test/code/prettyPrinter/indent.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
Indentation
-----
<?php

class Test {
/**
* Comment
*/
public function foo() {
if (1) {
echo $bar;
}
}
}
-----
!!indent=" "
class Test
{
/**
* Comment
*/
public function foo()
{
if (1) {
echo $bar;
}
}
}
-----
<?php

class Test {
/**
* Comment
*/
public function foo() {
if (1) {
echo $bar;
}
}
}
-----
!!indent=" "
class Test
{
/**
* Comment
*/
public function foo()
{
if (1) {
echo $bar;
}
}
}
-----
<?php

class Test {
/**
* Comment
*/
public function foo() {
if (1) {
echo $bar;
}
}
}
-----
!!indent="\t"
class Test
{
@@{"\t"}@@/**
@@{"\t"}@@ * Comment
@@{"\t"}@@ */
@@{"\t"}@@public function foo()
@@{"\t"}@@{
@@{"\t\t"}@@if (1) {
@@{"\t\t\t"}@@echo $bar;
@@{"\t\t"}@@}
@@{"\t"}@@}
}

0 comments on commit e50c67b

Please sign in to comment.