Skip to content

Commit 22923ac

Browse files
phpstan-botondrejmirtes
authored andcommitted
Recurse into parent expression in IssetCheck and MutatingScope::issetCheck when property has propagated error
- In `IssetCheck::check()`, when a non-null `$error` is passed in from a deeper property check, recurse into `$expr->var` (or `$expr->class` for static properties) instead of returning the error immediately. This lets array dim fetches further up the chain clear the error when the offset might not exist. - Apply the same fix in `MutatingScope::issetCheck()` for the `$result` parameter, ensuring type inference for `??` expressions also considers parent array accesses. - Affects `??`, `??=`, `isset()`, and `empty()` — all share the same `IssetCheck::check()` code path. `empty()` was probed but doesn't trigger the false positive for scalar types because the falsiness callback returns null before an error is generated. - Static property access (`$array[0]::$prop->value ?? null`) also fixed by the same change (the `$expr->class instanceof Expr` branch).
1 parent c161e9f commit 22923ac

6 files changed

Lines changed: 105 additions & 0 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,14 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n
10871087
}
10881088

10891089
if ($result !== null) {
1090+
if ($expr instanceof Node\Expr\PropertyFetch) {
1091+
return $this->issetCheck($expr->var, $typeCallback, $result);
1092+
}
1093+
1094+
if ($expr->class instanceof Expr) {
1095+
return $this->issetCheck($expr->class, $typeCallback, $result);
1096+
}
1097+
10901098
return $result;
10911099
}
10921100

src/Rules/IssetCheck.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ static function (Type $type) use ($typeMessageCallback): ?string {
196196
$propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr);
197197
$propertyType = $propertyReflection->getWritableType();
198198
if ($error !== null) {
199+
if ($expr instanceof Node\Expr\PropertyFetch) {
200+
return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error);
201+
}
202+
203+
if ($expr->class instanceof Expr) {
204+
return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error);
205+
}
206+
199207
return $error;
200208
}
201209
if (!$this->checkAdvancedIsset) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14555Nsrt;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class ValueObject {
10+
function __construct(
11+
public readonly string $value,
12+
) {}
13+
}
14+
15+
class SomeDTO {
16+
function __construct(
17+
public readonly ValueObject $value,
18+
) {}
19+
}
20+
21+
/** @param array<string, list<SomeDTO>> $array */
22+
function testCoalesceType(array $array): void
23+
{
24+
$someValue = $array['someKey'][0]->value->value ?? null;
25+
assertType('string|null', $someValue);
26+
}

tests/PHPStan/Rules/Variables/IssetRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,13 @@ public function testBug9503(): void
531531
$this->analyse([__DIR__ . '/data/bug-9503.php'], []);
532532
}
533533

534+
public function testBug14555(): void
535+
{
536+
$this->treatPhpDocTypesAsCertain = true;
537+
538+
$this->analyse([__DIR__ . '/data/bug-14555.php'], []);
539+
}
540+
534541
public function testBug14393(): void
535542
{
536543
$this->treatPhpDocTypesAsCertain = true;

tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,12 @@ public function testBug14458(): void
397397
$this->analyse([__DIR__ . '/data/bug-14458.php'], []);
398398
}
399399

400+
#[RequiresPhp('>= 8.1.0')]
401+
public function testBug14555(): void
402+
{
403+
$this->analyse([__DIR__ . '/data/bug-14555.php'], []);
404+
}
405+
400406
#[RequiresPhp('>= 8.1.0')]
401407
public function testBug14459(): void
402408
{
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14555;
6+
7+
class ValueObject {
8+
function __construct(
9+
public readonly string $value,
10+
) {}
11+
}
12+
13+
class SomeDTO {
14+
function __construct(
15+
public readonly ValueObject $value,
16+
) {}
17+
}
18+
19+
class StaticHolder {
20+
public static ValueObject $value;
21+
}
22+
23+
/** @param array<string, list<SomeDTO>> $array */
24+
function exampleNullCoalesce(array $array): void
25+
{
26+
$someValue = $array['someKey'][0]->value->value ?? null;
27+
28+
$dto = $array['someKey'][0] ?? null;
29+
$someValue2 = $dto->value->value ?? null;
30+
}
31+
32+
/** @param array<string, list<SomeDTO>> $array */
33+
function exampleIsset(array $array): void
34+
{
35+
if (isset($array['someKey'][0]->value->value)) {
36+
echo 'yes';
37+
}
38+
}
39+
40+
/** @param array<string, list<SomeDTO>> $array */
41+
function exampleNullCoalesceAssign(array $array): void
42+
{
43+
$someValue = $array['someKey'][0]->value->value ??= 'default';
44+
}
45+
46+
/** @param array<string, list<StaticHolder>> $array */
47+
function exampleStaticProperty(array $array): void
48+
{
49+
$someValue = $array['someKey'][0]::$value->value ?? null;
50+
}

0 commit comments

Comments
 (0)