Skip to content

Commit 45c195a

Browse files
committed
Fix TypeError dead catch false positive when assigning mixed to typed property
- Changed property assignment TypeError throw point check from accepts() to isSuperTypeOf() using native types - MixedType::isAcceptedBy() always returns yes, causing accepts() to miss that mixed-to-int can throw TypeError - Using isSuperTypeOf() on native types correctly identifies mixed as not guaranteed to be compatible - Widened property type check to include int when property type contains float, preserving PHP's int-to-float coercion - Added regression test with final classes to verify behavior independent of property hooks Closes phpstan/phpstan#9146
1 parent 106fc93 commit 45c195a

File tree

3 files changed

+54
-1
lines changed

3 files changed

+54
-1
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6366,8 +6366,14 @@ private function processAssignVar(
63666366
$declaringClass = $propertyReflection->getDeclaringClass();
63676367
if ($declaringClass->hasNativeProperty($propertyName)) {
63686368
$nativeProperty = $declaringClass->getNativeProperty($propertyName);
6369+
$propertyNativeType = $nativeProperty->getNativeType();
6370+
$assignedNativeType = $scope->getNativeType($assignedExpr);
6371+
// Widen property type to accept int for float properties (PHP allows int-to-float coercion)
6372+
$propertyNativeTypeForCheck = !$propertyNativeType->isFloat()->no()
6373+
? TypeCombinator::union($propertyNativeType, new IntegerType())
6374+
: $propertyNativeType;
63696375
if (
6370-
!$nativeProperty->getNativeType()->accepts($assignedExprType, true)->yes()
6376+
!$propertyNativeTypeForCheck->isSuperTypeOf($assignedNativeType)->yes()
63716377
) {
63726378
$throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false);
63736379
}

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

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

646+
public function testBug9146(): void
647+
{
648+
$this->analyse([__DIR__ . '/data/bug-9146.php'], []);
649+
}
650+
646651
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
}

0 commit comments

Comments
 (0)