Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ _(optional)_ You can simplify generation with e.g. composer script:
"scripts": {
"generate:baseline:phpstan": [
"phpstan --generate-baseline=baselines/_loader.neon",
"find baselines/ -type f -not -name _loader.neon -delete",
"split-phpstan-baseline baselines/_loader.neon"
]
}
Expand Down
8 changes: 7 additions & 1 deletion bin/split-phpstan-baseline
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ try {
$writtenBaselines = $splitter->split($loaderFile);

foreach ($writtenBaselines as $writtenBaseline => $errorsCount) {
echo "Writing baseline file $writtenBaseline" . ($errorsCount !== null ? " with $errorsCount errors\n" : "\n");
if ($errorsCount === 0) {
echo "Deleting baseline file $writtenBaseline\n";
} elseif ($errorsCount !== null) {
echo "Writing baseline file $writtenBaseline with $errorsCount errors\n";
} else {
echo "Writing baseline file $writtenBaseline\n";
}
}

} catch (ErrorException $e) {
Expand Down
55 changes: 54 additions & 1 deletion src/BaselineSplitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
use ShipMonk\PHPStan\Baseline\Handler\HandlerFactory;
use SplFileInfo;
use function array_reduce;
use function basename;
use function dirname;
use function file_put_contents;
use function glob;
use function is_file;
use function ksort;
use function str_replace;
use function unlink;

class BaselineSplitter
{
Expand All @@ -31,7 +34,7 @@ public function __construct(
}

/**
* @return array<string, int|null>
* @return array<string, int|null> file path => error count (null for loader, 0 for deleted)
*
* @throws ErrorException
*/
Expand All @@ -45,6 +48,7 @@ public function split(string $loaderFilePath): array
}

$folder = dirname($realPath);
$loaderFileName = $splFile->getFilename();
$extension = $splFile->getExtension();

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

$outputInfo = [];
$baselineFiles = [];
$writtenFiles = [];
$totalErrorCount = 0;

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

$outputInfo[$filePath] = $errorsCount;
$baselineFiles[] = $fileName;
$writtenFiles[$filePath] = true;

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

$outputInfo[$realPath] = null;

// Delete orphaned baseline files
$deletedFiles = $this->deleteOrphanedFiles($folder, $extension, $loaderFileName, $writtenFiles);

foreach ($deletedFiles as $deletedFile) {
$outputInfo[$deletedFile] = 0;
}

return $outputInfo;
}

Expand Down Expand Up @@ -220,4 +233,44 @@ private function sortErrors(
return $result;
}

/**
* @param array<string, true> $writtenFiles
* @return list<string>
*/
private function deleteOrphanedFiles(
string $folder,
string $extension,
string $loaderFileName,
array $writtenFiles
): array
{
$deletedFiles = [];
$existingFiles = glob($folder . '/*.' . $extension);

if ($existingFiles === false) {
return [];
}

foreach ($existingFiles as $existingFile) {
$fileName = basename($existingFile);

// Skip the loader file
if ($fileName === $loaderFileName) {
continue;
}

// Skip files that were written in this run
if (isset($writtenFiles[$existingFile])) {
continue;
}

// Delete orphaned file
if (unlink($existingFile)) {
$deletedFiles[] = $existingFile;
}
}

return $deletedFiles;
}

}
132 changes: 132 additions & 0 deletions tests/SplitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,138 @@ public function testFirstRunCreatesFileSorted(): void
self::assertStringContainsString('Error C', $result);
}

