diff --git a/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php b/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php index 02e197a28e..79f5fe0b67 100644 --- a/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php +++ b/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php @@ -70,7 +70,7 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina $exprToSpecify, $exprTypeWithoutNull, TypeCombinator::removeNull($nativeType), - TrinaryLogic::createYes(), + $certainty, ); return new EnsuredNonNullabilityResult( diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 9faedfce2f..742d054b7c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2654,7 +2654,7 @@ private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Clo if ($expr instanceof ArrayDimFetch) { $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); - } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { + } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) { $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback); diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 3747fca81a..9f2e93fc72 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -757,6 +757,33 @@ public function testNullsafeIsset(): void $this->analyse([__DIR__ . '/data/variable-nullsafe-isset.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug7291(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-7291.php'], [ + [ + 'Variable $a might not be defined.', + 23, + ], + [ + 'Variable $b might not be defined.', + 32, + ], + [ + 'Variable $c might not be defined.', + 41, + ], + [ + 'Variable $d might not be defined.', + 50, + ], + ]); + } + public function testBug1306(): void { $this->cliArgumentsVariablesRegistered = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-7291.php b/tests/PHPStan/Rules/Variables/data/bug-7291.php new file mode 100644 index 0000000000..5c9af9c578 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-7291.php @@ -0,0 +1,96 @@ += 8.0 + +namespace Bug7291; + +class Foo +{ + + public ?Foo $foo = null; + + public function bar(): self + { + return $this; + } + +} + +function bareProp(): void +{ + if (rand(0, 1)) { + $a = rand(0, 1) ? new Foo() : null; + } + + echo $a?->foo; // warn +} + +function bareMethod(): void +{ + if (rand(0, 1)) { + $b = rand(0, 1) ? new Foo() : null; + } + + echo $b?->bar(); // warn +} + +function bareChain(): void +{ + if (rand(0, 1)) { + $c = rand(0, 1) ? new Foo() : null; + } + + echo $c?->bar()?->foo; // warn +} + +function notNullableStillWarns(): void +{ + if (rand(0, 1)) { + $d = new Foo(); + } + + echo $d?->foo; // warn +} + +function propCoalesce(): void +{ + if (rand(0, 1)) { + $e = rand(0, 1) ? new Foo() : null; + } + + echo $e?->foo ?? 0; // no warn, ?? handles it +} + +function methodCoalesce(): void +{ + if (rand(0, 1)) { + $f = rand(0, 1) ? new Foo() : null; + } + + echo $f?->bar() ?? 0; // no warn, ?? handles it +} + +function propIsset(): void +{ + if (rand(0, 1)) { + $g = rand(0, 1) ? new Foo() : null; + } + + var_dump(isset($g?->foo)); // no warn, isset handles it +} + +function propEmpty(): void +{ + if (rand(0, 1)) { + $h = rand(0, 1) ? new Foo() : null; + } + + var_dump(empty($h?->foo)); // no warn, empty handles it +} + +function methodEmpty(): void +{ + if (rand(0, 1)) { + $i = rand(0, 1) ? new Foo() : null; + } + + var_dump(empty($i?->bar())); // no warn, empty handles it +}