From 88879cbbfa6e10c6958ba22ca4a88722c7929f13 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 12 Oct 2026 09:43:34 +0200 Subject: [PATCH 1/2] Detect getIterator() return type of ArrayHash by reflection Composer\InstalledVersions also sees the nette/utils bundled in the PHPStan phar and reported 3.2.10 even with nette/utils 4.1.5 installed, so the iterator assertions never ran. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/Type/Nette/ArrayHashStubTest.php | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/Type/Nette/ArrayHashStubTest.php b/tests/Type/Nette/ArrayHashStubTest.php index 0c5d19d..7fa06a6 100644 --- a/tests/Type/Nette/ArrayHashStubTest.php +++ b/tests/Type/Nette/ArrayHashStubTest.php @@ -2,11 +2,11 @@ namespace PHPStan\Type\Nette; -use Composer\InstalledVersions; -use OutOfBoundsException; +use Iterator; +use Nette\Utils\ArrayHash; use PHPStan\Testing\TypeInferenceTestCase; -use function class_exists; -use function version_compare; +use ReflectionMethod; +use ReflectionNamedType; final class ArrayHashStubTest extends TypeInferenceTestCase { @@ -15,16 +15,9 @@ public static function dataFileAsserts(): iterable { yield from self::gatherAssertTypes(__DIR__ . '/data/array-hash.php'); - try { - $utilsVersion = class_exists(InstalledVersions::class) - ? InstalledVersions::getVersion('nette/utils') - : null; - } catch (OutOfBoundsException $e) { - $utilsVersion = null; - } - // nette/utils 4.0+ declares getIterator(): \Iterator, older versions \RecursiveArrayIterator - if ($utilsVersion === null || version_compare($utilsVersion, '4.0.0', '<')) { + $iteratorReturnType = (new ReflectionMethod(ArrayHash::class, 'getIterator'))->getReturnType(); + if (!$iteratorReturnType instanceof ReflectionNamedType || $iteratorReturnType->getName() !== Iterator::class) { return; } From 39e0412d1cc7324d405e86ba8ccc5767f8e77957 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 12 Oct 2026 09:43:35 +0200 Subject: [PATCH 2/2] Declare ArrayHash offset methods in the stub Without offsetGet(), offset access on ArrayHash was T|null from ArrayAccess instead of T as declared by nette/utils. The PHPDocs mirror nette/utils 4.1, except for the $key parameter of offsetSet(): array-key is not contravariant with ArrayAccess, which accepts null. Co-Authored-By: Claude Opus 5.5 (1M context) --- stubs/Utils/ArrayHash.stub | 35 +++++++++++++++++++++++++++- tests/Type/Nette/data/array-hash.php | 3 ++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/stubs/Utils/ArrayHash.stub b/stubs/Utils/ArrayHash.stub index cff2bc7..9c4faf9 100644 --- a/stubs/Utils/ArrayHash.stub +++ b/stubs/Utils/ArrayHash.stub @@ -7,7 +7,7 @@ namespace Nette\Utils; * @implements \IteratorAggregate * @implements \ArrayAccess */ -class ArrayHash extends \stdClass implements \ArrayAccess, \IteratorAggregate +class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \IteratorAggregate { /** @@ -18,4 +18,37 @@ class ArrayHash extends \stdClass implements \ArrayAccess, \IteratorAggregate // nothing } + /** + * @param T $value + */ + public function offsetSet($key, $value): void + { + // nothing + } + + /** + * @param array-key $key + * @return T + */ + public function offsetGet($key): mixed + { + // nothing + } + + /** + * @param array-key $key + */ + public function offsetExists($key): bool + { + // nothing + } + + /** + * @param array-key $key + */ + public function offsetUnset($key): void + { + // nothing + } + } diff --git a/tests/Type/Nette/data/array-hash.php b/tests/Type/Nette/data/array-hash.php index c15d257..875c697 100644 --- a/tests/Type/Nette/data/array-hash.php +++ b/tests/Type/Nette/data/array-hash.php @@ -11,6 +11,7 @@ function test(ArrayHash $hash): void { assertType('Nette\\Utils\\ArrayHash', $hash); - assertType('int|null', $hash['foo']); + assertType('int', $hash['foo']); + assertType('int', $hash->offsetGet('foo')); assertType('mixed', $hash->foo); }