Skip to content
Draft
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
8 changes: 8 additions & 0 deletions build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ parameters:
- '#^Dynamic call to static method PHPUnit\\Framework\\\S+\(\)\.$#'
- '#should be contravariant with parameter \$node \(PhpParser\\Node\) of method PHPStan\\Rules\\Rule<PhpParser\\Node>::processNode\(\)$#'
- '#Variable property access on PhpParser\\Node#'
-
# FFI's methods are the C functions declared in the cdef string at
# runtime, so no reflection can know them
identifier: method.notFound
message: '#^Call to an undefined method FFI::#'
paths:
- ../src/File/FsEventsFileMonitor.php
- ../src/File/InotifyFileMonitor.php
-
identifier: shipmonk.deadMethod
message: '#^Unused .*?::__construct#' # likely used in DIC
Expand Down
24 changes: 16 additions & 8 deletions src/Command/FixerApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\File\FileMonitor;
use PHPStan\File\FileMonitorFactory;
use PHPStan\File\FileMonitorResult;
use PHPStan\File\FileReader;
use PHPStan\File\FileWriter;
Expand Down Expand Up @@ -75,7 +76,7 @@ final class FixerApplication
* @param string[] $bootstrapFiles
*/
public function __construct(
private FileMonitor $fileMonitor,
private FileMonitorFactory $fileMonitorFactory,
private IgnoredErrorHelper $ignoredErrorHelper,
private StubFilesProvider $stubFilesProvider,
#[AutowiredParameter]
Expand Down Expand Up @@ -103,6 +104,8 @@ public function __construct(
{
}

private ?FileMonitor $fileMonitor = null;

public function run(
InceptionResult $inceptionResult,
InputInterface $input,
Expand Down Expand Up @@ -154,7 +157,7 @@ public function run(
}
});

$this->fileMonitor->initialize(array_merge(
$this->fileMonitor = $this->fileMonitorFactory->create(array_merge(
$this->getComposerLocks(),
$this->getComposerInstalled(),
$this->getExecutedFiles(),
Expand Down Expand Up @@ -404,24 +407,29 @@ private function writeInfoFile(string $infoPath, string $version, string $branch
*/
private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCallback): void
{
$callback = function () use (&$callback, $loop, $hasChangesCallback): void {
if ($this->fileMonitor === null) {
throw new ShouldNotHappenException();
}
$fileMonitor = $this->fileMonitor;
$interval = $fileMonitor->getPollInterval();
$callback = function () use (&$callback, $loop, $hasChangesCallback, $fileMonitor, $interval): void {
if (!$this->fileMonitorActive) {
$loop->addTimer(1.0, $callback);
$loop->addTimer($interval, $callback);
return;
}
if ($this->processInProgress !== null) {
$loop->addTimer(1.0, $callback);
$loop->addTimer($interval, $callback);
return;
}
$changes = $this->fileMonitor->getChanges();
$changes = $fileMonitor->getChanges();

if ($changes->hasAnyChanges()) {
$hasChangesCallback($changes);
}

$loop->addTimer(1.0, $callback);
$loop->addTimer($interval, $callback);
};
$loop->addTimer(1.0, $callback);
$loop->addTimer($interval, $callback);
}

private function analyse(
Expand Down
152 changes: 19 additions & 133 deletions src/File/FileMonitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,146 +2,32 @@

namespace PHPStan\File;

use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\ShouldNotHappenException;
use function array_diff;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_unique;
use function hash_file;
use function is_dir;
use function is_file;

#[AutowiredService]
final class FileMonitor
/**
* Watches the analysed and scanned files for changes between PHPStan Pro analyses.
*
* {@see HashingFileMonitor} is the portable implementation: it re-hashes every
* monitored file on every poll. The native implementations
* ({@see KqueueFileMonitor}, {@see InotifyFileMonitor}) wrap it and let the
* kernel answer "did anything change at all", so an idle poll touches no files;
* once the kernel says yes, they delegate to the hashing monitor so the reported
* result is identical either way.
*
* {@see FileMonitorFactory} picks the implementation for the current platform.
*/
interface FileMonitor
{

/** @var array<string, string>|null */
private ?array $fileHashes = null;

/** @var array<string>|null */
private ?array $filePaths = null;

/**
* @param string[] $analysedPaths
* @param string[] $analysedPathsFromConfig
* @param string[] $scanFiles
* @param string[] $scanDirectories
*/
public function __construct(
#[AutowiredParameter(ref: '@fileFinderAnalyse')]
private FileFinder $analyseFileFinder,
#[AutowiredParameter(ref: '@fileFinderScan')]
private FileFinder $scanFileFinder,
#[AutowiredParameter]
private array $analysedPaths,
#[AutowiredParameter]
private array $analysedPathsFromConfig,
#[AutowiredParameter]
private array $scanFiles,
#[AutowiredParameter]
private array $scanDirectories,
)
{
}

/**
* @param array<string> $filePaths
* @param array<string> $filePaths extra files to monitor besides the analysed and scanned ones
*/
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);
}

$this->fileHashes = $fileHashes;
$this->filePaths = $filePaths;
}

public function getChanges(): FileMonitorResult
{
if ($this->fileHashes === null || $this->filePaths === null) {
throw new ShouldNotHappenException();
}
$finderResult = $this->analyseFileFinder->findFiles($this->analysedPaths);
$oldFileHashes = $this->fileHashes;
$fileHashes = [];
$newFiles = [];
$changedFiles = [];
$deletedFiles = [];
$filePaths = array_unique(array_merge($finderResult->getFiles(), $this->filePaths, $this->getScannedFiles($finderResult->getFiles())));
foreach ($filePaths as $filePath) {
if (!array_key_exists($filePath, $oldFileHashes)) {
$newFiles[] = $filePath;
$fileHashes[$filePath] = $this->getFileHash($filePath);
continue;
}

$oldHash = $oldFileHashes[$filePath];
unset($oldFileHashes[$filePath]);
$newHash = $this->getFileHash($filePath);
$fileHashes[$filePath] = $newHash;
if ($oldHash === $newHash) {
continue;
}

$changedFiles[] = $filePath;
}
public function initialize(array $filePaths): void;

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, ubuntu-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.4)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, ubuntu-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.5)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, windows-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, windows-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, windows-latest)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (ubuntu-latest, 8.4, zts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (ubuntu-latest, 8.3, zts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (ubuntu-latest, 8.4, nts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (ubuntu-latest, 8.5, nts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (ubuntu-latest, 8.3, nts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (ubuntu-latest, 8.5, zts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (macos-latest, 8.5, nts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (windows-latest, 8.3, zts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (windows-latest, 8.3, nts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (windows-latest, 8.5, zts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (windows-latest, 8.4, nts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (windows-latest, 8.4, zts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

Check failure on line 23 in src/File/FileMonitor.php

View workflow job for this annotation

GitHub Actions / Run with Turbo Extension (windows-latest, 8.5, nts, make phpstan)

Unused PHPStan\File\FileMonitor::initialize

$this->fileHashes = $fileHashes;

foreach (array_keys($oldFileHashes) as $file) {
$deletedFiles[] = $file;
}

return new FileMonitorResult(
$newFiles,
$changedFiles,
$deletedFiles,
);
}

private function getFileHash(string $filePath): string
{
$hash = hash_file('sha256', $filePath);

if ($hash === false) {
throw new CouldNotReadFileException($filePath);
}

return $hash;
}
public function getChanges(): FileMonitorResult;

/**
* @param string[] $allAnalysedFiles
* @return array<string>
* How often the caller should poll. A monitor whose idle poll is free can
* afford a much shorter interval, which is what makes an edit noticed sooner.
*/
private function getScannedFiles(array $allAnalysedFiles): array
{
$scannedFiles = $this->scanFiles;
$analysedDirectories = [];
foreach (array_merge($this->analysedPaths, $this->analysedPathsFromConfig) as $analysedPath) {
if (is_file($analysedPath)) {
continue;
}

if (!is_dir($analysedPath)) {
continue;
}

$analysedDirectories[] = $analysedPath;
}

$directories = array_unique(array_merge($analysedDirectories, $this->scanDirectories));
foreach ($this->scanFileFinder->findFiles($directories)->getFiles() as $file) {
$scannedFiles[] = $file;
}

return array_diff($scannedFiles, $allAnalysedFiles);
}
public function getPollInterval(): float;

}
94 changes: 94 additions & 0 deletions src/File/FileMonitorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php declare(strict_types = 1);

namespace PHPStan\File;

use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use function getenv;
use const PHP_OS_FAMILY;

/**
* Picks the {@see FileMonitor} for this platform and hands it back initialized.
*
* Whether a native monitor can actually run is only known once it has opened
* its kernel handle and armed every watch, so the fallback decision belongs
* here rather than at the call site: a native monitor that cannot promise to
* see every change is discarded whole, and {@see HashingFileMonitor} - correct
* everywhere, just slower - answers instead.
*/
#[AutowiredService]
final class FileMonitorFactory
{

/** Set to anything to force {@see HashingFileMonitor}. */
private const DISABLE_ENV_VARIABLE = 'PHPSTAN_DISABLE_NATIVE_FILE_MONITOR';

/**
* @param string[] $analysedPaths
* @param string[] $analysedPathsFromConfig
* @param string[] $scanDirectories
*/
public function __construct(
private HashingFileMonitor $hashingFileMonitor,
#[AutowiredParameter]
private array $analysedPaths,
#[AutowiredParameter]
private array $analysedPathsFromConfig,
#[AutowiredParameter]
private array $scanDirectories,
)
{
}

/**
* @param array<string> $filePaths extra files to monitor besides the analysed and scanned ones
*/
public function create(array $filePaths): FileMonitor
{
$native = $this->createNative();
if ($native !== null) {
try {
$native->initialize($filePaths);

return $native;
} catch (FileMonitorNotSupportedException) {
// fall through to hashing
}
}

$this->hashingFileMonitor->initialize($filePaths);

return $this->hashingFileMonitor;
}

private function createNative(): ?NativeFileMonitor
{
// escape hatch: the native monitors depend on FFI and on kernel
// facilities a container or a hardened host can take away in ways they
// cannot detect
if (getenv(self::DISABLE_ENV_VARIABLE) !== false) {
return null;
}

if (PHP_OS_FAMILY === 'Darwin') {
return new FsEventsFileMonitor(
$this->hashingFileMonitor,
$this->analysedPaths,
$this->analysedPathsFromConfig,
$this->scanDirectories,
);
}

if (PHP_OS_FAMILY === 'Linux') {
return new InotifyFileMonitor(
$this->hashingFileMonitor,
$this->analysedPaths,
$this->analysedPathsFromConfig,
$this->scanDirectories,
);
}

return null;
}

}
15 changes: 15 additions & 0 deletions src/File/FileMonitorNotSupportedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types = 1);

namespace PHPStan\File;

use Exception;

/**
* A native file monitor cannot run here - no FFI, the kernel refused a watch,
* or the project has more directories than we are willing to watch.
* {@see FileMonitorFactory} answers with {@see HashingFileMonitor} instead.
*/
final class FileMonitorNotSupportedException extends Exception
{

}
16 changes: 16 additions & 0 deletions src/File/FileMonitorResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public function __construct(
{
}

/**
* @return string[]
*/
public function getNewFiles(): array
{
return $this->newFiles;
}

/**
* @return string[]
*/
Expand All @@ -28,6 +36,14 @@ public function getChangedFiles(): array
return $this->changedFiles;
}

/**
* @return string[]
*/
public function getDeletedFiles(): array
{
return $this->deletedFiles;
}

public function hasAnyChanges(): bool
{
return count($this->newFiles) > 0
Expand Down
Loading
Loading