Describe Random\Randomizer array, bytes and integer methods with dedicated dynamic return type extensions - #6469
Conversation
|
@staabm I have mixed feeling about this PR and I would like your opinion.
Which solution(s) do you prefer ? |
|
I think this PR implements a lot of stuff which is not necessary to solve phpstan/phpstan#15256 atm I see no reason why we should built such a extensive extensions for super precise result-types. |
VincentLanglet
left a comment
There was a problem hiding this comment.
Rather than introducing lot of helper to deduplicate some utility method, we could write the Randomizer extension by relying on similar method.
For instance
- pickArrayKeys($a, $b) could just return the return value of
array_rand($a, $b) - shuffleBytes same idea with
str_shuffle - getInt, same idea with
random_int
I consider splitting this PR, but still I think some extensions are usefull like the one for And, considering lot of Randomizer method are just identical to native php functions which already have dynamicExtensions (and Randomizer methods are sometimes recommend like in https://www.php.net/manual/en/function.array-rand.php) ; It could be great to not lose type inference when switching them. I'll try to improve/simplify the pr |
|
Done — pushed to What changedI took the review literally: the three new helper services are gone, and
One thing I dropped rather than rewired, which is a deliberate scope reduction — flagging it in case you'd rather have it back. Net: −3 classes, −87 lines against the previous head. Verification
One note on the diff: |
VincentLanglet
left a comment
There was a problem hiding this comment.
I didn't meant to inject the FunctionReturnTypeExtension as a dependency ; I meant to use something like $scope->getType(new FuncCall('array_shuffle')) ; would it be possible ?
And for Strrev and StrShuffle, it's ok to duplicate the logic ; so let strtrev untouched.
|
Done — pushed to What changed
The
The three helper methods that were made public for the injection approach are private again:
Verification
|
00de151 to
2432be0
Compare
0e7a60d to
0b2449d
Compare
|
I'm happy with the current state of the PR, I'm not sure it's needed to split it for review. Lot of tests are failing, but I think it's already the case on 2.2.x, isn't it @staabm. |
staabm
left a comment
There was a problem hiding this comment.
I think this looks good.
I would not merge this atm until the CI pipeline is green on 2.2.x again
…dicated dynamic return type extensions * Add `RandomizerMethodReturnTypeExtension` covering `shuffleArray()` (via `Type::shuffleArray()`, so it is a list and keeps non-emptiness and the value type), `pickArrayKeys()` (a non-empty list of the array's own key type), `shuffleBytes()`, `getBytesFromString()` and `getInt()`. * Add `StringBytesReturnTypeHelper`, describing strings built from the bytes of another string. `StrrevFunctionReturnTypeExtension` now delegates to it, and the new `StrShuffleFunctionReturnTypeExtension` uses it so `str_shuffle()` keeps non-falsy-ness, lowercase-ness and uppercase-ness instead of only non-emptiness. * Add `RandomArrayKeysReturnTypeHelper` shared by `pickArrayKeys()` and `array_rand()`; `array_rand()` now returns a `non-empty-list` of the array's own key type instead of `array<int, int|string>`. * Extract `RandomIntRangeHelper` out of `RandomIntFunctionReturnTypeExtension` and reuse it for `Randomizer::getInt()`. * Signature map: `Randomizer::getBytes()` returns `non-empty-string` and takes a `positive-int`, `Randomizer::nextInt()` returns `int<0, max>`, `Randomizer::getBytesFromString()` returns `non-empty-string` and takes a `non-empty-string` plus a `positive-int`, and the four final `Random\Engine\*::generate()` implementations return `non-empty-string`. * Drop the now-redundant conditional `@return` of `str_shuffle()` from `stubs/core.stub`.
Preserving the lowercase/uppercase-ness of the source alphabet was the only
thing the extension added on top of the `non-empty-string` the function map
already describes, and it needed a second, subtly different flavour of the
byte-level string helper: unlike a reordering, a selection of bytes does not
keep non-falsy-ness ('10' can yield '0').
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hey mirror Each `Randomizer` method handled here has a global-function counterpart PHPStan already describes, so instead of extracting the shared logic into standalone helper services, ask those extensions directly: * `pickArrayKeys()` asks `ArrayRandFunctionReturnTypeExtension` for the keys picked out of the array, always as a list (`$num` is validated to be at least 1, so unlike `array_rand()` it never returns a bare key), * `shuffleBytes()` asks `StrShuffleFunctionReturnTypeExtension` for the string made of the same bytes in another order - which is also what `strrev()` does outside of the constant-string case, so that extension asks for it too, * `getInt()` asks `RandomIntFunctionReturnTypeExtension` for the range, * `shuffleArray()` keeps using the `Type::shuffleArray()` primitive `shuffle()` is built on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…irror Instead of injecting the mirrored functions' dynamic return type extensions and calling into them, build the equivalent FuncCall and hand it to Scope::getType(). The helper methods that were made public for the previous approach go back to being private. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
str_shuffle() and strrev() both reorder the bytes of their argument, but sharing that through an injected extension is not worth the coupling - the duplication stays local to each extension instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Passing a hardcoded 2 as array_rand()'s $num sidestepped the bare-key
branch, but it also pinned pickArrayKeys() to whatever array_rand()
happens to infer for "more than one key" rather than to the call's own
arguments - so any future refinement of array_rand() (array shapes, for
instance) would be wrong here.
Forward $num verbatim and cast the result to array instead. $num is
validated to be between 1 and the size of the array, so a successful
call always returns an array of keys, which is exactly what the cast
expresses: picking a single key now gives array{'a'}|array{'b'} where
array_rand() gives 'a'|'b'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Random\Randomizer is a PHP 8.2 class, so the assertions in bug-15256.php only hold on 8.2+. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The declaration is identical to the one phpstorm-stubs already provides, so with the conditional @return gone it adds nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers uppercase-string&non-empty-string, where more than one accessory type has to survive the shuffle at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were added in PHP 8.3 alongside getBytesFromString(), but unlike it they have nothing more precise to say than the float they declare - PHPStan has no float range types - so they get no signature map entry, only assertions pinning that down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e namespace Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing keys Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7457f31 to
27f4408
Compare
|
I checked and didn't see extra error coming from this PR. |
Summary
Random\Randomizer::shuffleArray()was described as plainarrayandpickArrayKeys()asnon-empty-array<int|string>, so generic wrappers around them could not returnlist<T>/non-empty-list<TKey>. Both methods actually return a list —shuffleArray()the values of the given array,pickArrayKeys()some of its keys — and both can keep the value/key type of the argument.The fix describes those two methods precisely, and sweeps the rest of the
Randomfamily (and the global functions that mirror it) for the same kind of imprecision.Changes
New services in
src/Type/Php/:RandomizerMethodReturnTypeExtension—Random\Randomizer::shuffleArray(),pickArrayKeys(),shuffleBytes(),getBytesFromString()andgetInt().RandomArrayKeysReturnTypeHelper— the type of keys picked out of an array at random; shared bypickArrayKeys()andarray_rand().StringBytesReturnTypeHelper— the type of a string built out of the bytes of another string, either reordered (strrev(),str_shuffle(),shuffleBytes()) or selected (getBytesFromString()).StrShuffleFunctionReturnTypeExtension—str_shuffle().RandomIntRangeHelper— the integer range of a bounded random number generator, extracted fromRandomIntFunctionReturnTypeExtensionand reused byRandomizer::getInt().Changed:
src/Type/Php/ArrayRandFunctionReturnTypeExtension.php— returns the array's own key type (so constant arrays give'a'|'b'instead ofstring) and anon-empty-listinstead ofarray<int, ...>when more than one key is picked.src/Type/Php/StrrevFunctionReturnTypeExtension.php— accessory handling moved intoStringBytesReturnTypeHelper.src/Type/Php/RandomIntFunctionReturnTypeExtension.php— range computation moved intoRandomIntRangeHelper.resources/functionMap_php82delta.php—Randomizer::getBytes()→non-empty-stringwith apositive-intlength,Randomizer::nextInt()→int<0, max>,Random\Engine\Mt19937|PcgOneseq128XslRr64|Secure|Xoshiro256StarStar::generate()→non-empty-string, andpickArrayKeys()'s fallback return updated tonon-empty-list<int|string>.resources/functionMap_php83delta.php—Randomizer::getBytesFromString()→non-empty-string, with anon-empty-stringsubject and apositive-intlength (both areValueErrors otherwise).stubs/core.stub— droppedstr_shuffle()'s conditional@return, now subsumed by the extension.Analogous cases probed and left alone:
shuffle(),random_int(),rand(),mt_rand(),random_bytes()andstrrev()were already precise.Randomizer::getFloat()/nextFloat()cannot be improved — PHPStan has no float ranges.Random\Engine::generate()was deliberately not narrowed on the interface: the four built-in implementations arefinal, but narrowing the interface would make every user-land engine declaringstringreport a covariance error.openssl_random_pseudo_bytes()has the same "random bytes of a positive length" shape but its signature still carries a PHP 7-erastring|false; changing that is a separate decision.Root cause
Two patterns, each appearing in more than one place:
Random\Randomizer::pickArrayKeys()only had a signature-map entry (non-empty-array<int|string>), andarray_rand()'s extension collapsed the key type toint,stringorint|stringand built a plainarray<int, ...>. Both actually return the keys re-indexed from zero, so they are non-empty lists of the array's own key type. Centralised inRandomArrayKeysReturnTypeHelper.strrev()preserved non-empty/non-falsy/lowercase/uppercase, butstr_shuffle()only had a conditional@returnfor non-emptiness andRandomizer::shuffleBytes()/getBytesFromString()had nothing at all, even though all of them build their result from the bytes of the input. Centralised inStringBytesReturnTypeHelper.shuffleArray()is fixed by routing it through the existingType::shuffleArray(), the same primitiveshuffle()already uses, which is what makes the result a list and keeps non-emptiness and the value type (including template types, which is what the reported snippet needed).Test
tests/PHPStan/Analyser/nsrt/bug-15256.php— the reporter's playground snippet (@template T/@param T[]aroundshuffleArray(),@template TKey of array-keyaroundpickArrayKeys()) plus coverage for non-empty arrays, lists, mixed-key arrays, constant arrays, all the accessory string flavours throughshuffleBytes(),getBytes(),getInt(),nextInt()and the four engines'generate().tests/PHPStan/Analyser/nsrt/bug-15256-83.php—getBytesFromString()(PHP 8.3+).tests/PHPStan/Analyser/nsrt/str-shuffle.php— extended to the same matrix asstrrev.php; the previously assertednon-empty-stringresults are nownon-falsy-string/lowercase-string/uppercase-string.tests/PHPStan/Analyser/nsrt/array-functions.php,tests/PHPStan/Rules/Functions/data/bug-9803.phpand thebug-12981expectations inInvalidKeyInArrayDimFetchRuleTest/NonexistentOffsetInArrayDimFetchRuleTest— updated to the more precisearray_rand()types.All three new/extended test files were verified to fail before the fix.
Fixes phpstan/phpstan#15256