Skip to content

Commit e50c67b

Browse files
committed
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.
1 parent 26a0197 commit e50c67b

File tree

6 files changed

+176
-14
lines changed

6 files changed

+176
-14
lines changed

doc/component/Pretty_printing.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ integer should be printed as decimal, hexadecimal, etc). Additionally, it suppor
3737
* `phpVersion` (defaults to 7.4) allows opting into formatting that is not supported by older PHP
3838
versions.
3939
* `newline` (defaults to `"\n"`) can be set to `"\r\n"` in order to produce Windows newlines.
40+
* `indent` (defaults to four spaces `" "`) can be set to any number of spaces or a single tab.
4041
* `shortArraySyntax` determines the used array syntax if the `kind` attribute is not set. This is
4142
a legacy option, and `phpVersion` should be used to control this behavior instead.
4243

lib/PhpParser/Internal/TokenStream.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ class TokenStream {
2020
*
2121
* @param Token[] $tokens Tokens in PhpToken::tokenize() format
2222
*/
23-
public function __construct(array $tokens) {
23+
public function __construct(array $tokens, int $tabWidth) {
2424
$this->tokens = $tokens;
25-
$this->indentMap = $this->calcIndentMap();
25+
$this->indentMap = $this->calcIndentMap($tabWidth);
2626
}
2727

2828
/**
@@ -248,7 +248,7 @@ public function getTokenCode(int $from, int $to, int $indent): string {
248248
*
249249
* @return int[] Token position to indentation map
250250
*/
251-
private function calcIndentMap(): array {
251+
private function calcIndentMap(int $tabWidth): array {
252252
$indentMap = [];
253253
$indent = 0;
254254
foreach ($this->tokens as $i => $token) {
@@ -258,11 +258,11 @@ private function calcIndentMap(): array {
258258
$content = $token->text;
259259
$newlinePos = \strrpos($content, "\n");
260260
if (false !== $newlinePos) {
261-
$indent = \strlen($content) - $newlinePos - 1;
261+
$indent = $this->getIndent(\substr($content, $newlinePos + 1), $tabWidth);
262262
} elseif ($i === 1 && $this->tokens[0]->id === \T_OPEN_TAG &&
263263
$this->tokens[0]->text[\strlen($this->tokens[0]->text) - 1] === "\n") {
264264
// Special case: Newline at the end of opening tag followed by whitespace.
265-
$indent = \strlen($content);
265+
$indent = $this->getIndent($content, $tabWidth);
266266
}
267267
}
268268
}
@@ -272,4 +272,11 @@ private function calcIndentMap(): array {
272272

273273
return $indentMap;
274274
}
275+
276+
private function getIndent(string $ws, int $tabWidth): int {
277+
$spaces = \substr_count($ws, " ");
278+
$tabs = \substr_count($ws, "\t");
279+
assert(\strlen($ws) === $spaces + $tabs);
280+
return $spaces + $tabs * $tabWidth;
281+
}
275282
}

lib/PhpParser/PrettyPrinterAbstract.php

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter {
106106

107107
/** @var int Current indentation level. */
108108
protected int $indentLevel;
109+
/** @var string String for single level of indentation */
110+
private string $indent;
111+
/** @var int Width in spaces to indent by. */
112+
private int $indentWidth;
113+
/** @var bool Whether to use tab indentation. */
114+
private bool $useTabs;
115+
/** @var int Width in spaces of one tab. */
116+
private int $tabWidth = 4;
117+
109118
/** @var string Newline style. Does not include current indentation. */
110119
protected string $newline;
111120
/** @var string Newline including current indentation. */
@@ -170,12 +179,14 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter {
170179
* PHP version while specifying an older target (but the result will
171180
* of course not be compatible with the older version in that case).
172181
* * string $newline: The newline style to use. Should be "\n" (default) or "\r\n".
182+
* * string $indent: The indentation to use. Should either be all spaces or a single
183+
* tab. Defaults to four spaces (" ").
173184
* * bool $shortArraySyntax: Whether to use [] instead of array() as the default array
174185
* syntax, if the node does not specify a format. Defaults to whether
175186
* the phpVersion support short array syntax.
176187
*
177188
* @param array{
178-
* phpVersion?: PhpVersion, newline?: string, shortArraySyntax?: bool
189+
* phpVersion?: PhpVersion, newline?: string, indent?: string, shortArraySyntax?: bool
179190
* } $options Dictionary of formatting options
180191
*/
181192
public function __construct(array $options = []) {
@@ -190,6 +201,17 @@ public function __construct(array $options = []) {
190201
$options['shortArraySyntax'] ?? $this->phpVersion->supportsShortArraySyntax();
191202
$this->docStringEndToken =
192203
$this->phpVersion->supportsFlexibleHeredoc() ? null : '_DOC_STRING_END_' . mt_rand();
204+
205+
$this->indent = $indent = $options['indent'] ?? ' ';
206+
if ($indent === "\t") {
207+
$this->useTabs = true;
208+
$this->indentWidth = $this->tabWidth;
209+
} elseif ($indent === \str_repeat(' ', \strlen($indent))) {
210+
$this->useTabs = false;
211+
$this->indentWidth = \strlen($indent);
212+
} else {
213+
throw new \LogicException('Option "indent" must either be all spaces or a single tab');
214+
}
193215
}
194216

195217
/**
@@ -208,24 +230,29 @@ protected function resetState(): void {
208230
*/
209231
protected function setIndentLevel(int $level): void {
210232
$this->indentLevel = $level;
211-
$this->nl = $this->newline . \str_repeat(' ', $level);
233+
if ($this->useTabs) {
234+
$tabs = \intdiv($level, $this->tabWidth);
235+
$spaces = $level % $this->tabWidth;
236+
$this->nl = $this->newline . \str_repeat("\t", $tabs) . \str_repeat(' ', $spaces);
237+
} else {
238+
$this->nl = $this->newline . \str_repeat(' ', $level);
239+
}
212240
}
213241

214242
/**
215243
* Increase indentation level.
216244
*/
217245
protected function indent(): void {
218-
$this->indentLevel += 4;
219-
$this->nl .= ' ';
246+
$this->indentLevel += $this->indentWidth;
247+
$this->nl .= $this->indent;
220248
}
221249

222250
/**
223251
* Decrease indentation level.
224252
*/
225253
protected function outdent(): void {
226-
assert($this->indentLevel >= 4);
227-
$this->indentLevel -= 4;
228-
$this->nl = $this->newline . str_repeat(' ', $this->indentLevel);
254+
assert($this->indentLevel >= $this->indentWidth);
255+
$this->setIndentLevel($this->indentLevel - $this->indentWidth);
229256
}
230257

231258
/**
@@ -537,7 +564,7 @@ public function printFormatPreserving(array $stmts, array $origStmts, array $ori
537564
$this->initializeModifierChangeMap();
538565

539566
$this->resetState();
540-
$this->origTokens = new TokenStream($origTokens);
567+
$this->origTokens = new TokenStream($origTokens, $this->tabWidth);
541568

542569
$this->preprocessNodes($stmts);
543570

test/PhpParser/PrettyPrinterTest.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ class PrettyPrinterTest extends CodeTestAbstract {
1717
private function createParserAndPrinter(array $options): array {
1818
$parserVersion = $options['parserVersion'] ?? $options['version'] ?? null;
1919
$printerVersion = $options['version'] ?? null;
20+
$indent = isset($options['indent']) ? json_decode($options['indent']) : null;
2021
$factory = new ParserFactory();
2122
$parser = $factory->createForVersion($parserVersion !== null
2223
? PhpVersion::fromString($parserVersion) : PhpVersion::getNewestSupported());
2324
$prettyPrinter = new Standard([
24-
'phpVersion' => $printerVersion !== null ? PhpVersion::fromString($printerVersion) : null
25+
'phpVersion' => $printerVersion !== null ? PhpVersion::fromString($printerVersion) : null,
26+
'indent' => $indent,
2527
]);
2628
return [$parser, $prettyPrinter];
2729
}
@@ -297,4 +299,10 @@ public function testInvalidNewline(): void {
297299
$this->expectExceptionMessage('Option "newline" must be one of "\n" or "\r\n"');
298300
new PrettyPrinter\Standard(['newline' => 'foo']);
299301
}
302+
303+
public function testInvalidIndent(): void {
304+
$this->expectException(\LogicException::class);
305+
$this->expectExceptionMessage('Option "indent" must either be all spaces or a single tab');
306+
new PrettyPrinter\Standard(['indent' => "\t "]);
307+
}
300308
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Indentation
2+
-----
3+
<?php
4+
$x;
5+
-----
6+
$stmts[0] = new Stmt\If_(new Expr\Variable('a'), ['stmts' => $stmts]);
7+
-----
8+
!!indent=" "
9+
<?php
10+
if ($a) {
11+
$x;
12+
}
13+
-----
14+
<?php
15+
$x;
16+
-----
17+
$stmts[0] = new Stmt\If_(new Expr\Variable('a'), ['stmts' => $stmts]);
18+
-----
19+
!!indent="\t"
20+
<?php
21+
if ($a) {
22+
@@{"\t"}@@$x;
23+
}
24+
-----
25+
<?php
26+
if ($a) {
27+
@@{"\t"}@@$x;
28+
}
29+
-----
30+
$stmts[0]->stmts[] = new Stmt\Expression(new Expr\Variable('y'));
31+
-----
32+
!!indent="\t"
33+
<?php
34+
if ($a) {
35+
@@{"\t"}@@$x;
36+
@@{"\t"}@@$y;
37+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
Indentation
2+
-----
3+
<?php
4+
5+
class Test {
6+
/**
7+
* Comment
8+
*/
9+
public function foo() {
10+
if (1) {
11+
echo $bar;
12+
}
13+
}
14+
}
15+
-----
16+
!!indent=" "
17+
class Test
18+
{
19+
/**
20+
* Comment
21+
*/
22+
public function foo()
23+
{
24+
if (1) {
25+
echo $bar;
26+
}
27+
}
28+
}
29+
-----
30+
<?php
31+
32+
class Test {
33+
/**
34+
* Comment
35+
*/
36+
public function foo() {
37+
if (1) {
38+
echo $bar;
39+
}
40+
}
41+
}
42+
-----
43+
!!indent=" "
44+
class Test
45+
{
46+
/**
47+
* Comment
48+
*/
49+
public function foo()
50+
{
51+
if (1) {
52+
echo $bar;
53+
}
54+
}
55+
}
56+
-----
57+
<?php
58+
59+
class Test {
60+
/**
61+
* Comment
62+
*/
63+
public function foo() {
64+
if (1) {
65+
echo $bar;
66+
}
67+
}
68+
}
69+
-----
70+
!!indent="\t"
71+
class Test
72+
{
73+
@@{"\t"}@@/**
74+
@@{"\t"}@@ * Comment
75+
@@{"\t"}@@ */
76+
@@{"\t"}@@public function foo()
77+
@@{"\t"}@@{
78+
@@{"\t\t"}@@if (1) {
79+
@@{"\t\t\t"}@@echo $bar;
80+
@@{"\t\t"}@@}
81+
@@{"\t"}@@}
82+
}

0 commit comments

Comments
 (0)