Skip to content

Return the picked keys from array_rand() - #6223

Open
zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:fix/array-rand-return-type
Open

Return the picked keys from array_rand()#6223
zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:fix/array-rand-return-type

Conversation

@zonuexe

@zonuexe zonuexe commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Builds on #6222. Its first commit is cherry-picked here so this branch analyses on its own; I will rebase it away once it lands.

Problem

array_rand() threw away what it knew about the keys. It collapsed the key type into int, string or int|string, and returned array<int, key> whenever $num was more than one:

/** @param array{a: 1, b: 2, c: 3} $shape */
function f(array $shape): void {
    array_rand($shape);      // string
    array_rand($shape, 2);   // array<int, string>
}

Change

Hand back the key type itself, and build a tuple when $num is a known constant:

call before after
array_rand($shape) string 'a'|'b'|'c'
array_rand($list) int int<0, max>
array_rand($shape, 2) array<int, string> array{'a'|'b'|'c', 'a'|'b'|'c'}
array_rand($shape, $atLeastTwo) array<int, string> non-empty-list<'a'|'b'|'c'>
array_rand($shape, $positive) array<int, string>|string 'a'|'b'|'c'|non-empty-list<'a'|'b'|'c'>

An array comes back only when $num is at least 2, since a single pick returns the key on its own. That makes the list non-empty in every branch that produces one.

KEY_COUNT_LIMIT caps the tuple at 100 elements. Past that ConstantArrayTypeBuilder would degrade the shape anyway.

The keys run through castReadKeyType() from #6222, so array<string, X> gives (int|string) rather than a certain string.

array_rand([]) returns never now

array_rand([]) throws a ValueError on PHP 8, so never is the honest type. It also makes everything after the call unreachable, which cost the second call in data/array_rand.php its diagnostics. The two calls sit in separate functions now.

Tests

nsrt/array-rand.php is new. nsrt/array-functions.php and data/bug-9803.php gain precision, and two rule tests carry the new types in their messages.

@zonuexe
zonuexe force-pushed the fix/array-rand-return-type branch 2 times, most recently from 6d26322 to a6c2c68 Compare August 13, 2026 20:59
zonuexe and others added 2 commits August 14, 2026 13:08
PHP casts a decimal-integer string array key ("123") to int, so
`array_key_first([$string => null])` is not necessarily a string. PHPStan
inferred `string` for it and reported `is_int()` on the result as always false.

UnsafeArrayStringKeyCastingTraverser already modelled that cast, but only for
the key type an array *has* — a type that also decides how the array describes
itself and what it accepts, which is why only
`reportUnsafeArrayStringKeyCasting: detect` widens it. Widening it with the
toggle off turns `array<string, X>` into `array<X>` everywhere.

castReadKeyType() is the second entry point, for a key that leaves the array as
a value of its own. With the toggle off it widens `string` to the benevolent
`(int|string)`, so neither branch reports anything; `detect` and `prevent` keep
the types they have today. unionWithReadKeyType() adds the `null` an empty array
gives back without losing the benevolence on the way.

The remaining accessors follow in the next commit.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
array_rand() collapsed the array's key type into int, string or int|string, and
returned `array<int, key>` whenever $num was more than one. Hand back the key
type itself, and build a tuple when $num is a known constant:

    array_rand(['a' => 1, 'b' => 2])   'a'|'b'                 (was string)
    array_rand($list)                  int<0, max>             (was int)
    array_rand($shape, 2)              array{key, key}         (was array<int, key>)
    array_rand($shape, $atLeastTwo)    non-empty-list<key>

An array comes back only when $num is at least 2, since a single pick returns
the key on its own, so the list is non-empty in every branch that produces one.
KEY_COUNT_LIMIT caps the tuple at 100 elements, past which
ConstantArrayTypeBuilder would degrade the shape anyway.

The keys run through castReadKeyType(), so `array<string, X>` gives
`(int|string)` rather than a certain `string`.

array_rand([]) returns never now, which is what PHP 8 does (ValueError). That
also makes everything after the call unreachable, so the two calls in
data/array_rand.php moved into separate functions to keep their diagnostics.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@zonuexe
zonuexe force-pushed the fix/array-rand-return-type branch from a6c2c68 to 7601b6a Compare August 14, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant