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()); +} 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); + } +};