diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index da37080cfa..1cdd33ebf7 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -98,6 +98,11 @@ public static function reorderFuncArguments( return null; } + // return identical object if not reordered, as TypeSpecifier relies on object identity + if ($reorderedArgs === $functionCall->getArgs()) { + return $functionCall; + } + return new FuncCall( $functionCall->name, $reorderedArgs, @@ -116,6 +121,11 @@ public static function reorderMethodArguments( return null; } + // return identical object if not reordered, as TypeSpecifier relies on object identity + if ($reorderedArgs === $methodCall->getArgs()) { + return $methodCall; + } + return new MethodCall( $methodCall->var, $methodCall->name, @@ -135,6 +145,11 @@ public static function reorderStaticCallArguments( return null; } + // return identical object if not reordered, as TypeSpecifier relies on object identity + if ($reorderedArgs === $staticCall->getArgs()) { + return $staticCall; + } + return new StaticCall( $staticCall->class, $staticCall->name, @@ -154,6 +169,11 @@ public static function reorderNewArguments( return null; } + // return identical object if not reordered, as TypeSpecifier relies on object identity + if ($reorderedArgs === $new->getArgs()) { + return $new; + } + return new New_( $new->class, $reorderedArgs, diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 2b43724ff0..12bd0d2153 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -476,7 +476,15 @@ public function specifyTypesInCondition( } elseif ($expr instanceof FuncCall && $expr->name instanceof Name) { if ($this->reflectionProvider->hasFunction($expr->name, $scope)) { + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; + } + foreach ($this->getFunctionTypeSpecifyingExtensions() as $extension) { if (!$extension->isFunctionSupported($functionReflection, $expr, $context)) { continue; @@ -485,10 +493,10 @@ public function specifyTypesInCondition( return $extension->specifyTypes($functionReflection, $expr, $scope, $context); } - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + if ($parametersAcceptor === null) { + throw new ShouldNotHappenException(); + } $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { @@ -518,6 +526,14 @@ public function specifyTypesInCondition( $methodCalledOnType = $scope->getType($expr->var); $methodReflection = $scope->getMethodReflection($methodCalledOnType, $expr->name->name); if ($methodReflection !== null) { + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; + } + $referencedClasses = $methodCalledOnType->getObjectClassNames(); if ( count($referencedClasses) === 1 @@ -533,10 +549,10 @@ public function specifyTypesInCondition( } } - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + if ($parametersAcceptor === null) { + throw new ShouldNotHappenException(); + } $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { @@ -571,6 +587,14 @@ public function specifyTypesInCondition( $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name); if ($staticMethodReflection !== null) { + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; + } + $referencedClasses = $calleeType->getObjectClassNames(); if ( count($referencedClasses) === 1 @@ -586,10 +610,10 @@ public function specifyTypesInCondition( } } - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + if ($parametersAcceptor === null) { + throw new ShouldNotHappenException(); + } $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-13088.php b/tests/PHPStan/Analyser/nsrt/bug-13088.php new file mode 100644 index 0000000000..2963034496 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13088.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug13088; + +use function PHPStan\dumpType; +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(string $s, int $offset): void + { + if (preg_match('~msgstr "(.*)"\n~', $s, $matches, 0, $offset) === 1) { + assertType('array{non-falsy-string, string}', $matches); + } + } + + public function sayHello2(string $s, int $offset): void + { + if (preg_match('~msgstr "(.*)"\n~', $s, $matches, offset: $offset) === 1) { + assertType('array{non-falsy-string, string}', $matches); + } + } +}