From 2cccb917449c8e77002f05f561c213ed6c659b67 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sat, 23 May 2026 22:49:00 +0000 Subject: [PATCH 1/2] Add precise return types for `RedisArray` methods in function signature map - Add `RedisArray::keys` with return type `array>|false` instead of the imprecise `bool|array` from phpstorm stubs - Add `RedisArray::info` with return type `array>|false` - Add `RedisArray::mget` with return type `list|false` - Add `RedisArray::scan` with return type `list|false` - Add `RedisArray::hscan` with return type `array|false` - Add `RedisArray::sscan` with return type `list|false` - Add `RedisArray::zscan` with return type `array|false` - Add `RedisArray` to stubs/Redis.stub with `@phpstan-all-methods-impure` for consistency with `Redis` and `RedisCluster` --- resources/functionMap.php | 7 +++ stubs/Redis.stub | 5 ++ tests/PHPStan/Analyser/nsrt/bug-9748.php | 60 ++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-9748.php diff --git a/resources/functionMap.php b/resources/functionMap.php index 8674d733188..9d020b268b3 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -8822,6 +8822,13 @@ 'RedisArray::_hosts' => ['array'], 'RedisArray::_rehash' => ['', 'callable='=>'callable'], 'RedisArray::_target' => ['string', 'key'=>'string'], +'RedisArray::hscan' => ['array|false', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'RedisArray::info' => ['array>|false'], +'RedisArray::keys' => ['array>|false', 'pattern'=>'string'], +'RedisArray::mget' => ['list|false', 'keys'=>'string[]'], +'RedisArray::scan' => ['list|false', '&iterator'=>'?int', 'node'=>'string', 'pattern='=>'?string', 'count='=>'?int'], +'RedisArray::sscan' => ['list|false', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'RedisArray::zscan' => ['array|false', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], 'RedisCluster::__construct' => ['void', 'name'=>'string|null', 'seeds='=>'string[]|null', 'timeout='=>'int|float', 'read_timeout='=>'int|float', 'persistent='=>'bool', 'auth='=>'mixed', 'context='=>'array|null'], 'RedisCluster::_prefix' => ['string', 'value'=>'mixed'], 'RedisCluster::_serialize' => ['mixed', 'value'=>'mixed'], diff --git a/stubs/Redis.stub b/stubs/Redis.stub index 072475482fc..f713d4e5f14 100644 --- a/stubs/Redis.stub +++ b/stubs/Redis.stub @@ -5,6 +5,11 @@ */ class Redis {} +/** + * @phpstan-all-methods-impure + */ +class RedisArray {} + /** * @phpstan-all-methods-impure */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-9748.php b/tests/PHPStan/Analyser/nsrt/bug-9748.php new file mode 100644 index 00000000000..6d2bfc1c0a9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9748.php @@ -0,0 +1,60 @@ +keys('*'); + assertType('array>|false', $keys); + if ($keys === false) { + return; + } + assertType('array>', $keys); + foreach ($keys as $host => $hostKeys) { + assertType('string', $host); + assertType('list', $hostKeys); + foreach ($hostKeys as $i => $hostKey) { + assertType('int<0, max>', $i); + assertType('string', $hostKey); + } + } +} + +function testInfo(\RedisArray $ra): void { + $info = $ra->info(); + assertType('array>|false', $info); + if ($info === false) { + return; + } + assertType('array>', $info); +} + +function testMget(\RedisArray $ra): void { + $values = $ra->mget(['key1', 'key2']); + assertType('list|false', $values); +} + +function testScan(\RedisArray $ra): void { + $iterator = null; + $result = $ra->scan($iterator, 'node1'); + assertType('list|false', $result); +} + +function testHscan(\RedisArray $ra): void { + $iterator = null; + $result = $ra->hscan('myhash', $iterator); + assertType('array|false', $result); +} + +function testSscan(\RedisArray $ra): void { + $iterator = null; + $result = $ra->sscan('myset', $iterator); + assertType('list|false', $result); +} + +function testZscan(\RedisArray $ra): void { + $iterator = null; + $result = $ra->zscan('myzset', $iterator); + assertType('array|false', $result); +} From de90589fd4124a6e29fb3bd8da344c56a203a90f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 22:58:13 +0000 Subject: [PATCH 2/2] Use __benevolent union types for RedisArray methods Consistent with Redis method signatures, wrap RedisArray return types in __benevolent<> so users are not forced to check for false on every call. Co-Authored-By: Claude Opus 4.6 --- resources/functionMap.php | 14 +++++++------- tests/PHPStan/Analyser/nsrt/bug-9748.php | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 9d020b268b3..1ace8423dfa 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -8822,13 +8822,13 @@ 'RedisArray::_hosts' => ['array'], 'RedisArray::_rehash' => ['', 'callable='=>'callable'], 'RedisArray::_target' => ['string', 'key'=>'string'], -'RedisArray::hscan' => ['array|false', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], -'RedisArray::info' => ['array>|false'], -'RedisArray::keys' => ['array>|false', 'pattern'=>'string'], -'RedisArray::mget' => ['list|false', 'keys'=>'string[]'], -'RedisArray::scan' => ['list|false', '&iterator'=>'?int', 'node'=>'string', 'pattern='=>'?string', 'count='=>'?int'], -'RedisArray::sscan' => ['list|false', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], -'RedisArray::zscan' => ['array|false', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'RedisArray::hscan' => ['__benevolent|false>', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'RedisArray::info' => ['__benevolent>|false>'], +'RedisArray::keys' => ['__benevolent>|false>', 'pattern'=>'string'], +'RedisArray::mget' => ['__benevolent|false>', 'keys'=>'string[]'], +'RedisArray::scan' => ['__benevolent|false>', '&iterator'=>'?int', 'node'=>'string', 'pattern='=>'?string', 'count='=>'?int'], +'RedisArray::sscan' => ['__benevolent|false>', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'RedisArray::zscan' => ['__benevolent|false>', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], 'RedisCluster::__construct' => ['void', 'name'=>'string|null', 'seeds='=>'string[]|null', 'timeout='=>'int|float', 'read_timeout='=>'int|float', 'persistent='=>'bool', 'auth='=>'mixed', 'context='=>'array|null'], 'RedisCluster::_prefix' => ['string', 'value'=>'mixed'], 'RedisCluster::_serialize' => ['mixed', 'value'=>'mixed'], diff --git a/tests/PHPStan/Analyser/nsrt/bug-9748.php b/tests/PHPStan/Analyser/nsrt/bug-9748.php index 6d2bfc1c0a9..3a061f4ff51 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9748.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9748.php @@ -6,7 +6,7 @@ function testKeys(\RedisArray $ra): void { $keys = $ra->keys('*'); - assertType('array>|false', $keys); + assertType('(array>|false)', $keys); if ($keys === false) { return; } @@ -23,7 +23,7 @@ function testKeys(\RedisArray $ra): void { function testInfo(\RedisArray $ra): void { $info = $ra->info(); - assertType('array>|false', $info); + assertType('(array>|false)', $info); if ($info === false) { return; } @@ -32,29 +32,29 @@ function testInfo(\RedisArray $ra): void { function testMget(\RedisArray $ra): void { $values = $ra->mget(['key1', 'key2']); - assertType('list|false', $values); + assertType('(list|false)', $values); } function testScan(\RedisArray $ra): void { $iterator = null; $result = $ra->scan($iterator, 'node1'); - assertType('list|false', $result); + assertType('(list|false)', $result); } function testHscan(\RedisArray $ra): void { $iterator = null; $result = $ra->hscan('myhash', $iterator); - assertType('array|false', $result); + assertType('(array|false)', $result); } function testSscan(\RedisArray $ra): void { $iterator = null; $result = $ra->sscan('myset', $iterator); - assertType('list|false', $result); + assertType('(list|false)', $result); } function testZscan(\RedisArray $ra): void { $iterator = null; $result = $ra->zscan('myzset', $iterator); - assertType('array|false', $result); + assertType('(array|false)', $result); }