public function testOrphanedFilesAreDeleted(): void
{
$folder = $this->prepareSampleFolder();
$loaderPath = $folder . '/baselines/loader.neon';

// Create existing baseline files for two identifiers
$existingBaseline1 = <<<'NEON'
parameters:
ignoreErrors:
-
message: '#^Error A$#'
count: 1
path: ../app/a.php

NEON;
$existingBaseline2 = <<<'NEON'
parameters:
ignoreErrors:
-
message: '#^Error B$#'
count: 1
path: ../app/b.php

NEON;
file_put_contents($folder . '/baselines/first.identifier.neon', $existingBaseline1);
file_put_contents($folder . '/baselines/second.identifier.neon', $existingBaseline2);

// Now regenerate with only first.identifier (second.identifier should be deleted)
$inputErrors = [
'parameters' => [
'ignoreErrors' => [
['message' => '#^Error A$#', 'count' => 1, 'path' => '../app/a.php', 'identifier' => 'first.identifier'],
],
],
];
file_put_contents($loaderPath, Neon::encode($inputErrors));

$splitter = new BaselineSplitter("\t", true);
$result = $splitter->split($loaderPath);

$firstIdentifierPath = $folder . '/baselines/first.identifier.neon';
$secondIdentifierPath = $folder . '/baselines/second.identifier.neon';

// first.identifier.neon should still exist
self::assertFileExists($firstIdentifierPath);
self::assertArrayHasKey($firstIdentifierPath, $result);
self::assertSame(1, $result[$firstIdentifierPath]);

// second.identifier.neon should be deleted
self::assertFileDoesNotExist($secondIdentifierPath);
self::assertArrayHasKey($secondIdentifierPath, $result);
self::assertSame(0, $result[$secondIdentifierPath]);

// loader should still exist
self::assertFileExists($loaderPath);
self::assertArrayHasKey($loaderPath, $result);
self::assertNull($result[$loaderPath]);
}

public function testLoaderFileIsNotDeleted(): void
{
$folder = $this->prepareSampleFolder();
$loaderPath = $folder . '/baselines/loader.neon';
$orphanPath = $folder . '/baselines/orphan.neon';

// Create an empty baseline (no errors)
$inputErrors = [
'parameters' => [
'ignoreErrors' => [],
],
];
file_put_contents($loaderPath, Neon::encode($inputErrors));

// Create an orphaned file that happens to exist
file_put_contents($orphanPath, 'some content');

$splitter = new BaselineSplitter("\t", true);
$result = $splitter->split($loaderPath);

// Loader should still exist
self::assertFileExists($loaderPath);
self::assertArrayHasKey($loaderPath, $result);
self::assertNull($result[$loaderPath]);

// Orphaned file should be deleted
self::assertFileDoesNotExist($orphanPath);
self::assertArrayHasKey($orphanPath, $result);
self::assertSame(0, $result[$orphanPath]);
}

public function testOnlyFilesWithMatchingExtensionAreDeleted(): void
{
$folder = $this->prepareSampleFolder();
$loaderPath = $folder . '/baselines/loader.neon';

// Create a .neon baseline file and a .php file
$existingBaseline = <<<'NEON'
parameters:
ignoreErrors:
-
message: '#^Error A$#'
count: 1
path: ../app/a.php

NEON;
file_put_contents($folder . '/baselines/old.identifier.neon', $existingBaseline);
file_put_contents($folder . '/baselines/some.file.php', '<?php // some php file');

// Regenerate with empty baseline
$inputErrors = [
'parameters' => [
'ignoreErrors' => [],
],
];
file_put_contents($loaderPath, Neon::encode($inputErrors));

$splitter = new BaselineSplitter("\t", true);
$result = $splitter->split($loaderPath);

$oldIdentifierPath = $folder . '/baselines/old.identifier.neon';
$phpFilePath = $folder . '/baselines/some.file.php';

// .neon file should be deleted
self::assertFileDoesNotExist($oldIdentifierPath);
self::assertArrayHasKey($oldIdentifierPath, $result);
self::assertSame(0, $result[$oldIdentifierPath]);

// .php file should NOT be deleted (wrong extension)
self::assertFileExists($phpFilePath);
self::assertArrayNotHasKey($phpFilePath, $result);
}

/**
* @return array{parameters: array{ignoreErrors: array{0: array{message?: string, rawMessage?: string, count: int, path: string, identifier?: string}}}}
*/
Expand Down