diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index cec300a8da..3f37564e95 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -49,50 +49,56 @@ public function getTypeFromFunctionCall( } $formatType = $scope->getType($args[0]->value); + if (count($args) === 1) { + return $this->getConstantType($args, null, $functionReflection, $scope); + } - if (count($formatType->getConstantStrings()) > 0) { - $singlePlaceholderEarlyReturn = null; - foreach ($formatType->getConstantStrings() as $constantString) { - // The printf format is %[argnum$][flags][width][.precision] - if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) { - if ($matches[1] !== '') { - // invalid positional argument - if ($matches[1] === '0$') { - return null; - } - $checkArg = intval(substr($matches[1], 0, -1)); - } else { - $checkArg = 1; - } + $formatStrings = $formatType->getConstantStrings(); + if (count($formatStrings) === 0) { + return null; + } - // constant string specifies a numbered argument that does not exist - if (!array_key_exists($checkArg, $args)) { + $singlePlaceholderEarlyReturn = null; + foreach ($formatType->getConstantStrings() as $constantString) { + // The printf format is %[argnum$][flags][width][.precision] + if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) { + if ($matches[1] !== '') { + // invalid positional argument + if ($matches[1] === '0$') { return null; } + $checkArg = intval(substr($matches[1], 0, -1)); + } else { + $checkArg = 1; + } - // if the format string is just a placeholder and specified an argument - // of stringy type, then the return value will be of the same type - $checkArgType = $scope->getType($args[$checkArg]->value); - - if ($matches[2] === 's' && $checkArgType->isString()->yes()) { - $singlePlaceholderEarlyReturn = $checkArgType; - } elseif ($matches[2] !== 's') { - $singlePlaceholderEarlyReturn = new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - } + // constant string specifies a numbered argument that does not exist + if (!array_key_exists($checkArg, $args)) { + return null; + } - continue; + // if the format string is just a placeholder and specified an argument + // of stringy type, then the return value will be of the same type + $checkArgType = $scope->getType($args[$checkArg]->value); + + if ($matches[2] === 's' && $checkArgType->isString()->yes()) { + $singlePlaceholderEarlyReturn = $checkArgType; + } elseif ($matches[2] !== 's') { + $singlePlaceholderEarlyReturn = new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); } - $singlePlaceholderEarlyReturn = null; - break; + continue; } - if ($singlePlaceholderEarlyReturn !== null) { - return $singlePlaceholderEarlyReturn; - } + $singlePlaceholderEarlyReturn = null; + break; + } + + if ($singlePlaceholderEarlyReturn !== null) { + return $singlePlaceholderEarlyReturn; } if ($formatType->isNonFalsyString()->yes()) { @@ -115,11 +121,15 @@ public function getTypeFromFunctionCall( /** * @param Arg[] $args */ - private function getConstantType(array $args, Type $fallbackReturnType, FunctionReflection $functionReflection, Scope $scope): Type + private function getConstantType(array $args, ?Type $fallbackReturnType, FunctionReflection $functionReflection, Scope $scope): ?Type { $values = []; $combinationsCount = 1; foreach ($args as $arg) { + if ($arg->unpack) { + return $fallbackReturnType; + } + $argType = $scope->getType($arg->value); $constantScalarValues = $argType->getConstantScalarValues(); diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index 60a7b9ec93..55bb8ad77c 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -349,9 +349,9 @@ public function doFoo(string $s, string $nonEmpty, int $i, bool $bool, $constUni assertType('non-empty-string', preg_quote($nonEmpty)); assertType('string', sprintf($s)); - assertType('non-empty-string', sprintf($nonEmpty)); + assertType('string', sprintf($nonEmpty)); assertType('string', vsprintf($s, [])); - assertType('non-empty-string', vsprintf($nonEmpty, [])); + assertType('string', vsprintf($nonEmpty, [])); assertType('0', strlen('')); assertType('5', strlen('hallo')); diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index ebe223f864..fa1f1c16ed 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -74,7 +74,7 @@ function concat(string $s, string $nonFalsey, $numericS, $nonEmpty, $literalStri * @param non-empty-array $arrayOfNonFalsey * @param non-empty-array $nonEmptyArray */ - function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArray) + function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArray, array $arr) { assertType('string', implode($nonFalsey, [])); assertType('non-falsy-string', implode($nonFalsey, $nonEmptyArray)); @@ -104,8 +104,15 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', preg_quote($nonFalsey)); - assertType('non-falsy-string', sprintf($nonFalsey)); - assertType('non-falsy-string', vsprintf($nonFalsey, [])); + assertType('string', sprintf($nonFalsey)); + assertType("'foo'", sprintf('foo')); + assertType("string", sprintf(...$arr)); + assertType("non-falsy-string", sprintf('%s', ...$arr)); // should be 'string' + assertType('string', vsprintf($nonFalsey, [])); + assertType('string', vsprintf($nonFalsey, [])); + assertType("non-falsy-string", vsprintf('foo', [])); // should be 'foo' + assertType("non-falsy-string", vsprintf('%s', ...$arr)); // should be 'string' + assertType("string", vsprintf(...$arr)); assertType('int<1, max>', strlen($nonFalsey)); diff --git a/tests/PHPStan/Analyser/nsrt/printf-errors-php8.php b/tests/PHPStan/Analyser/nsrt/printf-errors-php8.php new file mode 100644 index 0000000000..872332b073 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/printf-errors-php8.php @@ -0,0 +1,11 @@ += 8.0 + +namespace PrintFErrorsPhp8; + +use function PHPStan\Testing\assertType; + +function doFoo() +{ + assertType("string", sprintf('%s')); // error + assertType("string", vsprintf('%s')); // error +}