From 5dc8d002f2772e5a8f449544edf2b32210c49fd2 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 9 Oct 2026 15:01:30 +0200 Subject: [PATCH] Let another program that loads the phar use PHPStan Turbo Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Turbo/TurboExtensionEnabler.php | 39 +++++++++++++++++++++++++--- src/Turbo/TurboExtensionSelector.php | 15 ++++++----- src/Turbo/TurboProcessRestarter.php | 8 ++++-- tests/bootstrap.php | 2 +- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 88c83cac563..8df841b925d 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -12,6 +12,7 @@ use function is_file; use function json_decode; use function phpversion; +use function realpath; /** * Activates the phpstan_turbo extension's shadowing classes. @@ -62,6 +63,27 @@ public static function getIncompatibleLoadedVersion(): ?string return $version === false ? 'unknown' : $version; } + /** + * The phar the running code is compiled from. Code compiled out of a phar + * carries the phar:// prefix of the path the phar was opened with, and + * Composer's bootstrap.php opens it through __DIR__, a resolved path. A + * path another program passes in, such as one with '..' in it, is + * resolved the same way, so that it matches that prefix. + */ + private static function resolvePharPath(?string $pharPath): string + { + if ($pharPath === null) { + return Phar::running(false); + } + + $resolvedPath = realpath($pharPath); + if ($resolvedPath === false) { + return ''; + } + + return $resolvedPath; + } + private static function isCompatible(): bool { return self::isLoaded() && phpversion('phpstan_turbo') === self::EXPECTED_EXTENSION_VERSION; @@ -90,8 +112,13 @@ public static function isTypeCombinatorCacheEnabled(): bool * implement userland interfaces and may extend userland classes, resolved * through it) and before anything could autoload one of the shadowed * classes — a twin already declared cannot be shadowed. + * + * Another program that loads PHPStan's phar, Rector for example, passes + * the path to that phar: Phar::running() only knows the phar that runs. + * + * @api */ - public static function activateIfCompatible(): void + public static function activateIfCompatible(?string $pharPath = null): void { if (!self::isCompatible()) { return; @@ -136,7 +163,7 @@ public static function activateIfCompatible(): void // would otherwise race on. Fork mode requires the guard, and // ForkParallelChecker only allows fork with the extension active. if (class_exists('Phar', false)) { - $pharPath = Phar::running(false); + $pharPath = self::resolvePharPath($pharPath); if ($pharPath !== '') { Runtime::enablePharForkGuard($pharPath); } @@ -178,9 +205,13 @@ public static function isTrustingOwnTypes(): bool * compiled earlier keeps its checks. Independent of activateIfCompatible() * — only the version gate matters here. * + * Another program that loads PHPStan's phar, Rector for example, passes + * the path to that phar: Phar::running() only knows the phar that runs. + * + * @api * @param list $argv */ - public static function trustOwnTypesIfSuitable(array $argv): void + public static function trustOwnTypesIfSuitable(array $argv, ?string $pharPath = null): void { if (!self::isCompatible()) { return; @@ -191,7 +222,7 @@ public static function trustOwnTypesIfSuitable(array $argv): void if (!class_exists('Phar', false)) { return; } - $pharPath = Phar::running(false); + $pharPath = self::resolvePharPath($pharPath); if ($pharPath === '') { return; } diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index 11b7449fe20..e9205de89dc 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -55,7 +55,7 @@ final class TurboExtensionSelector */ public const MINIMUM_PHP_VERSION_ID = 80300; - public static function findExtensionForWorkers(): ?string + public static function findExtensionForWorkers(?string $pharPath = null): ?string { if (TurboExtensionEnabler::isLoaded()) { $restartPath = TurboProcessRestarter::getRestartExtensionPath(); @@ -71,7 +71,7 @@ public static function findExtensionForWorkers(): ?string return null; } - return self::findExtension(); + return self::findExtension($pharPath); } /** @@ -96,10 +96,13 @@ public static function isMissingNextToPhar(): bool /** * Locates the distributed extension binary for the current platform — * present only next to a phar-based installation. + * + * Another program that loads PHPStan's phar, Rector for example, passes + * the path to that phar: Phar::running() only knows the phar that runs. */ - public static function findExtension(): ?string + public static function findExtension(?string $pharPath = null): ?string { - $platformDirectory = self::getPlatformDirectoryNextToPhar(); + $platformDirectory = self::getPlatformDirectoryNextToPhar($pharPath); if ($platformDirectory === null) { return null; } @@ -131,7 +134,7 @@ public static function findExtension(): ?string * binary is built for this runtime: PHP older than the minimum, a debug * build, a run from source, or a platform without binaries. */ - private static function getPlatformDirectoryNextToPhar(): ?string + private static function getPlatformDirectoryNextToPhar(?string $pharPath = null): ?string { if (PHP_VERSION_ID < self::MINIMUM_PHP_VERSION_ID) { return null; @@ -140,7 +143,7 @@ private static function getPlatformDirectoryNextToPhar(): ?string return null; } - $pharPath = Phar::running(false); + $pharPath ??= Phar::running(false); if ($pharPath === '') { return null; } diff --git a/src/Turbo/TurboProcessRestarter.php b/src/Turbo/TurboProcessRestarter.php index 6ceca9c2a82..8dab17bd1c8 100644 --- a/src/Turbo/TurboProcessRestarter.php +++ b/src/Turbo/TurboProcessRestarter.php @@ -97,9 +97,13 @@ public static function getRestartExtensionPath(): ?string /** * On success the call never returns — the process image is replaced. * + * Another program that loads PHPStan's phar, Rector for example, passes + * the path to that phar: Phar::running() only knows the phar that runs. + * + * @api * @param list $argv */ - public static function restartIfSuitable(array $argv): void + public static function restartIfSuitable(array $argv, ?string $pharPath = null): void { if (get_cfg_var(self::RESTARTED_INI) !== false) { // already restarted — whatever did not take effect (a binary that @@ -127,7 +131,7 @@ public static function restartIfSuitable(array $argv): void return; } - $extensionPath = extension_loaded('phpstan_turbo') ? null : TurboExtensionSelector::findExtension(); + $extensionPath = extension_loaded('phpstan_turbo') ? null : TurboExtensionSelector::findExtension($pharPath); $opcacheArgs = self::getOpcacheArgs(); if ( $extensionPath === null diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 497ed33d800..602e8ba49fa 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -6,7 +6,7 @@ error_reporting(E_ALL); require_once __DIR__ . '/../vendor/autoload.php'; -TurboExtensionEnabler::activateIfCompatible(); // @phpstan-ignore phpstanApi.method +TurboExtensionEnabler::activateIfCompatible(); require_once __DIR__ . '/PHPStan/Rules/AlwaysFailRule.php'; require_once __DIR__ . '/PHPStan/Rules/DummyRule.php'; require_once __DIR__ . '/phpstan-bootstrap.php';