diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index a792c0d6b8..07d7fbe09c 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; @@ -45,7 +46,7 @@ public function __construct( * @param array $expressionTypes * @param array $nativeExpressionTypes * @param array $conditionalExpressions - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack + * @param list $inFunctionCallsStack * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions */ diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index dcaf118ed9..7d3414f297 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; interface InternalScopeFactory @@ -16,7 +17,7 @@ interface InternalScopeFactory * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param array $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 3c70556f53..0d74666e85 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; @@ -41,8 +42,7 @@ public function __construct( * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack - * + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8189dbfd44..3f08be9435 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -174,7 +174,7 @@ class MutatingScope implements Scope * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function __construct( private InternalScopeFactory $scopeFactory, @@ -2410,10 +2410,10 @@ public function hasExpressionType(Expr $node): TrinaryLogic /** * @param MethodReflection|FunctionReflection $reflection */ - public function pushInFunctionCall($reflection): self + public function pushInFunctionCall($reflection, ?ParameterReflection $parameter): self { $stack = $this->inFunctionCallsStack; - $stack[] = $reflection; + $stack[] = [$reflection, $parameter]; $scope = $this->scopeFactory->create( $this->context, @@ -2473,7 +2473,7 @@ public function popInFunctionCall(): self /** @api */ public function isInClassExists(string $className): bool { - foreach ($this->inFunctionCallsStack as $inFunctionCall) { + foreach ($this->inFunctionCallsStack as [$inFunctionCall]) { if (!$inFunctionCall instanceof FunctionReflection) { continue; } @@ -2494,6 +2494,11 @@ public function isInClassExists(string $className): bool } public function getFunctionCallStack(): array + { + return array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack); + } + + public function getFunctionCallStackWithParameters(): array { return $this->inFunctionCallsStack; } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b516987f31..45e4c0c2f5 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3633,22 +3633,21 @@ private function processArgs( } } - if ($calleeReflection !== null) { - $scope = $scope->pushInFunctionCall($calleeReflection); - } - $hasYield = false; $throwPoints = []; foreach ($args as $i => $arg) { $assignByReference = false; + $parameter = null; if (isset($parameters) && $parametersAcceptor !== null) { if (isset($parameters[$i])) { $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); $parameterType = $parameters[$i]->getType(); + $parameter = $parameters[$i]; } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { $lastParameter = $parameters[count($parameters) - 1]; $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); + $parameter = $lastParameter; } } @@ -3658,6 +3657,10 @@ private function processArgs( } } + if ($calleeReflection !== null) { + $scope = $scope->pushInFunctionCall($calleeReflection, $parameter); + } + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; $nodeCallback($originalArg, $scope); @@ -3682,6 +3685,11 @@ private function processArgs( $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); } } + + if ($calleeReflection !== null) { + $scope = $scope->popInFunctionCall(); + } + $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); if ($i !== 0 || $closureBindScope === null) { @@ -3690,11 +3698,6 @@ private function processArgs( $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } - - if ($calleeReflection !== null) { - $scope = $scope->popInFunctionCall(); - } - foreach ($args as $i => $arg) { if (!isset($parameters) || $parametersAcceptor === null) { continue; diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index d941d5c6a9..30d5caf4e8 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -13,6 +13,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\NamespaceAnswerer; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\TrinaryLogic; @@ -108,6 +109,9 @@ public function isInClosureBind(): bool; /** @return list */ public function getFunctionCallStack(): array; + /** @return list */ + public function getFunctionCallStackWithParameters(): array; + public function isParameterValueNullable(Param $parameter): bool; /** diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php new file mode 100644 index 0000000000..b26df715b8 --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php @@ -0,0 +1,42 @@ + */ +class ScopeFunctionCallStackWithParametersRule implements Rule +{ + + public function getNodeType(): string + { + return Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $messages = []; + foreach ($scope->getFunctionCallStackWithParameters() as [$reflection, $parameter]) { + if ($parameter === null) { + throw new ShouldNotHappenException(); + } + if ($reflection instanceof FunctionReflection) { + $messages[] = sprintf('%s ($%s)', $reflection->getName(), $parameter->getName()); + continue; + } + + $messages[] = sprintf('%s::%s ($%s)', $reflection->getDeclaringClass()->getDisplayName(), $reflection->getName(), $parameter->getName()); + } + + return [ + RuleErrorBuilder::message(implode("\n", $messages))->identifier('dummy')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php new file mode 100644 index 0000000000..38a0aecd61 --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php @@ -0,0 +1,41 @@ + + */ +class ScopeFunctionCallStackWithParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ScopeFunctionCallStackWithParametersRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/scope-function-call-stack.php'], [ + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 7, + ], + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 10, + ], + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 13, + ], + ]); + } + +}