Skip to content

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
phpstan-bot:create-pull-request/patch-y2goimp
Open

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
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-y2goimp

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When using value-of<TArray[TGroupColumnName]> as the key type in a generic return type, PHPStan was producing array<int|string, list<*ERROR*>> instead of the expected array<string, list<int>>. This fixes the resolution of value-of when it wraps an OffsetAccessType that resolves to a non-iterable type.

Changes

  • src/PhpDoc/TypeNodeResolver.php: Skip the toArrayKey() normalization when the array key type is a LateResolvableType (e.g., ValueOfType, KeyOfType). This prevents premature resolution that cached incorrect results and destroyed the late-resolvable type wrapper.
  • src/Type/TypehintHelper.php: In decideType(), resolve late-resolvable types after resolveToBounds() before checking PHPDoc type compatibility with the native type. Without this, the unresolved ValueOfType wrapper caused the compatibility check to fail, rejecting the PHPDoc type entirely.
  • src/Type/ValueOfType.php: In getResult(), when getIterableValueType() returns ErrorType and the inner type is a LateResolvableType, fall back to the resolved inner type instead of propagating the error. This handles the case where value-of wraps an OffsetAccessType that resolves to a scalar type (e.g., string), where getIterableValueType() would incorrectly return ErrorType.

Root cause

Three interacting issues:

  1. Premature resolution: TypeNodeResolver called toArrayKey() on the key type during PHPDoc parsing, which triggered resolve() on the ValueOfType before template types were available. This cached an incorrect MixedType result and replaced the ValueOfType with a normalized int|string key type, losing the late-resolvable type information entirely.

  2. Compatibility check failure: When the ValueOfType was preserved as the key type, TypehintHelper::decideType() used resolveToBounds() to check if the PHPDoc type was compatible with the native array type. However, resolveToBounds() only replaces template types with bounds — it doesn't resolve LateResolvableType wrappers. The unresolved ValueOfType in the key position caused getIterableKeyType() to return the wrapper instead of a concrete type, making the compatibility check fail and falling back to the native array return type.

  3. 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 call getIterableValueType() on that scalar, producing ErrorType since scalars aren't iterable.

Test

Added tests/PHPStan/Analyser/nsrt/bug-10231.php — an NSRT test that verifies value-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 calling groupByColumn() with array<array{event_id: string, id: int}> returns array<string, list<int>>.

Fixes phpstan/phpstan#10231

…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
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