diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 7bb7e2da26..dc30f2afdd 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -8,9 +8,11 @@ use PHPStan\Internal\CombinationsHelper; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -60,6 +62,13 @@ public function getTypeFromFunctionCall( $formatType = $scope->getType($args[0]->value); $formatStrings = $formatType->getConstantStrings(); + $isLowercase = $formatType->isLowercaseString()->yes() && $this->allValuesSatisfies( + $functionReflection, + $scope, + $args, + static fn (Type $type): bool => $type->toString()->isLowercaseString()->yes() + ); + $singlePlaceholderEarlyReturn = null; $allPatternsNonEmpty = count($formatStrings) !== 0; $allPatternsNonFalsy = count($formatStrings) !== 0; @@ -130,10 +139,10 @@ public function getTypeFromFunctionCall( $singlePlaceholderEarlyReturn = $checkArgType->toString(); } elseif ($matches['specifier'] !== 's') { - $singlePlaceholderEarlyReturn = new IntersectionType([ - new StringType(), + $singlePlaceholderEarlyReturn = $this->getStringReturnType( new AccessoryNumericStringType(), - ]); + $isLowercase, + ); } continue; @@ -148,10 +157,7 @@ public function getTypeFromFunctionCall( } if ($allPatternsNonFalsy) { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); + return $this->getStringReturnType(new AccessoryNonFalsyStringType(), $isLowercase); } $isNonEmpty = $allPatternsNonEmpty; @@ -165,13 +171,10 @@ public function getTypeFromFunctionCall( } if ($isNonEmpty) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + return $this->getStringReturnType(new AccessoryNonEmptyStringType(), $isLowercase); } - return new StringType(); + return $this->getStringReturnType(null, $isLowercase); } /** @@ -347,4 +350,23 @@ private function getConstantType(array $args, FunctionReflection $functionReflec return TypeCombinator::union(...$returnTypes); } + private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowercase): Type + { + $accessoryTypes = []; + if ($accessoryType !== null) { + $accessoryTypes[] = $accessoryType; + } + if ($isLowercase) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + + if (count($accessoryTypes) === 0) { + return new StringType(); + } + + $accessoryTypes[] = new StringType(); + + return new IntersectionType($accessoryTypes); + } + } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11201.php b/tests/PHPStan/Analyser/nsrt/bug-11201.php index 74a41fa235..202e5b1700 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11201.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11201.php @@ -53,4 +53,4 @@ function returnsBool(): bool { assertType("' 1'", $s); $s = sprintf('%20s', returnsBool()); -assertType("non-falsy-string", $s); +assertType("lowercase-string&non-falsy-string", $s); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php index cfb1a97642..909de25e1f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7387.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -107,11 +107,11 @@ public function escapedPercent(int $i) { public function vsprintf(array $array) { - assertType('numeric-string', vsprintf("%4d", explode('-', '1988-8-1'))); + assertType('lowercase-string&numeric-string', vsprintf("%4d", explode('-', '1988-8-1'))); assertType('numeric-string', vsprintf("%4d", $array)); - assertType('numeric-string', vsprintf("%4d", ['123'])); + assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123'])); assertType('\'123\'', vsprintf("%s", ['123'])); // too many arguments.. php silently allows it - assertType('numeric-string', vsprintf("%4d", ['123', '456'])); + assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123', '456'])); } } diff --git a/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php index ec25be47cd..3555613fe0 100644 --- a/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php +++ b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php @@ -33,7 +33,7 @@ public function integerRange(int $a, string $b): void */ public function tooBigRange(int $a, string $b): void { - assertType("non-falsy-string", sprintf('%d %s', $a, $b)); + assertType("lowercase-string&non-falsy-string", sprintf('%d %s', $a, $b)); } } diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-sprintf.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-sprintf.php new file mode 100644 index 0000000000..db7127a15c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-sprintf.php @@ -0,0 +1,116 @@ +