Skip to content

Do not normalize union types in tryRemove to preserve finite type combinations#5744

Open
phpstan-bot wants to merge 6 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-6m54aw0
Open

Do not normalize union types in tryRemove to preserve finite type combinations#5744
phpstan-bot wants to merge 6 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-6m54aw0

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

@phpstan-bot phpstan-bot commented May 23, 2026

Summary

  • Fix UnionType::tryRemove() to bypass TypeCombinator::union() normalization, which lossily collapses ConstantArrayType members 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] => ... }).
  • Fix TemplateTypeTrait::toArgument() to use TemplateTypeFactory::create() instead of new self(...), avoiding a TypeError when the traversed bound changes type class (e.g. UnionType → single type after traversal).
  • Update finite-types.php test assertion from array{bool, true} to the semantically equivalent array{false, true}|array{true, true}, reflecting the preserved individual shapes.

Test plan

  • New test testBug10128 with 3-case and 2-case enums: all 9 (and 4) pair combinations covered in match expressions with individual arms, grouped arms, and different grouping patterns
  • make tests — 12139 tests pass, 0 failures
  • make phpstan — no errors
  • make cs-fix — clean

Closes phpstan/phpstan#10128
Closes phpstan/phpstan#11453

Comment thread src/Type/UnionType.php Outdated
Comment thread src/Type/UnionType.php Outdated
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add non regression test for phpstan/phpstan#11453

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Added a non-regression test for phpstan/phpstan#11453 — match expressions with arrays of union types (array{1|2|3, 'a'|'b'|'c'}) are correctly recognized as exhaustive. All 12140 tests pass and make phpstan reports no errors.

@staabm staabm force-pushed the create-pull-request/patch-6m54aw0 branch 3 times, most recently from e646f63 to 38e155c Compare May 25, 2026 10:56
VincentLanglet and others added 4 commits May 25, 2026 21:39
…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>
@staabm staabm force-pushed the create-pull-request/patch-6m54aw0 branch from 38e155c to 0a839cc Compare May 25, 2026 19:39
@staabm staabm requested a review from VincentLanglet May 26, 2026 08:41
Comment thread src/Type/UnionType.php

public function tryRemove(Type $typeToRemove): ?Type
{
return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

@VincentLanglet
Copy link
Copy Markdown
Contributor

WDYT about the phpbench failure @staabm ; I really have trouble understanding the result...

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.

3 participants