Skip to content

Commit 6622401

Browse files
committed
Type-specified nullsafe call also removes null from the chain
1 parent 2ec878b commit 6622401

File tree

5 files changed

+300
-4
lines changed

5 files changed

+300
-4
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1981,7 +1981,17 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
19811981
$exprResult = $this->processExprNode(new MethodCall($expr->var, $expr->name, $expr->args, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context);
19821982
$scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
19831983

1984-
return new ExpressionResult($scope, $exprResult->hasYield(), $exprResult->getThrowPoints());
1984+
return new ExpressionResult(
1985+
$scope,
1986+
$exprResult->hasYield(),
1987+
$exprResult->getThrowPoints(),
1988+
static function () use ($scope, $expr): MutatingScope {
1989+
return $scope->filterByTruthyValue($expr);
1990+
},
1991+
static function () use ($scope, $expr): MutatingScope {
1992+
return $scope->filterByFalseyValue($expr);
1993+
}
1994+
);
19851995
} elseif ($expr instanceof StaticCall) {
19861996
$hasYield = false;
19871997
$throwPoints = [];

src/Analyser/TypeSpecifier.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,18 @@ public function specifyTypesInCondition(
793793
$context
794794
);
795795

796+
$nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
797+
return $context->true() ? $types->unionWith($nullSafeTypes) : $types->intersectWith($nullSafeTypes);
798+
} elseif ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) {
799+
$types = $this->specifyTypesInCondition(
800+
$scope,
801+
new BooleanAnd(
802+
new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
803+
new MethodCall($expr->var, $expr->name, $expr->args)
804+
),
805+
$context
806+
);
807+
796808
$nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
797809
return $context->true() ? $types->unionWith($nullSafeTypes) : $types->intersectWith($nullSafeTypes);
798810
} elseif (!$context->null()) {
@@ -955,12 +967,38 @@ public function create(
955967
$propertyFetchTypes = $propertyFetchTypes->unionWith(
956968
$this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope)
957969
);
970+
return $types->unionWith($propertyFetchTypes);
958971
} elseif ($context->false() && TypeCombinator::containsNull($type)) {
959972
$propertyFetchTypes = $propertyFetchTypes->unionWith(
960973
$this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope)
961974
);
975+
return $types->unionWith($propertyFetchTypes);
976+
}
977+
}
978+
979+
if ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) {
980+
$methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, false, $scope);
981+
if ($context->true() && $scope !== null) {
982+
$resultType = TypeCombinator::intersect($scope->getType($expr), $type);
983+
if (!TypeCombinator::containsNull($resultType)) {
984+
$methodCallTypes = $methodCallTypes->unionWith(
985+
$this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope)
986+
);
987+
return $types->unionWith($methodCallTypes);
988+
}
989+
990+
return new SpecifiedTypes();
991+
} elseif ($context->false() && $scope !== null) {
992+
$resultType = TypeCombinator::remove($scope->getType($expr), $type);
993+
if (!TypeCombinator::containsNull($resultType)) {
994+
$methodCallTypes = $methodCallTypes->unionWith(
995+
$this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope)
996+
);
997+
return $types->unionWith($methodCallTypes);
998+
}
999+
1000+
return new SpecifiedTypes();
9621001
}
963-
return $types->unionWith($propertyFetchTypes);
9641002
}
9651003

