Skip to content

Commit 308c57c

Browse files
committed
Format-preserving printer
1 parent 6c04009 commit 308c57c

17 files changed

+2315
-4
lines changed

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"php": "^7.2 || ^8.0"
77
},
88
"require-dev": {
9+
"nikic/php-parser": "^4.15",
910
"php-parallel-lint/php-parallel-lint": "^1.2",
1011
"phpstan/extension-installer": "^1.0",
1112
"phpstan/phpstan": "^1.5",

phpstan-baseline.neon

+5
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,8 @@ parameters:
2424
message: "#^Method PHPStan\\\\PhpDocParser\\\\Parser\\\\StringUnescaper\\:\\:parseEscapeSequences\\(\\) should return string but returns string\\|null\\.$#"
2525
count: 1
2626
path: src/Parser/StringUnescaper.php
27+
28+
-
29+
message: "#^Variable property access on PHPStan\\\\PhpDocParser\\\\Ast\\\\Node\\.$#"
30+
count: 2
31+
path: src/Printer/Printer.php

phpstan.neon

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ parameters:
55
paths:
66
- src
77
- tests
8+
excludePaths:
9+
- tests/PHPStan/*/data/*
810
level: 8
911
ignoreErrors:
1012
- '#^Dynamic call to static method PHPUnit\\Framework\\Assert#'

src/Parser/ConstExprParser.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con
126126
);
127127
case 'array':
128128
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
129-
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES);
129+
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
130130
}
131131

132132
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
@@ -177,7 +177,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con
177177
);
178178

179179
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
180-
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
180+
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex);
181181
}
182182

183183
throw new ParserException(
@@ -191,12 +191,11 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con
191191
}
192192

193193

194-
private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr\ConstExprArrayNode
194+
private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
195195
{
196196
$items = [];
197197

198198
$startLine = $tokens->currentTokenLine();
199-
$startIndex = $tokens->currentTokenIndex();
200199

201200
if (!$tokens->tryConsumeTokenType($endToken)) {
202201
do {

src/Parser/TokenIterator.php

+22
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\PhpDocParser\Parser;
44

5+
use LogicException;
56
use PHPStan\PhpDocParser\Lexer\Lexer;
67
use function array_pop;
78
use function assert;
@@ -46,6 +47,27 @@ public function getTokens(): array
4647
}
4748

4849

50+
public function getContentBetween(int $startPos, int $endPos): string
51+
{
52+
if ($startPos < 0 || $endPos > count($this->tokens)) {
53+
throw new LogicException();
54+
}
55+
56+
$content = '';
57+
for ($i = $startPos; $i < $endPos; $i++) {
58+
$content .= $this->tokens[$i][Lexer::VALUE_OFFSET];
59+
}
60+
61+
return $content;
62+
}
63+
64+
65+
public function getTokenCount(): int
66+
{
67+
return count($this->tokens);
68+
}
69+
70+
4971
public function currentTokenValue(): string
5072
{
5173
return $this->tokens[$this->index][Lexer::VALUE_OFFSET];

src/Printer/DiffElem.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Printer;
4+
5+
/**
6+
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
7+
*
8+
* Copyright (c) 2011, Nikita Popov
9+
* All rights reserved.
10+
*
11+
* Implements the Myers diff algorithm.
12+
*
13+
* @internal
14+
*/
15+
class DiffElem
16+
{
17+
18+
public const TYPE_KEEP = 0;
19+
public const TYPE_REMOVE = 1;
20+
public const TYPE_ADD = 2;
21+
public const TYPE_REPLACE = 3;
22+
23+
/** @var self::TYPE_* */
24+
public $type;
25+
26+
/** @var mixed Is null for add operations */
27+
public $old;
28+
29+
/** @var mixed Is null for remove operations */
30+
public $new;
31+
32+
/**
33+
* @param self::TYPE_* $type
34+
* @param mixed $old Is null for add operations
35+
* @param mixed $new Is null for remove operations
36+
*/
37+
public function __construct(int $type, $old, $new)
38+
{
39+
$this->type = $type;
40+
$this->old = $old;
41+
$this->new = $new;
42+
}
43+
44+
}

src/Printer/Differ.php

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Printer;
4+
5+
use Exception;
6+
use function array_reverse;
7+
use function count;
8+
9+
/**
10+
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
11+
*
12+
* Copyright (c) 2011, Nikita Popov
13+
* All rights reserved.
14+
*
15+
* Implements the Myers diff algorithm.
16+
*
17+
* Myers, Eugene W. "An O (ND) difference algorithm and its variations."
18+
* Algorithmica 1.1 (1986): 251-266.
19+
*
20+
* @template T
21+
* @internal
22+
*/
23+
class Differ
24+
{
25+
26+
/** @var callable(T, T): bool */
27+
private $isEqual;
28+
29+
/**
30+
* Create differ over the given equality relation.
31+
*
32+
* @param callable(T, T): bool $isEqual Equality relation
33+
*/
34+
public function __construct(callable $isEqual)
35+
{
36+
$this->isEqual = $isEqual;
37+
}
38+
39+
/**
40+
* Calculate diff (edit script) from $old to $new.
41+
*
42+
* @param T[] $old Original array
43+
* @param T[] $new New array
44+
*
45+
* @return DiffElem[] Diff (edit script)
46+
*/
47+
public function diff(array $old, array $new): array
48+
{
49+
[$trace, $x, $y] = $this->calculateTrace($old, $new);
50+
return $this->extractDiff($trace, $x, $y, $old, $new);
51+
}
52+
53+
/**
54+
* Calculate diff, including "replace" operations.
55+
*
56+
* If a sequence of remove operations is followed by the same number of add operations, these
57+
* will be coalesced into replace operations.
58+
*
59+
* @param T[] $old Original array
60+
* @param T[] $new New array
61+
*
62+
* @return DiffElem[] Diff (edit script), including replace operations
63+
*/
64+
public function diffWithReplacements(array $old, array $new): array
65+
{
66+
return $this->coalesceReplacements($this->diff($old, $new));
67+
}
68+
69+
/**
70+
* @param T[] $old
71+
* @param T[] $new
72+
* @return array{array<int, array<int, int>>, int, int}
73+
*/
74+
private function calculateTrace(array $old, array $new): array
75+
{
76+
$n = count($old);
77+
$m = count($new);
78+
$max = $n + $m;
79+
$v = [1 => 0];
80+
$trace = [];
81+
for ($d = 0; $d <= $max; $d++) {
82+
$trace[] = $v;
83+
for ($k = -$d; $k <= $d; $k += 2) {
84+
if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
85+
$x = $v[$k + 1];
86+
} else {
87+
$x = $v[$k - 1] + 1;
88+
}
89+
90+
$y = $x - $k;
91+
while ($x < $n && $y < $m && ($this->isEqual)($old[$x], $new[$y])) {
92+
$x++;
93+
$y++;
94+
}
95+
96+
$v[$k] = $x;
97+
if ($x >= $n && $y >= $m) {
98+
return [$trace, $x, $y];
99+
}
100+
}
101+
}
102+
throw new Exception('Should not happen');
103+
}
104+
105+
/**
106+
* @param array<int, array<int, int>> $trace
107+
* @param T[] $old
108+
* @param T[] $new
109+
* @return DiffElem[]
110+
*/
111+
private function extractDiff(array $trace, int $x, int $y, array $old, array $new): array
112+
{
113+
$result = [];
114+
for ($d = count($trace) - 1; $d >= 0; $d--) {
115+
$v = $trace[$d];
116+
$k = $x - $y;
117+
118+
if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
119+
$prevK = $k + 1;
120+
} else {
121+
$prevK = $k - 1;
122+
}
123+
124+
$prevX = $v[$prevK];
125+
$prevY = $prevX - $prevK;
126+
127+
while ($x > $prevX && $y > $prevY) {
128+
$result[] = new DiffElem(DiffElem::TYPE_KEEP, $old[$x - 1], $new[$y - 1]);
129+
$x--;
130+
$y--;
131+
}
132+
133+
if ($d === 0) {
134+
break;
135+
}
136+
137+
while ($x > $prevX) {
138+
$result[] = new DiffElem(DiffElem::TYPE_REMOVE, $old[$x - 1], null);
139+
$x--;
140+
}
141+
142+
while ($y > $prevY) {
143+
$result[] = new DiffElem(DiffElem::TYPE_ADD, null, $new[$y - 1]);
144+
$y--;
145+
}
146+
}
147+
return array_reverse($result);
148+
}
149+
150+
/**
151+
* Coalesce equal-length sequences of remove+add into a replace operation.
152+
*
153+
* @param DiffElem[] $diff
154+
* @return DiffElem[]
155+
*/
156+
private function coalesceReplacements(array $diff): array
157+
{
158+
$newDiff = [];
159+
$c = count($diff);
160+
for ($i = 0; $i < $c; $i++) {
161+
$diffType = $diff[$i]->type;
162+
if ($diffType !== DiffElem::TYPE_REMOVE) {
163+
$newDiff[] = $diff[$i];
164+
continue;
165+
}
166+
167+
$j = $i;
168+
while ($j < $c && $diff[$j]->type === DiffElem::TYPE_REMOVE) {
169+
$j++;
170+
}
171+
172+
$k = $j;
173+
while ($k < $c && $diff[$k]->type === DiffElem::TYPE_ADD) {
174+
$k++;
175+
}
176+
177+
if ($j - $i === $k - $j) {
178+
$len = $j - $i;
179+
for ($n = 0; $n < $len; $n++) {
180+
$newDiff[] = new DiffElem(
181+
DiffElem::TYPE_REPLACE,
182+
$diff[$i + $n]->old,
183+
$diff[$j + $n]->new
184+
);
185+
}
186+
} else {
187+
for (; $i < $k; $i++) {
188+
$newDiff[] = $diff[$i];
189+
}
190+
}
191+
$i = $k - 1;
192+
}
193+
return $newDiff;
194+
}
195+
196+
}

0 commit comments

Comments
 (0)