From 5c682b9e967298e29d7f4e5063803d89d26e03f3 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Sun, 23 Aug 2026 22:29:12 +0200 Subject: [PATCH 1/6] feat(reflection): select versioned extension stubs --- composer.json | 10 +- composer.lock | 63 +++++-- conf/config.neon | 1 - .../ResultCache/ResultCacheManager.php | 3 + src/PhpDoc/ExtDsStubFilesExtension.php | 25 +++ .../BetterReflectionSourceLocatorFactory.php | 2 + .../CachedPhpInternalSourceLocator.php | 5 +- .../ExtensionVersionProvider.php | 178 ++++++++++++++++++ .../PhpStormStubsSourceStubberFactory.php | 2 + .../ExtensionVersionProviderTest.php | 60 ++++++ .../data/ext-ds-v1-platform/composer.json | 7 + .../data/ext-ds-v2-platform/composer.json | 7 + .../data/ext-ds-v2-require/composer.json | 5 + 13 files changed, 346 insertions(+), 22 deletions(-) create mode 100644 src/PhpDoc/ExtDsStubFilesExtension.php create mode 100644 src/Reflection/BetterReflection/SourceStubber/ExtensionVersionProvider.php create mode 100644 tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php create mode 100644 tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform/composer.json create mode 100644 tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-platform/composer.json create mode 100644 tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-require/composer.json 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..dbd3836cc6a 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": "efb7b409f7a49c67be1462d2d51a436010ce1ce1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/b67bcda6eac8d2faa57b3c9aa01746b9a43a37bd", - "reference": "b67bcda6eac8d2faa57b3c9aa01746b9a43a37bd", + "url": "https://api.github.com/repos/simPod/BetterReflection/zipball/efb7b409f7a49c67be1462d2d51a436010ce1ce1", + "reference": "efb7b409f7a49c67be1462d2d51a436010ce1ce1", "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-22T07:47:19+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 47840e7dba8..e39f0e0b009 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; @@ -99,6 +100,7 @@ public function __construct( private StubFilesProvider $stubFilesProvider, private FileHelper $fileHelper, private PackageDependencyResolver $packageDependencyResolver, + private ExtensionVersionProvider $extensionVersionProvider, #[AutowiredParameter(ref: '%resultCachePath%')] private string $cacheFilePath, #[AutowiredParameter] @@ -1525,6 +1527,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" + } +} From 783d5d41b760334e191766e9eb039481fb8773fa Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Sun, 23 Aug 2026 22:47:11 +0200 Subject: [PATCH 2/6] chore(deps): update BetterReflection fork --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index dbd3836cc6a..d19de783f0a 100644 --- a/composer.lock +++ b/composer.lock @@ -2389,12 +2389,12 @@ "source": { "type": "git", "url": "https://github.com/simPod/BetterReflection.git", - "reference": "efb7b409f7a49c67be1462d2d51a436010ce1ce1" + "reference": "165422d408bc16a3d8a784886cc4157ae744ba76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simPod/BetterReflection/zipball/efb7b409f7a49c67be1462d2d51a436010ce1ce1", - "reference": "efb7b409f7a49c67be1462d2d51a436010ce1ce1", + "url": "https://api.github.com/repos/simPod/BetterReflection/zipball/165422d408bc16a3d8a784886cc4157ae744ba76", + "reference": "165422d408bc16a3d8a784886cc4157ae744ba76", "shasum": "" }, "require": { @@ -2472,7 +2472,7 @@ "support": { "source": "https://github.com/simPod/BetterReflection/tree/feat/versioned-extension-stubs" }, - "time": "2026-08-22T07:47:19+00:00" + "time": "2026-08-23T20:46:29+00:00" }, { "name": "ondrejmirtes/composer-attribute-collector", From b595bb707b4006b1fe60b303da84c4d659f1d12a Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Sun, 23 Aug 2026 23:00:20 +0200 Subject: [PATCH 3/6] fix(stubs): omit v1 overlay for versioned ds maps --- src/PhpDoc/ExtDsStubFilesExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpDoc/ExtDsStubFilesExtension.php b/src/PhpDoc/ExtDsStubFilesExtension.php index 9a39cc124d6..7939ca22c90 100644 --- a/src/PhpDoc/ExtDsStubFilesExtension.php +++ b/src/PhpDoc/ExtDsStubFilesExtension.php @@ -15,7 +15,7 @@ public function __construct(private ExtensionVersionProvider $extensionVersionPr public function getFiles(): array { - if (($this->extensionVersionProvider->getExtensionVersions()['ds'] ?? null) === 2) { + if (($this->extensionVersionProvider->getExtensionVersions()['ds'] ?? null) !== null) { return []; } From 169f089c297776f153767a69d0655bcbdf42cf18 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 24 Aug 2026 12:53:15 +0200 Subject: [PATCH 4/6] fix(stubs): complete versioned extension selection Continue through unknown platform constraints, preserve default major maps, and keep project stub precedence stable. Isolate version-specific tests and patch unrelated conditional throw annotations from the required stub update. --- build/composer-dependency-analyser.php | 1 - composer.json | 3 +- composer.lock | 2 +- patches/conditional-function-throws.patch | 27 ++++++++ src/PhpDoc/DefaultStubFilesProvider.php | 19 +++++- src/PhpDoc/ExtDsStubFilesExtension.php | 5 +- src/PhpDoc/PredefinedStubFilesExtension.php | 9 +++ .../CachedPhpInternalSourceLocator.php | 4 +- .../ExtensionVersionProvider.php | 33 ++++++---- ...ComposerAutoloaderProjectPathsProvider.php | 12 ++++ src/Testing/PHPStanTestCaseTrait.php | 9 ++- .../Analyser/NodeScopeResolverTest.php | 8 ++- .../Analyser/ParamClosureThisStubsTest.php | 8 ++- .../ResultCacheExtensionVersionTest.php | 61 +++++++++++++++++++ .../PhpDoc/DefaultStubFilesProviderTest.php | 20 ++++++ .../CachedPhpInternalSourceLocatorTest.php | 51 ++++++++++++++++ .../ExtensionVersionProviderTest.php | 18 +++++- .../composer.json | 5 ++ .../ext-ds-v2-unknown-platform/composer.json | 10 +++ .../CatchWithUnthrownExceptionRuleTest.php | 8 ++- .../Rules/Functions/ReturnTypeRuleTest.php | 8 ++- 21 files changed, 293 insertions(+), 28 deletions(-) create mode 100644 patches/conditional-function-throws.patch create mode 100644 src/PhpDoc/PredefinedStubFilesExtension.php create mode 100644 src/Testing/ComposerAutoloaderProjectPathsProvider.php create mode 100644 tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php create mode 100644 tests/PHPStan/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocatorTest.php create mode 100644 tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-same-major-or-require/composer.json create mode 100644 tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-unknown-platform/composer.json diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php index cfd8cdc33a9..ebaec5d9750 100644 --- a/build/composer-dependency-analyser.php +++ b/build/composer-dependency-analyser.php @@ -43,6 +43,5 @@ [ErrorType::UNUSED_DEPENDENCY], ) ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // prepared test tooling - ->ignoreErrorsOnPackage('jetbrains/phpstorm-stubs', [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]) // there is no direct usage, but we need newer version then required by ondrejmirtes/BetterReflection ->ignoreErrorsOnPath(__DIR__ . '/../tests', [ErrorType::UNKNOWN_CLASS, ErrorType::UNKNOWN_FUNCTION, ErrorType::SHADOW_DEPENDENCY]) // to be able to test invalid symbols ->ignoreUnknownClasses($unknownClasses); diff --git a/composer.json b/composer.json index 1d4e7a2b6f1..6e0d1b495c7 100644 --- a/composer.json +++ b/composer.json @@ -142,7 +142,8 @@ "patches/ReflectionProperty.patch", "patches/SessionHandler.patch", "patches/xmlreader.patch", - "patches/dom_c.patch" + "patches/dom_c.patch", + "patches/conditional-function-throws.patch" ], "nette/di": [ "patches/Resolver.patch" diff --git a/composer.lock b/composer.lock index d19de783f0a..7656c5f66a9 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": "dba4ceefffec82c5e1cb1028f49f32a7", + "content-hash": "a8a7b0bc80b76b01d7f6585494d8fd11", "packages": [ { "name": "clue/ndjson-react", diff --git a/patches/conditional-function-throws.patch b/patches/conditional-function-throws.patch new file mode 100644 index 00000000000..e09d63c6625 --- /dev/null +++ b/patches/conditional-function-throws.patch @@ -0,0 +1,27 @@ +--- Core/Core.php ++++ Core/Core.php +@@ -660,8 +660,6 @@ + *

