├── src ├── Exception │ └── ErrorException.php ├── Handler │ ├── HandlerFactory.php │ ├── NeonBaselineHandler.php │ ├── PhpBaselineHandler.php │ └── BaselineHandler.php ├── NeonHelper.php ├── BaselinePerIdentifierFormatter.php └── BaselineSplitter.php ├── extension.neon ├── bin └── split-phpstan-baseline ├── composer.json └── README.md /src/Exception/ErrorException.php: -------------------------------------------------------------------------------- 1 | getMessage(), $e); 29 | } 30 | } 31 | 32 | public function encodeBaseline( 33 | ?string $comment, 34 | array $errors, 35 | string $indent 36 | ): string 37 | { 38 | $prefix = $comment !== null ? "# $comment\n\n" : ''; 39 | return $prefix . NeonHelper::encode(['parameters' => ['ignoreErrors' => $errors]], $indent); 40 | } 41 | 42 | public function encodeBaselineLoader( 43 | ?string $comment, 44 | array $filePaths, 45 | string $indent 46 | ): string 47 | { 48 | $prefix = $comment !== null ? "# $comment\n\n" : ''; 49 | return $prefix . NeonHelper::encode(['includes' => $filePaths], $indent); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /bin/split-phpstan-baseline: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | split($loaderFile); 57 | 58 | foreach ($writtenBaselines as $writtenBaseline => $errorsCount) { 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 | } 66 | } 67 | 68 | } catch (ErrorException $e) { 69 | error($e->getMessage()); 70 | } 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipmonk/phpstan-baseline-per-identifier", 3 | "description": "Split your PHPStan baseline into multiple files, one per error identifier. Supports both neon baseline and PHP baseline.", 4 | "license": [ 5 | "MIT" 6 | ], 7 | "type": "phpstan-extension", 8 | "keywords": [ 9 | "dev", 10 | "phpstan", 11 | "phpstan baseline", 12 | "phpstan extension", 13 | "error identifier" 14 | ], 15 | "require": { 16 | "php": "^7.4 || ^8.0", 17 | "nette/neon": "^3.3.3 || ^4.0", 18 | "phpstan/phpstan": "^2" 19 | }, 20 | "require-dev": { 21 | "editorconfig-checker/editorconfig-checker": "10.7.0", 22 | "ergebnis/composer-normalize": "^2.45", 23 | "phpstan/phpstan-phpunit": "2.0.6", 24 | "phpstan/phpstan-strict-rules": "2.0.4", 25 | "phpunit/phpunit": "9.6.23", 26 | "shipmonk/coding-standard": "^0.2.0", 27 | "shipmonk/composer-dependency-analyser": "1.8.3", 28 | "shipmonk/dead-code-detector": "^0.11.0", 29 | "shipmonk/name-collision-detector": "2.1.1", 30 | "shipmonk/phpstan-rules": "4.1.2" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "ShipMonk\\PHPStan\\Baseline\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "ShipMonk\\PHPStan\\Baseline\\": "tests/" 40 | } 41 | }, 42 | "bin": [ 43 | "bin/split-phpstan-baseline" 44 | ], 45 | "config": { 46 | "allow-plugins": { 47 | "dealerdirect/phpcodesniffer-composer-installer": false, 48 | "ergebnis/composer-normalize": true 49 | }, 50 | "sort-packages": true 51 | }, 52 | "extra": { 53 | "phpstan": { 54 | "includes": [ 55 | "extension.neon" 56 | ] 57 | } 58 | }, 59 | "scripts": { 60 | "check": [ 61 | "@check:composer", 62 | "@check:ec", 63 | "@check:cs", 64 | "@check:types", 65 | "@check:tests", 66 | "@check:collisions", 67 | "@check:dependencies" 68 | ], 69 | "check:collisions": "detect-collisions src tests", 70 | "check:composer": [ 71 | "composer normalize --dry-run --no-check-lock --no-update-lock", 72 | "composer validate --strict" 73 | ], 74 | "check:cs": "phpcs", 75 | "check:dependencies": "composer-dependency-analyser", 76 | "check:ec": "ec src tests", 77 | "check:tests": "phpunit tests", 78 | "check:types": "phpstan analyse -vv --ansi", 79 | "fix:cs": "phpcbf" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Handler/PhpBaselineHandler.php: -------------------------------------------------------------------------------- 1 | require $filepath)(); 19 | 20 | if (!is_array($decoded)) { 21 | throw new ErrorException("File '$filepath' must return array, " . gettype($decoded) . ' given'); 22 | } 23 | 24 | return $decoded; 25 | 26 | } catch (ErrorException $e) { 27 | throw $e; 28 | } catch (Throwable $e) { 29 | throw new ErrorException("Error while loading baseline file '$filepath': " . $e->getMessage(), $e); 30 | } 31 | } 32 | 33 | public function encodeBaseline( 34 | ?string $comment, 35 | array $errors, 36 | string $indent 37 | ): string 38 | { 39 | $php = ' %s,\n{$indent}'count' => %d,\n{$indent}'path' => __DIR__ . %s,\n];\n", 60 | var_export($messageKey, true), 61 | var_export($message, true), 62 | var_export($error['count'], true), 63 | var_export('/' . $error['path'], true), 64 | ); 65 | } 66 | 67 | $php .= "\n"; 68 | $php .= 'return [\'parameters\' => [\'ignoreErrors\' => $ignoreErrors]];'; 69 | $php .= "\n"; 70 | 71 | return $php; 72 | } 73 | 74 | public function encodeBaselineLoader( 75 | ?string $comment, 76 | array $filePaths, 77 | string $indent 78 | ): string 79 | { 80 | $php = " [\n"; 87 | 88 | foreach ($filePaths as $filePath) { 89 | $php .= "{$indent}__DIR__ . '/$filePath',\n"; 90 | } 91 | 92 | $php .= "]];\n"; 93 | 94 | return $php; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPStan baseline per error identifier 2 | 3 | Split your [PHPStan baseline](https://phpstan.org/user-guide/baseline) into multiple files, one per error identifier: 4 | 5 | ```txt 6 | baselines/ 7 | ├─ _loader.neon 8 | ├─ empty.notAllowed.neon 9 | ├─ foreach.nonIterable.neon 10 | ├─ identical.alwaysFalse.neon 11 | └─ if.condNotBoolean.neon 12 | ``` 13 | 14 | Each file looks like this: 15 | 16 | ```neon 17 | # total 1 error 18 | 19 | parameters: 20 | ignoreErrors: 21 | - 22 | message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' 23 | path: ../app/index.php 24 | count: 1 25 | ``` 26 | 27 | ## Installation: 28 | 29 | ```sh 30 | composer require --dev shipmonk/phpstan-baseline-per-identifier 31 | ``` 32 | 33 | ## Usage 34 | 35 | > [!IMPORTANT] 36 | > _This usage is available since version 2.0. See legacy usage below if you are still using PHPStan 1.x_ 37 | 38 | Remove old single baseline include: 39 | ```diff 40 | -includes: 41 | - - phpstan-baseline.neon 42 | ``` 43 | 44 | Run native baseline generation and split it into multiple files via our script (other baseline files will be placed beside the loader): 45 | ```sh 46 | vendor/bin/phpstan --generate-baseline=baselines/_loader.neon && vendor/bin/split-phpstan-baseline baselines/_loader.neon 47 | ``` 48 | 49 | Setup the baselines loader: 50 | ```neon 51 | # phpstan.neon.dist 52 | includes: 53 | - baselines/_loader.neon 54 | ``` 55 | 56 | _(optional)_ You can simplify generation with e.g. composer script: 57 | ```json 58 | { 59 | "scripts": { 60 | "generate:baseline:phpstan": [ 61 | "phpstan --generate-baseline=baselines/_loader.neon", 62 | "split-phpstan-baseline baselines/_loader.neon" 63 | ] 64 | } 65 | } 66 | ``` 67 | 68 |
69 |

Legacy usage

70 | 71 | > _This usage is deprecated since 2.0, but it works in all versions. Downside is that it cannot utilize result cache and does not support `rawMessage`._ 72 | 73 | Setup where your baseline files should be stored and include its loader: 74 | ```neon 75 | # phpstan.neon.dist 76 | includes: 77 | - vendor/shipmonk/phpstan-baseline-per-identifier/extension.neon # or use extension-installer 78 | - baselines/loader.neon 79 | 80 | parameters: 81 | shipmonkBaselinePerIdentifier: 82 | directory: %currentWorkingDirectory%/baselines 83 | indent: ' ' 84 | ``` 85 | 86 | Prepare composer script to simplify generation: 87 | 88 | ```json 89 | { 90 | "scripts": { 91 | "generate:baseline:phpstan": [ 92 | "rm baselines/*.neon", 93 | "touch baselines/loader.neon", 94 | "phpstan analyse --error-format baselinePerIdentifier" 95 | ] 96 | } 97 | } 98 | ``` 99 | 100 |
101 | 102 | ## Cli options 103 | - ``--tabs`` to use tabs as indents in generated neon files 104 | - ``--no-error-count`` to remove errors count in generated files 105 | 106 | ## PHP Baseline 107 | - If the loader file extension is php, the generated files will be php files as well 108 | 109 | -------------------------------------------------------------------------------- /src/Handler/BaselineHandler.php: -------------------------------------------------------------------------------- 1 | 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 | 84 | /** 85 | * @return array 86 | * 87 | * @throws ErrorException 88 | */ 89 | abstract protected function decodeBaselineFile(string $filepath): array; 90 | 91 | /** 92 | * @param list $errors 93 | */ 94 | abstract public function encodeBaseline( 95 | ?string $comment, 96 | array $errors, 97 | string $indent 98 | ): string; 99 | 100 | /** 101 | * @param list $filePaths 102 | */ 103 | abstract public function encodeBaselineLoader( 104 | ?string $comment, 105 | array $filePaths, 106 | string $indent 107 | ): string; 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/BaselinePerIdentifierFormatter.php: -------------------------------------------------------------------------------- 1 | baselinesDir = $baselinesRealDir; 48 | $this->indent = $indent; 49 | } 50 | 51 | public function formatErrors( 52 | AnalysisResult $analysisResult, 53 | Output $output 54 | ): int 55 | { 56 | foreach ($analysisResult->getInternalErrorObjects() as $internalError) { 57 | $output->writeLineFormatted('' . $internalError->getMessage() . ''); 58 | } 59 | 60 | $fileErrors = []; 61 | 62 | foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { 63 | if (!$fileSpecificError->canBeIgnored()) { 64 | continue; 65 | } 66 | 67 | $usedIdentifier = $fileSpecificError->getIdentifier() ?? 'missing-identifier'; 68 | 69 | $relativeFilePath = $this->getPathDifference($this->baselinesDir, $fileSpecificError->getFilePath()); 70 | $fileErrors[$usedIdentifier][$relativeFilePath][] = $fileSpecificError->getMessage(); 71 | } 72 | 73 | ksort($fileErrors, SORT_STRING); 74 | 75 | $includes = []; 76 | $totalErrorsCount = 0; 77 | 78 | foreach ($fileErrors as $identifier => $errors) { 79 | $errorsToOutput = []; 80 | $errorsCount = 0; 81 | 82 | foreach ($errors as $file => $errorMessages) { 83 | $fileErrorsCounts = []; 84 | 85 | foreach ($errorMessages as $errorMessage) { 86 | if (!isset($fileErrorsCounts[$errorMessage])) { 87 | $fileErrorsCounts[$errorMessage] = 1; 88 | continue; 89 | } 90 | 91 | $fileErrorsCounts[$errorMessage]++; 92 | } 93 | 94 | ksort($fileErrorsCounts, SORT_STRING); 95 | 96 | foreach ($fileErrorsCounts as $message => $count) { 97 | $errorsToOutput[] = [ 98 | 'message' => NeonHelper::escape('#^' . preg_quote($message, '#') . '$#'), 99 | 'count' => $count, 100 | 'path' => NeonHelper::escape($file), 101 | ]; 102 | $errorsCount += $count; 103 | } 104 | } 105 | 106 | $includes[] = $identifier . '.neon'; 107 | $baselineFilePath = $this->baselinesDir . '/' . $identifier . '.neon'; 108 | 109 | $totalErrorsCount += $errorsCount; 110 | $output->writeLineFormatted(sprintf('Writing baseline file %s with %d errors', $baselineFilePath, $errorsCount)); 111 | 112 | $plurality = $errorsCount === 1 ? '' : 's'; 113 | $prefix = "# total $errorsCount error$plurality\n\n"; 114 | $contents = $prefix . NeonHelper::encode(['parameters' => ['ignoreErrors' => $errorsToOutput]], $this->indent); 115 | $written = file_put_contents($baselineFilePath, $contents); 116 | 117 | if ($written === false) { 118 | throw new LogicException('Error while writing to ' . $baselineFilePath); 119 | } 120 | } 121 | 122 | $plurality = $totalErrorsCount === 1 ? '' : 's'; 123 | $prefix = "# total $totalErrorsCount error$plurality\n\n"; 124 | $writtenLoader = file_put_contents($this->baselinesDir . '/loader.neon', $prefix . NeonHelper::encode(['includes' => $includes], $this->indent)); 125 | 126 | if ($writtenLoader === false) { 127 | throw new LogicException('Error while writing to ' . $this->baselinesDir . '/loader.neon'); 128 | } 129 | 130 | $output->writeLineFormatted(''); 131 | $output->writeLineFormatted('⚠️ You are using deprecated approach to split baselines which cannot utilize PHPStan result cache ⚠️'); 132 | $output->writeLineFormatted(' Consider switching to new approach via:'); 133 | $output->writeLineFormatted(" vendor/bin/phpstan --generate-baseline=$this->baselinesDir/loader.neon && vendor/bin/split-phpstan-baseline $this->baselinesDir/loader.neon"); 134 | $output->writeLineFormatted(''); 135 | 136 | return 0; 137 | } 138 | 139 | private function getPathDifference( 140 | string $from, 141 | string $to 142 | ): string 143 | { 144 | $fromParts = explode(DIRECTORY_SEPARATOR, $from); 145 | $toParts = explode(DIRECTORY_SEPARATOR, $to); 146 | 147 | // Find the common base path 148 | while ($fromParts !== [] && $toParts !== [] && ($fromParts[0] === $toParts[0])) { 149 | array_shift($fromParts); 150 | array_shift($toParts); 151 | } 152 | 153 | // Add '..' for each remaining part in $fromParts 154 | $relativePath = str_repeat('..' . DIRECTORY_SEPARATOR, count($fromParts)); 155 | 156 | // Append the remaining parts from $toParts 157 | $relativePath .= implode(DIRECTORY_SEPARATOR, $toParts); 158 | 159 | return $relativePath; 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/BaselineSplitter.php: -------------------------------------------------------------------------------- 1 | indent = $indent; 33 | $this->includeCount = $includeCount; 34 | } 35 | 36 | /** 37 | * @return array file path => error count (null for loader, 0 for deleted) 38 | * 39 | * @throws ErrorException 40 | */ 41 | public function split(string $loaderFilePath): array 42 | { 43 | $splFile = new SplFileInfo($loaderFilePath); 44 | $realPath = $splFile->getRealPath(); 45 | 46 | if ($realPath === false) { 47 | throw new ErrorException("Unable to realpath '$loaderFilePath'"); 48 | } 49 | 50 | $folder = dirname($realPath); 51 | $loaderFileName = $splFile->getFilename(); 52 | $extension = $splFile->getExtension(); 53 | 54 | $handler = HandlerFactory::create($extension); 55 | $ignoredErrors = $handler->decodeBaseline($realPath); 56 | $groupedErrors = $this->groupErrorsByIdentifier($ignoredErrors, $folder); 57 | 58 | $outputInfo = []; 59 | $baselineFiles = []; 60 | $writtenFiles = []; 61 | $totalErrorCount = 0; 62 | 63 | foreach ($groupedErrors as $identifier => $newErrors) { 64 | $fileName = $identifier . '.' . $extension; 65 | $filePath = $folder . '/' . $fileName; 66 | 67 | $oldErrors = $this->readExistingErrors($filePath, $handler) ?? []; 68 | $sortedErrors = $this->sortErrors($oldErrors, $newErrors); 69 | 70 | $errorsCount = array_reduce($sortedErrors, static fn (int $carry, array $item): int => $carry + $item['count'], 0); 71 | $totalErrorCount += $errorsCount; 72 | 73 | $outputInfo[$filePath] = $errorsCount; 74 | $baselineFiles[] = $fileName; 75 | $writtenFiles[$filePath] = true; 76 | 77 | $plural = $errorsCount === 1 ? '' : 's'; 78 | $prefix = $this->includeCount ? "total $errorsCount error$plural" : null; 79 | 80 | $encodedData = $handler->encodeBaseline($prefix, $sortedErrors, $this->indent); 81 | $this->writeFile($filePath, $encodedData); 82 | } 83 | 84 | $plural = $totalErrorCount === 1 ? '' : 's'; 85 | $prefix = $this->includeCount ? "total $totalErrorCount error$plural" : null; 86 | $baselineLoaderData = $handler->encodeBaselineLoader($prefix, $baselineFiles, $this->indent); 87 | $this->writeFile($realPath, $baselineLoaderData); 88 | 89 | $outputInfo[$realPath] = null; 90 | 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 | 98 | return $outputInfo; 99 | } 100 | 101 | /** 102 | * @param list $errors 103 | * @return array> 104 | * 105 | * @throws ErrorException 106 | */ 107 | private function groupErrorsByIdentifier( 108 | array $errors, 109 | string $folder 110 | ): array 111 | { 112 | $groupedErrors = []; 113 | 114 | foreach ($errors as $error) { 115 | $identifier = $error['identifier'] ?? 'missing-identifier'; 116 | $normalizedPath = str_replace($folder . '/', '', $error['path']); 117 | 118 | if (isset($error['rawMessage'])) { 119 | $groupedErrors[$identifier][] = [ 120 | 'rawMessage' => $error['rawMessage'], 121 | 'count' => $error['count'], 122 | 'path' => $normalizedPath, 123 | ]; 124 | 125 | } elseif (isset($error['message'])) { 126 | $groupedErrors[$identifier][] = [ 127 | 'message' => $error['message'], 128 | 'count' => $error['count'], 129 | 'path' => $normalizedPath, 130 | ]; 131 | 132 | } else { 133 | throw new ErrorException('Error is missing message or rawMessage'); 134 | } 135 | } 136 | 137 | ksort($groupedErrors); 138 | 139 | return $groupedErrors; 140 | } 141 | 142 | /** 143 | * @throws ErrorException 144 | */ 145 | private function writeFile( 146 | string $filePath, 147 | string $contents 148 | ): void 149 | { 150 | $written = file_put_contents($filePath, $contents); 151 | 152 | if ($written === false) { 153 | throw new ErrorException('Error while writing to ' . $filePath); 154 | } 155 | } 156 | 157 | /** 158 | * @param array{message?: string, rawMessage?: string, count: int, path: string} $error 159 | */ 160 | private function getErrorKey(array $error): string 161 | { 162 | return $error['path'] . "\x00" . ($error['rawMessage'] ?? $error['message'] ?? ''); 163 | } 164 | 165 | /** 166 | * @return list|null 167 | */ 168 | private function readExistingErrors( 169 | string $filePath, 170 | BaselineHandler $handler 171 | ): ?array 172 | { 173 | if (!is_file($filePath)) { 174 | return null; 175 | } 176 | 177 | try { 178 | return $handler->decodeBaseline($filePath); 179 | 180 | } catch (ErrorException $e) { 181 | return null; 182 | } 183 | } 184 | 185 | /** 186 | * @param list $oldErrors 187 | * @param list $newErrors 188 | * @return list 189 | */ 190 | private function sortErrors( 191 | array $oldErrors, 192 | array $newErrors 193 | ): array 194 | { 195 | $newErrorsByKey = []; 196 | 197 | foreach ($newErrors as $newError) { 198 | $key = $this->getErrorKey($newError); 199 | $newErrorsByKey[$key] = $newError; 200 | } 201 | 202 | // collect errors that existed before 203 | $existingByKey = []; 204 | 205 | foreach ($oldErrors as $oldError) { 206 | $key = $this->getErrorKey($oldError); 207 | 208 | if (isset($newErrorsByKey[$key])) { 209 | $existingByKey[$key] = $newErrorsByKey[$key]; 210 | unset($newErrorsByKey[$key]); 211 | } 212 | } 213 | 214 | // insert new errors at their sorted positions among existing errors 215 | ksort($newErrorsByKey); 216 | $newErrorsIterator = new ArrayIterator($newErrorsByKey); 217 | $result = []; 218 | 219 | foreach ($existingByKey as $existingKey => $existingError) { 220 | while ($newErrorsIterator->valid() && $newErrorsIterator->key() < $existingKey) { 221 | $result[] = $newErrorsIterator->current(); 222 | $newErrorsIterator->next(); 223 | } 224 | 225 | $result[] = $existingError; 226 | } 227 | 228 | while ($newErrorsIterator->valid()) { 229 | $result[] = $newErrorsIterator->current(); 230 | $newErrorsIterator->next(); 231 | } 232 | 233 | return $result; 234 | } 235 | 236 | /** 237 | * @param array $writtenFiles 238 | * @return list 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 | 276 | } 277 | --------------------------------------------------------------------------------