Skip to content

Commit 717cd3f

Browse files
authored
Add minimal diff support when regenerating baseline files (#52)
1 parent 0926fb0 commit 717cd3f

5 files changed

Lines changed: 422 additions & 70 deletions

File tree

src/BaselineSplitter.php

Lines changed: 106 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22

33
namespace ShipMonk\PHPStan\Baseline;
44

5+
use ArrayIterator;
56
use ShipMonk\PHPStan\Baseline\Exception\ErrorException;
7+
use ShipMonk\PHPStan\Baseline\Handler\BaselineHandler;
68
use ShipMonk\PHPStan\Baseline\Handler\HandlerFactory;
79
use SplFileInfo;
810
use function array_reduce;
9-
use function assert;
1011
use function dirname;
1112
use function file_put_contents;
12-
use function is_array;
13-
use function is_int;
14-
use function is_string;
13+
use function is_file;
1514
use function ksort;
1615
use function str_replace;
1716

@@ -49,28 +48,21 @@ public function split(string $loaderFilePath): array
4948
$extension = $splFile->getExtension();
5049

5150
$handler = HandlerFactory::create($extension);
52-
$data = $handler->decodeBaseline($realPath);
53-
54-
$ignoredErrors = $data['parameters']['ignoreErrors'] ?? null; // @phpstan-ignore offsetAccess.nonOffsetAccessible
55-
56-
if (!is_array($ignoredErrors)) {
57-
throw new ErrorException(
58-
"Invalid argument, expected $extension file with 'parameters.ignoreErrors' key in '$loaderFilePath'." .
59-
"\n - Did you run native baseline generation first?" .
60-
"\n - You can so via vendor/bin/phpstan --generate-baseline=$loaderFilePath",
61-
);
62-
}
63-
51+
$ignoredErrors = $handler->decodeBaseline($realPath);
6452
$groupedErrors = $this->groupErrorsByIdentifier($ignoredErrors, $folder);
6553

6654
$outputInfo = [];
6755
$baselineFiles = [];
6856
$totalErrorCount = 0;
6957

70-
foreach ($groupedErrors as $identifier => $errors) {
58+
foreach ($groupedErrors as $identifier => $newErrors) {
7159
$fileName = $identifier . '.' . $extension;
7260
$filePath = $folder . '/' . $fileName;
73-
$errorsCount = array_reduce($errors, static fn (int $carry, array $item): int => $carry + $item['count'], 0);
61+
62+
$oldErrors = $this->readExistingErrors($filePath, $handler) ?? [];
63+
$sortedErrors = $this->sortErrors($oldErrors, $newErrors);
64+
65+
$errorsCount = array_reduce($sortedErrors, static fn (int $carry, array $item): int => $carry + $item['count'], 0);
7466
$totalErrorCount += $errorsCount;
7567

7668
$outputInfo[$filePath] = $errorsCount;
@@ -79,7 +71,7 @@ public function split(string $loaderFilePath): array
7971
$plural = $errorsCount === 1 ? '' : 's';
8072
$prefix = $this->includeCount ? "total $errorsCount error$plural" : null;
8173

82-
$encodedData = $handler->encodeBaseline($prefix, $errors, $this->indent);
74+
$encodedData = $handler->encodeBaseline($prefix, $sortedErrors, $this->indent);
8375
$this->writeFile($filePath, $encodedData);
8476
}
8577

@@ -94,7 +86,7 @@ public function split(string $loaderFilePath): array
9486
}
9587

9688
/**
97-
* @param array<mixed> $errors
89+
* @param list<array{message: string, count: int, path: string, identifier: string|null}|array{rawMessage: string, count: int, path: string, identifier: string|null}> $errors
9890
* @return array<string, list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}>>
9991
*
10092
* @throws ErrorException
@@ -106,53 +98,27 @@ private function groupErrorsByIdentifier(
10698
{
10799
$groupedErrors = [];
108100

109-
foreach ($errors as $index => $error) {
110-
if (!is_array($error)) {
111-
throw new ErrorException("Ignored error #$index is not an array");
112-
}
113-
101+
foreach ($errors as $error) {
114102
$identifier = $error['identifier'] ?? 'missing-identifier';
103+
$normalizedPath = str_replace($folder . '/', '', $error['path']);
115104

116105
if (isset($error['rawMessage'])) {
117-
$message = $error['rawMessage'];
118-
$rawMessage = true;
119-
} elseif (isset($error['message'])) {
120-
$message = $error['message'];
121-
$rawMessage = false;
122-
} else {
123-
throw new ErrorException("Ignored error #$index is missing 'message' or 'rawMessage'");
124-
}
106+
$groupedErrors[$identifier][] = [
107+
'rawMessage' => $error['rawMessage'],
108+
'count' => $error['count'],
109+
'path' => $normalizedPath,
110+
];
125111

126-
if (!isset($error['count'])) {
127-
throw new ErrorException("Ignored error #$index is missing 'count'");
128-
}
129-
130-
$count = $error['count'];
112+
} elseif (isset($error['message'])) {
113+
$groupedErrors[$identifier][] = [
114+
'message' => $error['message'],
115+
'count' => $error['count'],
116+
'path' => $normalizedPath,
117+
];
131118

132-
if (!isset($error['path'])) {
133-
throw new ErrorException("Ignored error #$index is missing 'path'");
119+
} else {
120+
throw new ErrorException('Error is missing message or rawMessage');
134121
}
135-
136-
$path = $error['path'];
137-
138-
assert(is_string($identifier));
139-
assert(is_string($message));
140-
assert(is_int($count));
141-
assert(is_string($path));
142-
143-
$normalizedPath = str_replace($folder . '/', '', $path);
144-
145-
unset($error['identifier']);
146-
147-
$groupedErrors[$identifier][] = $rawMessage ? [
148-
'rawMessage' => $message,
149-
'count' => $count,
150-
'path' => $normalizedPath,
151-
] : [
152-
'message' => $message,
153-
'count' => $count,
154-
'path' => $normalizedPath,
155-
];
156122
}
157123

158124
ksort($groupedErrors);
@@ -175,4 +141,83 @@ private function writeFile(
175141
}
176142
}
177143

144+
/**
145+
* @param array{message?: string, rawMessage?: string, count: int, path: string} $error
146+
*/
147+
private function getErrorKey(array $error): string
148+
{
149+
return $error['path'] . "\x00" . ($error['rawMessage'] ?? $error['message'] ?? '');
150+
}
151+
152+
/**
153+
* @return list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}>|null
154+
*/
155+
private function readExistingErrors(
156+
string $filePath,
157+
BaselineHandler $handler
158+
): ?array
159+
{
160+
if (!is_file($filePath)) {
161+
return null;
162+
}
163+
164+
try {
165+
return $handler->decodeBaseline($filePath);
166+
167+
} catch (ErrorException $e) {
168+
return null;
169+
}
170+
}
171+
172+
/**
173+
* @param list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}> $oldErrors
174+
* @param list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}> $newErrors
175+
* @return list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}>
176+
*/
177+
private function sortErrors(
178+
array $oldErrors,
179+
array $newErrors
180+
): array
181+
{
182+
$newErrorsByKey = [];
183+
184+
foreach ($newErrors as $newError) {
185+
$key = $this->getErrorKey($newError);
186+
$newErrorsByKey[$key] = $newError;
187+
}
188+
189+
// collect errors that existed before
190+
$existingByKey = [];
191+
192+
foreach ($oldErrors as $oldError) {
193+
$key = $this->getErrorKey($oldError);
194+
195+
if (isset($newErrorsByKey[$key])) {
196+
$existingByKey[$key] = $newErrorsByKey[$key];
197+
unset($newErrorsByKey[$key]);
198+
}
199+
}
200+
201+
// insert new errors at their sorted positions among existing errors
202+
ksort($newErrorsByKey);
203+
$newErrorsIterator = new ArrayIterator($newErrorsByKey);
204+
$result = [];
205+
206+
foreach ($existingByKey as $existingKey => $existingError) {
207+
while ($newErrorsIterator->valid() && $newErrorsIterator->key() < $existingKey) {
208+
$result[] = $newErrorsIterator->current();
209+
$newErrorsIterator->next();
210+
}
211+
212+
$result[] = $existingError;
213+
}
214+
215+
while ($newErrorsIterator->valid()) {
216+
$result[] = $newErrorsIterator->current();
217+
$newErrorsIterator->next();
218+
}
219+
220+
return $result;
221+
}
222+
178223
}

src/Handler/BaselineHandler.php

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,95 @@
33
namespace ShipMonk\PHPStan\Baseline\Handler;
44

55
use ShipMonk\PHPStan\Baseline\Exception\ErrorException;
6+
use function is_array;
7+
use function is_int;
8+
use function is_string;
69

7-
interface BaselineHandler
10+
abstract class BaselineHandler
811
{
912

13+
/**
14+
* @return list<array{message: string, count: int, path: string, identifier: string|null}|array{rawMessage: string, count: int, path: string, identifier: string|null}>
15+
*
16+
* @throws ErrorException
17+
*/
18+
public function decodeBaseline(string $filepath): array
19+
{
20+
$decoded = $this->decodeBaselineFile($filepath);
21+
22+
if (!isset($decoded['parameters']) || !is_array($decoded['parameters'])) {
23+
throw new ErrorException("File '$filepath' must contain 'parameters' array");
24+
}
25+
26+
if (!isset($decoded['parameters']['ignoreErrors']) || !is_array($decoded['parameters']['ignoreErrors'])) {
27+
throw new ErrorException("File '$filepath' must contain 'parameters.ignoreErrors' array");
28+
}
29+
30+
$errors = $decoded['parameters']['ignoreErrors'];
31+
$result = [];
32+
33+
foreach ($errors as $index => $error) {
34+
if (!is_array($error)) {
35+
throw new ErrorException("Ignored error #$index in '$filepath' is not an array");
36+
}
37+
38+
if (!isset($error['path']) || !is_string($error['path'])) {
39+
throw new ErrorException("Ignored error #$index in '$filepath' is missing 'path'");
40+
}
41+
42+
if (!isset($error['count']) || !is_int($error['count'])) {
43+
throw new ErrorException("Ignored error #$index in '$filepath' is missing 'count'");
44+
}
45+
46+
$error['identifier'] ??= null;
47+
48+
if ($error['identifier'] !== null && !is_string($error['identifier'])) {
49+
throw new ErrorException("Ignored error #$index in '$filepath' has invalid 'identifier'");
50+
}
51+
52+
if (isset($error['rawMessage'])) {
53+
if (!is_string($error['rawMessage'])) {
54+
throw new ErrorException("Ignored error #$index in '$filepath' has invalid 'rawMessage'");
55+
}
56+
57+
$result[] = [
58+
'rawMessage' => $error['rawMessage'],
59+
'count' => $error['count'],
60+
'path' => $error['path'],
61+
'identifier' => $error['identifier'],
62+
];
63+
64+
} elseif (isset($error['message'])) {
65+
if (!is_string($error['message'])) {
66+
throw new ErrorException("Ignored error #$index in '$filepath' has invalid 'message'");
67+
}
68+
69+
$result[] = [
70+
'message' => $error['message'],
71+
'count' => $error['count'],
72+
'path' => $error['path'],
73+
'identifier' => $error['identifier'],
74+
];
75+
76+
} else {
77+
throw new ErrorException("Ignored error #$index in '$filepath' is missing 'message' or 'rawMessage'");
78+
}
79+
}
80+
81+
return $result;
82+
}
83+
1084
/**
1185
* @return array<mixed>
1286
*
1387
* @throws ErrorException
1488
*/
15-
public function decodeBaseline(string $filepath): array;
89+
abstract protected function decodeBaselineFile(string $filepath): array;
1690

1791
/**
1892
* @param list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}> $errors
1993
*/
20-
public function encodeBaseline(
94+
abstract public function encodeBaseline(
2195
?string $comment,
2296
array $errors,
2397
string $indent
@@ -26,7 +100,7 @@ public function encodeBaseline(
26100
/**
27101
* @param list<string> $filePaths
28102
*/
29-
public function encodeBaselineLoader(
103+
abstract public function encodeBaselineLoader(
30104
?string $comment,
31105
array $filePaths,
32106
string $indent

src/Handler/NeonBaselineHandler.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
use function gettype;
1010
use function is_array;
1111

12-
class NeonBaselineHandler implements BaselineHandler
12+
class NeonBaselineHandler extends BaselineHandler
1313
{
1414

15-
public function decodeBaseline(string $filepath): array
15+
protected function decodeBaselineFile(string $filepath): array
1616
{
1717
try {
1818
/** @throws NeonException */

src/Handler/PhpBaselineHandler.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
use function sprintf;
1010
use function var_export;
1111

12-
class PhpBaselineHandler implements BaselineHandler
12+
class PhpBaselineHandler extends BaselineHandler
1313
{
1414

15-
public function decodeBaseline(string $filepath): array
15+
protected function decodeBaselineFile(string $filepath): array
1616
{
1717
try {
1818
$decoded = (static fn () => require $filepath)();
@@ -23,8 +23,10 @@ public function decodeBaseline(string $filepath): array
2323

2424
return $decoded;
2525

26+
} catch (ErrorException $e) {
27+
throw $e;
2628
} catch (Throwable $e) {
27-
throw new ErrorException("Error while loading baseline file '$filepath':" . $e->getMessage(), $e);
29+
throw new ErrorException("Error while loading baseline file '$filepath': " . $e->getMessage(), $e);
2830
}
2931
}
3032

0 commit comments

Comments
 (0)