Skip to content

Preserve TemplateArrayType across offset writes and traversal#5553

Merged
ondrejmirtes merged 2 commits into2.1.xfrom
template-array-recreate
Apr 27, 2026
Merged

Preserve TemplateArrayType across offset writes and traversal#5553
ondrejmirtes merged 2 commits into2.1.xfrom
template-array-recreate

Conversation

@ondrejmirtes
Copy link
Copy Markdown
Member

@ondrejmirtes ondrejmirtes commented Apr 27, 2026

Summary

ArrayType::setOffsetValueType, traverse, and traverseSimultaneously built the result via new self(...), which always produces a plain ArrayType even when called on a TemplateArrayType. The template wrapping was dropped — $arr['mykey'] = $value on a T of array parameter returned non-empty-array & hasOffsetValue('mykey', int), losing the T part (see bug-3931 and bug-10749).

  • Add a protected ArrayType::withTypes(Type, Type): self factory that subclasses override. Mirrors the existing recreate() pattern in ConstantArrayType / TemplateConstantArrayType and GenericObjectType / TemplateGenericObjectType. Different name to avoid confusion with ConstantArrayType::recreate.
  • Route the relevant new self(...) sites in ArrayType through $this->withTypes(...) and override on TemplateArrayType to rebuild the template wrapper around the new bound.
  • Limit the change to operations that don't break T's contract: setOffsetValueType adds/updates a key (the user can express the augmentation with @return T & array{...}); traverse and traverseSimultaneously are pure type rewrites. unsetOffset, setExistingOffsetValueType, and generalizeValues keep their plain new self(...) — they can break a more specific T's contract, and the existing return-type diagnostic (bug-6568) relies on the widening.
  • Fix IntersectionType::describeItself to preserve a TemplateArrayType's own describe ("T of array") instead of collapsing it into a generic non-empty-array<...> prefix; emit non-empty-array / list separately when the template carries the array refinement.
  • Update the bug-3931 fixture to assert the new (correct) inferred type.
  • Add regression test for #10749 in TypesAssignedToPropertiesRuleTest.

Closes phpstan/phpstan#10749

Test plan

  • make phpstan
  • vendor/bin/phpunit tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php --filter testBug3931
  • vendor/bin/phpunit tests/PHPStan/Analyser/NodeScopeResolverTest.php --filter bug-3931
  • vendor/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

ondrejmirtes and others added 2 commits April 27, 2026 11:52
`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>
@ondrejmirtes ondrejmirtes merged commit f15b7a2 into 2.1.x Apr 27, 2026
130 of 131 checks passed
@ondrejmirtes ondrejmirtes deleted the template-array-recreate branch April 27, 2026 10:26
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