From 5ee90b96e3e65c2a484724b5d567ad13c30d447a Mon Sep 17 00:00:00 2001
From: Jan Nedbal <janedbal@gmail.com>
Date: Wed, 29 May 2024 15:02:27 +0200
Subject: [PATCH 1/3] QueryResultTypeWalker: fix TypedExpression handling;
 COUNT(x) is always int

---
 .../Doctrine/Query/QueryResultTypeWalker.php  | 25 +-------
 .../Query/QueryResultTypeWalkerTest.php       | 57 ++++++++++---------
 2 files changed, 32 insertions(+), 50 deletions(-)

diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php
index d68bcdb8..08fc3bb4 100644
--- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php
+++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php
@@ -3,7 +3,6 @@
 namespace PHPStan\Type\Doctrine\Query;
 
 use BackedEnum;
-use Doctrine\DBAL\Types\Types;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Mapping\ClassMetadata;
 use Doctrine\ORM\Query;
@@ -816,28 +815,8 @@ public function walkSelectExpression($selectExpression): string
 			$resultAlias = $selectExpression->fieldIdentificationVariable ?? $this->scalarResultCounter++;
 			$type = $this->unmarshalType($expr->dispatch($this));
 
-			if (class_exists(TypedExpression::class) && $expr instanceof TypedExpression) {
-				$enforcedType = $this->resolveDoctrineType(Types::INTEGER);
-				$type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($enforcedType): Type {
-					if ($type instanceof UnionType || $type instanceof IntersectionType) {
-						return $traverse($type);
-					}
-					if ($type instanceof NullType) {
-						return $type;
-					}
-					if ($enforcedType->accepts($type, true)->yes()) {
-						return $type;
-					}
-					if ($enforcedType instanceof StringType) {
-						if ($type instanceof IntegerType || $type instanceof FloatType) {
-							return TypeCombinator::union($type->toString(), $type);
-						}
-						if ($type instanceof BooleanType) {
-							return TypeCombinator::union($type->toInteger()->toString(), $type);
-						}
-					}
-					return $enforcedType;
-				});
+			if ($expr instanceof TypedExpression) {
+				$type = $this->resolveDoctrineType($expr->getReturnType()->getName(), null, TypeCombinator::containsNull($type));
 			} else {
 				// Expressions default to Doctrine's StringType, whose
 				// convertToPHPValue() is a no-op. So the actual type depends on
diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
index 983362c2..0c2b1af0 100644
--- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
+++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
@@ -9,7 +9,6 @@
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Mapping\Column;
-use Doctrine\ORM\Query\AST\TypedExpression;
 use Doctrine\ORM\Tools\SchemaTool;
 use PHPStan\Testing\PHPStanTestCase;
 use PHPStan\Type\Accessory\AccessoryNumericStringType;
@@ -611,9 +610,7 @@ public function getTestData(): iterable
 				],
 				[
 					new ConstantIntegerType(3),
-					$this->hasTypedExpressions()
-					? $this->uint()
-					: $this->uintStringified(),
+					new IntegerType(),
 				],
 				[
 					new ConstantIntegerType(4),
@@ -621,9 +618,7 @@ public function getTestData(): iterable
 				],
 				[
 					new ConstantIntegerType(5),
-					$this->hasTypedExpressions()
-					? $this->uint()
-					: $this->uintStringified(),
+					new IntegerType(),
 				],
 				[
 					new ConstantIntegerType(6),
@@ -645,6 +640,29 @@ public function getTestData(): iterable
 			',
 		];
 
+		yield 'count' => [
+			$this->constantArray([
+				[
+					new ConstantIntegerType(1),
+					new IntegerType(),
+				],
+				[
+					new ConstantIntegerType(2),
+					new IntegerType(),
+				],
+				[
+					new ConstantIntegerType(3),
+					new IntegerType(),
+				],
+			]),
+			'
+				SELECT		COUNT(m.stringNullColumn),
+							COUNT(m.stringColumn),
+					 		COUNT(m)
+				FROM		QueryResult\Entities\Many m
+			',
+		];
+
 		yield 'aggregate lowercase' => [
 			$this->constantArray([
 				[
@@ -678,9 +696,7 @@ public function getTestData(): iterable
 				],
 				[
 					new ConstantStringType('count'),
-					$this->hasTypedExpressions()
-					? $this->uint()
-					: $this->uintStringified(),
+					new IntegerType(),
 				],
 			]),
 			'
@@ -1346,23 +1362,17 @@ public function getTestData(): iterable
 			$this->constantArray([
 				[
 					new ConstantIntegerType(1),
-					$this->hasTypedExpressions()
-					? $this->uint()
-					: $this->uintStringified(),
+					new IntegerType(),
 				],
 				[
 					new ConstantIntegerType(2),
 					TypeCombinator::addNull(
-						$this->hasTypedExpressions()
-						? $this->uint()
-						: $this->uintStringified()
+						new IntegerType()
 					),
 				],
 				[
 					new ConstantIntegerType(3),
-					$this->hasTypedExpressions()
-					? $this->uint()
-					: $this->uintStringified(),
+					new IntegerType(),
 				],
 			]),
 			'
@@ -1554,9 +1564,7 @@ public function getTestData(): iterable
 				[new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())],
 				[
 					new ConstantIntegerType(2),
-					$this->hasTypedExpressions()
-					? $this->uint()
-					: $this->uintStringified(),
+					new IntegerType(),
 				],
 			]),
 			'
@@ -1678,11 +1686,6 @@ private function unumericStringified(): Type
 		);
 	}
 
