Skip to content

Fix #11441: @phpstan-assert isn't working with Union#4900

Merged
ondrejmirtes merged 2 commits into2.1.xfrom
create-pull-request/patch-m9p8ca3
Feb 12, 2026
Merged

Fix #11441: @phpstan-assert isn't working with Union#4900
ondrejmirtes merged 2 commits into2.1.xfrom
create-pull-request/patch-m9p8ca3

Conversation

@phpstan-bot
Copy link
Collaborator

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

Summary

When calling a method with @phpstan-assert on a union type (e.g. Foo|Bar), the assertion was not applied, so type narrowing did not occur. For example, @phpstan-assert !null $this->getParam() on both Foo::checkNotNull() and Bar::checkNotNull() should narrow $fooOrBar->getParam() from int|string|null to int|string after the call, but it remained int|string|null.

Changes

  • Modified src/Reflection/Type/UnionTypeMethodReflection.php: Changed getAsserts() from returning Assertions::createEmpty() to iterating over all member methods and combining their assertions via intersectWith(), matching the pattern already used by IntersectionTypeMethodReflection::getAsserts()
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-11441.php
  • Updated CLAUDE.md with guidance about maintaining parity between UnionTypeMethodReflection and IntersectionTypeMethodReflection

Root cause

UnionTypeMethodReflection::getAsserts() was hardcoded to return Assertions::createEmpty(). When TypeSpecifier processes a method call and checks for @phpstan-assert annotations, it calls $methodReflection->getAsserts(). For union types, the method reflection is a UnionTypeMethodReflection, which returned no assertions, so the type narrowing was never applied. The fix combines assertions from all union member methods, which is the same approach used by IntersectionTypeMethodReflection.

Test

The regression test in tests/PHPStan/Analyser/nsrt/bug-11441.php reproduces the exact scenario from the issue: two classes (Foo and Bar) each with a checkNotNull() method annotated with @phpstan-assert !null $this->getParam(). When called on a Foo|Bar union, the test verifies that getParam() is narrowed from int|string|null to int|string.

Fixes phpstan/phpstan#11441

Closes phpstan/phpstan#13358

ondrejmirtes and others added 2 commits February 12, 2026 15:11
- UnionTypeMethodReflection::getAsserts() returned empty assertions,
  causing @phpstan-assert tags to be ignored when calling methods on
  union types like Foo|Bar
- Changed getAsserts() to combine assertions from all union member
  methods, mirroring IntersectionTypeMethodReflection's implementation
- New regression test in tests/PHPStan/Analyser/nsrt/bug-11441.php
Closes phpstan/phpstan#13358

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ondrejmirtes ondrejmirtes merged commit 0da93b4 into 2.1.x Feb 12, 2026
629 of 641 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-m9p8ca3 branch February 12, 2026 15:48
$assertions = Assertions::createEmpty();

foreach ($this->methods as $method) {
$assertions = $assertions->intersectWith($method->getAsserts());
Copy link
Contributor

Choose a reason for hiding this comment

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

looking at this implementation and the tests, I think we miss test coverage for the case when asserts do not intersect.

maybe thats someting we could enhance the prompt about so more test cases are generated

Copy link
Contributor

Choose a reason for hiding this comment

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

alternatively thats something which could be detected by infection and claude could run mutation tests (which could make the whole process slow though)

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.

Narrowing type using @phpstan-assert-if-true doesn't work with $this on abstract class @phpstan-assert isn't working with Union

3 participants