Skip to content

Commit 1bd832f

Browse files
committed
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
1 parent e30d1e5 commit 1bd832f

File tree

3 files changed

+69
-1
lines changed

3 files changed

+69
-1
lines changed

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ When adding or editing PHPDoc comments in this codebase, follow these guidelines
389389
- Use imperative voice without "Returns the..." preambles when a brief note suffices. Prefer `/** Replaces unresolved TemplateTypes with their bounds. */` over a multi-line block.
390390
- Preserve `@api` and type tags on their own lines, with no redundant description alongside them.
391391

392+
### UnionTypeMethodReflection and IntersectionTypeMethodReflection parity
393+
394+
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.
395+
392396
## Important dependencies
393397

394398
- `nikic/php-parser` ^5.7.0 - PHP AST parsing

src/Reflection/Type/UnionTypeMethodReflection.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,13 @@ public function getDocComment(): ?string
178178

179179
public function getAsserts(): Assertions
180180
{
181-
return Assertions::createEmpty();
181+
$assertions = Assertions::createEmpty();
182+
183+
foreach ($this->methods as $method) {
184+
$assertions = $assertions->intersectWith($method->getAsserts());
185+
}
186+
187+
return $assertions;
182188
}
183189

184190
public function acceptsNamedArguments(): TrinaryLogic
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug11441;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
public function __construct(private ?string $param)
10+
{
11+
}
12+
13+
public function getParam(): ?string
14+
{
15+
return $this->param;
16+
}
17+
18+
/**
19+
* @phpstan-assert !null $this->getParam()
20+
*/
21+
public function checkNotNull(): void
22+
{
23+
if ($this->getParam() === null) {
24+
throw new \Exception();
25+
}
26+
}
27+
}
28+
29+
class Bar
30+
{
31+
public function __construct(private ?int $param)
32+
{
33+
}
34+
35+
public function getParam(): ?int
36+
{
37+
return $this->param;
38+
}
39+
40+
/**
41+
* @phpstan-assert !null $this->getParam()
42+
*/
43+
public function checkNotNull(): void
44+
{
45+
if ($this->getParam() === null) {
46+
throw new \Exception();
47+
}
48+
}
49+
}
50+
51+
function test(Foo|Bar $fooOrBar): void
52+
{
53+
assertType('int|string|null', $fooOrBar->getParam());
54+
55+
$fooOrBar->checkNotNull();
56+
57+
assertType('int|string', $fooOrBar->getParam());
58+
}

0 commit comments

Comments
 (0)