-	private function hasTypedExpressions(): bool
-	{
-		return class_exists(TypedExpression::class);
-	}
-
 	/**
 	 * @param array<mixed[]> $arrays
 	 *

From 3049ecd6d0e5ba71de6ec62cd5a77bfe22a1f538 Mon Sep 17 00:00:00 2001
From: Jan Nedbal <janedbal@gmail.com>
Date: Wed, 29 May 2024 15:27:25 +0200
Subject: [PATCH 2/3] Ensure int range is kept

---
 .../Doctrine/Query/QueryResultTypeWalker.php  |  9 +++++--
 .../Query/QueryResultTypeWalkerTest.php       | 27 +++++++++++--------
 2 files changed, 23 insertions(+), 13 deletions(-)

diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php
index 08fc3bb4..8694d3a6 100644
--- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php
+++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php
@@ -360,9 +360,11 @@ public function walkFunction($function): string
 			case $function instanceof AST\Functions\MaxFunction:
 			case $function instanceof AST\Functions\MinFunction:
 			case $function instanceof AST\Functions\SumFunction:
-			case $function instanceof AST\Functions\CountFunction:
 				return $function->getSql($this);
 
+			case $function instanceof AST\Functions\CountFunction:
+				return $this->marshalType(IntegerRangeType::fromInterval(0, null));
+
 			case $function instanceof AST\Functions\AbsFunction:
 				$exprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->simpleArithmeticExpression));
 
@@ -816,7 +818,10 @@ public function walkSelectExpression($selectExpression): string
 			$type = $this->unmarshalType($expr->dispatch($this));
 
 			if ($expr instanceof TypedExpression) {
-				$type = $this->resolveDoctrineType($expr->getReturnType()->getName(), null, TypeCombinator::containsNull($type));
+				$type = TypeCombinator::intersect(
+					$type,
+					$this->resolveDoctrineType($expr->getReturnType()->getName(), null, TypeCombinator::containsNull($type))
+				);
 			} else {
 				// Expressions default to Doctrine's StringType, whose
 				// convertToPHPValue() is a no-op. So the actual type depends on
diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
index 0c2b1af0..9801fd5f 100644
--- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
+++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
@@ -610,7 +610,7 @@ public function getTestData(): iterable
 				],
 				[
 					new ConstantIntegerType(3),
-					new IntegerType(),
+					$this->uint(),
 				],
 				[
 					new ConstantIntegerType(4),
@@ -618,7 +618,7 @@ public function getTestData(): iterable
 				],
 				[
 					new ConstantIntegerType(5),
-					new IntegerType(),
+					$this->uint(),
 				],
 				[
 					new ConstantIntegerType(6),
@@ -644,15 +644,15 @@ public function getTestData(): iterable
 			$this->constantArray([
 				[
 					new ConstantIntegerType(1),
-					new IntegerType(),
+					$this->uint(),
 				],
 				[
 					new ConstantIntegerType(2),
-					new IntegerType(),
+					$this->uint(),
 				],
 				[
 					new ConstantIntegerType(3),
-					new IntegerType(),
+					$this->uint(),
 				],
 			]),
 			'
@@ -696,7 +696,7 @@ public function getTestData(): iterable
 				],
 				[
 					new ConstantStringType('count'),
-					new IntegerType(),
+					$this->uint(),
 				],
 			]),
 			'
@@ -1362,23 +1362,28 @@ public function getTestData(): iterable
 			$this->constantArray([
 				[
 					new ConstantIntegerType(1),
-					new IntegerType(),
+					$this->uint(),
 				],
 				[
 					new ConstantIntegerType(2),
 					TypeCombinator::addNull(
-						new IntegerType()
+						$this->uint()
 					),
 				],
 				[
 					new ConstantIntegerType(3),
-					new IntegerType(),
+					$this->uint(),
+				],
+				[
+					new ConstantIntegerType(4),
+					$this->uint(),
 				],
 			]),
 			'
 				SELECT		LENGTH(m.stringColumn),
 							LENGTH(m.stringNullColumn),
-							LENGTH(\'foo\')
+							LENGTH(\'foo\'),
+							LENGTH(COALESCE(m.stringNullColumn, \'\'))
 				FROM		QueryResult\Entities\Many m
 			',
 		];
@@ -1564,7 +1569,7 @@ public function getTestData(): iterable
 				[new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())],
 				[
 					new ConstantIntegerType(2),
-					new IntegerType(),
+					$this->uint(),
 				],
 			]),
 			'

From 0c75dec1390e746366a7729155b117532d923ad6 Mon Sep 17 00:00:00 2001
From: Jan Nedbal <janedbal@gmail.com>
Date: Wed, 29 May 2024 15:36:02 +0200
Subject: [PATCH 3/3] Fix dbal v4

---
 src/Type/Doctrine/Query/QueryResultTypeWalker.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php
index 8694d3a6..b053a696 100644
--- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php
+++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php
@@ -3,6 +3,7 @@
 namespace PHPStan\Type\Doctrine\Query;
 
 use BackedEnum;
+use Doctrine\DBAL\Types\Type as DbalType;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Mapping\ClassMetadata;
 use Doctrine\ORM\Query;
@@ -820,7 +821,7 @@ public function walkSelectExpression($selectExpression): string
 			if ($expr instanceof TypedExpression) {
 				$type = TypeCombinator::intersect(
 					$type,
-					$this->resolveDoctrineType($expr->getReturnType()->getName(), null, TypeCombinator::containsNull($type))
+					$this->resolveDoctrineType(DbalType::lookupName($expr->getReturnType()), null, TypeCombinator::containsNull($type))
 				);
 			} else {
 				// Expressions default to Doctrine's StringType, whose