diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index ad7b58c227..6d0051d213 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -33,7 +33,7 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { @@ -43,7 +43,13 @@ public function getTypeFromFunctionCall( $formatType = $scope->getType($args[0]->value); if ($formatType instanceof ConstantStringType) { - if (preg_match('/^%[0-9]*\.?[0-9]+[bdeEfFgGhHouxX]$/', $formatType->getValue()) === 1) { + // The printf format is %[argnum$][flags][width][.precision] + if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]+[bdeEfFgGhHouxX]$/', $formatType->getValue(), $matches) === 1) { + // invalid positional argument + if (array_key_exists(1, $matches) && $matches[1] === '0$') { + return null; + } + return new IntersectionType([ new StringType(), new AccessoryNumericStringType(), diff --git a/tests/PHPStan/Analyser/data/bug-7387.php b/tests/PHPStan/Analyser/data/bug-7387.php index ef8ab3d44d..f13038a1ee 100644 --- a/tests/PHPStan/Analyser/data/bug-7387.php +++ b/tests/PHPStan/Analyser/data/bug-7387.php @@ -42,4 +42,27 @@ public function specifiers(int $i) { assertType('numeric-string', sprintf('%14X', $i)); } + + public function positionalArgs($mixed, int $i, float $f, string $s) { + // https://3v4l.org/vVL0c + assertType('non-empty-string', sprintf('%2$14s', $mixed, $i)); + + assertType('numeric-string', sprintf('%2$.14F', $mixed, $i)); + assertType('numeric-string', sprintf('%2$.14F', $mixed, $f)); + assertType('numeric-string', sprintf('%2$.14F', $mixed, $s)); + + assertType('numeric-string', sprintf('%2$1.14F', $mixed, $i)); + assertType('numeric-string', sprintf('%2$2.14F', $mixed, $f)); + assertType('numeric-string', sprintf('%2$3.14F', $mixed, $s)); + + assertType('numeric-string', sprintf('%2$14F', $mixed, $i)); + assertType('numeric-string', sprintf('%2$14F', $mixed, $f)); + assertType('numeric-string', sprintf('%2$14F', $mixed, $s)); + + assertType('numeric-string', sprintf('%10$14F', $mixed, $s)); + } + + public function invalidPositionalArgFormat($mixed, string $s) { + assertType('string', sprintf('%0$14F', $mixed, $s)); + } }