diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index e345e82365..ceda96abce 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -208,6 +208,72 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['call_user_func_array', 'call_user_func'], true) + ) { + $innerResult = $functionReflection->getName() === 'call_user_func_array' + ? ArgumentsNormalizer::reorderCallUserFuncArrayArguments($expr, $scope) + : ArgumentsNormalizer::reorderCallUserFuncArguments($expr, $scope); + + if ($innerResult !== null) { + [$innerParametersAcceptor, $innerFuncCall] = $innerResult; + $innerParameters = $innerParametersAcceptor->getParameters(); + $innerArgs = $innerFuncCall->getArgs(); + + foreach ($innerArgs as $i => $innerArg) { + $argValue = $innerArg->value; + if ($argValue instanceof Variable && $argValue->name === 'this') { + continue; + } + + $innerParameter = null; + if (isset($innerParameters[$i])) { + $innerParameter = $innerParameters[$i]; + } elseif (count($innerParameters) > 0 && $innerParametersAcceptor->isVariadic()) { + $innerParameter = $innerParameters[count($innerParameters) - 1]; + } + + if ($innerParameter === null || !$innerParameter->passedByReference()->createsNewVariable()) { + continue; + } + + $innerCalleeReflections = []; + if ($innerFuncCall->name instanceof Expr) { + $calledOnType = $scope->getType($innerFuncCall->name); + if (!$calledOnType->isCallable()->no()) { + $innerCalleeReflections = array_map( + static fn (CallableParametersAcceptor $callableAcceptor) => $callableAcceptor->getCalleeReflection(), + $calledOnType->getCallableParametersAcceptors($scope), + ); + } + } elseif ($this->reflectionProvider->hasFunction($innerFuncCall->name, $scope)) { + $innerCalleeReflections = [$this->reflectionProvider->getFunction($innerFuncCall->name, $scope)]; + } + + if ($innerCalleeReflections === []) { + $byRefType = $nodeScopeResolver->resolveByRefParameterType($innerFuncCall, null, $innerParameter, $scope); + } else { + $byRefTypes = []; + foreach ($innerCalleeReflections as $innerCalleeReflection) { + $byRefTypes[] = $nodeScopeResolver->resolveByRefParameterType($innerFuncCall, $innerCalleeReflection, $innerParameter, $scope); + } + $byRefType = TypeCombinator::union(...$byRefTypes); + } + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $argValue, + new TypeExpr($byRefType), + $nodeCallback, + )->getScope(); + $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); + } + } + } + if ($normalizedExpr->name instanceof Expr) { $nameType = $scope->getType($normalizedExpr->name); if ( diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ca1daa6f34..caad0c321b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3475,27 +3475,7 @@ public function processArgs( $argValue = $arg->value; if (!$argValue instanceof Variable || $argValue->name !== 'this') { - $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); - if ($paramOutType !== null) { - $byRefType = $paramOutType; - } elseif ( - $currentParameter instanceof ExtendedParameterReflection - && $currentParameter->getOutType() !== null - ) { - $byRefType = $currentParameter->getOutType(); - } elseif ( - $calleeReflection instanceof MethodReflection - && !$calleeReflection->getDeclaringClass()->isBuiltin() - ) { - $byRefType = $currentParameter->getType(); - } elseif ( - $calleeReflection instanceof FunctionReflection - && !$calleeReflection->isBuiltin() - ) { - $byRefType = $currentParameter->getType(); - } else { - $byRefType = new MixedType(); - } + $byRefType = $this->resolveByRefParameterType($callLike, $calleeReflection, $currentParameter, $scope); $scope = $this->processVirtualAssign( $scope, @@ -3605,6 +3585,37 @@ private function getParameterTypeFromParameterClosureTypeExtension(CallLike $cal return null; } + public function resolveByRefParameterType(CallLike $callLike, MethodReflection|FunctionReflection|null $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): Type + { + $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); + if ($paramOutType !== null) { + return $paramOutType; + } + + if ( + $currentParameter instanceof ExtendedParameterReflection + && $currentParameter->getOutType() !== null + ) { + return $currentParameter->getOutType(); + } + + if ( + $calleeReflection instanceof MethodReflection + && !$calleeReflection->getDeclaringClass()->isBuiltin() + ) { + return $currentParameter->getType(); + } + + if ( + $calleeReflection instanceof FunctionReflection + && !$calleeReflection->isBuiltin() + ) { + return $currentParameter->getType(); + } + + return new MixedType(); + } + /** * @param MethodReflection|FunctionReflection|null $calleeReflection */ diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php index bcef9878ee..097f081838 100644 --- a/src/Reflection/Callables/CallableParametersAcceptor.php +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -4,6 +4,8 @@ use PHPStan\Node\InvalidateExprNode; use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\TrinaryLogic; @@ -60,4 +62,6 @@ public function mustUseReturnValue(): TrinaryLogic; public function getAsserts(): Assertions; + public function getCalleeReflection(): FunctionReflection|MethodReflection|null; + } diff --git a/src/Reflection/Callables/FunctionCallableVariant.php b/src/Reflection/Callables/FunctionCallableVariant.php index 6c48e4b010..009ef199aa 100644 --- a/src/Reflection/Callables/FunctionCallableVariant.php +++ b/src/Reflection/Callables/FunctionCallableVariant.php @@ -179,4 +179,9 @@ public function getAsserts(): Assertions return $this->function->getAsserts(); } + public function getCalleeReflection(): FunctionReflection|ExtendedMethodReflection + { + return $this->function; + } + } diff --git a/src/Reflection/ExtendedCallableFunctionVariant.php b/src/Reflection/ExtendedCallableFunctionVariant.php index 389893394e..7ac2c25953 100644 --- a/src/Reflection/ExtendedCallableFunctionVariant.php +++ b/src/Reflection/ExtendedCallableFunctionVariant.php @@ -6,6 +6,8 @@ use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; @@ -92,4 +94,9 @@ public function getAsserts(): Assertions return $this->assertions ?? Assertions::createEmpty(); } + public function getCalleeReflection(): FunctionReflection|MethodReflection|null + { + return null; + } + } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index 68fce995f8..a108a67a7d 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -98,4 +98,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getCalleeReflection(): ExtendedMethodReflection + { + return $this->methodReflection; + } + } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php index 6f816fa0ac..d8e15a6c3a 100644 --- a/src/Reflection/ResolvedFunctionVariantWithCallable.php +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -6,6 +6,8 @@ use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; @@ -124,4 +126,9 @@ public function getAsserts(): Assertions return $this->assertions ?? Assertions::createEmpty(); } + public function getCalleeReflection(): FunctionReflection|MethodReflection|null + { + return null; + } + } diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 157368d4c0..e332110e0c 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -4,6 +4,8 @@ use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; @@ -108,4 +110,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getCalleeReflection(): FunctionReflection|MethodReflection|null + { + return null; + } + } diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 380c87982a..3e61568820 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -17,6 +17,8 @@ use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; @@ -404,6 +406,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getCalleeReflection(): FunctionReflection|MethodReflection|null + { + return null; + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 0a380dff93..6b240a0f33 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -18,6 +18,8 @@ use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassConstantReflection; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; @@ -138,6 +140,11 @@ public function getAsserts(): Assertions return $this->assertions; } + public function getCalleeReflection(): FunctionReflection|MethodReflection|null + { + return null; + } + /** * @return array */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php new file mode 100644 index 0000000000..1c4bdee79e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -0,0 +1,116 @@ + $items + */ +function functionProcessInts(array &$items): void +{ + $items = [1, 2]; +} + +/** + * @param array $items + */ +function functionProcessStrings(array &$items): void +{ + $items = ['a', 'b']; +} + +class HelloWorld +{ + /** + * @param string[] $where + * @param string $sqlTableName + * @param mixed[] $filter + * @param string $value + */ + protected function listingAddWhereFilterAtableDefault(array &$where, string $sqlTableName, array $filter, string $value): void + { + if ($value != "" && !empty($filter) && !empty($filter['sql']) && is_string($filter['sql'])) { + $where[] = "`" . $sqlTableName . "`.`" . (string)$filter['sql'] . "` = '" . $value . "'"; + } + } + + /** + * @param string[] $filterValues + * @param string[] $where + * @param string[] $tables + * @param mixed[] $filters + */ + protected function listingAddWhereFilterAtable(array $filterValues, array &$where, array &$tables, array $filters): void + { + if (!empty($filterValues) && !empty($filters)) { + $whereFilter = array(); + foreach ($filterValues as $type => $value) { + call_user_func_array(array($this, 'listingAddWhereFilterAtableDefault'), array(&$whereFilter, 'xxxx', $filters[$type], $value)); + } + assertType('array', $whereFilter); + } + } + + /** + * @param array $items + * @param-out list $items + */ + protected function processWithParamOut(array &$items): void + { + $items = [1, 2, 3]; + } + + protected function testParamOut(): void + { + $items = []; + call_user_func_array([$this, 'processWithParamOut'], [&$items]); + assertType('list', $items); + } + + /** + * @param array $items + */ + protected function processInts(array &$items): void + { + $items = [1, 2]; + } + + /** + * @param array $items + */ + protected function processStrings(array &$items): void + { + $items = ['a', 'b']; + } + + /** + * @param 'Bug6799\functionProcessInts'|'Bug6799\functionProcessStrings' $function + */ + protected function testUnionStringCallbacks(string $function): void + { + $items = []; + call_user_func_array($function, [&$items]); + assertType('array', $items); + } + + /** + * @param array{$this, 'processInts'}|array{$this, 'processStrings'} $callback + */ + protected function testUnionArrayCallbacks(array $callback): void + { + $items = []; + call_user_func_array($callback, [&$items]); + assertType('array', $items); + } + + /** + * @param 'Bug6799\functionProcessInts'|array{$this, 'processStrings'} $callback + */ + protected function testMixedUnionCallback($callback): void + { + $items = []; + call_user_func_array($callback, [&$items]); + assertType('array', $items); + } +} diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 5400e3edee..b6bd38c930 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -288,6 +288,11 @@ public function testBug13874(): void $this->analyse([__DIR__ . '/data/bug-13874.php'], []); } + public function testBug6799(): void + { + $this->analyse([__DIR__ . '/data/bug-6799.php'], []); + } + public function testBug12163(): void { $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-12163.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6799.php b/tests/PHPStan/Rules/Comparison/data/bug-6799.php new file mode 100644 index 0000000000..0e248f4543 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6799.php @@ -0,0 +1,38 @@ + $value) { + call_user_func_array(array($this, 'listingAddWhereFilterAtableDefault'), array(&$whereFilter, 'xxxx', $filters[$type], $value)); + } + if (count($whereFilter) > 0) { + $where[] = implode(' OR ', $whereFilter); + } + } + } +}