diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 3f37564e95..1125b24364 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -48,18 +48,19 @@ public function getTypeFromFunctionCall( return null; } - $formatType = $scope->getType($args[0]->value); - if (count($args) === 1) { - return $this->getConstantType($args, null, $functionReflection, $scope); + $constantType = $this->getConstantType($args, $functionReflection, $scope); + if ($constantType !== null) { + return $constantType; } + $formatType = $scope->getType($args[0]->value); $formatStrings = $formatType->getConstantStrings(); if (count($formatStrings) === 0) { return null; } $singlePlaceholderEarlyReturn = null; - foreach ($formatType->getConstantStrings() as $constantString) { + foreach ($formatStrings 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] !== '') { @@ -80,9 +81,10 @@ 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' && $checkArgType->isString()->yes()) { - $singlePlaceholderEarlyReturn = $checkArgType; + if ($matches[2] === 's' + && ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes()) + ) { + $singlePlaceholderEarlyReturn = $checkArgType->toString(); } elseif ($matches[2] !== 's') { $singlePlaceholderEarlyReturn = new IntersectionType([ new StringType(), @@ -115,19 +117,19 @@ public function getTypeFromFunctionCall( $returnType = new StringType(); } - return $this->getConstantType($args, $returnType, $functionReflection, $scope); + return $returnType; } /** * @param Arg[] $args */ - private function getConstantType(array $args, ?Type $fallbackReturnType, FunctionReflection $functionReflection, Scope $scope): ?Type + private function getConstantType(array $args, FunctionReflection $functionReflection, Scope $scope): ?Type { $values = []; $combinationsCount = 1; foreach ($args as $arg) { if ($arg->unpack) { - return $fallbackReturnType; + return null; } $argType = $scope->getType($arg->value); @@ -142,7 +144,7 @@ private function getConstantType(array $args, ?Type $fallbackReturnType, Functio } if (count($constantScalarValues) === 0) { - return $fallbackReturnType; + return null; } $values[] = $constantScalarValues; @@ -150,7 +152,7 @@ private function getConstantType(array $args, ?Type $fallbackReturnType, Functio } if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { - return $fallbackReturnType; + return null; } $combinations = CombinationsHelper::combinations($values); @@ -158,7 +160,7 @@ private function getConstantType(array $args, ?Type $fallbackReturnType, Functio foreach ($combinations as $combination) { $format = array_shift($combination); if (!is_string($format)) { - return $fallbackReturnType; + return null; } try { @@ -168,12 +170,12 @@ private function getConstantType(array $args, ?Type $fallbackReturnType, Functio $returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination)); } } catch (Throwable) { - return $fallbackReturnType; + return null; } } if (count($returnTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { - return $fallbackReturnType; + return null; } return TypeCombinator::union(...$returnTypes); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11201.php b/tests/PHPStan/Analyser/nsrt/bug-11201.php index fa935a7756..74a41fa235 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11201.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11201.php @@ -27,6 +27,10 @@ function returnsJustString(): string return rand(0,1) === 1 ? 'foo' : ''; } +function returnsBool(): bool { + return true; +} + $s = sprintf("%s", returnsNonEmptyString()); assertType('non-empty-string', $s); @@ -41,3 +45,12 @@ function returnsJustString(): string $s = sprintf('%2$s', 1234, returnsNonFalsyString()); assertType('non-falsy-string', $s); + +$s = sprintf('%20s', 'abc'); +assertType("' abc'", $s); + +$s = sprintf('%20s', true); +assertType("' 1'", $s); + +$s = sprintf('%20s', returnsBool()); +assertType("non-falsy-string", $s); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php index 4ab6ef987c..3d67bdffb3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7387.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -23,7 +23,7 @@ public function inputTypes(int $i, float $f, string $s) { public function specifiers(int $i) { // https://3v4l.org/fmVIg - assertType('non-falsy-string', sprintf('%14s', $i)); + assertType('numeric-string', sprintf('%14s', $i)); assertType('numeric-string', sprintf('%d', $i)); @@ -45,9 +45,19 @@ public function specifiers(int $i) { } - public function positionalArgs($mixed, int $i, float $f, string $s) { + /** + * @param positive-int $posInt + * @param negative-int $negInt + * @param int<1, 5> $nonZeroIntRange + * @param int<-1, 5> $intRange + */ + public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, int $negInt, int $nonZeroIntRange, int $intRange) { // https://3v4l.org/vVL0c - assertType('non-falsy-string', sprintf('%2$14s', $mixed, $i)); + assertType('numeric-string', sprintf('%2$14s', $mixed, $i)); + assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $posInt)); + assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $negInt)); + assertType('numeric-string', sprintf('%2$14s', $mixed, $intRange)); + assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $nonZeroIntRange)); assertType('numeric-string', sprintf('%2$.14F', $mixed, $i)); assertType('numeric-string', sprintf('%2$.14F', $mixed, $f));