Skip to content

Commit 8918e72

Browse files
committed
Fix TypeError dead catch when assigning mixed to int in property
1 parent 40b3f24 commit 8918e72

File tree

4 files changed

+238
-1
lines changed

4 files changed

+238
-1
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6366,8 +6366,23 @@ private function processAssignVar(
63666366
$declaringClass = $propertyReflection->getDeclaringClass();
63676367
if ($declaringClass->hasNativeProperty($propertyName)) {
63686368
$nativeProperty = $declaringClass->getNativeProperty($propertyName);
6369+
$propertyNativeType = $nativeProperty->getNativeType();
6370+
6371+
$assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes();
6372+
if (!$assignedTypeIsCompatible && !$assignedExprType instanceof MixedType) {
6373+
foreach (TypeUtils::flattenTypes($assignedExprType->toCoercedArgumentType(true)) as $type) {
6374+
$accepts = $propertyNativeType->accepts($type, true);
6375+
if ($accepts->yes()) {
6376+
$assignedTypeIsCompatible = true;
6377+
continue;
6378+
}
6379+
$assignedTypeIsCompatible = false;
6380+
break;
6381+
}
6382+
}
6383+
63696384
if (
6370-
!$nativeProperty->getNativeType()->accepts($assignedExprType, true)->yes()
6385+
!$assignedTypeIsCompatible
63716386
) {
63726387
$throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false);
63736388
}

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,32 @@ public function testPropertyHooks(): void
643643
]);
644644
}
645645

646+
public function testBug9146(): void
647+
{
648+
$this->analyse([__DIR__ . '/data/bug-9146.php'], [
649+
[
650+
'Dead catch - TypeError is never thrown in the try block.',
651+
52,
652+
],
653+
[
654+
'Dead catch - TypeError is never thrown in the try block.',
655+
80,
656+
],
657+
]);
658+
}
659+
660+
public function testBug9146NonStrict(): void
661+
{
662+
$this->analyse([__DIR__ . '/data/bug-9146-non-strict.php'], [
663+
[
664+
'Dead catch - TypeError is never thrown in the try block.',
665+
52,
666+
],
667+
[
668+
'Dead catch - TypeError is never thrown in the try block.',
669+
80,
670+
],
671+
]);
672+
}
673+
646674
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php declare(strict_types = 0);
2+
3+
namespace Bug9146NonStrict;
4+
5+
final class HelloWorld
6+
{
7+
public int $number;
8+
public function __construct(mixed $number)
9+
{
10+
try {
11+
$this->number = $number;
12+
} catch (\TypeError $e) {
13+
throw new \UnexpectedValueException();
14+
}
15+
}
16+
}
17+
18+
final class HelloWorld2
19+
{
20+
public string $name;
21+
public function setName(mixed $value): void
22+
{
23+
try {
24+
$this->name = $value;
25+
} catch (\TypeError $e) {
26+
throw new \InvalidArgumentException('Expected string');
27+
}
28+
}
29+
}
30+
31+
final class HelloWorld3
32+
{
33+
public float $amount;
34+
public function setAmount(mixed $value): void
35+
{
36+
try {
37+
$this->amount = $value;
38+
} catch (\TypeError $e) {
39+
echo "caught";
40+
}
41+
}
42+
}
43+
44+
// Dead catch: int assigned to ?float, PHP coerces int to float without TypeError
45+
final class FloatNullCoercion
46+
{
47+
public ?float $amount;
48+
public function setAmount(int $value): void
49+
{
50+
try {
51+
$this->amount = $value;
52+
} catch (\TypeError $e) { // error: Dead catch - TypeError is never thrown in the try block.
53+
echo "caught";
54+
}
55+
}
56+
}
57+
58+
// Not dead: int|string assigned to int, string part could throw TypeError
59+
final class PartialTypeMatch
60+
{
61+
public int $number;
62+
/** @param int|string $value */
63+
public function setNumber($value): void
64+
{
65+
try {
66+
$this->number = $value;
67+
} catch (\TypeError $e) {
68+
echo "caught";
69+
}
70+
}
71+
}
72+
73+
final class MixedWillNotThrow
74+
{
75+
public mixed $name;
76+
public function setName(mixed $value): void
77+
{
78+
try {
79+
$this->name = $value;
80+
} catch (\TypeError $e) {
81+
throw new \InvalidArgumentException('Expected string');
82+
}
83+
}
84+
}
85+
86+
final class IntJustWarnsOnFloat
87+
{
88+
public int $amount;
89+
public function setAmount(float $value): void
90+
{
91+
try {
92+
$this->amount = $value;
93+
} catch (\TypeError $e) {
94+
echo "caught";
95+
}
96+
}
97+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9146;
4+
5+
final class HelloWorld
6+
{
7+
public int $number;
8+
public function __construct(mixed $number)
9+
{
10+
try {
11+
$this->number = $number;
12+
} catch (\TypeError $e) {
13+
throw new \UnexpectedValueException();
14+
}
15+
}
16+
}
17+
18+
final class HelloWorld2
19+
{
20+
public string $name;
21+
public function setName(mixed $value): void
22+
{
23+
try {
24+
$this->name = $value;
25+
} catch (\TypeError $e) {
26+
throw new \InvalidArgumentException('Expected string');
27+
}
28+
}
29+
}
30+
31+
final class HelloWorld3
32+
{
33+
public float $amount;
34+
public function setAmount(mixed $value): void
35+
{
36+
try {
37+
$this->amount = $value;
38+
} catch (\TypeError $e) {
39+
echo "caught";
40+
}
41+
}
42+
}
43+
44+
// Dead catch: int assigned to ?float, PHP coerces int to float without TypeError
45+
final class FloatNullCoercion
46+
{
47+
public ?float $amount;
48+
public function setAmount(int $value): void
49+
{
50+
try {
51+
$this->amount = $value;
52+
} catch (\TypeError $e) { // error: Dead catch - TypeError is never thrown in the try block.
53+
echo "caught";
54+
}
55+
}
56+
}
57+
58+
// Not dead: int|string assigned to int, string part could throw TypeError
59+
final class PartialTypeMatch
60+
{
61+
public int $number;
62+
/** @param int|string $value */
63+
public function setNumber($value): void
64+
{
65+
try {
66+
$this->number = $value;
67+
} catch (\TypeError $e) {
68+
echo "caught";
69+
}
70+
}
71+
}
72+
73+
final class MixedWillNotThrow
74+
{
75+
public mixed $name;
76+
public function setName(mixed $value): void
77+
{
78+
try {
79+
$this->name = $value;
80+
} catch (\TypeError $e) {
81+
throw new \InvalidArgumentException('Expected string');
82+
}
83+
}
84+
}
85+
86+
final class IntDoesNotAcceptFloat
87+
{
88+
public int $amount;
89+
public function setAmount(float $value): void
90+
{
91+
try {
92+
$this->amount = $value;
93+
} catch (\TypeError $e) {
94+
echo "caught";
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)