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 9e95980e29d..e7e29a41d6c 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,13 @@
"shipmonk/dead-code-detector": "^1.0.0",
"shipmonk/name-collision-detector": "^2.0"
},
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/simPod/BetterReflection",
+ "no-api": true
+ }
+ ],
"config": {
"platform": {
"php": "8.2.99"
@@ -136,7 +143,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 ba22f0dd15f..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": "83ac8eae39eeb3d9d2d813a2808f421c",
+ "content-hash": "f068d80d8bea78f131bc76a85dbe8f8b",
"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",
+ "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/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/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/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
new file mode 100644
index 00000000000..c4b2162f5d3
--- /dev/null
+++ b/src/PhpDoc/ExtDsStubFilesExtension.php
@@ -0,0 +1,26 @@
+extensionVersionProvider->getExtensionVersions()['ds'] ?? null;
+ if ($version !== null && $version !== 1) {
+ return [];
+ }
+
+ return [__DIR__ . '/../../stubs/ext-ds.stub'];
+ }
+
+}
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 @@
+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..713772c9e26 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;
@@ -34,8 +35,8 @@
* entry, and shared across a run's parallel workers through the arena.
*
* The stubber's output depends on the stubs package, the reflection library
- * and the target PHP version (stub members are version-filtered), so all
- * three are part of the cache key.
+ * and the target PHP and extension versions (stub members are version-filtered),
+ * so all four are part of the cache key.
*
* The exported blob contains the absolute path of the stub file, but the
* cache key deliberately contains no paths - the same entries are shared by
@@ -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..e5e58d800c4
--- /dev/null
+++ b/src/Reflection/BetterReflection/SourceStubber/ExtensionVersionProvider.php
@@ -0,0 +1,187 @@
+ 3,
+ 'ds' => 1,
+ ];
+
+ /** @var array|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)) {
+ $knownVersion = $this->getKnownVersion($extensionName, $platformVersion, $versionMaps);
+ if ($knownVersion !== null) {
+ return [$knownVersion, false];
+ }
+ }
+
+ $requiredVersion = $composerConfig['require'][$extensionPackageName] ?? null;
+ if (!is_string($requiredVersion)) {
+ return [null, true];
+ }
+
+ $majorVersion = $this->getConstraintMajor($requiredVersion);
+ if ($majorVersion === null) {
+ return [null, true];
+ }
+
+ $knownVersion = $this->getKnownVersion($extensionName, (string) $majorVersion, $versionMaps);
+ if ($knownVersion === null) {
+ return [null, true];
+ }
+
+ return [$knownVersion, 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($extensionName, $version, $versionMaps);
+ }
+
+ /**
+ * @param array, functions: array, constants: array}> $versionMaps
+ */
+ 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)
+ && (self::DEFAULT_EXTENSION_MAJOR_VERSIONS[$extensionName] ?? null) !== $majorVersion
+ ) {
+ return null;
+ }
+
+ return $majorVersion;
+ }
+
+ private function getConstraintMajor(string $constraint): ?int
+ {
+ 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/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..5900e2f4928
--- /dev/null
+++ b/tests/PHPStan/Analyser/ResultCache/ResultCacheExtensionVersionTest.php
@@ -0,0 +1,62 @@
+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,
+ anchorDirectory: $container->getParameter('rootDir'),
+ );
+
+ 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
new file mode 100644
index 00000000000..8c5a318f436
--- /dev/null
+++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/ExtensionVersionProviderTest.php
@@ -0,0 +1,76 @@
+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 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']);
+ $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
+ {
+ $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-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"
+ }
+}
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(