diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 4688917e1f..26cc34d860 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -84,13 +84,13 @@ public function getTypeFromFunctionCall( } // The printf format is %[argnum$][flags][width][.precision]specifier. - if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) { - if ($matches[1] !== '') { + if (preg_match('/^%(?P[0-9]*\$)?(?P[0-9]*)\.?[0-9]*(?P[sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) { + if ($matches['argnum'] !== '') { // invalid positional argument - if ($matches[1] === '0$') { + if ($matches['argnum'] === '0$') { return null; } - $checkArg = intval(substr($matches[1], 0, -1)); + $checkArg = intval(substr($matches['argnum'], 0, -1)); } else { $checkArg = 1; } @@ -103,11 +103,13 @@ public function getTypeFromFunctionCall( // 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' + if ( + $matches['specifier'] === 's' + && ($checkArgType->isConstantValue()->no() || $matches['width'] === '') && ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes()) ) { $singlePlaceholderEarlyReturn = $checkArgType->toString(); - } elseif ($matches[2] !== 's') { + } elseif ($matches['specifier'] !== 's') { $singlePlaceholderEarlyReturn = new IntersectionType([ new StringType(), new AccessoryNumericStringType(), diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php index 4623719b4d..0081826133 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7387.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -59,6 +59,13 @@ public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, assertType('numeric-string', sprintf('%2$14s', $mixed, $intRange)); assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $nonZeroIntRange)); + assertType("non-falsy-string", sprintf('%2$14s', $mixed, 1)); + assertType("non-falsy-string", sprintf('%2$14s', $mixed, '1')); + assertType("non-falsy-string", sprintf('%2$14s', $mixed, 'abc')); + assertType("'1'", sprintf('%2$s', $mixed, 1)); + assertType("'1'", sprintf('%2$s', $mixed, '1')); + assertType("'abc'", sprintf('%2$s', $mixed, 'abc')); + assertType('numeric-string', sprintf('%2$.14F', $mixed, $i)); assertType('numeric-string', sprintf('%2$.14F', $mixed, $f)); assertType('numeric-string', sprintf('%2$.14F', $mixed, $s));