diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 4e00f1d8ec..2a9b991915 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -155,37 +155,50 @@ public function getTypeFromFunctionCall( } $isNonEmpty = $allPatternsNonEmpty; - if ( - !$isNonEmpty - && $functionReflection->getName() === 'sprintf' - && count($args) >= 2 - && $formatType->isNonEmptyString()->yes() - ) { - $allArgsNonEmpty = true; + if (!$isNonEmpty && $formatType->isNonEmptyString()->yes()) { + $isNonEmpty = $this->allValuesSatisfies( + $functionReflection, + $scope, + $args, + static fn (Type $type): bool => $type->toString()->isNonEmptyString()->yes() + ); + } + + if ($isNonEmpty) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + + return new StringType(); + } + + /** + * @param array $args + * @param callable(Type): bool $cb + */ + private function allValuesSatisfies(FunctionReflection $functionReflection, Scope $scope, array $args, callable $cb): bool + { + if ($functionReflection->getName() === 'sprintf' && count($args) >= 2) { foreach ($args as $key => $arg) { if ($key === 0) { continue; } - if (!$scope->getType($arg->value)->toString()->isNonEmptyString()->yes()) { - $allArgsNonEmpty = false; - break; + if (!$cb($scope->getType($arg->value))) { + return false; } } - if ($allArgsNonEmpty) { - $isNonEmpty = true; - } + return true; } - if ($isNonEmpty) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + if ($functionReflection->getName() === 'vsprintf' && count($args) >= 2) { + return $cb($scope->getType($args[1]->value)->getIterableValueType()); } - return new StringType(); + return false; } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php index ad53206b25..fb7468ca33 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7387.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -110,7 +110,7 @@ public function vsprintf(array $array) assertType('numeric-string', vsprintf("%4d", explode('-', '1988-8-1'))); assertType('numeric-string', vsprintf("%4d", $array)); assertType('numeric-string', vsprintf("%4d", ['123'])); - assertType('string', vsprintf("%s", ['123'])); // could be '123' + assertType('non-empty-string', vsprintf("%s", ['123'])); // could be '123' // too many arguments.. php silently allows it assertType('numeric-string', vsprintf("%4d", ['123', '456'])); } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index 19a5d6b96a..da8426c905 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -364,6 +364,13 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo assertType('non-empty-string', sprintf($nonFalsy, $nonFalsy, $nonFalsy)); assertType('string', vsprintf($s, [])); assertType('string', vsprintf($nonEmpty, [])); + assertType('non-empty-string', vsprintf($nonEmpty, [$nonEmpty])); + assertType('non-empty-string', vsprintf($nonEmpty, [$nonEmpty, $nonEmpty])); + assertType('non-empty-string', vsprintf($nonEmpty, [$nonFalsy, $nonFalsy])); + assertType('non-empty-string', vsprintf($nonFalsy, [$nonEmpty])); + assertType('non-empty-string', vsprintf($nonFalsy, [$nonEmpty, $nonEmpty])); + assertType('non-empty-string', vsprintf($nonFalsy, [$nonFalsy, $nonEmpty])); + assertType('non-empty-string', vsprintf($nonFalsy, [$nonFalsy, $nonFalsy])); assertType('non-empty-string', sprintf("%s0%s", $s, $s)); assertType('non-empty-string', sprintf("%s0%s%s%s%s", $s, $s, $s, $s, $s));