Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/Command/FixerApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});
});
Expand Down
71 changes: 65 additions & 6 deletions src/Command/FixerWorkerRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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' => [
Expand Down Expand Up @@ -150,7 +185,7 @@ public function run(
return $aTime <=> $bTime;
});

$this->runAnalyser(
return $this->runAnalyser(
$loop,
$filesToAnalyse,
$inceptionFiles,
Expand Down Expand Up @@ -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();

Expand Down
55 changes: 49 additions & 6 deletions src/File/FileMonitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use function hash_file;
use function is_dir;
use function is_file;
use function is_readable;

#[AutowiredService]
final class FileMonitor
Expand Down Expand Up @@ -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<string> $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) {
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Loading