From 1d92feb556e04fd674460650d0265d7c5bccf688 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 6 Jan 2025 16:02:57 +0100 Subject: [PATCH 1/3] More precise implode() return type --- src/Type/Php/ImplodeFunctionReturnTypeExtension.php | 11 ++++++----- tests/PHPStan/Analyser/nsrt/bug-11201.php | 2 +- tests/PHPStan/Analyser/nsrt/implode.php | 12 ++++++++++++ tests/PHPStan/Analyser/nsrt/non-empty-string.php | 4 ++-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index a052a43416..8113ce34e0 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -81,22 +81,23 @@ private function implode(Type $arrayType, Type $separatorType): Type } $accessoryTypes = []; + $valueTypeAsString = $arrayType->getIterableValueType()->toString(); if ($arrayType->isIterableAtLeastOnce()->yes()) { - if ($arrayType->getIterableValueType()->isNonFalsyString()->yes() || $separatorType->isNonFalsyString()->yes()) { + if ($valueTypeAsString->isNonFalsyString()->yes() || $separatorType->isNonFalsyString()->yes()) { $accessoryTypes[] = new AccessoryNonFalsyStringType(); - } elseif ($arrayType->getIterableValueType()->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { + } elseif ($valueTypeAsString->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { $accessoryTypes[] = new AccessoryNonEmptyStringType(); } } // implode is one of the four functions that can produce literal strings as blessed by the original RFC: wiki.php.net/rfc/is_literal - if ($arrayType->getIterableValueType()->isLiteralString()->yes() && $separatorType->isLiteralString()->yes()) { + if ($valueTypeAsString->isLiteralString()->yes() && $separatorType->isLiteralString()->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); } - if ($arrayType->getIterableValueType()->isLowercaseString()->yes() && $separatorType->isLowercaseString()->yes()) { + if ($valueTypeAsString->isLowercaseString()->yes() && $separatorType->isLowercaseString()->yes()) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } - if ($arrayType->getIterableValueType()->isUppercaseString()->yes() && $separatorType->isUppercaseString()->yes()) { + if ($valueTypeAsString->isUppercaseString()->yes() && $separatorType->isUppercaseString()->yes()) { $accessoryTypes[] = new AccessoryUppercaseStringType(); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11201.php b/tests/PHPStan/Analyser/nsrt/bug-11201.php index 202e5b1700..705e237a85 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11201.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11201.php @@ -41,7 +41,7 @@ function returnsBool(): bool { assertType('string', $s); $s = sprintf("%s", implode(', ', array_map('intval', returnsArray()))); -assertType('string', $s); +assertType('lowercase-string&uppercase-string', $s); $s = sprintf('%2$s', 1234, returnsNonFalsyString()); assertType('non-falsy-string', $s); diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php index 51e121a4c1..32b82e4e3b 100644 --- a/tests/PHPStan/Analyser/nsrt/implode.php +++ b/tests/PHPStan/Analyser/nsrt/implode.php @@ -6,6 +6,18 @@ class Foo { + /** + * @param array $arr + */ + public function ints(array $arr, int $i) + { + assertType("lowercase-string&uppercase-string", implode($arr)); + assertType("lowercase-string&non-empty-string&uppercase-string", implode([$i, $i])); + if ($i !== 0) { + assertType("lowercase-string&non-falsy-string&uppercase-string", implode([$i, $i])); + } + } + const X = 'x'; const ONE = 1; diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index cd831db4d8..189ab72272 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -212,7 +212,7 @@ public function sayHello(int $i): void // coming from issue #5291 $s = array(1, $i); - assertType('non-falsy-string', implode("a", $s)); + assertType('lowercase-string&non-falsy-string', implode("a", $s)); } /** @@ -233,7 +233,7 @@ public function sayHello2(int $i): void // coming from issue #5291 $s = array(1, $i); - assertType('non-falsy-string', join("a", $s)); + assertType('lowercase-string&non-falsy-string', join("a", $s)); } /** From b53b92f2fd478eada3870e091a78330ab208c0ce Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 6 Jan 2025 17:51:09 +0100 Subject: [PATCH 2/3] More precise string-casing functions --- src/Type/Php/StrCaseFunctionsReturnTypeExtension.php | 11 ++++++----- tests/PHPStan/Analyser/nsrt/non-falsy-string.php | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index db36561ab6..c6226f5a5f 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -142,18 +142,19 @@ public function getTypeFromFunctionCall( } $accessoryTypes = []; - if ($forceLowercase || ($keepLowercase && $argType->isLowercaseString()->yes())) { + $argStringType = $argType->toString(); + if ($forceLowercase || ($keepLowercase && $argStringType->isLowercaseString()->yes())) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } - if ($forceUppercase || ($keepUppercase && $argType->isUppercaseString()->yes())) { + if ($forceUppercase || ($keepUppercase && $argStringType->isUppercaseString()->yes())) { $accessoryTypes[] = new AccessoryUppercaseStringType(); } - if ($argType->isNumericString()->yes()) { + if ($argStringType->isNumericString()->yes()) { $accessoryTypes[] = new AccessoryNumericStringType(); - } elseif ($argType->isNonFalsyString()->yes()) { + } elseif ($argStringType->isNonFalsyString()->yes()) { $accessoryTypes[] = new AccessoryNonFalsyStringType(); - } elseif ($argType->isNonEmptyString()->yes()) { + } elseif ($argStringType->isNonEmptyString()->yes()) { $accessoryTypes[] = new AccessoryNonEmptyStringType(); } diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index c5fd9fc1d8..598a358927 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -87,6 +87,7 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', escapeshellarg($nonFalsey)); assertType('non-falsy-string', escapeshellcmd($nonFalsey)); + assertType('non-falsy-string&uppercase-string', strtoupper($s ?: 1)); assertType('non-falsy-string&uppercase-string', strtoupper($nonFalsey)); assertType('lowercase-string&non-falsy-string', strtolower($nonFalsey)); assertType('non-falsy-string&uppercase-string', mb_strtoupper($nonFalsey)); From 224d41d9d852be07da592899cbbad033f1c368f3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 6 Jan 2025 17:58:54 +0100 Subject: [PATCH 3/3] More precise string-containing functions --- src/Type/Php/StrContainingTypeSpecifyingExtension.php | 2 +- tests/PHPStan/Analyser/nsrt/implode.php | 2 +- .../nsrt/non-empty-string-str-containing-fns.php | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/StrContainingTypeSpecifyingExtension.php b/src/Type/Php/StrContainingTypeSpecifyingExtension.php index 8cf678ae56..1f1a0e168e 100644 --- a/src/Type/Php/StrContainingTypeSpecifyingExtension.php +++ b/src/Type/Php/StrContainingTypeSpecifyingExtension.php @@ -66,7 +66,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n [$hackstackArg, $needleArg] = self::STR_CONTAINING_FUNCTIONS[strtolower($functionReflection->getName())]; $haystackType = $scope->getType($args[$hackstackArg]->value); - $needleType = $scope->getType($args[$needleArg]->value); + $needleType = $scope->getType($args[$needleArg]->value)->toString(); if ($needleType->isNonEmptyString()->yes() && $haystackType->isString()->yes()) { $accessories = [ diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php index 32b82e4e3b..211524be05 100644 --- a/tests/PHPStan/Analyser/nsrt/implode.php +++ b/tests/PHPStan/Analyser/nsrt/implode.php @@ -61,6 +61,6 @@ public function constArrays5($constArr) { /** @param array{0: 1, 1: 'a'|'b', 3?: 'c'|'d', 4?: 'e'|'f', 5?: 'g'|'h', 6?: 'x'|'y'} $constArr */ public function constArrays6($constArr) { - assertType("string", implode('', $constArr)); + assertType("literal-string&lowercase-string&non-falsy-string", implode('', $constArr)); } } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php index 12d3495098..19482b7fd0 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php @@ -14,6 +14,16 @@ class Foo { */ public function strContains(string $s, string $s2, $nonES, $nonFalsy, $numS, $literalS, $nonEAndNumericS, int $i): void { + if (str_contains($i, 0)) { + assertType('int', $i); + } + if (str_contains($s, 0)) { + assertType('non-empty-string', $s); + } + if (str_contains($s, 1)) { + assertType('non-falsy-string', $s); + } + if (str_contains($s, ':')) { assertType('non-falsy-string', $s); }