Skip to content

Fix #14108: @phpstan-assert isn't working with Union - part 2#4916

Closed
phpstan-bot wants to merge 2 commits into2.1.xfrom
create-pull-request/patch-dvbc7r7
Closed

Fix #14108: @phpstan-assert isn't working with Union - part 2#4916
phpstan-bot wants to merge 2 commits into2.1.xfrom
create-pull-request/patch-dvbc7r7

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

@phpstan-bot phpstan-bot commented Feb 13, 2026

Summary

When calling a method with @phpstan-assert on a union type (e.g., Foo|Bar), the asserted types from different union members were being intersected instead of unioned. This caused the resulting type to be *NEVER* (e.g., string & int) instead of the correct union (int|string).

Changes

  • Added Assertions::unionWith() method in src/Reflection/Assertions.php that groups assertions by parameter identity (parameter name, if-condition, negated, equality) and unions their asserted types via TypeCombinator::union()
  • Changed UnionTypeMethodReflection::getAsserts() in src/Reflection/Type/UnionTypeMethodReflection.php to use unionWith() instead of intersectWith()
  • Added a private helper Assertions::getAssertKey() to compute the grouping key for assertions
  • Updated CLAUDE.md to document the distinction between union and intersection type assert handling

Root cause

PR #4900 added @phpstan-assert support for union types by using Assertions::intersectWith(), which simply concatenates all assert tags from all union members. When specifyTypesFromAsserts() processes these concatenated tags, it creates separate SpecifiedTypes for each assertion (e.g., $fooOrBar->getParam() is string and $fooOrBar->getParam() is int) and combines them via SpecifiedTypes::unionWith(). That method intersects sureTypes for the same expression, producing string & int = never.

The fix uses a new Assertions::unionWith() method that matches assertions targeting the same parameter and merges them by unioning their types before they reach specifyTypesFromAsserts(). This produces a single assertion like $this->getParam() is int|string, which is the correct semantics for union types (only one member's assertion applies at runtime).

This is distinct from intersection types, which correctly use intersectWith() because both assertions must hold simultaneously.

Test

Added regression test tests/PHPStan/Analyser/nsrt/bug-14108.php that verifies:

  • Foo|Bar where Foo has @phpstan-assert string $this->getParam() and Bar has @phpstan-assert int $this->getParam()
  • After calling narrowGetParam(), $fooOrBar->getParam() is correctly narrowed to int|string (not *NEVER*)

Fixes phpstan/phpstan#14108

- UnionTypeMethodReflection::getAsserts() was using intersectWith() which
  concatenates assert tags, causing specifyTypesFromAsserts to intersect
  the asserted types (string & int = never) instead of unioning them
- Added Assertions::unionWith() that groups assertions by parameter identity
  and unions their types via TypeCombinator::union()
- Changed UnionTypeMethodReflection to use unionWith() instead of intersectWith()
- Assertions without a matching counterpart in the other union member are dropped
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14108.php
* Combines assertions from union type members by unioning the asserted types
* for assertions that target the same parameter with the same condition.
*/
public function unionWith(Assertions $other): self
Copy link
Copy Markdown
Contributor

@staabm staabm Feb 13, 2026

Choose a reason for hiding this comment

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

I feel what is implented here is actual the intersectWith and the intersectWith we had before this PR is more a unionWith.. (so names are inverted - not sure we can fix this because of BC though)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have the same impression.

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.

can we fix it? or should we stay with it as is because BC?

Closes phpstan/phpstan#14107

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm self-assigned this Feb 13, 2026
@staabm
Copy link
Copy Markdown
Contributor

staabm commented Feb 13, 2026

will continue in a new PR in which I have commit permissions

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.

@phpstan-assert isn't working with Union - part 2

3 participants