diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 8f1782b0a5..8829b4a4b8 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -125,32 +125,45 @@ public function getTypeFromFunctionCall( return $singlePlaceholderEarlyReturn; } + if ($allPatternsNonFalsy) { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + $isNonEmpty = $allPatternsNonEmpty; if ( - count($formatStrings) === 0 + !$isNonEmpty && $functionReflection->getName() === 'sprintf' - && count($args) === 2 + && count($args) >= 2 && $formatType->isNonEmptyString()->yes() - && $scope->getType($args[1]->value)->isNonEmptyString()->yes() ) { - $isNonEmpty = true; + $allArgsNonEmpty = true; + foreach ($args as $key => $arg) { + if ($key === 0) { + continue; + } + + if (!$scope->getType($arg->value)->toString()->isNonEmptyString()->yes()) { + $allArgsNonEmpty = false; + break; + } + } + + if ($allArgsNonEmpty) { + $isNonEmpty = true; + } } - if ($allPatternsNonFalsy) { - $returnType = new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); - } elseif ($isNonEmpty) { - $returnType = new IntersectionType([ + if ($isNonEmpty) { + return new IntersectionType([ new StringType(), new AccessoryNonEmptyStringType(), ]); - } else { - $returnType = new StringType(); } - return $returnType; + return new StringType(); } /** diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index 9e6ceed668..976071cb84 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -356,8 +356,12 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo assertType('string', sprintf($s, $nonFalsy)); assertType('string', sprintf($nonFalsy, $s)); assertType('non-empty-string', sprintf($nonEmpty, $nonEmpty)); - assertType('non-empty-string', sprintf($nonEmpty, $nonFalsy)); + assertType('non-empty-string', sprintf($nonEmpty, $nonEmpty, $nonEmpty)); + assertType('non-empty-string', sprintf($nonEmpty, $nonFalsy, $nonFalsy)); assertType('non-empty-string', sprintf($nonFalsy, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonEmpty, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonFalsy, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonFalsy, $nonFalsy)); assertType('string', vsprintf($s, [])); assertType('string', vsprintf($nonEmpty, [])); diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 801bfd2d22..6aecee85fe 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1034,4 +1034,9 @@ public function testBug10721(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10721.php'], []); } + public function testBug11491(): void + { + $this->analyse([__DIR__ . '/data/bug-11491.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-11491.php b/tests/PHPStan/Rules/Methods/data/bug-11491.php new file mode 100644 index 0000000000..9369b36610 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11491.php @@ -0,0 +1,49 @@ +id; + } + + /** @return non-empty-string */ + public function name(): string + { + return $this->name; + } + + /** @return non-empty-string */ + public function toFacetValue(): string + { + return sprintf( + '%s%s%s', + $this->name, + self::SEPARATOR, + $this->id, + ); + } +}