Preserve TemplateArrayType across offset writes and traversal#5553
Merged
ondrejmirtes merged 2 commits into2.1.xfrom Apr 27, 2026
Merged
Preserve TemplateArrayType across offset writes and traversal#5553ondrejmirtes merged 2 commits into2.1.xfrom
ondrejmirtes merged 2 commits into2.1.xfrom
Conversation
`ArrayType::setOffsetValueType`, `traverse`, and `traverseSimultaneously`
constructed the result via `new self(...)`, which always created a plain
`ArrayType` even when called on a `TemplateArrayType`. The template
wrapping was dropped — `$arr['mykey'] = $value` on a `T of array`
returned `non-empty-array & hasOffsetValue('mykey', int)`, losing T.
Add a protected `withTypes(Type, Type): self` factory that subclasses
override (mirrors the `recreate()` pattern in `ConstantArrayType` /
`TemplateConstantArrayType` and `GenericObjectType` /
`TemplateGenericObjectType`) and route the affected `new self(...)`
sites through it. `TemplateArrayType::withTypes` rebuilds the template
wrapper around the new bound, so T is preserved through these
operations. `unsetOffset`, `setExistingOffsetValueType`, and
`generalizeValues` keep their plain `new self(...)` — those operations
can break a more specific `T`'s contract, and the existing return-type
diagnostic (see bug-6568) relies on the widening.
`IntersectionType::describeItself` was also collapsing
`TemplateArrayType` into a generic `non-empty-array<...>` prefix.
Special-case it so the template's own describe ("T of array") survives,
and emit `non-empty-array` / `list` separately when the template carries
the array refinement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes phpstan/phpstan#10749 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
ArrayType::setOffsetValueType,traverse, andtraverseSimultaneouslybuilt the result vianew self(...), which always produces a plainArrayTypeeven when called on aTemplateArrayType. The template wrapping was dropped —$arr['mykey'] = $valueon aT of arrayparameter returnednon-empty-array & hasOffsetValue('mykey', int), losing the T part (see bug-3931 and bug-10749).ArrayType::withTypes(Type, Type): selffactory that subclasses override. Mirrors the existingrecreate()pattern inConstantArrayType/TemplateConstantArrayTypeandGenericObjectType/TemplateGenericObjectType. Different name to avoid confusion withConstantArrayType::recreate.new self(...)sites inArrayTypethrough$this->withTypes(...)and override onTemplateArrayTypeto rebuild the template wrapper around the new bound.setOffsetValueTypeadds/updates a key (the user can express the augmentation with@return T & array{...});traverseandtraverseSimultaneouslyare pure type rewrites.unsetOffset,setExistingOffsetValueType, andgeneralizeValueskeep their plainnew self(...)— they can break a more specific T's contract, and the existing return-type diagnostic (bug-6568) relies on the widening.IntersectionType::describeItselfto preserve aTemplateArrayType's own describe ("T of array") instead of collapsing it into a genericnon-empty-array<...>prefix; emitnon-empty-array/listseparately when the template carries the array refinement.TypesAssignedToPropertiesRuleTest.Closes phpstan/phpstan#10749
Test plan
make phpstanvendor/bin/phpunit tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php --filter testBug3931vendor/bin/phpunit tests/PHPStan/Analyser/NodeScopeResolverTest.php --filter bug-3931vendor/bin/phpunit tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php --filter testBug6568(still detects the contract-break warning)vendor/bin/phpunit tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php --filter testBug10749(fails when the fix is reverted)🤖 Generated with Claude Code