|
2 | 2 |
|
3 | 3 | namespace PHPStan\Command; |
4 | 4 |
|
5 | | -use Clue\React\NDJson\Encoder; |
6 | 5 | use Override; |
7 | | -use PHPStan\Analyser\AnalyserResult; |
8 | | -use PHPStan\Analyser\AnalyserResultFinalizer; |
9 | | -use PHPStan\Analyser\Error; |
10 | | -use PHPStan\Analyser\Ignore\IgnoredErrorHelper; |
11 | | -use PHPStan\Analyser\Ignore\IgnoredErrorHelperResult; |
12 | | -use PHPStan\Analyser\InternalError; |
13 | | -use PHPStan\Analyser\ResultCache\ResultCacheManager; |
14 | | -use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; |
15 | | -use PHPStan\DependencyInjection\Container; |
16 | 6 | use PHPStan\File\PathNotFoundException; |
17 | | -use PHPStan\Parallel\ParallelAnalyser; |
18 | | -use PHPStan\Parallel\Scheduler; |
19 | | -use PHPStan\Process\CpuCoreCounter; |
20 | 7 | use PHPStan\ShouldNotHappenException; |
21 | | -use React\EventLoop\LoopInterface; |
22 | | -use React\EventLoop\StreamSelectLoop; |
23 | | -use React\Promise\PromiseInterface; |
24 | | -use React\Socket\ConnectionInterface; |
25 | | -use React\Socket\TcpConnector; |
26 | 8 | use Symfony\Component\Console\Command\Command; |
27 | 9 | use Symfony\Component\Console\Input\InputArgument; |
28 | 10 | use Symfony\Component\Console\Input\InputInterface; |
29 | 11 | use Symfony\Component\Console\Input\InputOption; |
30 | 12 | use Symfony\Component\Console\Output\OutputInterface; |
31 | | -use function array_diff; |
32 | | -use function array_key_exists; |
33 | | -use function count; |
34 | | -use function filemtime; |
35 | | -use function in_array; |
36 | 13 | use function is_array; |
37 | 14 | use function is_bool; |
38 | | -use function is_file; |
39 | 15 | use function is_string; |
40 | | -use function memory_get_peak_usage; |
41 | | -use function React\Promise\resolve; |
42 | | -use function sprintf; |
43 | | -use function usort; |
44 | | -use const JSON_INVALID_UTF8_IGNORE; |
45 | 16 |
|
46 | 17 | final class FixerWorkerCommand extends Command |
47 | 18 | { |
@@ -121,318 +92,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int |
121 | 92 |
|
122 | 93 | $container = $inceptionResult->getContainer(); |
123 | 94 |
|
124 | | - /** @var IgnoredErrorHelper $ignoredErrorHelper */ |
125 | | - $ignoredErrorHelper = $container->getByType(IgnoredErrorHelper::class); |
126 | | - $ignoredErrorHelperResult = $ignoredErrorHelper->initialize(); |
127 | | - if (count($ignoredErrorHelperResult->getErrors()) > 0) { |
| 95 | + try { |
| 96 | + [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); |
| 97 | + } catch (PathNotFoundException | InceptionNotSuccessfulException) { |
128 | 98 | throw new ShouldNotHappenException(); |
129 | 99 | } |
130 | 100 |
|
131 | | - $loop = new StreamSelectLoop(); |
132 | | - $tcpConnector = new TcpConnector($loop); |
133 | | - $tcpConnector->connect(sprintf('127.0.0.1:%d', (int) $serverPort))->then(function (ConnectionInterface $connection) use ($container, $inceptionResult, $configuration, $input, $ignoredErrorHelperResult, $loop): void { |
134 | | - // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly |
135 | | - $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; |
136 | | - // phpcs:enable |
137 | | - $out = new Encoder($connection, $jsonInvalidUtf8Ignore); |
138 | | - //$in = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); |
139 | | - |
140 | | - /** @var ResultCacheManager $resultCacheManager */ |
141 | | - $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create([]); |
142 | | - $projectConfigArray = $inceptionResult->getProjectConfigArray(); |
143 | | - |
144 | | - /** @var AnalyserResultFinalizer $analyserResultFinalizer */ |
145 | | - $analyserResultFinalizer = $container->getByType(AnalyserResultFinalizer::class); |
146 | | - |
147 | | - try { |
148 | | - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); |
149 | | - } catch (PathNotFoundException | InceptionNotSuccessfulException) { |
150 | | - throw new ShouldNotHappenException(); |
151 | | - } |
152 | | - |
153 | | - $out->write([ |
154 | | - 'action' => 'analysisStart', |
155 | | - 'result' => [ |
156 | | - 'analysedFiles' => $inceptionFiles, |
157 | | - ], |
158 | | - ]); |
159 | | - |
160 | | - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput()); |
161 | | - |
162 | | - $errorsFromResultCacheTmp = $resultCache->getErrors(); |
163 | | - $locallyIgnoredErrorsFromResultCacheTmp = $resultCache->getLocallyIgnoredErrors(); |
164 | | - foreach ($resultCache->getFilesToAnalyse() as $fileToAnalyse) { |
165 | | - unset($errorsFromResultCacheTmp[$fileToAnalyse]); |
166 | | - unset($locallyIgnoredErrorsFromResultCacheTmp[$fileToAnalyse]); |
167 | | - } |
168 | | - |
169 | | - $errorsFromResultCache = []; |
170 | | - foreach ($errorsFromResultCacheTmp as $errorsByFile) { |
171 | | - foreach ($errorsByFile as $error) { |
172 | | - $errorsFromResultCache[] = $error; |
173 | | - } |
174 | | - } |
175 | | - |
176 | | - [$errorsFromResultCache, $ignoredErrorsFromResultCache] = $this->filterErrors($errorsFromResultCache, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); |
177 | | - |
178 | | - foreach ($locallyIgnoredErrorsFromResultCacheTmp as $locallyIgnoredErrors) { |
179 | | - foreach ($locallyIgnoredErrors as $locallyIgnoredError) { |
180 | | - $ignoredErrorsFromResultCache[] = [$locallyIgnoredError, null]; |
181 | | - } |
182 | | - } |
183 | | - |
184 | | - $out->write([ |
185 | | - 'action' => 'analysisStream', |
186 | | - 'result' => [ |
187 | | - 'errors' => $errorsFromResultCache, |
188 | | - 'ignoredErrors' => $ignoredErrorsFromResultCache, |
189 | | - 'analysedFiles' => array_diff($inceptionFiles, $resultCache->getFilesToAnalyse()), |
190 | | - ], |
191 | | - ]); |
192 | | - |
193 | | - $filesToAnalyse = $resultCache->getFilesToAnalyse(); |
194 | | - usort($filesToAnalyse, static function (string $a, string $b): int { |
195 | | - $aTime = @filemtime($a); |
196 | | - if ($aTime === false) { |
197 | | - return 1; |
198 | | - } |
199 | | - |
200 | | - $bTime = @filemtime($b); |
201 | | - if ($bTime === false) { |
202 | | - return -1; |
203 | | - } |
204 | | - |
205 | | - // files are sorted from the oldest |
206 | | - // because ParallelAnalyser reverses the scheduler jobs to do the smallest |
207 | | - // jobs first |
208 | | - return $aTime <=> $bTime; |
209 | | - }); |
210 | | - |
211 | | - $this->runAnalyser( |
212 | | - $loop, |
213 | | - $container, |
214 | | - $filesToAnalyse, |
215 | | - $inceptionFiles, |
216 | | - $configuration, |
217 | | - $input, |
218 | | - function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use ($out, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles): void { |
219 | | - $internalErrors = []; |
220 | | - foreach ($errors as $fileSpecificError) { |
221 | | - if (!$fileSpecificError->hasNonIgnorableException()) { |
222 | | - continue; |
223 | | - } |
224 | | - |
225 | | - $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); |
226 | | - } |
227 | | - |
228 | | - if (count($internalErrors) > 0) { |
229 | | - $out->write(['action' => 'analysisCrash', 'data' => [ |
230 | | - 'internalErrors' => $internalErrors, |
231 | | - ]]); |
232 | | - return; |
233 | | - } |
234 | | - |
235 | | - [$errors, $ignoredErrors] = $this->filterErrors($errors, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); |
236 | | - foreach ($locallyIgnoredErrors as $locallyIgnoredError) { |
237 | | - $ignoredErrors[] = [$locallyIgnoredError, null]; |
238 | | - } |
239 | | - $out->write([ |
240 | | - 'action' => 'analysisStream', |
241 | | - 'result' => [ |
242 | | - 'errors' => $errors, |
243 | | - 'ignoredErrors' => $ignoredErrors, |
244 | | - 'analysedFiles' => $analysedFiles, |
245 | | - ], |
246 | | - ]); |
247 | | - }, |
248 | | - )->then(function (AnalyserResult $intermediateAnalyserResult) use ($analyserResultFinalizer, $resultCacheManager, $resultCache, $inceptionResult, $isOnlyFiles, $ignoredErrorHelperResult, $inceptionFiles, $out): void { |
249 | | - $analyserResult = $resultCacheManager->process( |
250 | | - $intermediateAnalyserResult, |
251 | | - $resultCache, |
252 | | - $inceptionResult->getErrorOutput(), |
253 | | - false, |
254 | | - true, |
255 | | - )->getAnalyserResult(); |
256 | | - $finalizerResult = $analyserResultFinalizer->finalize($analyserResult, $isOnlyFiles, false); |
257 | | - |
258 | | - $internalErrors = []; |
259 | | - foreach ($finalizerResult->getAnalyserResult()->getInternalErrors() as $internalError) { |
260 | | - $internalErrors[] = new InternalError( |
261 | | - $internalError->getTraceAsString() !== null ? sprintf('Internal error: %s', $internalError->getMessage()) : $internalError->getMessage(), |
262 | | - $internalError->getContextDescription(), |
263 | | - $internalError->getTrace(), |
264 | | - $internalError->getTraceAsString(), |
265 | | - $internalError->shouldReportBug(), |
266 | | - ); |
267 | | - } |
268 | | - |
269 | | - foreach ($finalizerResult->getAnalyserResult()->getUnorderedErrors() as $fileSpecificError) { |
270 | | - if (!$fileSpecificError->hasNonIgnorableException()) { |
271 | | - continue; |
272 | | - } |
273 | | - |
274 | | - $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); |
275 | | - } |
276 | | - |
277 | | - $hasInternalErrors = count($internalErrors) > 0 || $finalizerResult->getAnalyserResult()->hasReachedInternalErrorsCountLimit(); |
278 | | - |
279 | | - if ($hasInternalErrors) { |
280 | | - $out->write(['action' => 'analysisCrash', 'data' => [ |
281 | | - 'internalErrors' => count($internalErrors) > 0 ? $internalErrors : [ |
282 | | - new InternalError( |
283 | | - 'Internal error occurred', |
284 | | - 'running analyser in PHPStan Pro worker', |
285 | | - trace: [], |
286 | | - traceAsString: null, |
287 | | - shouldReportBug: false, |
288 | | - ), |
289 | | - ], |
290 | | - ]]); |
291 | | - } |
292 | | - |
293 | | - [$collectorErrors, $ignoredCollectorErrors] = $this->filterErrors($finalizerResult->getCollectorErrors(), $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, $hasInternalErrors); |
294 | | - foreach ($finalizerResult->getLocallyIgnoredCollectorErrors() as $locallyIgnoredCollectorError) { |
295 | | - $ignoredCollectorErrors[] = [$locallyIgnoredCollectorError, null]; |
296 | | - } |
297 | | - $out->write([ |
298 | | - 'action' => 'analysisStream', |
299 | | - 'result' => [ |
300 | | - 'errors' => $collectorErrors, |
301 | | - 'ignoredErrors' => $ignoredCollectorErrors, |
302 | | - 'analysedFiles' => [], |
303 | | - ], |
304 | | - ]); |
305 | | - |
306 | | - $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process( |
307 | | - $finalizerResult->getErrors(), |
308 | | - $isOnlyFiles, |
309 | | - $inceptionFiles, |
310 | | - $hasInternalErrors, |
311 | | - ); |
312 | | - $ignoreFileErrors = []; |
313 | | - foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { |
314 | | - if ($error->getIdentifier() === null) { |
315 | | - continue; |
316 | | - } |
317 | | - if (!in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched', 'ignore.unmatchedLine', 'ignore.unmatchedIdentifier', 'ignore.noComment'], true)) { |
318 | | - continue; |
319 | | - } |
320 | | - $ignoreFileErrors[] = $error; |
321 | | - } |
322 | | - |
323 | | - $out->end([ |
324 | | - 'action' => 'analysisEnd', |
325 | | - 'result' => [ |
326 | | - 'ignoreFileErrors' => $ignoreFileErrors, |
327 | | - 'ignoreNotFileErrors' => $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(), |
328 | | - ], |
329 | | - ]); |
330 | | - }); |
331 | | - }); |
332 | | - $loop->run(); |
333 | | - |
334 | | - return 0; |
335 | | - } |
336 | | - |
337 | | - private function transformErrorIntoInternalError(Error $error): InternalError |
338 | | - { |
339 | | - $message = $error->getMessage(); |
340 | | - $metadata = $error->getMetadata(); |
341 | | - if ( |
342 | | - $error->getIdentifier() === 'phpstan.internal' |
343 | | - && array_key_exists(InternalError::STACK_TRACE_AS_STRING_METADATA_KEY, $metadata) |
344 | | - ) { |
345 | | - $message = sprintf('Internal error: %s', $message); |
346 | | - } |
347 | | - |
348 | | - return new InternalError( |
349 | | - $message, |
350 | | - sprintf('analysing file %s', $error->getTraitFilePath() ?? $error->getFilePath()), |
351 | | - $metadata[InternalError::STACK_TRACE_METADATA_KEY] ?? [], |
352 | | - $metadata[InternalError::STACK_TRACE_AS_STRING_METADATA_KEY] ?? null, |
353 | | - shouldReportBug: true, |
354 | | - ); |
355 | | - } |
356 | | - |
357 | | - /** |
358 | | - * @param string[] $inceptionFiles |
359 | | - * @param list<Error> $errors |
360 | | - * @return array{list<Error>, list<array{Error, mixed[]|string}>} |
361 | | - */ |
362 | | - private function filterErrors(array $errors, IgnoredErrorHelperResult $ignoredErrorHelperResult, bool $onlyFiles, array $inceptionFiles, bool $hasInternalErrors): array |
363 | | - { |
364 | | - $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($errors, $onlyFiles, $inceptionFiles, $hasInternalErrors); |
365 | | - $finalErrors = []; |
366 | | - foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { |
367 | | - if ($error->getIdentifier() === null) { |
368 | | - $finalErrors[] = $error; |
369 | | - continue; |
370 | | - } |
371 | | - if (in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched'], true)) { |
372 | | - continue; |
373 | | - } |
374 | | - $finalErrors[] = $error; |
375 | | - } |
376 | | - |
377 | | - return [ |
378 | | - $finalErrors, |
379 | | - $ignoredErrorHelperProcessedResult->getIgnoredErrors(), |
380 | | - ]; |
381 | | - } |
382 | | - |
383 | | - /** |
384 | | - * @param string[] $files |
385 | | - * @param string[] $allAnalysedFiles |
386 | | - * @param callable(list<Error>, list<Error>, string[]): void $onFileAnalysisHandler |
387 | | - * @return PromiseInterface<AnalyserResult> |
388 | | - */ |
389 | | - private function runAnalyser(LoopInterface $loop, Container $container, array $files, array $allAnalysedFiles, ?string $configuration, InputInterface $input, callable $onFileAnalysisHandler): PromiseInterface |
390 | | - { |
391 | | - /** @var ParallelAnalyser $parallelAnalyser */ |
392 | | - $parallelAnalyser = $container->getByType(ParallelAnalyser::class); |
393 | | - $filesCount = count($files); |
394 | | - if ($filesCount === 0) { |
395 | | - return resolve(new AnalyserResult( |
396 | | - unorderedErrors: [], |
397 | | - filteredPhpErrors: [], |
398 | | - allPhpErrors: [], |
399 | | - locallyIgnoredErrors: [], |
400 | | - linesToIgnore: [], |
401 | | - unmatchedLineIgnores: [], |
402 | | - internalErrors: [], |
403 | | - collectedData: [], |
404 | | - dependencies: [], |
405 | | - usedTraitDependencies: [], |
406 | | - exportedNodes: [], |
407 | | - reachedInternalErrorsCountLimit: false, |
408 | | - peakMemoryUsageBytes: memory_get_peak_usage(true), |
409 | | - processedFiles: [], |
410 | | - )); |
411 | | - } |
412 | | - |
413 | | - /** @var Scheduler $scheduler */ |
414 | | - $scheduler = $container->getByType(Scheduler::class); |
415 | | - |
416 | | - /** @var CpuCoreCounter $cpuCoreCounter */ |
417 | | - $cpuCoreCounter = $container->getByType(CpuCoreCounter::class); |
418 | | - |
419 | | - $schedule = $scheduler->scheduleWork($cpuCoreCounter->getNumberOfCpuCores(), $files); |
420 | | - $mainScript = null; |
421 | | - if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { |
422 | | - $mainScript = $_SERVER['argv'][0]; |
423 | | - } |
| 101 | + // Everything after the boot lives in FixerWorkerRunner so a |
| 102 | + // pcntl_fork()-ed child can reuse it without re-booting (see |
| 103 | + // FixerApplication). |
| 104 | + $fixerWorkerRunner = $container->getByType(FixerWorkerRunner::class); |
424 | 105 |
|
425 | | - return $parallelAnalyser->analyse( |
426 | | - $loop, |
427 | | - $schedule, |
428 | | - $allAnalysedFiles, |
429 | | - $mainScript, |
430 | | - null, |
| 106 | + return $fixerWorkerRunner->run( |
| 107 | + $inceptionResult->getErrorOutput(), |
| 108 | + $inceptionFiles, |
| 109 | + $isOnlyFiles, |
| 110 | + $inceptionResult->getProjectConfigArray(), |
431 | 111 | $configuration, |
432 | | - null, |
433 | | - null, |
| 112 | + (int) $serverPort, |
434 | 113 | $input, |
435 | | - $onFileAnalysisHandler, |
436 | 114 | ); |
437 | 115 | } |
438 | 116 |
|
|
0 commit comments