Skip to content

Commit f1756d8

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 75173b0 + 03834ec commit f1756d8

File tree

5 files changed

+120
-8
lines changed

5 files changed

+120
-8
lines changed

src/Analyser/MutatingScope.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,15 +1070,18 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n
10701070

10711071
if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) {
10721072
if (!$this->hasExpressionType($expr)->yes()) {
1073-
if ($expr instanceof Node\Expr\PropertyFetch) {
1074-
return $this->issetCheckUndefined($expr->var);
1075-
}
1073+
$nativeReflection = $propertyReflection->getNativeReflection();
1074+
if ($nativeReflection === null || !$nativeReflection->isPromoted() || (!$nativeReflection->isReadOnly() && !$nativeReflection->isHooked())) {
1075+
if ($expr instanceof Node\Expr\PropertyFetch) {
1076+
return $this->issetCheckUndefined($expr->var);
1077+
}
10761078

1077-
if ($expr->class instanceof Expr) {
1078-
return $this->issetCheckUndefined($expr->class);
1079-
}
1079+
if ($expr->class instanceof Expr) {
1080+
return $this->issetCheckUndefined($expr->class);
1081+
}
10801082

1081-
return null;
1083+
return null;
1084+
}
10821085
}
10831086
}
10841087

src/Rules/IssetCheck.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,11 @@ static function (Type $type) use ($typeMessageCallback): ?string {
183183

184184
if (!$scope->hasExpressionType($expr)->yes()) {
185185
$nativeReflection = $propertyReflection->getNativeReflection();
186-
if ($nativeReflection !== null && !$nativeReflection->getNativeReflection()->hasDefaultValue()) {
186+
if (
187+
$nativeReflection !== null
188+
&& !$nativeReflection->getNativeReflection()->hasDefaultValue()
189+
&& (!$nativeReflection->isPromoted() || (!$nativeReflection->isReadOnly() && !$nativeReflection->isHooked()))
190+
) {
187191
return null;
188192
}
189193
}

tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,28 @@ public function testBug14458(): void
392392
$this->analyse([__DIR__ . '/data/bug-14458.php'], []);
393393
}
394394

395+
#[RequiresPhp('>= 8.1')]
396+
public function testBug14459(): void
397+
{
398+
$this->analyse([__DIR__ . '/data/bug-14459.php'], [
399+
[
400+
'Property Bug14459\Dto::$policyholderId (stdClass) on left side of ?? is not nullable.',
401+
34,
402+
],
403+
]);
404+
}
405+
406+
#[RequiresPhp('>= 8.4')]
407+
public function testBug14459Hooked(): void
408+
{
409+
$this->analyse([__DIR__ . '/data/bug-14459-hooked.php'], [
410+
[
411+
'Property Bug14459Hooked\DtoHooked::$policyholderId (stdClass) on left side of ?? is not nullable.',
412+
21,
413+
],
414+
]);
415+
}
416+
395417
public function testBug14393(): void
396418
{
397419
$this->analyse([__DIR__ . '/data/bug-14393.php'], [
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php // lint >= 8.4
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14459Hooked;
6+
7+
final class DtoHooked
8+
{
9+
public function __construct(
10+
public \stdClass $policyholderId {
11+
set => $value;
12+
},
13+
public ?\stdClass $nullablePolicyholderId {
14+
set => $value;
15+
},
16+
) {}
17+
}
18+
19+
function testHooked(DtoHooked $dto): \stdClass
20+
{
21+
$x = $dto->policyholderId ?? new \stdClass();
22+
return $x;
23+
}
24+
25+
function testHookedNullable(DtoHooked $dto): \stdClass
26+
{
27+
$x = $dto->nullablePolicyholderId ?? new \stdClass();
28+
return $x;
29+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14459;
6+
7+
final class Dto
8+
{
9+
public function __construct(
10+
public readonly \stdClass $payerId,
11+
public readonly \stdClass $policyholderId,
12+
public readonly ?\stdClass $nullablePolicyholderId,
13+
) {}
14+
}
15+
16+
final class DtoNonReadonly
17+
{
18+
public function __construct(
19+
public \stdClass $payerId,
20+
) {}
21+
}
22+
23+
class DtoNonPromotedReadonly
24+
{
25+
public readonly \stdClass $payerId;
26+
27+
public function __construct(\stdClass $payerId) {
28+
$this->payerId = $payerId;
29+
}
30+
}
31+
32+
function test(Dto $dto): \stdClass
33+
{
34+
$x = $dto->policyholderId ?? $dto->payerId;
35+
return $x;
36+
}
37+
38+
function testNullable(Dto $dto): \stdClass
39+
{
40+
$x = $dto->nullablePolicyholderId ?? $dto->payerId;
41+
return $x;
42+
}
43+
44+
function testNonReadonly(DtoNonReadonly $dto): \stdClass
45+
{
46+
$x = $dto->payerId ?? new \stdClass();
47+
return $x;
48+
}
49+
50+
function testNonPromotedReadonly(DtoNonPromotedReadonly $dto): \stdClass
51+
{
52+
$x = $dto->payerId ?? new \stdClass();
53+
return $x;
54+
}

0 commit comments

Comments
 (0)