9661004
return $types;

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,11 @@ public function dataFileAsserts(): iterable
389389
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4820.php');
390390
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4822.php');
391391
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4816.php');
392+
393+
if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 80000) {
394+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4757.php');
395+
}
396+
392397
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4814.php');
393398
}
394399

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug4757;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
public function sayHello(?Reservation $oldReservation): void
10+
{
11+
if ($oldReservation?->isFoo()) {
12+
assertType(Reservation::class, $oldReservation);
13+
assertType('true', $oldReservation->isFoo());
14+
return;
15+
}
16+
17+
assertType(Reservation::class . '|null', $oldReservation);
18+
}
19+
20+
public function sayHello2(?Reservation $oldReservation): void
21+
{
22+
if (!$oldReservation?->isFoo()) {
23+
assertType(Reservation::class . '|null', $oldReservation);
24+
assertType('bool', $oldReservation->isFoo());
25+
return;
26+
}
27+
28+
assertType(Reservation::class, $oldReservation);
29+
assertType('true', $oldReservation->isFoo());
30+
}
31+
32+
public function sayHello3(?Reservation $oldReservation): void
33+
{
34+
if ($oldReservation?->isFoo() === true) {
35+
assertType(Reservation::class, $oldReservation);
36+
assertType('true', $oldReservation->isFoo());
37+
return;
38+
}
39+
40+
assertType(Reservation::class . '|null', $oldReservation);
41+
assertType('bool', $oldReservation->isFoo());
42+
}
43+
44+
public function sayHello4(?Reservation $oldReservation): void
45+
{
46+
if ($oldReservation?->isFoo() === false) {
47+
assertType(Reservation::class , $oldReservation);
48+
assertType('false', $oldReservation->isFoo());
49+
return;
50+
}
51+
52+
//assertType(Reservation::class . '|null', $oldReservation);
53+
assertType('bool', $oldReservation->isFoo());
54+
}
55+
56+
public function sayHello5(?Reservation $oldReservation): void
57+
{
58+
if ($oldReservation?->isFoo() === null) {
59+
assertType(Reservation::class . '|null', $oldReservation);
60+
return;
61+
}
62+
63+
assertType(Reservation::class, $oldReservation);
64+
}
65+
66+
public function sayHello6(?Reservation $oldReservation): void
67+
{
68+
if ($oldReservation?->isFoo() !== null) {
69+
assertType(Reservation::class, $oldReservation);
70+
assertType('bool', $oldReservation->isFoo());
71+
return;
72+
}
73+
74+
assertType(Reservation::class . '|null', $oldReservation);
75+
assertType('bool', $oldReservation->isFoo());
76+
}
77+
78+
public function sayHelloImpure(?Reservation $oldReservation): void
79+
{
80+
if ($oldReservation?->isFooImpure()) {
81+
assertType(Reservation::class, $oldReservation);
82+
assertType('bool', $oldReservation->isFooImpure());
83+
return;
84+
}
85+
86+
assertType(Reservation::class . '|null', $oldReservation);
87+
}
88+
89+
public function sayHello2Impure(?Reservation $oldReservation): void
90+
{
91+
if (!$oldReservation?->isFooImpure()) {
92+
assertType(Reservation::class . '|null', $oldReservation);
93+
return;
94+
}
95+
96+
assertType(Reservation::class, $oldReservation);
97+
}
98+
99+
public function sayHello3Impure(?Reservation $oldReservation): void
100+
{
101+
if ($oldReservation?->isFooImpure() === true) {
102+
assertType(Reservation::class, $oldReservation);
103+
return;
104+
}
105+
106+
assertType(Reservation::class . '|null', $oldReservation);
107+
}
108+
109+
public function sayHello4Impure(?Reservation $oldReservation): void
110+
{
111+
if ($oldReservation?->isFooImpure() === false) {
112+
assertType(Reservation::class , $oldReservation);
113+
return;
114+
}
115+
116+
//assertType(Reservation::class . '|null', $oldReservation);
117+
}
118+
119+
public function sayHello5Impure(?Reservation $oldReservation): void
120+
{
121+
if ($oldReservation?->isFooImpure() === null) {
122+
assertType(Reservation::class . '|null', $oldReservation);
123+
return;
124+
}
125+
126+
assertType(Reservation::class, $oldReservation);
127+
}
128+
129+
public function sayHello6Impure(?Reservation $oldReservation): void
130+
{
131+
if ($oldReservation?->isFooImpure() !== null) {
132+
assertType(Reservation::class, $oldReservation);
133+
return;
134+
}
135+
136+
assertType(Reservation::class . '|null', $oldReservation);
137+
}
138+
}
139+
140+
interface Reservation {
141+
public function isFoo(): bool;
142+
143+
/** @phpstan-impure */
144+
public function isFooImpure(): bool;
145+
}
146+
147+
interface Bar
148+
{
149+
public function get(): ?int;
150+
151+
/** @phpstan-impure */
152+
public function getImpure(): ?int;
153+
}
154+
155+
class Foo
156+
{
157+
158+
public function getBarOrNull(): ?Bar
159+
{
160+
return null;
161+
}
162+
163+
public function doFoo(Bar $b): void
164+
{
165+
$barOrNull = $this->getBarOrNull();
166+
if ($barOrNull?->get() === null) {
167+
assertType(Bar::class . '|null', $barOrNull);
168+
assertType('int|null', $barOrNull->get());
169+
//assertType('null', $barOrNull?->get());
170+
return;
171+
}
172+
173+
assertType(Bar::class, $barOrNull);
174+
assertType('int', $barOrNull->get());
175+
}
176+
177+
public function doFooImpire(Bar $b): void
178+
{
179+
$barOrNull = $this->getBarOrNull();
180+
if ($barOrNull?->getImpure() === null) {
181+
assertType(Bar::class . '|null', $barOrNull);
182+
assertType('int|null', $barOrNull->getImpure());
183+
assertType('int|null', $barOrNull?->getImpure());
184+
return;
185+
}
186+
187+
assertType(Bar::class, $barOrNull);
188+
assertType('int|null', $barOrNull->getImpure());
189+
}
190+
191+
public function doFoo2(Bar $b): void
192+
{
193+
$barOrNull = $this->getBarOrNull();
194+
if ($barOrNull?->get() !== null) {
195+
assertType(Bar::class, $barOrNull);
196+
assertType('int', $barOrNull->get());
197+
return;
198+
}
199+
200+
assertType(Bar::class . '|null', $barOrNull);
201+
assertType('int|null', $barOrNull->get());
202+
}
203+
204+
public function doFoo2Impure(Bar $b): void
205+
{
206+
$barOrNull = $this->getBarOrNull();
207+
if ($barOrNull?->getImpure() !== null) {
208+
assertType(Bar::class, $barOrNull);
209+
assertType('int|null', $barOrNull->getImpure());
210+
return;
211+
}
212+
213+
assertType(Bar::class . '|null', $barOrNull);
214+
assertType('int|null', $barOrNull->getImpure());
215+
}
216+
217+
public function doFoo3(Bar $b): void
218+
{
219+
$barOrNull = $this->getBarOrNull();
220+
if ($barOrNull?->get()) {
221+
assertType(Bar::class, $barOrNull);
222+
assertType('int<min, -1>|int<1, max>', $barOrNull->get());
223+
return;
224+
}
225+
226+
assertType(Bar::class . '|null', $barOrNull);
227+
assertType('int|null', $barOrNull->get());
228+
}
229+
230+
public function doFoo3Impure(Bar $b): void
231+
{
232+
$barOrNull = $this->getBarOrNull();
233+
if ($barOrNull?->getImpure()) {
234+
assertType(Bar::class, $barOrNull);
235+
assertType('int|null', $barOrNull->getImpure());
236+
return;
237+
}
238+
239+
assertType(Bar::class . '|null', $barOrNull);
240+
assertType('int|null', $barOrNull->getImpure());
241+
}
242+
243+
}

tests/PHPStan/Analyser/data/nullsafe.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function doLorem(?self $self)
5656
assertType('Nullsafe\Foo', $self?->nullableSelf);
5757
} else {
5858
assertType('Nullsafe\Foo|null', $self);
59-
assertType('null', $self->nullableSelf);
59+
assertType('Nullsafe\Foo|null', $self->nullableSelf);
6060
assertType('null', $self?->nullableSelf);
6161
}
6262

@@ -69,7 +69,7 @@ public function doIpsum(?self $self)
6969
{
7070
if ($self?->nullableSelf === null) {
7171
assertType('Nullsafe\Foo|null', $self);
72-
assertType('null', $self->nullableSelf);
72+
assertType('Nullsafe\Foo|null', $self);
7373
assertType('null', $self?->nullableSelf);
7474
} else {
7575
assertType('Nullsafe\Foo', $self);

0 commit comments

Comments
 (0)