From 7f8f9cce7f3903e505916c7afe04b7912570b5e2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 14 Apr 2024 18:34:59 +0200 Subject: [PATCH] Allow undefined variables passed into by-ref parameters only if the type is nullable --- src/Analyser/NodeScopeResolver.php | 22 +++++++++--- .../Variables/DefinedVariableRuleTest.php | 18 ++++++++++ .../pass-by-reference-into-not-nullable.php | 36 +++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/pass-by-reference-into-not-nullable.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 73277bb29..32c4e7e63 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3797,6 +3797,7 @@ private function processArgs( foreach ($args as $i => $arg) { $assignByReference = false; $parameter = null; + $parameterType = null; if (isset($parameters) && $parametersAcceptor !== null) { if (isset($parameters[$i])) { $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); @@ -3810,9 +3811,22 @@ private function processArgs( } } + $lookForUnset = false; if ($assignByReference) { if ($arg->value instanceof Variable) { - $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); + $isBuiltin = false; + if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { + $isBuiltin = true; + } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { + $isBuiltin = true; + } + if ( + $isBuiltin + || ($parameterType === null || !$parameterType->isNull()->no()) + ) { + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); + $lookForUnset = true; + } } } @@ -3839,10 +3853,8 @@ private function processArgs( $result = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); } $scope = $result->getScope(); - if ($assignByReference) { - if ($arg->value instanceof Variable) { - $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); - } + if ($assignByReference && $lookForUnset) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); } if ($calleeReflection !== null) { diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 6c5574374..da7c48e02 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1042,4 +1042,22 @@ public function testBug10418(): void $this->analyse([__DIR__ . '/data/bug-10418.php'], []); } + public function testPassByReferenceIntoNotNullable(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/pass-by-reference-into-not-nullable.php'], [ + [ + 'Undefined variable: $three', + 32, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/pass-by-reference-into-not-nullable.php b/tests/PHPStan/Rules/Variables/data/pass-by-reference-into-not-nullable.php new file mode 100644 index 000000000..d0df427cd --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/pass-by-reference-into-not-nullable.php @@ -0,0 +1,36 @@ += 8.0 + +namespace PassByReferenceIntoNotNullable; + +class Foo +{ + + public function doFooNoType(&$test) + { + + } + + public function doFooMixedType(mixed &$test) + { + + } + + public function doFooIntType(int &$test) + { + + } + + public function doFooNullableType(?int &$test) + { + + } + + public function test() + { + $this->doFooNoType($one); + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +}