Skip to content

Commit b997803

Browse files
authored
Automatically delete orphaned baseline files (#53)
1 parent 717cd3f commit b997803

File tree

4 files changed

+193
-3
lines changed

4 files changed

+193
-3
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ _(optional)_ You can simplify generation with e.g. composer script:
5959
"scripts": {
6060
"generate:baseline:phpstan": [
6161
"phpstan --generate-baseline=baselines/_loader.neon",
62-
"find baselines/ -type f -not -name _loader.neon -delete",
6362
"split-phpstan-baseline baselines/_loader.neon"
6463
]
6564
}

bin/split-phpstan-baseline

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ try {
5656
$writtenBaselines = $splitter->split($loaderFile);
5757

5858
foreach ($writtenBaselines as $writtenBaseline => $errorsCount) {
59-
echo "Writing baseline file $writtenBaseline" . ($errorsCount !== null ? " with $errorsCount errors\n" : "\n");
59+
if ($errorsCount === 0) {
60+
echo "Deleting baseline file $writtenBaseline\n";
61+
} elseif ($errorsCount !== null) {
62+
echo "Writing baseline file $writtenBaseline with $errorsCount errors\n";
63+
} else {
64+
echo "Writing baseline file $writtenBaseline\n";
65+
}
6066
}
6167

6268
} catch (ErrorException $e) {

src/BaselineSplitter.php

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
use ShipMonk\PHPStan\Baseline\Handler\HandlerFactory;
99
use SplFileInfo;
1010
use function array_reduce;
11+
use function basename;
1112
use function dirname;
1213
use function file_put_contents;
14+
use function glob;
1315
use function is_file;
1416
use function ksort;
1517
use function str_replace;
18+
use function unlink;
1619

1720
class BaselineSplitter
1821
{
@@ -31,7 +34,7 @@ public function __construct(
3134
}
3235

3336
/**
34-
* @return array<string, int|null>
37+
* @return array<string, int|null> file path => error count (null for loader, 0 for deleted)
3538
*
3639
* @throws ErrorException
3740
*/
@@ -45,6 +48,7 @@ public function split(string $loaderFilePath): array
4548
}
4649

4750
$folder = dirname($realPath);
51+
$loaderFileName = $splFile->getFilename();
4852
$extension = $splFile->getExtension();
4953

5054
$handler = HandlerFactory::create($extension);
@@ -53,6 +57,7 @@ public function split(string $loaderFilePath): array
5357

5458
$outputInfo = [];
5559
$baselineFiles = [];
60+
$writtenFiles = [];
5661
$totalErrorCount = 0;
5762

5863
foreach ($groupedErrors as $identifier => $newErrors) {
@@ -67,6 +72,7 @@ public function split(string $loaderFilePath): array
6772

6873
$outputInfo[$filePath] = $errorsCount;
6974
$baselineFiles[] = $fileName;
75+
$writtenFiles[$filePath] = true;
7076

7177
$plural = $errorsCount === 1 ? '' : 's';
7278
$prefix = $this->includeCount ? "total $errorsCount error$plural" : null;
@@ -82,6 +88,13 @@ public function split(string $loaderFilePath): array
8288

8389
$outputInfo[$realPath] = null;
8490

91+
// Delete orphaned baseline files
92+
$deletedFiles = $this->deleteOrphanedFiles($folder, $extension, $loaderFileName, $writtenFiles);
93+
94+
foreach ($deletedFiles as $deletedFile) {
95+
$outputInfo[$deletedFile] = 0;
96+
}
97+
8598
return $outputInfo;
8699
}
87100

@@ -220,4 +233,44 @@ private function sortErrors(
220233
return $result;
221234
}
222235

236+
/**
237+
* @param array<string, true> $writtenFiles
238+
* @return list<string>
239+
*/
240+
private function deleteOrphanedFiles(
241+
string $folder,
242+
string $extension,
243+
string $loaderFileName,
244+
array $writtenFiles
245+
): array
246+
{
247+
$deletedFiles = [];
248+
$existingFiles = glob($folder . '/*.' . $extension);
249+
250+
if ($existingFiles === false) {
251+
return [];
252+
}
253+
254+
foreach ($existingFiles as $existingFile) {
255+
$fileName = basename($existingFile);
256+
257+
// Skip the loader file
258+
if ($fileName === $loaderFileName) {
259+
continue;
260+
}
261+
262+
// Skip files that were written in this run
263+
if (isset($writtenFiles[$existingFile])) {
264+
continue;
265+
}
266+
267+
// Delete orphaned file
268+
if (unlink($existingFile)) {
269+
$deletedFiles[] = $existingFile;
270+
}
271+
}
272+
273+
return $deletedFiles;
274+
}
275+
223276
}

tests/SplitterTest.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,138 @@ public function testFirstRunCreatesFileSorted(): void
334334
self::assertStringContainsString('Error C', $result);
335335
}
336336

337+
public function testOrphanedFilesAreDeleted(): void
338+
{
339+
$folder = $this->prepareSampleFolder();
340+
$loaderPath = $folder . '/baselines/loader.neon';
341+
342+
// Create existing baseline files for two identifiers
343+
$existingBaseline1 = <<<'NEON'
344+
parameters:
345+
ignoreErrors:
346+
-
347+
message: '#^Error A$#'
348+
count: 1
349+
path: ../app/a.php
350+
351+
NEON;
352+
$existingBaseline2 = <<<'NEON'
353+
parameters:
354+
ignoreErrors:
355+
-
356+
message: '#^Error B$#'
357+
count: 1
358+
path: ../app/b.php
359+
360+
NEON;
361+
file_put_contents($folder . '/baselines/first.identifier.neon', $existingBaseline1);
362+
file_put_contents($folder . '/baselines/second.identifier.neon', $existingBaseline2);
363+
364+
// Now regenerate with only first.identifier (second.identifier should be deleted)
365+
$inputErrors = [
366+
'parameters' => [
367+
'ignoreErrors' => [
368+
['message' => '#^Error A$#', 'count' => 1, 'path' => '../app/a.php', 'identifier' => 'first.identifier'],
369+
],
370+
],
371+
];
372+
file_put_contents($loaderPath, Neon::encode($inputErrors));
373+
374+
$splitter = new BaselineSplitter("\t", true);
375+
$result = $splitter->split($loaderPath);
376+
377+
$firstIdentifierPath = $folder . '/baselines/first.identifier.neon';
378+
$secondIdentifierPath = $folder . '/baselines/second.identifier.neon';
379+
380+
// first.identifier.neon should still exist
381+
self::assertFileExists($firstIdentifierPath);
382+
self::assertArrayHasKey($firstIdentifierPath, $result);
383+
self::assertSame(1, $result[$firstIdentifierPath]);
384+
385+
// second.identifier.neon should be deleted
386+
self::assertFileDoesNotExist($secondIdentifierPath);
387+
self::assertArrayHasKey($secondIdentifierPath, $result);
388+
self::assertSame(0, $result[$secondIdentifierPath]);
389+
390+
// loader should still exist
391+
self::assertFileExists($loaderPath);
392+
self::assertArrayHasKey($loaderPath, $result);
393+
self::assertNull($result[$loaderPath]);
394+
}
395+
396+
public function testLoaderFileIsNotDeleted(): void
397+
{
398+
$folder = $this->prepareSampleFolder();
399+
$loaderPath = $folder . '/baselines/loader.neon';
400+
$orphanPath = $folder . '/baselines/orphan.neon';
401+
402+
// Create an empty baseline (no errors)
403+
$inputErrors = [
404+
'parameters' => [
405+
'ignoreErrors' => [],
406+
],
407+
];
408+
file_put_contents($loaderPath, Neon::encode($inputErrors));
409+
410+
// Create an orphaned file that happens to exist
411+
file_put_contents($orphanPath, 'some content');
412+
413+
$splitter = new BaselineSplitter("\t", true);
414+
$result = $splitter->split($loaderPath);
415+
416+
// Loader should still exist
417+
self::assertFileExists($loaderPath);
418+
self::assertArrayHasKey($loaderPath, $result);
419+
self::assertNull($result[$loaderPath]);
420+
421+
// Orphaned file should be deleted
422+
self::assertFileDoesNotExist($orphanPath);
423+
self::assertArrayHasKey($orphanPath, $result);
424+
self::assertSame(0, $result[$orphanPath]);
425+
}
426+
427+
public function testOnlyFilesWithMatchingExtensionAreDeleted(): void
428+
{
429+
$folder = $this->prepareSampleFolder();
430+
$loaderPath = $folder . '/baselines/loader.neon';
431+
432+
// Create a .neon baseline file and a .php file
433+
$existingBaseline = <<<'NEON'
434+
parameters:
435+
ignoreErrors:
436+
-
437+
message: '#^Error A$#'
438+
count: 1
439+
path: ../app/a.php
440+
441+
NEON;
442+
file_put_contents($folder . '/baselines/old.identifier.neon', $existingBaseline);
443+
file_put_contents($folder . '/baselines/some.file.php', '<?php // some php file');
444+
445+
// Regenerate with empty baseline
446+
$inputErrors = [
447+
'parameters' => [
448+
'ignoreErrors' => [],
449+
],
450+
];
451+
file_put_contents($loaderPath, Neon::encode($inputErrors));
452+
453+
$splitter = new BaselineSplitter("\t", true);
454+
$result = $splitter->split($loaderPath);
455+
456+
$oldIdentifierPath = $folder . '/baselines/old.identifier.neon';
457+
$phpFilePath = $folder . '/baselines/some.file.php';
458+
459+
// .neon file should be deleted
460+
self::assertFileDoesNotExist($oldIdentifierPath);
461+
self::assertArrayHasKey($oldIdentifierPath, $result);
462+
self::assertSame(0, $result[$oldIdentifierPath]);
463+
464+
// .php file should NOT be deleted (wrong extension)
465+
self::assertFileExists($phpFilePath);
466+
self::assertArrayNotHasKey($phpFilePath, $result);
467+
}
468+
337469
/**
338470
* @return array{parameters: array{ignoreErrors: array{0: array{message?: string, rawMessage?: string, count: int, path: string, identifier?: string}}}}
339471
*/

0 commit comments

Comments
 (0)