Skip to content

Commit 4450e46

Browse files
authored
Fix false positive dead catch on property assignment
1 parent 71c60ae commit 4450e46

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-0
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
use PHPStan\Type\VoidType;
140140
use Throwable;
141141
use Traversable;
142+
use TypeError;
142143
use function array_fill_keys;
143144
use function array_filter;
144145
use function array_key_exists;
@@ -3314,11 +3315,24 @@ private function processAssignVar(
33143315
if ($propertyReflection->canChangeTypeAfterAssignment()) {
33153316
$scope = $scope->assignExpression($var, $assignedExprType);
33163317
}
3318+
if (!$propertyReflection->getWritableType()->isSuperTypeOf($assignedExprType)->yes()) {
3319+
$throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false);
3320+
}
33173321
} else {
33183322
// fallback
33193323
$assignedExprType = $scope->getType($assignedExpr);
33203324
$nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
33213325
$scope = $scope->assignExpression($var, $assignedExprType);
3326+
// simulate dynamic property assign by __set to get throw points
3327+
if (!$propertyHolderType->hasMethod('__set')->no()) {
3328+
$throwPoints = array_merge($throwPoints, $this->processExprNode(
3329+
new MethodCall($var->var, '__set'),
3330+
$scope,
3331+
static function (): void {
3332+
},
3333+
$context,
3334+
)->getThrowPoints());
3335+
}
33223336
}
33233337

33243338
} elseif ($var instanceof Expr\StaticPropertyFetch) {

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,38 @@ public function testBug6262(): void
215215
$this->analyse([__DIR__ . '/data/bug-6262.php'], []);
216216
}
217217

218+
public function testBug6256(): void
219+
{
220+
if (PHP_VERSION_ID < 70400) {
221+
self::markTestSkipped('Test requires PHP 7.4.');
222+
}
223+
224+
$this->analyse([__DIR__ . '/data/bug-6256.php'], [
225+
[
226+
'Dead catch - TypeError is never thrown in the try block.',
227+
25,
228+
],
229+
[
230+
'Dead catch - TypeError is never thrown in the try block.',
231+
31,
232+
],
233+
[
234+
'Dead catch - TypeError is never thrown in the try block.',
235+
45,
236+
],
237+
[
238+
'Dead catch - Exception is never thrown in the try block.',
239+
57,
240+
],
241+
[
242+
'Dead catch - Throwable is never thrown in the try block.',
243+
63,
244+
],
245+
[
246+
'Dead catch - Exception is never thrown in the try block.',
247+
100,
248+
],
249+
]);
250+
}
251+
218252
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php // lint >= 7.4
2+
3+
namespace Bug6256;
4+
5+
use Exception;
6+
7+
final class A
8+
{
9+
public int $integerType = 1;
10+
public $mixedType;
11+
public string $stringType;
12+
/** @var string|int */
13+
public $stringOrIntType;
14+
15+
function doFoo()
16+
{
17+
try {
18+
$this->integerType = "string";
19+
} catch (\TypeError $e) {
20+
// not dead
21+
}
22+
23+
try {
24+
$this->mixedType = "string";
25+
} catch (\TypeError $e) {
26+
// dead
27+
}
28+
29+
try {
30+
$this->stringType = "string";
31+
} catch (\TypeError $e) {
32+
// dead
33+
}
34+
35+
/** @var string|int $intOrString */
36+
$intOrString = '';
37+
try {
38+
$this->integerType = $intOrString;
39+
} catch (\TypeError $e) {
40+
// not dead
41+
}
42+
43+
try {
44+
$this->stringOrIntType = 1;
45+
} catch (\TypeError $e) {
46+
// dead
47+
}
48+
49+
try {
50+
$this->integerType = "string";
51+
} catch (\Error $e) {
52+
// not dead
53+
}
54+
55+
try {
56+
$this->integerType = "string";
57+
} catch (\Exception $e) {
58+
// dead
59+
}
60+
61+
try {
62+
$this->dynamicProperty = 1;
63+
} catch (\Throwable $e) {
64+
// dead
65+
}
66+
}
67+
}
68+
69+
final class B {
70+
71+
/**
72+
* @throws Exception
73+
*/
74+
public function __set(string $name, $value)
75+
{
76+
throw new Exception();
77+
}
78+
79+
function doFoo()
80+
{
81+
try {
82+
$this->dynamicProperty = "string";
83+
} catch (\Exception $e) {
84+
// not dead
85+
}
86+
}
87+
}
88+
89+
final class C {
90+
91+
/**
92+
* @throws void
93+
*/
94+
public function __set(string $name, $value) {}
95+
96+
function doFoo()
97+
{
98+
try {
99+
$this->dynamicProperty = "string";
100+
} catch (\Exception $e) {
101+
// dead
102+
}
103+
}
104+
}
105+
106+
class D {
107+
function doFoo()
108+
{
109+
try {
110+
$this->dynamicProperty = "string";
111+
} catch (\Exception $e) {
112+
// not dead because class is not final
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)