From 0ccbd3fe992dd2a9f9dd0bbb9ca2acf6fa651d58 Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Mon, 6 Jan 2025 16:02:57 +0100
Subject: [PATCH 1/4] 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<int> $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 30dcd00bf97c5d7aba6aad419039635ea819f4ef Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Mon, 6 Jan 2025 17:51:09 +0100
Subject: [PATCH 2/4] 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 5d6ddf3d2e9519903393d478c99c2d74871a8f12 Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Mon, 6 Jan 2025 17:58:54 +0100
Subject: [PATCH 3/4] 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);
 		}

From ed682a9f670fc4602d61016b79b520922dacf49e Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Sat, 15 Feb 2025 11:16:41 +0100
Subject: [PATCH 4/4] fix

---
 src/Type/Php/ImplodeFunctionReturnTypeExtension.php | 2 +-
 tests/PHPStan/Analyser/nsrt/implode.php             | 2 +-
 tests/PHPStan/Analyser/nsrt/non-empty-string.php    | 1 +
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php
index 8113ce34e0..15d1d86706 100644
--- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php
+++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php
@@ -91,7 +91,7 @@ private function implode(Type $arrayType, Type $separatorType): Type
 		}
 
 		// 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 ($valueTypeAsString->isLiteralString()->yes() && $separatorType->isLiteralString()->yes()) {
+		if ($arrayType->getIterableValueType()->isLiteralString()->yes() && $separatorType->isLiteralString()->yes()) {
 			$accessoryTypes[] = new AccessoryLiteralStringType();
 		}
 		if ($valueTypeAsString->isLowercaseString()->yes() && $separatorType->isLowercaseString()->yes()) {
diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php
index 211524be05..5c96b64e67 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("literal-string&lowercase-string&non-falsy-string", implode('', $constArr));
+		assertType("lowercase-string&non-falsy-string", implode('', $constArr));
 	}
 }
diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php
index 189ab72272..11adfa5dcb 100644
--- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php
+++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php
@@ -213,6 +213,7 @@ public function sayHello(int $i): void
 		$s = array(1, $i);
 
 		assertType('lowercase-string&non-falsy-string', implode("a", $s));
+		assertType('non-falsy-string&uppercase-string', implode("A", $s));
 	}
 
 	/**