Fix phpstan/phpstan#10231: Using generics to return array with value-of of an inner passed array results in array<int|string, list<*ERROR*>>#5358
Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Conversation
…o *ERROR* - Skip toArrayKey() normalization in TypeNodeResolver when array key type is a LateResolvableType, preventing premature resolution and loss of type information - Resolve late-resolvable types after resolveToBounds in TypehintHelper decideType check, so the PHPDoc type compatibility check works correctly - Handle value-of<non-iterable> gracefully in ValueOfType::getResult() when the inner type is a LateResolvableType that resolves to a scalar - New regression test in tests/PHPStan/Analyser/nsrt/bug-10231.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When using
value-of<TArray[TGroupColumnName]>as the key type in a generic return type, PHPStan was producingarray<int|string, list<*ERROR*>>instead of the expectedarray<string, list<int>>. This fixes the resolution ofvalue-ofwhen it wraps anOffsetAccessTypethat resolves to a non-iterable type.Changes
src/PhpDoc/TypeNodeResolver.php: Skip thetoArrayKey()normalization when the array key type is aLateResolvableType(e.g.,ValueOfType,KeyOfType). This prevents premature resolution that cached incorrect results and destroyed the late-resolvable type wrapper.src/Type/TypehintHelper.php: IndecideType(), resolve late-resolvable types afterresolveToBounds()before checking PHPDoc type compatibility with the native type. Without this, the unresolvedValueOfTypewrapper caused the compatibility check to fail, rejecting the PHPDoc type entirely.src/Type/ValueOfType.php: IngetResult(), whengetIterableValueType()returnsErrorTypeand the inner type is aLateResolvableType, fall back to the resolved inner type instead of propagating the error. This handles the case wherevalue-ofwraps anOffsetAccessTypethat resolves to a scalar type (e.g.,string), wheregetIterableValueType()would incorrectly returnErrorType.Root cause
Three interacting issues:
Premature resolution:
TypeNodeResolvercalledtoArrayKey()on the key type during PHPDoc parsing, which triggeredresolve()on theValueOfTypebefore template types were available. This cached an incorrectMixedTyperesult and replaced theValueOfTypewith a normalizedint|stringkey type, losing the late-resolvable type information entirely.Compatibility check failure: When the
ValueOfTypewas preserved as the key type,TypehintHelper::decideType()usedresolveToBounds()to check if the PHPDoc type was compatible with the nativearraytype. However,resolveToBounds()only replaces template types with bounds — it doesn't resolveLateResolvableTypewrappers. The unresolvedValueOfTypein the key position causedgetIterableKeyType()to return the wrapper instead of a concrete type, making the compatibility check fail and falling back to the nativearrayreturn type.ErrorType from value-of on scalars: Even when templates were correctly resolved,
value-of<OffsetAccessType(array{...}, key)>would resolve the offset access to a scalar type (e.g.,string), then callgetIterableValueType()on that scalar, producingErrorTypesince scalars aren't iterable.Test
Added
tests/PHPStan/Analyser/nsrt/bug-10231.php— an NSRT test that verifiesvalue-of<TArray[TGroupColumnName]>correctly resolves to the value type at the given key when template types are substituted with concrete types. The test asserts that callinggroupByColumn()witharray<array{event_id: string, id: int}>returnsarray<string, list<int>>.Fixes phpstan/phpstan#10231