diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 2a9b991915..7bb7e2da26 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -23,6 +23,7 @@ use function array_fill; use function array_key_exists; use function array_shift; +use function array_values; use function count; use function in_array; use function intval; @@ -95,14 +96,13 @@ public function getTypeFromFunctionCall( $checkArg = 1; } - // constant string specifies a numbered argument that does not exist - if (!array_key_exists($checkArg, $args)) { + $checkArgType = $this->getValueType($functionReflection, $scope, $args, $checkArg); + if ($checkArgType === null) { return null; } // 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['specifier'] === 's' && ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes()) @@ -201,6 +201,48 @@ private function allValuesSatisfies(FunctionReflection $functionReflection, Scop return false; } + /** + * @param Arg[] $args + */ + private function getValueType(FunctionReflection $functionReflection, Scope $scope, array $args, int $argNumber): ?Type + { + if ($functionReflection->getName() === 'sprintf') { + // constant string specifies a numbered argument that does not exist + if (!array_key_exists($argNumber, $args)) { + return null; + } + + return $scope->getType($args[$argNumber]->value); + } + + if ($functionReflection->getName() === 'vsprintf') { + if (!array_key_exists(1, $args)) { + return null; + } + + $valuesType = $scope->getType($args[1]->value); + $resultTypes = []; + + $valuesConstantArrays = $valuesType->getConstantArrays(); + foreach ($valuesConstantArrays as $valuesConstantArray) { + // vsprintf does not care about the keys of the array, only the order + $types = array_values($valuesConstantArray->getValueTypes()); + if (!array_key_exists($argNumber - 1, $types)) { + return null; + } + + $resultTypes[] = $types[$argNumber - 1]; + } + if (count($resultTypes) === 0) { + return $valuesType->getIterableValueType(); + } + + return TypeCombinator::union(...$resultTypes); + } + + return null; + } + /** * Detect constant strings in the format which neither depend on placeholders nor on given value arguments. */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php index fb7468ca33..cfb1a97642 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('non-empty-string', vsprintf("%s", ['123'])); // could be '123' + assertType('\'123\'', vsprintf("%s", ['123'])); // too many arguments.. php silently allows it assertType('numeric-string', vsprintf("%4d", ['123', '456'])); }