Skip to content

Commit 009038d

Browse files
ondrejmirtesclaude
andcommitted
Extract FixerWorkerRunner from FixerWorkerCommand
Moves everything FixerWorkerCommand does after CommandHelper::begin() — connecting back to FixerApplication, restoring the result cache, running the analyser and streaming results — into a reusable FixerWorkerRunner service. FixerWorkerCommand now only boots and delegates, making the post-boot logic callable without a re-boot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4ac1d65 commit 009038d

2 files changed

Lines changed: 387 additions & 335 deletions

File tree

src/Command/FixerWorkerCommand.php

Lines changed: 13 additions & 335 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,17 @@
22

33
namespace PHPStan\Command;
44

5-
use Clue\React\NDJson\Encoder;
65
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;
166
use PHPStan\File\PathNotFoundException;
17-
use PHPStan\Parallel\ParallelAnalyser;
18-
use PHPStan\Parallel\Scheduler;
19-
use PHPStan\Process\CpuCoreCounter;
207
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;
268
use Symfony\Component\Console\Command\Command;
279
use Symfony\Component\Console\Input\InputArgument;
2810
use Symfony\Component\Console\Input\InputInterface;
2911
use Symfony\Component\Console\Input\InputOption;
3012
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;
3613
use function is_array;
3714
use function is_bool;
38-
use function is_file;
3915
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;
4516

4617
final class FixerWorkerCommand extends Command
4718
{
@@ -121,318 +92,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
12192

12293
$container = $inceptionResult->getContainer();
12394

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) {
12898
throw new ShouldNotHappenException();
12999
}
130100

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);
424105

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(),
431111
$configuration,
432-
null,
433-
null,
112+
(int) $serverPort,
434113
$input,
435-
$onFileAnalysisHandler,
436114
);
437115
}
438116

0 commit comments

Comments
 (0)