Skip to content

Commit 2523f58

Browse files
staabmphpstan-bot
authored andcommitted
Fix #6120: Nullsafe result !== null narrows the caller to non-null
- When assigning from a nullsafe expression (e.g. $result = $clazz?->foo), checking $result !== null now correctly narrows $clazz to non-null - Added non-null/null conditional expressions in processAssignVar alongside the existing truthy/falsey ones, so !== null checks trigger narrowing - Updated bug-13546 test: array_key_first assignment narrowing now correctly narrows to non-empty-list/array{} when checking result !== null - Removed baseline entry for ClassReflection::getCacheKey() which is now correctly resolved by the improved narrowing
1 parent f844f7b commit 2523f58

4 files changed

Lines changed: 74 additions & 8 deletions

File tree

phpstan-baseline.neon

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -411,12 +411,6 @@ parameters:
411411
count: 1
412412
path: src/Reflection/ClassReflection.php
413413

414-
-
415-
rawMessage: 'Method PHPStan\Reflection\ClassReflection::getCacheKey() should return string but returns string|null.'
416-
identifier: return.type
417-
count: 1
418-
path: src/Reflection/ClassReflection.php
419-
420414
-
421415
rawMessage: Binary operation "&" between bool|float|int|string|null and bool|float|int|string|null results in an error.
422416
identifier: binaryOp.invalid

src/Analyser/NodeScopeResolver.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6108,6 +6108,23 @@ private function processAssignVar(
61086108
$conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
61096109
}
61106110

6111+
$nonNullType = TypeCombinator::removeNull($type);
6112+
if (
6113+
!$nonNullType->equals($type)
6114+
&& !$nonNullType->equals($truthyType)
6115+
) {
6116+
$notNullConditionExpr = new Expr\BinaryOp\NotIdentical($assignedExpr, new ConstFetch(new Name('null')));
6117+
$notNullSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $notNullConditionExpr, TypeSpecifierContext::createTrue());
6118+
$conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notNullSpecifiedTypes, $nonNullType);
6119+
$conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notNullSpecifiedTypes, $nonNullType);
6120+
6121+
$nullType = new NullType();
6122+
$nullConditionExpr = new Expr\BinaryOp\Identical($assignedExpr, new ConstFetch(new Name('null')));
6123+
$nullSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $nullConditionExpr, TypeSpecifierContext::createTrue());
6124+
$conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $nullSpecifiedTypes, $nullType);
6125+
$conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $nullSpecifiedTypes, $nullType);
6126+
}
6127+
61116128
$this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage);
61126129
$scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes());
61136130
foreach ($conditionalExpressions as $exprString => $holders) {

tests/PHPStan/Analyser/nsrt/bug-13546.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ function mixedLast($mixed): void
6666
function firstInCondition(array $array)
6767
{
6868
if (($key = array_key_first($array)) !== null) {
69-
assertType('list<string>', $array); // could be 'non-empty-list<string>'
69+
assertType('non-empty-list<string>', $array);
7070
return $array[$key];
7171
}
72-
assertType('list<string>', $array);
72+
assertType('array{}', $array);
7373
return null;
7474
}
7575

tests/PHPStan/Analyser/nsrt/bug-6120.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,58 @@ public function sayHello(): void
2727
}
2828
}
2929
}
30+
31+
final class Clazz
32+
{
33+
34+
public int $foo = 0;
35+
36+
public function bar(?Clazz $clazz): void
37+
{
38+
$result = $clazz?->foo;
39+
assertType('int|null', $result);
40+
if ($result !== null) {
41+
assertType('Bug6120\Clazz', $clazz);
42+
assertType('int', $result);
43+
$clazz->bar(null);
44+
}
45+
}
46+
47+
public function baz(?Clazz $clazz): void
48+
{
49+
$result = $clazz?->foo;
50+
if ($result === null) {
51+
assertType('Bug6120\Clazz|null', $clazz);
52+
assertType('null', $result);
53+
} else {
54+
assertType('Bug6120\Clazz', $clazz);
55+
assertType('int', $result);
56+
}
57+
}
58+
59+
public function withNullableProperty(?Clazz $clazz): void
60+
{
61+
$result = $clazz?->nullableProperty;
62+
if ($result !== null) {
63+
assertType('Bug6120\Clazz', $clazz);
64+
assertType('string', $result);
65+
}
66+
}
67+
68+
public ?string $nullableProperty = null;
69+
70+
public function withMethodCall(?Clazz $clazz): void
71+
{
72+
$result = $clazz?->getFoo();
73+
if ($result !== null) {
74+
assertType('Bug6120\Clazz', $clazz);
75+
assertType('int', $result);
76+
}
77+
}
78+
79+
public function getFoo(): int
80+
{
81+
return $this->foo;
82+
}
83+
84+
}

0 commit comments

Comments
 (0)