From 383c931a086de5687f7b7d61cb7c3ea4984b76bf Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:11:31 +0000 Subject: [PATCH 1/2] Fix @phpstan-assert not working with union types - 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 --- CLAUDE.md | 4 ++ .../Type/UnionTypeMethodReflection.php | 8 ++- tests/PHPStan/Analyser/nsrt/bug-11441.php | 58 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11441.php diff --git a/CLAUDE.md b/CLAUDE.md index f48abb644d..6503317b71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -373,6 +373,10 @@ When adding or editing PHPDoc comments in this codebase, follow these guidelines - Use imperative voice without "Returns the..." preambles when a brief note suffices. Prefer `/** Replaces unresolved TemplateTypes with their bounds. */` over a multi-line block. - Preserve `@api` and type tags on their own lines, with no redundant description alongside them. +### UnionTypeMethodReflection and IntersectionTypeMethodReflection parity + +When methods are called on union types (`Foo|Bar`), the resolved method reflection is a `UnionTypeMethodReflection` that wraps the individual method reflections. Similarly, `IntersectionTypeMethodReflection` handles intersection types. These two classes must maintain feature parity for things like `getAsserts()`, `getSelfOutType()`, etc. When one class correctly combines member data (e.g. `IntersectionTypeMethodReflection::getAsserts()` iterating over methods and calling `intersectWith()`), the other should do the same rather than returning empty/null. The `Assertions::intersectWith()` method merges assertion tag lists from multiple sources. + ## Important dependencies - `nikic/php-parser` ^5.7.0 - PHP AST parsing diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index f61f27cd5a..135fcfc39d 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -178,7 +178,13 @@ public function getDocComment(): ?string public function getAsserts(): Assertions { - return Assertions::createEmpty(); + $assertions = Assertions::createEmpty(); + + foreach ($this->methods as $method) { + $assertions = $assertions->intersectWith($method->getAsserts()); + } + + return $assertions; } public function acceptsNamedArguments(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/nsrt/bug-11441.php b/tests/PHPStan/Analyser/nsrt/bug-11441.php new file mode 100644 index 0000000000..a6a9bd8e48 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11441.php @@ -0,0 +1,58 @@ += 8.0 + +namespace Bug11441; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function __construct(private ?string $param) + { + } + + public function getParam(): ?string + { + return $this->param; + } + + /** + * @phpstan-assert !null $this->getParam() + */ + public function checkNotNull(): void + { + if ($this->getParam() === null) { + throw new \Exception(); + } + } +} + +class Bar +{ + public function __construct(private ?int $param) + { + } + + public function getParam(): ?int + { + return $this->param; + } + + /** + * @phpstan-assert !null $this->getParam() + */ + public function checkNotNull(): void + { + if ($this->getParam() === null) { + throw new \Exception(); + } + } +} + +function test(Foo|Bar $fooOrBar): void +{ + assertType('int|string|null', $fooOrBar->getParam()); + + $fooOrBar->checkNotNull(); + + assertType('int|string', $fooOrBar->getParam()); +} From f6e94eaccf45dfb99f59902e9ba04d280e676d1f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 12 Feb 2026 15:19:56 +0000 Subject: [PATCH 2/2] Add regression test for #13358 Closes https://github.com/phpstan/phpstan/issues/13358 Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-13358.php | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13358.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13358.php b/tests/PHPStan/Analyser/nsrt/bug-13358.php new file mode 100644 index 0000000000..43cdeeca2f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13358.php @@ -0,0 +1,45 @@ +isSystem()) { + assertType('Bug13358\SystemActor', $actor); + } else { + assertType('Bug13358\AnonymousVisitorActor', $actor); + } +};