├── 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 |
--------------------------------------------------------------------------------