Fix #14108: @phpstan-assert isn't working with Union - part 2#4916
Closed
phpstan-bot wants to merge 2 commits into2.1.xfrom
Closed
Fix #14108: @phpstan-assert isn't working with Union - part 2#4916phpstan-bot wants to merge 2 commits into2.1.xfrom
@phpstan-assert isn't working with Union - part 2#4916phpstan-bot wants to merge 2 commits into2.1.xfrom
Conversation
- 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
staabm
reviewed
Feb 13, 2026
| * 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 |
Contributor
There was a problem hiding this comment.
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)
Member
There was a problem hiding this comment.
I have the same impression.
Contributor
There was a problem hiding this comment.
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>
Contributor
|
will continue in a new PR in which I have commit permissions |
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
When calling a method with
@phpstan-asserton 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
Assertions::unionWith()method insrc/Reflection/Assertions.phpthat groups assertions by parameter identity (parameter name, if-condition, negated, equality) and unions their asserted types viaTypeCombinator::union()UnionTypeMethodReflection::getAsserts()insrc/Reflection/Type/UnionTypeMethodReflection.phpto useunionWith()instead ofintersectWith()Assertions::getAssertKey()to compute the grouping key for assertionsCLAUDE.mdto document the distinction between union and intersection type assert handlingRoot cause
PR #4900 added
@phpstan-assertsupport for union types by usingAssertions::intersectWith(), which simply concatenates all assert tags from all union members. WhenspecifyTypesFromAsserts()processes these concatenated tags, it creates separateSpecifiedTypesfor each assertion (e.g.,$fooOrBar->getParam() is stringand$fooOrBar->getParam() is int) and combines them viaSpecifiedTypes::unionWith(). That method intersects sureTypes for the same expression, producingstring & 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 reachspecifyTypesFromAsserts(). 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.phpthat verifies:Foo|BarwhereFoohas@phpstan-assert string $this->getParam()andBarhas@phpstan-assert int $this->getParam()narrowGetParam(),$fooOrBar->getParam()is correctly narrowed toint|string(not*NEVER*)Fixes phpstan/phpstan#14108