Skip to content

Commit 8ce904a

Browse files
committed
Fix phpstan/phpstan#14467: dynamic throw extensions on union types
1 parent 376f52a commit 8ce904a

4 files changed

Lines changed: 91 additions & 0 deletions

File tree

‎src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
1212
use PHPStan\Reflection\MethodReflection;
1313
use PHPStan\Reflection\ParametersAcceptor;
14+
use PHPStan\Reflection\Type\UnionTypeMethodReflection;
1415
use PHPStan\Type\NeverType;
1516
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\TypeCombinator;
1618
use ReflectionFunction;
1719
use ReflectionMethod;
1820
use Throwable;
21+
use function count;
1922
use function in_array;
2023

2124
#[AutowiredService]
@@ -37,6 +40,42 @@ public function getThrowPoint(
3740
MutatingScope $scope,
3841
): ?InternalThrowPoint
3942
{
43+
if ($methodReflection instanceof UnionTypeMethodReflection) {
44+
$throwPoints = [];
45+
foreach ($methodReflection->getMethods() as $innerMethodReflection) {
46+
$throwPoint = $this->getThrowPoint($innerMethodReflection, $parametersAcceptor, $normalizedMethodCall, $scope);
47+
if ($throwPoint === null) {
48+
continue;
49+
}
50+
51+
$throwPoints[] = $throwPoint;
52+
}
53+
54+
if (count($throwPoints) === 0) {
55+
return null;
56+
}
57+
58+
$throwTypes = [];
59+
$canContainAnyThrowable = false;
60+
$isExplicit = false;
61+
foreach ($throwPoints as $throwPoint) {
62+
$throwTypes[] = $throwPoint->getType();
63+
if ($throwPoint->canContainAnyThrowable()) {
64+
$canContainAnyThrowable = true;
65+
}
66+
if (!$throwPoint->isExplicit()) {
67+
continue;
68+
}
69+
$isExplicit = true;
70+
}
71+
72+
if (!$isExplicit) {
73+
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
74+
}
75+
76+
return InternalThrowPoint::createExplicit($scope, TypeCombinator::union(...$throwTypes), $normalizedMethodCall, $canContainAnyThrowable);
77+
}
78+
4079
if ($normalizedMethodCall instanceof MethodCall) {
4180
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
4281
if (!$extension->isMethodSupported($methodReflection)) {

‎src/Reflection/Type/UnionTypeMethodReflection.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ public function getDeclaringClass(): ClassReflection
3434
return $this->methods[0]->getDeclaringClass();
3535
}
3636

37+
/**
38+
* @return ExtendedMethodReflection[]
39+
*/
40+
public function getMethods(): array
41+
{
42+
return $this->methods;
43+
}
44+
3745
public function isStatic(): bool
3846
{
3947
foreach ($this->methods as $method) {

‎tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,12 @@ public function testBug13806(): void
748748
]);
749749
}
750750

751+
#[RequiresPhp('>= 8.3.0')]
752+
public function testBug14467(): void
753+
{
754+
$this->analyse([__DIR__ . '/data/bug-14467.php'], []);
755+
}
756+
751757
public function testBug5952(): void
752758
{
753759
$this->analyse([__DIR__ . '/data/bug-5952.php'], [
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php // lint >= 8.3
2+
3+
// FQN must sort after \DateTime so methods[0] resolves to DateTime.
4+
namespace TestBug14467;
5+
6+
class Foo
7+
{
8+
9+
/** @throws \DateMalformedStringException */
10+
public function modify(string $s): self
11+
{
12+
return $this;
13+
}
14+
15+
}
16+
17+
class Bar
18+
{
19+
20+
/** @param \DateTime|Foo $obj */
21+
public function caseDateTimeFirst($obj): void
22+
{
23+
try {
24+
$obj->modify('+1 day');
25+
} catch (\DateMalformedStringException $e) {
26+
}
27+
}
28+
29+
/** @param Foo|\DateTime $obj */
30+
public function caseFooFirst($obj): void
31+
{
32+
try {
33+
$obj->modify('+1 day');
34+
} catch (\DateMalformedStringException $e) {
35+
}
36+
}
37+
38+
}

0 commit comments

Comments
 (0)