diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index a28f2e18837..8b8e408d1e8 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -63,6 +63,9 @@ final class FixerApplication private bool $fileMonitorActive = true; + /** @var string[] watched files besides the analysed ones and the config files */ + private array $watchedFilesWithoutConfig = []; + /** * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths @@ -146,13 +149,13 @@ public function run( } }); - $this->fileMonitor->initialize(array_merge( + $this->watchedFilesWithoutConfig = array_merge( $this->getComposerLocks(), $this->getComposerInstalled(), $this->getExecutedFiles(), $this->getStubFiles(), - $this->allConfigFiles, - )); + ); + $this->fileMonitor->initialize(array_merge($this->watchedFilesWithoutConfig, $this->allConfigFiles)); $this->analyse( $loop, @@ -427,12 +430,18 @@ private function analyse( /** @var int<0, 65535> $serverPort */ $serverPort = parse_url($serverAddress, PHP_URL_PORT); - $server->on('connection', static function (ConnectionInterface $connection) use ($phpstanFixerEncoder): void { + $server->on('connection', function (ConnectionInterface $connection) use ($phpstanFixerEncoder): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable $decoder = new Decoder($connection, true, options: $jsonInvalidUtf8Ignore, maxlength: 128 * 1024 * 1024); - $decoder->on('data', static function (array $data) use ($phpstanFixerEncoder): void { + $decoder->on('data', function (array $data) use ($phpstanFixerEncoder): void { + // the config files of this analysis, the config may include other files than at the start + if ($data['action'] === 'watchedConfigFiles') { + $this->fileMonitor->watchFiles(array_merge($this->watchedFilesWithoutConfig, $data['result']['files'])); + return; + } + $phpstanFixerEncoder->write($data); }); }); diff --git a/src/Command/FixerWorkerRunner.php b/src/Command/FixerWorkerRunner.php index f0205dda5da..db57edb5156 100644 --- a/src/Command/FixerWorkerRunner.php +++ b/src/Command/FixerWorkerRunner.php @@ -12,22 +12,24 @@ use PHPStan\Analyser\ResultCache\LazyCollectedData; use PHPStan\Analyser\ResultCache\ResultCacheManager; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Parallel\ParallelAnalyser; use PHPStan\Parallel\Scheduler; use PHPStan\Process\CpuCoreCounter; -use PHPStan\ShouldNotHappenException; use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\TcpConnector; use Symfony\Component\Console\Input\InputInterface; +use Throwable; use function array_diff; use function array_key_exists; use function count; use function filemtime; use function filesize; +use function implode; use function in_array; use function is_file; use function memory_get_peak_usage; @@ -50,7 +52,12 @@ final class FixerWorkerRunner { + /** + * @param string[] $allConfigFiles + */ public function __construct( + #[AutowiredParameter] + private array $allConfigFiles, private IgnoredErrorHelper $ignoredErrorHelper, private ResultCacheManagerFactory $resultCacheManagerFactory, private AnalyserResultFinalizer $analyserResultFinalizer, @@ -76,22 +83,50 @@ public function run( ): int { $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); - if (count($ignoredErrorHelperResult->getErrors()) > 0) { - throw new ShouldNotHappenException(); - } $loop = new StreamSelectLoop(); $tcpConnector = new TcpConnector($loop); - $tcpConnector->connect(sprintf('127.0.0.1:%d', $serverPort))->then(function (ConnectionInterface $connection) use ($errorOutput, $inceptionFiles, $isOnlyFiles, $projectConfigArray, $configuration, $input, $ignoredErrorHelperResult, $loop): void { + $out = null; + $tcpConnector->connect(sprintf('127.0.0.1:%d', $serverPort))->then(function (ConnectionInterface $connection) use ($errorOutput, $inceptionFiles, $isOnlyFiles, $projectConfigArray, $configuration, $input, $ignoredErrorHelperResult, $loop, &$out): PromiseInterface { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable $out = new Encoder($connection, $jsonInvalidUtf8Ignore); + + // the config was edited since PHPStan Pro started, it reports what is wrong like the analyse command + if (count($ignoredErrorHelperResult->getErrors()) > 0) { + $out->write(['action' => 'analysisCrash', 'data' => [ + 'internalErrors' => [new InternalError( + implode("\n", $ignoredErrorHelperResult->getErrors()), + 'reading ignoreErrors in the configuration', + trace: [], + traceAsString: null, + shouldReportBug: false, + )], + ]]); + $out->end([ + 'action' => 'analysisEnd', + 'result' => [ + 'ignoreFileErrors' => [], + 'ignoreNotFileErrors' => [], + ], + ]); + + return resolve(null); + } //$in = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); /** @var ResultCacheManager $resultCacheManager */ $resultCacheManager = $this->resultCacheManagerFactory->create([]); + // the config read for this analysis, files included since PHPStan Pro started are watched from now on + $out->write([ + 'action' => 'watchedConfigFiles', + 'result' => [ + 'files' => $this->allConfigFiles, + ], + ]); + $out->write([ 'action' => 'analysisStart', 'result' => [ @@ -150,7 +185,7 @@ public function run( return $aTime <=> $bTime; }); - $this->runAnalyser( + return $this->runAnalyser( $loop, $filesToAnalyse, $inceptionFiles, @@ -270,6 +305,30 @@ function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use ], ]); }); + })->catch(static function (Throwable $e) use (&$out, $loop): void { + // without an end the worker would wait forever and FixerApplication would not analyse again + if (!$out instanceof Encoder) { + // FixerApplication is gone + $loop->stop(); + return; + } + + $out->write(['action' => 'analysisCrash', 'data' => [ + 'internalErrors' => [new InternalError( + $e->getMessage(), + 'running PHPStan Pro worker', + InternalError::prepareTrace($e), + $e->getTraceAsString(), + shouldReportBug: false, + )], + ]]); + $out->end([ + 'action' => 'analysisEnd', + 'result' => [ + 'ignoreFileErrors' => [], + 'ignoreNotFileErrors' => [], + ], + ]); }); $loop->run(); diff --git a/src/File/FileMonitor.php b/src/File/FileMonitor.php index 4e21aa2c11e..ae9711ce8e2 100644 --- a/src/File/FileMonitor.php +++ b/src/File/FileMonitor.php @@ -13,6 +13,7 @@ use function hash_file; use function is_dir; use function is_file; +use function is_readable; #[AutowiredService] final class FileMonitor @@ -56,13 +57,46 @@ public function initialize(array $filePaths): void $finderResult = $this->analyseFileFinder->findFiles($this->analysedPaths); $fileHashes = []; foreach (array_unique(array_merge($finderResult->getFiles(), $filePaths, $this->getScannedFiles($finderResult->getFiles()))) as $filePath) { - $fileHashes[$filePath] = $this->getFileHash($filePath); + $hash = $this->getFileHash($filePath); + if ($hash === null) { + continue; + } + + $fileHashes[$filePath] = $hash; } $this->fileHashes = $fileHashes; $this->filePaths = $filePaths; } + /** + * Files to watch besides the analysed and scanned ones, from now on. + * + * @param array $filePaths + */ + public function watchFiles(array $filePaths): void + { + if ($this->fileHashes === null) { + throw new ShouldNotHappenException(); + } + + foreach ($filePaths as $filePath) { + if (array_key_exists($filePath, $this->fileHashes)) { + continue; + } + + // as it is now, not a change to report + $hash = $this->getFileHash($filePath); + if ($hash === null) { + continue; + } + + $this->fileHashes[$filePath] = $hash; + } + + $this->filePaths = $filePaths; + } + public function getChanges(): FileMonitorResult { if ($this->fileHashes === null || $this->filePaths === null) { @@ -80,15 +114,21 @@ public function getChanges(): FileMonitorResult $deletedFiles = []; $filePaths = array_unique(array_merge($finderResult->getFiles(), $this->filePaths, $this->getScannedFiles($finderResult->getFiles()))); foreach ($filePaths as $filePath) { + // a file that cannot be read is missing, like a config file during a git checkout, + // it is deleted now and new when it is back + $newHash = $this->getFileHash($filePath); + if ($newHash === null) { + continue; + } + if (!array_key_exists($filePath, $oldFileHashes)) { $newFiles[] = $filePath; - $fileHashes[$filePath] = $this->getFileHash($filePath); + $fileHashes[$filePath] = $newHash; continue; } $oldHash = $oldFileHashes[$filePath]; unset($oldFileHashes[$filePath]); - $newHash = $this->getFileHash($filePath); $fileHashes[$filePath] = $newHash; if ($oldHash === $newHash) { continue; @@ -110,12 +150,15 @@ public function getChanges(): FileMonitorResult ); } - private function getFileHash(string $filePath): string + private function getFileHash(string $filePath): ?string { - $hash = hash_file('sha256', $filePath); + if (!is_file($filePath) || !is_readable($filePath)) { + return null; + } + $hash = @hash_file('sha256', $filePath); if ($hash === false) { - throw new CouldNotReadFileException($filePath); + return null; } return $hash;