diff --git a/composer.json b/composer.json index 9e95980e29d..1d4e7a2b6f1 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#709e512210784a7c0a677b3a89d35def844a59b9", + "jetbrains/phpstorm-stubs": "dev-master#2da66a03e379335dba086b0d493c70185eb01ddd", "nette/bootstrap": "^3.0", "nette/di": "^3.1.10", "nette/neon": "3.3.4", @@ -35,7 +35,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.8.0", "ondram/ci-detector": "^4.0", - "ondrejmirtes/better-reflection": "6.70.0.3", + "ondrejmirtes/better-reflection": "dev-feat/versioned-extension-stubs as 6.70.0.4", "ondrejmirtes/composer-attribute-collector": "^1.2.0", "ondrejmirtes/php-merge": "^4.1", "phpstan/php-8-stubs": "0.4.35", @@ -80,6 +80,12 @@ "shipmonk/dead-code-detector": "^1.0.0", "shipmonk/name-collision-detector": "^2.0" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/simPod/BetterReflection" + } + ], "config": { "platform": { "php": "8.2.99" diff --git a/composer.lock b/composer.lock index ba22f0dd15f..d19de783f0a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "83ac8eae39eeb3d9d2d813a2808f421c", + "content-hash": "dba4ceefffec82c5e1cb1028f49f32a7", "packages": [ { "name": "clue/ndjson-react", @@ -1637,19 +1637,19 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs", - "reference": "709e512210784a7c0a677b3a89d35def844a59b9" + "reference": "2da66a03e379335dba086b0d493c70185eb01ddd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/709e512210784a7c0a677b3a89d35def844a59b9", - "reference": "709e512210784a7c0a677b3a89d35def844a59b9", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/2da66a03e379335dba086b0d493c70185eb01ddd", + "reference": "2da66a03e379335dba086b0d493c70185eb01ddd", "shasum": "" }, "require-dev": { "friendsofphp/php-cs-fixer": "^v3.86", "nikic/php-parser": "^v5.6", - "phpdocumentor/reflection-docblock": "^5.6", - "phpunit/phpunit": "^12.3" + "phpdocumentor/reflection-docblock": "^6.0", + "phpunit/phpunit": "^13.2" }, "default-branch": true, "type": "library", @@ -1674,7 +1674,7 @@ "stubs", "type" ], - "time": "2026-06-12T13:19:10+00:00" + "time": "2026-08-21T14:57:18+00:00" }, { "name": "nette/bootstrap", @@ -2385,21 +2385,21 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.70.0.3", + "version": "dev-feat/versioned-extension-stubs", "source": { "type": "git", - "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "b67bcda6eac8d2faa57b3c9aa01746b9a43a37bd" + "url": "https://github.com/simPod/BetterReflection.git", + "reference": "165422d408bc16a3d8a784886cc4157ae744ba76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/b67bcda6eac8d2faa57b3c9aa01746b9a43a37bd", - "reference": "b67bcda6eac8d2faa57b3c9aa01746b9a43a37bd", + "url": "https://api.github.com/repos/simPod/BetterReflection/zipball/165422d408bc16a3d8a784886cc4157ae744ba76", + "reference": "165422d408bc16a3d8a784886cc4157ae744ba76", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#dfcad4524db603bd20bdec3aab1a31c5f5128ea3", + "jetbrains/phpstorm-stubs": "dev-master#2da66a03e379335dba086b0d493c70185eb01ddd", "nikic/php-parser": "^5.7.0", "php": "^7.4 || ^8.0" }, @@ -2423,7 +2423,26 @@ "PHPStan\\BetterReflection\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Roave\\BetterReflectionTest\\": "test/unit", + "Roave\\BetterReflectionBenchmark\\": "test/benchmark" + } + }, + "scripts": { + "cs": [ + "tools/vendor/bin/phpcs" + ], + "csfix": [ + "tools/vendor/bin/phpcbf" + ], + "psalm": [ + "tools/vendor/bin/psalm" + ], + "psalm-baseline": [ + "tools/vendor/bin/psalm --set-baseline=psalm-baseline.xml" + ] + }, "license": [ "MIT" ], @@ -2451,9 +2470,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.70.0.3" + "source": "https://github.com/simPod/BetterReflection/tree/feat/versioned-extension-stubs" }, - "time": "2026-07-02T06:50:33+00:00" + "time": "2026-08-23T20:46:29+00:00" }, { "name": "ondrejmirtes/composer-attribute-collector", @@ -6960,10 +6979,18 @@ "time": "2025-11-17T20:03:58+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "ondrejmirtes/better-reflection", + "version": "dev-feat/versioned-extension-stubs", + "alias": "6.70.0.4", + "alias_normalized": "6.70.0.4" + } + ], "minimum-stability": "dev", "stability-flags": { - "jetbrains/phpstorm-stubs": 20 + "jetbrains/phpstorm-stubs": 20, + "ondrejmirtes/better-reflection": 20 }, "prefer-stable": true, "prefer-lowest": false, diff --git a/conf/config.neon b/conf/config.neon index 8fc949c9361..f5e844a7296 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -154,7 +154,6 @@ parameters: - ../stubs/iterable.stub - ../stubs/ArrayObject.stub - ../stubs/WeakReference.stub - - ../stubs/ext-ds.stub - ../stubs/ImagickPixel.stub - ../stubs/PDOStatement.stub - ../stubs/date.stub diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 51736cb0292..631462c1a3c 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -25,6 +25,7 @@ use PHPStan\Internal\ArrayHelper; use PHPStan\Internal\ComposerHelper; use PHPStan\PhpDoc\StubFilesProvider; +use PHPStan\Reflection\BetterReflection\SourceStubber\ExtensionVersionProvider; use PHPStan\ShouldNotHappenException; use ReflectionClass; use ReflectionException; @@ -98,6 +99,7 @@ public function __construct( private StubFilesProvider $stubFilesProvider, private FileHelper $fileHelper, private PackageDependencyResolver $packageDependencyResolver, + private ExtensionVersionProvider $extensionVersionProvider, #[AutowiredParameter(ref: '%resultCachePath%')] private string $cacheFilePath, #[AutowiredParameter] @@ -1468,6 +1470,7 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a 'composerInstalled' => $this->getComposerInstalled(), 'executedFilesHashes' => $this->getExecutedFileHashes(), 'phpExtensions' => $extensions, + 'extensionVersions' => $this->extensionVersionProvider->getExtensionVersions(), 'stubFiles' => $this->getStubFiles(), 'level' => $this->usedLevel, ]; diff --git a/src/PhpDoc/ExtDsStubFilesExtension.php b/src/PhpDoc/ExtDsStubFilesExtension.php new file mode 100644 index 00000000000..9a39cc124d6 --- /dev/null +++ b/src/PhpDoc/ExtDsStubFilesExtension.php @@ -0,0 +1,25 @@ +extensionVersionProvider->getExtensionVersions()['ds'] ?? null) === 2) { + return []; + } + + return [__DIR__ . '/../../stubs/ext-ds.stub']; + } + +} diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index 055df460810..af2512cf1bc 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -58,6 +58,7 @@ public function __construct( private Cache $cache, private PhpVersion $phpVersion, private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, + private SourceStubber\ExtensionVersionProvider $extensionVersionProvider, private ReflectionSourceStubber $reflectionSourceStubber, private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, private OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, @@ -188,6 +189,7 @@ public function create(): SourceLocator new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber), $this->cache, $this->phpVersion, + $this->extensionVersionProvider, )); // Custom autoloaders registered *after* Composer's class loader are diff --git a/src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php index 9114a4c3b01..a31d2124eef 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php @@ -15,6 +15,7 @@ use PHPStan\Cache\Cache; use PHPStan\Internal\ComposerHelper; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\BetterReflection\SourceStubber\ExtensionVersionProvider; use function array_key_exists; use function is_array; use function is_file; @@ -59,6 +60,7 @@ public function __construct( private SourceLocator $inner, private Cache $cache, private PhpVersion $phpVersion, + private ExtensionVersionProvider $extensionVersionProvider, ) { } @@ -162,10 +164,11 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id private function getVariableCacheKey(): string { return $this->variableCacheKey ??= sprintf( - 'v2-%s-%s-%s', + 'v3-%s-%s-%s-%s', ComposerHelper::getBetterReflectionVersion(), ComposerHelper::getPhpStormStubsVersion(), $this->phpVersion->getVersionString(), + $this->extensionVersionProvider->getCacheKey(), ); } diff --git a/src/Reflection/BetterReflection/SourceStubber/ExtensionVersionProvider.php b/src/Reflection/BetterReflection/SourceStubber/ExtensionVersionProvider.php new file mode 100644 index 00000000000..5d6cdc40611 --- /dev/null +++ b/src/Reflection/BetterReflection/SourceStubber/ExtensionVersionProvider.php @@ -0,0 +1,178 @@ +|null */ + private ?array $extensionVersions = null; + + /** + * @param string[] $composerAutoloaderProjectPaths + */ + public function __construct( + #[AutowiredParameter] + private array $composerAutoloaderProjectPaths, + ) + { + } + + /** @return array */ + public function getExtensionVersions(): array + { + if ($this->extensionVersions !== null) { + return $this->extensionVersions; + } + + $composerConfig = null; + if (count($this->composerAutoloaderProjectPaths) > 0) { + $composerConfig = ComposerHelper::getComposerConfig(end($this->composerAutoloaderProjectPaths)); + } + + $extensionVersions = []; + foreach (PhpStormStubsMap::EXTENSION_VERSIONS as $extensionName => $versionMaps) { + [$version, $useRuntimeVersion] = $this->getComposerExtensionVersion($composerConfig, $extensionName, $versionMaps); + if ($version === null && $useRuntimeVersion) { + $version = $this->getRuntimeExtensionVersion($extensionName, $versionMaps); + } + if ($version === null) { + continue; + } + + $extensionVersions[$extensionName] = $version; + } + ksort($extensionVersions); + + return $this->extensionVersions = $extensionVersions; + } + + public function getCacheKey(): string + { + $parts = []; + foreach ($this->getExtensionVersions() as $extensionName => $version) { + $parts[] = sprintf('%s:%d', $extensionName, $version); + } + + return implode(',', $parts); + } + + /** + * @param array|null $composerConfig + * @param array, functions: array, constants: array}> $versionMaps + * @return array{int|null, bool} + */ + private function getComposerExtensionVersion(?array $composerConfig, string $extensionName, array $versionMaps): array + { + if ($composerConfig === null) { + return [null, true]; + } + + $extensionPackageName = 'ext-' . $extensionName; + $platformVersion = $composerConfig['config']['platform'][$extensionPackageName] ?? null; + if (is_string($platformVersion)) { + return [$this->getKnownVersion($platformVersion, $versionMaps), false]; + } + + $requiredVersion = $composerConfig['require'][$extensionPackageName] ?? null; + if (!is_string($requiredVersion)) { + return [null, true]; + } + + $majorVersion = $this->getConstraintMajor($requiredVersion); + if ($majorVersion === null) { + return [null, true]; + } + + return [$this->getKnownVersion((string) $majorVersion, $versionMaps), false]; + } + + /** + * @param array, functions: array, constants: array}> $versionMaps + */ + private function getRuntimeExtensionVersion(string $extensionName, array $versionMaps): ?int + { + $version = phpversion($extensionName); + if ($version === false) { + return null; + } + + return $this->getKnownVersion($version, $versionMaps); + } + + /** + * @param array, functions: array, constants: array}> $versionMaps + */ + private function getKnownVersion(string $version, array $versionMaps): ?int + { + if (preg_match('~^(\d+)~', $version, $matches) !== 1) { + return null; + } + + $majorVersion = (int) $matches[1]; + if (!array_key_exists($majorVersion, $versionMaps)) { + return null; + } + + return $majorVersion; + } + + /** + */ + private function getConstraintMajor(string $constraint): ?int + { + if (str_contains($constraint, '|')) { + return null; + } + + try { + $parsedConstraint = (new VersionParser())->parseConstraints($constraint); + } catch (UnexpectedValueException) { + return null; + } + + $lowerBound = $this->getVersionMajor($parsedConstraint->getLowerBound()->getVersion()); + $upperBound = $this->getVersionMajor($parsedConstraint->getUpperBound()->getVersion()); + if ($lowerBound === null || $upperBound === null) { + return null; + } + + if ($lowerBound === $upperBound) { + return $lowerBound; + } + + if ($upperBound !== $lowerBound + 1 || $parsedConstraint->getUpperBound()->isInclusive()) { + return null; + } + + return $lowerBound; + } + + private function getVersionMajor(string $version): ?int + { + if (preg_match('~^(\d+)~', $version, $matches) !== 1) { + return null; + } + + return (int) $matches[1]; + } + +} diff --git a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php index 778bce09152..da6a19a9b01 100644 --- a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php +++ b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php @@ -20,6 +20,7 @@ public function __construct( private PhpVersion $phpVersion, #[AutowiredParameter(ref: '%cache.phpStormStubsNodesCountMax%')] private int $phpStormStubsNodesCountMax, + private ExtensionVersionProvider $extensionVersionProvider, ) { } @@ -31,6 +32,7 @@ public function create(): PhpStormStubsSourceStubber $this->printer, $this->phpVersion->getVersionId(), $this->phpStormStubsNodesCountMax === 0 ? null : $this->phpStormStubsNodesCountMax, + $this->extensionVersionProvider->getExtensionVersions(), ); } diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php new file mode 100644 index 00000000000..06c48057ead --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php @@ -0,0 +1,60 @@ +assertSame(['ds' => 2], $provider->getExtensionVersions()); + $this->assertSame('ds:2', $provider->getCacheKey()); + } + + public function testSelectsExtensionVersionFromRequireConstraint(): void + { + $provider = new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v2-require']); + + $this->assertSame(['ds' => 2], $provider->getExtensionVersions()); + } + + public function testPassesSelectedVersionToPhpStormStubsSourceStubber(): void + { + $provider = new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v2-platform']); + $factory = new PhpStormStubsSourceStubberFactory( + self::getContainer()->getService('php8PhpParser'), + self::getContainer()->getByType(Printer::class), + new PhpVersion(80200), + 0, + $provider, + ); + + $sourceStubber = $factory->create(); + $this->assertFalse($sourceStubber->hasClass('Ds\\Vector')); + $this->assertTrue($sourceStubber->hasClass('Ds\\Seq')); + } + + public function testDoesNotLoadExtDsV1OverlayForV2(): void + { + $extension = new ExtDsStubFilesExtension(new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v2-platform'])); + + $this->assertSame([], $extension->getFiles()); + } + + public function testLoadsExtDsV1OverlayForV1(): void + { + $extension = new ExtDsStubFilesExtension(new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v1-platform'])); + + $files = $extension->getFiles(); + $this->assertCount(1, $files); + $this->assertStringEndsWith('/stubs/ext-ds.stub', $files[0]); + } + +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform/composer.json b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform/composer.json new file mode 100644 index 00000000000..0c2b249ac02 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform/composer.json @@ -0,0 +1,7 @@ +{ + "config": { + "platform": { + "ext-ds": "1.0.0" + } + } +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-platform/composer.json b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-platform/composer.json new file mode 100644 index 00000000000..7317ccc4abc --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-platform/composer.json @@ -0,0 +1,7 @@ +{ + "config": { + "platform": { + "ext-ds": "2.0.0" + } + } +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-require/composer.json b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-require/composer.json new file mode 100644 index 00000000000..4fbd85ae91b --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-require/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "ext-ds": "^2.0" + } +}