+ * @return bool This function returns false if wrong error_type is + * specified, true otherwise. +- * @throws \ValueError This function throws a ValueError if error_level is not one of E_USER_ERROR, +- * E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED. + */ + #[LanguageLevelTypeAware(['8.4' => 'true'], default: 'bool')] + function trigger_error(string $message, int $error_level = E_USER_NOTICE) {} +--- standard/standard_2.php ++++ standard/standard_2.php +@@ -337,13 +337,6 @@ + *

+ * @return string a string produced according to the formatting string + * format. +- * @throws \ValueError As of PHP 8.0.0, a ValueError is thrown if the number of arguments is zero. +- * Prior to PHP 8.0.0, a E_WARNING was emitted instead. As of PHP 8.0.0, a ValueError is thrown if +- * [width] is less than zero or bigger than PHP_INT_MAX. Prior to PHP 8.0.0, a E_WARNING was emitted +- * instead. As of PHP 8.0.0, a ValueError is thrown if [precision] is less than zero or bigger than +- * PHP_INT_MAX. Prior to PHP 8.0.0, a E_WARNING was emitted instead. +- * @throws \ArgumentCountError As of PHP 8.0.0, a ArgumentCountError is thrown when less arguments +- * are given than required. Prior to PHP 8.0.0, false was returned and a E_WARNING emitted instead. + */ + #[Pure] + function sprintf( diff --git a/src/PhpDoc/DefaultStubFilesProvider.php b/src/PhpDoc/DefaultStubFilesProvider.php index 0409304b261..e7e083da58d 100644 --- a/src/PhpDoc/DefaultStubFilesProvider.php +++ b/src/PhpDoc/DefaultStubFilesProvider.php @@ -9,7 +9,6 @@ use PHPStan\File\FileHelper; use PHPStan\Internal\ComposerHelper; use function array_filter; -use function array_map; use function array_values; use function dirname; use function str_contains; @@ -48,9 +47,25 @@ public function getStubFiles(): array return $this->cachedFiles; } - $files = array_map(fn ($path) => $this->fileHelper->normalizePath($path), $this->stubFiles); + $files = []; $extensions = $this->stubFilesExtensions->getAll(); foreach ($extensions as $extension) { + if (!$extension instanceof PredefinedStubFilesExtension) { + continue; + } + + foreach ($extension->getFiles() as $extensionFile) { + $files[] = $this->fileHelper->normalizePath($extensionFile); + } + } + foreach ($this->stubFiles as $stubFile) { + $files[] = $this->fileHelper->normalizePath($stubFile); + } + foreach ($extensions as $extension) { + if ($extension instanceof PredefinedStubFilesExtension) { + continue; + } + foreach ($extension->getFiles() as $extensionFile) { $files[] = $this->fileHelper->normalizePath($extensionFile); } diff --git a/src/PhpDoc/ExtDsStubFilesExtension.php b/src/PhpDoc/ExtDsStubFilesExtension.php index 7939ca22c90..c4b2162f5d3 100644 --- a/src/PhpDoc/ExtDsStubFilesExtension.php +++ b/src/PhpDoc/ExtDsStubFilesExtension.php @@ -6,7 +6,7 @@ use PHPStan\Reflection\BetterReflection\SourceStubber\ExtensionVersionProvider; #[AutowiredService] -final class ExtDsStubFilesExtension implements StubFilesExtension +final class ExtDsStubFilesExtension implements PredefinedStubFilesExtension { public function __construct(private ExtensionVersionProvider $extensionVersionProvider) @@ -15,7 +15,8 @@ public function __construct(private ExtensionVersionProvider $extensionVersionPr public function getFiles(): array { - if (($this->extensionVersionProvider->getExtensionVersions()['ds'] ?? null) !== null) { + $version = $this->extensionVersionProvider->getExtensionVersions()['ds'] ?? null; + if ($version !== null && $version !== 1) { return []; } diff --git a/src/PhpDoc/PredefinedStubFilesExtension.php b/src/PhpDoc/PredefinedStubFilesExtension.php new file mode 100644 index 00000000000..2f21c2ac18a --- /dev/null +++ b/src/PhpDoc/PredefinedStubFilesExtension.php @@ -0,0 +1,9 @@ + 3, + 'ds' => 1, + ]; + /** @var array|null */ private ?array $extensionVersions = null; @@ -89,7 +93,10 @@ private function getComposerExtensionVersion(?array $composerConfig, string $ext $extensionPackageName = 'ext-' . $extensionName; $platformVersion = $composerConfig['config']['platform'][$extensionPackageName] ?? null; if (is_string($platformVersion)) { - return [$this->getKnownVersion($platformVersion, $versionMaps), false]; + $knownVersion = $this->getKnownVersion($extensionName, $platformVersion, $versionMaps); + if ($knownVersion !== null) { + return [$knownVersion, false]; + } } $requiredVersion = $composerConfig['require'][$extensionPackageName] ?? null; @@ -102,7 +109,12 @@ private function getComposerExtensionVersion(?array $composerConfig, string $ext return [null, true]; } - return [$this->getKnownVersion((string) $majorVersion, $versionMaps), false]; + $knownVersion = $this->getKnownVersion($extensionName, (string) $majorVersion, $versionMaps); + if ($knownVersion === null) { + return [null, true]; + } + + return [$knownVersion, false]; } /** @@ -115,34 +127,31 @@ private function getRuntimeExtensionVersion(string $extensionName, array $versio return null; } - return $this->getKnownVersion($version, $versionMaps); + return $this->getKnownVersion($extensionName, $version, $versionMaps); } /** * @param array, functions: array, constants: array}> $versionMaps */ - private function getKnownVersion(string $version, array $versionMaps): ?int + private function getKnownVersion(string $extensionName, string $version, array $versionMaps): ?int { if (preg_match('~^(\d+)~', $version, $matches) !== 1) { return null; } $majorVersion = (int) $matches[1]; - if (!array_key_exists($majorVersion, $versionMaps)) { + if ( + !array_key_exists($majorVersion, $versionMaps) + && (self::DEFAULT_EXTENSION_MAJOR_VERSIONS[$extensionName] ?? null) !== $majorVersion + ) { return null; } return $majorVersion; } - /** - */ private function getConstraintMajor(string $constraint): ?int { - if (str_contains($constraint, '|')) { - return null; - } - try { $parsedConstraint = (new VersionParser())->parseConstraints($constraint); } catch (UnexpectedValueException) { diff --git a/src/Testing/ComposerAutoloaderProjectPathsProvider.php b/src/Testing/ComposerAutoloaderProjectPathsProvider.php new file mode 100644 index 00000000000..8036792aea7 --- /dev/null +++ b/src/Testing/ComposerAutoloaderProjectPathsProvider.php @@ -0,0 +1,12 @@ +create($tmpDir, array_merge([ $containerFactory->getConfigDirectory() . '/config.level8.neon', - ], $additionalConfigFiles), []); + ], $additionalConfigFiles), [], $composerAutoloaderProjectPaths); self::$containers[$cacheKey] = $container; foreach ($container->getParameter('bootstrapFiles') as $bootstrapFile) { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 24f55aad643..45f1beb3def 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -4,6 +4,7 @@ use EnumTypeAssertions\Foo; use PHPStan\File\FileHelper; +use PHPStan\Testing\ComposerAutoloaderProjectPathsProvider; use PHPStan\Testing\TypeInferenceTestCase; use PHPUnit\Framework\Attributes\DataProvider; use stdClass; @@ -20,7 +21,7 @@ use const PHP_INT_SIZE; use const PHP_VERSION_ID; -class NodeScopeResolverTest extends TypeInferenceTestCase +class NodeScopeResolverTest extends TypeInferenceTestCase implements ComposerAutoloaderProjectPathsProvider { /** @@ -360,6 +361,11 @@ public static function getAdditionalConfigFiles(): array ); } + public static function getComposerAutoloaderProjectPaths(): array + { + return [__DIR__ . '/../Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform']; + } + /** @return string[] */ protected static function getAdditionalAnalysedFiles(): array { diff --git a/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php index cd28ff80f74..5570bd28a87 100644 --- a/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php +++ b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php @@ -2,10 +2,11 @@ namespace PHPStan\Analyser; +use PHPStan\Testing\ComposerAutoloaderProjectPathsProvider; use PHPStan\Testing\TypeInferenceTestCase; use PHPUnit\Framework\Attributes\DataProvider; -class ParamClosureThisStubsTest extends TypeInferenceTestCase +class ParamClosureThisStubsTest extends TypeInferenceTestCase implements ComposerAutoloaderProjectPathsProvider { public static function dataAsserts(): iterable @@ -33,4 +34,9 @@ public static function getAdditionalConfigFiles(): array ]; } + public static function getComposerAutoloaderProjectPaths(): array + { + return [__DIR__ . '/../Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform']; + } + } diff --git a/tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php b/tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php new file mode 100644 index 00000000000..ee96bb2dea4 --- /dev/null +++ b/tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php @@ -0,0 +1,61 @@ +getMeta(new ExtensionVersionProvider([ + __DIR__ . '/../../Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform', + ])); + $v2Meta = $this->getMeta(new ExtensionVersionProvider([ + __DIR__ . '/../../Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-platform', + ])); + + $this->assertSame(['ds' => 1], $v1Meta['extensionVersions']); + $this->assertSame(['ds' => 2], $v2Meta['extensionVersions']); + } + + /** + * @return mixed[] + */ + private function getMeta(ExtensionVersionProvider $extensionVersionProvider): array + { + $container = self::getContainer(); + $manager = new ResultCacheManager( + resultCacheMetaExtensions: $container->getExtensionsCollection(ResultCacheMetaExtension::class), + exportedNodeFetcher: $container->getByType(ExportedNodeFetcher::class), + scanFileFinder: $container->getService('fileFinderScan'), + stubFilesProvider: $this->createStub(StubFilesProvider::class), + fileHelper: $container->getByType(FileHelper::class), + packageDependencyResolver: $container->getByType(PackageDependencyResolver::class), + extensionVersionProvider: $extensionVersionProvider, + cacheFilePath: '', + analysedPaths: [], + analysedPathsFromConfig: [], + composerAutoloaderProjectPaths: [], + usedLevel: '8', + cliAutoloadFile: null, + bootstrapFiles: [], + scanFiles: [], + scanDirectories: [], + fileReplacements: [], + checkDependenciesOfProjectExtensionFiles: false, + parametersNotInvalidatingCache: [], + skipResultCacheIfOlderThanDays: 7, + ); + + return $manager->restore([], true, false, null, $this->createStub(Output::class))->getMeta(); + } + +} diff --git a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php index 3603e5c0989..80e14a5945b 100644 --- a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php +++ b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDoc; use Override; +use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\File\FileHelper; use PHPStan\Testing\PHPStanTestCase; use function dirname; @@ -32,6 +33,25 @@ public function testGetStubFiles(): void $this->assertContains($fileHelper->normalizePath($thirdPartyStubFile), $stubFiles); } + public function testPredefinedStubFilesComeBeforeConfiguredAndExtensionFiles(): void + { + $extension = $this->createStub(StubFilesExtension::class); + $extension->method('getFiles')->willReturn(['/extensionStub.stub']); + $predefinedExtension = $this->createStub(PredefinedStubFilesExtension::class); + $predefinedExtension->method('getFiles')->willReturn(['/predefinedStub.stub']); + $extensions = $this->createStub(ExtensionsCollection::class); + $extensions->method('getAll')->willReturn([$extension, $predefinedExtension]); + + $fileHelper = new FileHelper(__DIR__); + $provider = new DefaultStubFilesProvider($extensions, $fileHelper, ['/projectStub.stub'], []); + + $this->assertSame([ + $fileHelper->normalizePath('/predefinedStub.stub'), + $fileHelper->normalizePath('/projectStub.stub'), + $fileHelper->normalizePath('/extensionStub.stub'), + ], $provider->getStubFiles()); + } + public function testGetProjectStubFiles(): void { $thirdPartyStubFile = sprintf('%s/vendor/thirdpartyStub.stub', $this->currentWorkingDirectory); diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocatorTest.php new file mode 100644 index 00000000000..5159a965491 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocatorTest.php @@ -0,0 +1,51 @@ +createMock(CacheStorage::class); + $storage->expects($this->exactly(2)) + ->method('load') + ->willReturnCallback(static function (string $key, string $variableKey) use (&$variableKeys) { + $variableKeys[] = $variableKey; + return null; + }); + + $cache = new Cache($storage); + $inner = $this->createStub(SourceLocator::class); + $reflector = $this->createStub(Reflector::class); + $identifier = new Identifier('CacheSeparationFixture', new IdentifierType()); + + foreach (['ext-ds-v1-platform', 'ext-ds-v2-platform'] as $fixture) { + $locator = new CachedPhpInternalSourceLocator( + $inner, + $cache, + new PhpVersion(80200), + new ExtensionVersionProvider([__DIR__ . '/../SourceStubber/data/' . $fixture]), + ); + $locator->locateIdentifier($reflector, $identifier); + } + + $this->assertCount(2, $variableKeys); + $this->assertNotSame($variableKeys[0], $variableKeys[1]); + $this->assertTrue(str_ends_with($variableKeys[0], '-ds:1')); + $this->assertTrue(str_ends_with($variableKeys[1], '-ds:2')); + } + +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php index 06c48057ead..8c5a318f436 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php @@ -25,6 +25,20 @@ public function testSelectsExtensionVersionFromRequireConstraint(): void $this->assertSame(['ds' => 2], $provider->getExtensionVersions()); } + public function testFallsBackToRequireWhenPlatformVersionIsUnknown(): void + { + $provider = new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v2-unknown-platform']); + + $this->assertSame(['ds' => 2], $provider->getExtensionVersions()); + } + + public function testSelectsExtensionVersionFromSameMajorOrConstraint(): void + { + $provider = new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v2-same-major-or-require']); + + $this->assertSame(['ds' => 2], $provider->getExtensionVersions()); + } + public function testPassesSelectedVersionToPhpStormStubsSourceStubber(): void { $provider = new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v2-platform']); @@ -50,8 +64,10 @@ public function testDoesNotLoadExtDsV1OverlayForV2(): void public function testLoadsExtDsV1OverlayForV1(): void { - $extension = new ExtDsStubFilesExtension(new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v1-platform'])); + $provider = new ExtensionVersionProvider([__DIR__ . '/data/ext-ds-v1-platform']); + $extension = new ExtDsStubFilesExtension($provider); + $this->assertSame(['ds' => 1], $provider->getExtensionVersions()); $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-v2-same-major-or-require/composer.json b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-same-major-or-require/composer.json new file mode 100644 index 00000000000..690bdf639be --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-same-major-or-require/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "ext-ds": "^2.0 || ^2.1" + } +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-unknown-platform/composer.json b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-unknown-platform/composer.json new file mode 100644 index 00000000000..7356ad7496e --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/data/ext-ds-v2-unknown-platform/composer.json @@ -0,0 +1,10 @@ +{ + "require": { + "ext-ds": "^2.0" + }, + "config": { + "platform": { + "ext-ds": "3.0.0" + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index d77785142b5..72b15a52c9b 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -5,13 +5,14 @@ use Error; use InvalidArgumentException; use PHPStan\Rules\Rule; +use PHPStan\Testing\ComposerAutoloaderProjectPathsProvider; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase */ -class CatchWithUnthrownExceptionRuleTest extends RuleTestCase +class CatchWithUnthrownExceptionRuleTest extends RuleTestCase implements ComposerAutoloaderProjectPathsProvider { private bool $reportUncheckedExceptionDeadCatch = true; @@ -19,6 +20,11 @@ class CatchWithUnthrownExceptionRuleTest extends RuleTestCase /** @var string[] */ private array $uncheckedExceptionClasses = []; + public static function getComposerAutoloaderProjectPaths(): array + { + return [__DIR__ . '/../../Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform']; + } + protected function getRule(): Rule { return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 88a4b5562cf..51f89709bcd 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -5,19 +5,25 @@ use PHPStan\Rules\FunctionReturnTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\ComposerAutoloaderProjectPathsProvider; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase */ -class ReturnTypeRuleTest extends RuleTestCase +class ReturnTypeRuleTest extends RuleTestCase implements ComposerAutoloaderProjectPathsProvider { private bool $checkNullables; private bool $checkExplicitMixed; + public static function getComposerAutoloaderProjectPaths(): array + { + return [__DIR__ . '/../../Reflection/BetterReflection/SourceStubber/data/ext-ds-v1-platform']; + } + protected function getRule(): Rule { return new ReturnTypeRule(new FunctionReturnTypeCheck( From 925b1f6623858282c5ae1f4997c39571cd8c4adb Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 24 Aug 2026 13:35:41 +0200 Subject: [PATCH 5/6] test(cache): provide result cache anchor directory --- .../Analyser/ResultCache/ResultCacheExtensionVersionTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php b/tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php index ee96bb2dea4..5900e2f4928 100644 --- a/tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php +++ b/tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php @@ -53,6 +53,7 @@ private function getMeta(ExtensionVersionProvider $extensionVersionProvider): ar checkDependenciesOfProjectExtensionFiles: false, parametersNotInvalidatingCache: [], skipResultCacheIfOlderThanDays: 7, + anchorDirectory: $container->getParameter('rootDir'), ); return $manager->restore([], true, false, null, $this->createStub(Output::class))->getMeta(); From b69977f4d2d311cbf5c727e5a9f597cc05854831 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 24 Aug 2026 14:24:58 +0200 Subject: [PATCH 6/6] build(deps): avoid GitHub API for BetterReflection fork --- composer.json | 3 ++- composer.lock | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 6e0d1b495c7..e7e29a41d6c 100644 --- a/composer.json +++ b/composer.json @@ -83,7 +83,8 @@ "repositories": [ { "type": "vcs", - "url": "https://github.com/simPod/BetterReflection" + "url": "https://github.com/simPod/BetterReflection", + "no-api": true } ], "config": { diff --git a/composer.lock b/composer.lock index 7656c5f66a9..48a165eb846 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": "a8a7b0bc80b76b01d7f6585494d8fd11", + "content-hash": "f068d80d8bea78f131bc76a85dbe8f8b", "packages": [ { "name": "clue/ndjson-react", @@ -2388,7 +2388,7 @@ "version": "dev-feat/versioned-extension-stubs", "source": { "type": "git", - "url": "https://github.com/simPod/BetterReflection.git", + "url": "https://github.com/simPod/BetterReflection", "reference": "165422d408bc16a3d8a784886cc4157ae744ba76" }, "dist": {