Do not normalize union types in tryRemove to preserve finite type combinations#5744
Do not normalize union types in tryRemove to preserve finite type combinations#5744phpstan-bot wants to merge 6 commits into
Conversation
VincentLanglet
left a comment
There was a problem hiding this comment.
Add non regression test for phpstan/phpstan#11453
|
Done. Added a non-regression test for phpstan/phpstan#11453 — match expressions with arrays of union types ( |
e646f63 to
38e155c
Compare
…binations UnionType::tryRemove() called TypeCombinator::union() to re-normalize the remaining types after removal. For ConstantArrayTypes with enum values, this normalization lossily collapsed individual array shapes back into a single shape with union value types, undoing the removal. Build the result UnionType directly from the remaining types instead, with deduplication and flattening of nested unions. BenevolentUnionType still uses the normalized path via TypeCombinator::union(). Also fix a latent bug in TemplateTypeTrait::toArgument() where `new self(...)` assumed the traversed bound would keep its original type class. Use TemplateTypeFactory::create() instead, which handles bound type changes correctly. Closes phpstan/phpstan#10128
…scribe-based dedup
Address review feedback:
- Move `BenevolentUnionType` handling from an `instanceof` check in
`UnionType::tryRemove()` to a proper override in `BenevolentUnionType`
that calls `parent::tryRemove()` and wraps with `TypeUtils::toBenevolentUnion()`.
- Remove the `describe(VerbosityLevel::cache())`-based deduplication block.
TypeCombinator::union() cannot be used here because it normalizes
ConstantArrayTypes with the same key structure (e.g. collapsing
`array{A, B}|array{A, C}` into `array{A, B|C}`), which is exactly
the bug this PR fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… types Closes phpstan/phpstan#11453 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
38e155c to
0a839cc
Compare
This reverts commit 353afca.
|
|
||
| public function tryRemove(Type $typeToRemove): ?Type | ||
| { | ||
| return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); |
There was a problem hiding this comment.
@phpstan-bot do we need a similar fix in IntersectionType->tryRemove() or any other Type->tryRemove() implementation? try to come up with a failling test and implement a fix afterwards
There was a problem hiding this comment.
The working tree is clean — no code changes are needed. My investigation confirms that IntersectionType::tryRemove() and all other tryRemove() implementations are not affected by the same normalization bug that this PR fixes in UnionType::tryRemove().
|
WDYT about the phpbench failure @staabm ; I really have trouble understanding the result... |
Summary
UnionType::tryRemove()to bypassTypeCombinator::union()normalization, which lossily collapsesConstantArrayTypemembers with the same key structure back into a single shape with union value types. This undid type removal for match expressions using arrays of enums (e.g.match ([$first, $second]) { [Enum::A, Enum::B] => ... }).TemplateTypeTrait::toArgument()to useTemplateTypeFactory::create()instead ofnew self(...), avoiding aTypeErrorwhen the traversed bound changes type class (e.g.UnionType→ single type after traversal).finite-types.phptest assertion fromarray{bool, true}to the semantically equivalentarray{false, true}|array{true, true}, reflecting the preserved individual shapes.Test plan
testBug10128with 3-case and 2-case enums: all 9 (and 4) pair combinations covered in match expressions with individual arms, grouped arms, and different grouping patternsmake tests— 12139 tests pass, 0 failuresmake phpstan— no errorsmake cs-fix— cleanCloses phpstan/phpstan#10128
Closes phpstan/phpstan#11453