Skip to content

Commit 8a4fd71

Browse files
VincentLangletphpstan-bot
authored andcommitted
Do not report undefined property in isset/empty/?? for types without class names
When checkDynamicProperties is enabled, PHPStan incorrectly reported "Access to an undefined property" for chained isset() checks on mixed types. After the first isset() narrows mixed to object&hasProperty(foo), the second isset($tmp->bar) was being flagged because pickProperty returned null for the intersection type that has no concrete class names. The fix skips the dynamic property check when the type has no class names (bare object or object&hasProperty), since we cannot know what properties such a type might have. Closes phpstan/phpstan#13539
1 parent 93d00a6 commit 8a4fd71

4 files changed

Lines changed: 70 additions & 3 deletions

File tree

src/Rules/Properties/AccessPropertiesCheck.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string
140140
return [];
141141
}
142142

143+
if (count($type->getObjectClassNames()) === 0) {
144+
return [];
145+
}
146+
143147
$maybePropertyReflection = $this->pickProperty($scope, $type, $name);
144148
if ($maybePropertyReflection !== null && $maybePropertyReflection->isDummy()->no()) {
145149
return [];

tests/PHPStan/Analyser/AnalyserWithCheckDynamicPropertiesTest.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ class AnalyserWithCheckDynamicPropertiesTest extends PHPStanTestCase
1212
public function testBug13529(): void
1313
{
1414
$errors = $this->runAnalyse(__DIR__ . '/data/bug-13529.php');
15-
$this->assertCount(1, $errors);
16-
$this->assertSame('Access to an undefined property object::$bar.', $errors[0]->getMessage());
17-
$this->assertSame(8, $errors[0]->getLine());
15+
$this->assertCount(0, $errors);
1816
}
1917

2018
/**

tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,4 +1272,13 @@ public function testBug13537(): void
12721272
$this->analyse([__DIR__ . '/data/bug-13537.php'], $errors);
12731273
}
12741274

1275+
public function testBug13539(): void
1276+
{
1277+
$this->checkThisOnly = false;
1278+
$this->checkUnionTypes = true;
1279+
$this->checkDynamicProperties = true;
1280+
1281+
$this->analyse([__DIR__ . '/data/bug-13539.php'], []);
1282+
}
1283+
12751284
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace Bug13539;
4+
5+
function broken(string $x): void {
6+
$tmp = json_decode($x, false);
7+
8+
if (!isset($tmp->foo) || !isset($tmp->bar)) {
9+
}
10+
}
11+
12+
function works(string $x): void {
13+
$tmp = json_decode($x, false);
14+
15+
if (!isset($tmp->foo, $tmp->bar)) {
16+
}
17+
}
18+
19+
function works_too(string $x): void {
20+
/** @var \stdClass $tmp */
21+
$tmp = json_decode($x, false);
22+
23+
if (!isset($tmp->foo) || !isset($tmp->bar)) {
24+
}
25+
}
26+
27+
function threeProperties(string $x): void {
28+
$tmp = json_decode($x, false);
29+
30+
if (!isset($tmp->foo) || !isset($tmp->bar) || !isset($tmp->baz)) {
31+
}
32+
}
33+
34+
function coalesceAfterIsset(string $x): void {
35+
$tmp = json_decode($x, false);
36+
37+
if (isset($tmp->foo)) {
38+
$bar = $tmp->bar ?? null;
39+
}
40+
}
41+
42+
function issetInAndChain(string $x): void {
43+
$tmp = json_decode($x, false);
44+
45+
if (isset($tmp->foo) && isset($tmp->bar)) {
46+
}
47+
}
48+
49+
function emptyAfterIsset(string $x): void {
50+
$tmp = json_decode($x, false);
51+
52+
if (isset($tmp->foo)) {
53+
if (empty($tmp->bar)) {
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)