From 03adf063ef9c1aa33e2b1829606fda984a755142 Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Sun, 13 Apr 2025 22:19:33 +0200
Subject: [PATCH 01/13] Fix MixedType->equals(ErrorType)

---
 src/Type/MixedType.php               |  3 +-
 tests/PHPStan/Type/MixedTypeTest.php | 45 ++++++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php
index 10c2b7cccd..13153c0ebc 100644
--- a/src/Type/MixedType.php
+++ b/src/Type/MixedType.php
@@ -37,6 +37,7 @@
 use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
 use PHPStan\Type\Traits\NonGenericTypeTrait;
 use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
+use function get_class;
 use function sprintf;
 
 /** @api */
@@ -310,7 +311,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope)
 
 	public function equals(Type $type): bool
 	{
-		if (!$type instanceof self) {
+		if (get_class($type) !== self::class) {
 			return false;
 		}
 
diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php
index 5322385963..a4b6f32b0e 100644
--- a/tests/PHPStan/Type/MixedTypeTest.php
+++ b/tests/PHPStan/Type/MixedTypeTest.php
@@ -1164,4 +1164,49 @@ public function testSubtractedHasOffsetValueType(MixedType $mixedType, Type $typ
 		);
 	}
 
+	/** @dataProvider dataEquals */
+	public function testEquals(MixedType $mixedType, Type $typeToCompare, bool $expectedResult): void
+	{
+		$this->assertSame(
+			$expectedResult,
+			$mixedType->equals($typeToCompare),
+		);
+	}
+
+	public function dataEquals(): array
+	{
+		return [
+			[
+				new MixedType(),
+				new MixedType(),
+				true,
+			],
+			[
+				new MixedType(true),
+				new MixedType(),
+				true,
+			],
+			[
+				new MixedType(),
+				new MixedType(true),
+				true,
+			],
+			[
+				new MixedType(),
+				new MixedType(true, new IntegerType()),
+				false,
+			],
+			[
+				new MixedType(),
+				new ErrorType(),
+				false,
+			],
+			[
+				new MixedType(true),
+				new ErrorType(),
+				false,
+			],
+		];
+	}
+
 }

From 9b93701b07378c5bc02edd5f234cee7591ea4923 Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Sun, 13 Apr 2025 22:25:28 +0200
Subject: [PATCH 02/13] Update MixedType.php

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

diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php
index 13153c0ebc..20ec4a691c 100644
--- a/src/Type/MixedType.php
+++ b/src/Type/MixedType.php
@@ -38,6 +38,7 @@
 use PHPStan\Type\Traits\NonGenericTypeTrait;
 use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
 use function get_class;
+use function in_array;
 use function sprintf;
 
 /** @api */
@@ -311,7 +312,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope)
 
 	public function equals(Type $type): bool
 	{
-		if (get_class($type) !== self::class) {
+		if (!in_array(get_class($type), [self::class, TemplateMixedType::class], true)) {
 			return false;
 		}
 

From 78f4d98d4fa457a032cdc328278bee6b24f26c29 Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Sun, 13 Apr 2025 22:29:47 +0200
Subject: [PATCH 03/13] simplify

---
 src/Type/MixedType.php | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php
index 20ec4a691c..297aabe7d6 100644
--- a/src/Type/MixedType.php
+++ b/src/Type/MixedType.php
@@ -32,6 +32,7 @@
 use PHPStan\Type\Constant\ConstantFloatType;
 use PHPStan\Type\Constant\ConstantIntegerType;
 use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\Generic\GenericObjectType;
 use PHPStan\Type\Generic\TemplateMixedType;
 use PHPStan\Type\Generic\TemplateType;
 use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
@@ -312,7 +313,11 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope)
 
 	public function equals(Type $type): bool
 	{
-		if (!in_array(get_class($type), [self::class, TemplateMixedType::class], true)) {
+		if (!$type instanceof self) {
+			return false;
+		}
+
+		if ($type instanceof ErrorType) {
 			return false;
 		}
 

From 3afdd177203b3b97586783ed95205dbb0028bd2c Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Sun, 13 Apr 2025 22:36:59 +0200
Subject: [PATCH 04/13] cs

---
 src/Type/MixedType.php | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php
index 297aabe7d6..485c7c5270 100644
--- a/src/Type/MixedType.php
+++ b/src/Type/MixedType.php
@@ -32,14 +32,11 @@
 use PHPStan\Type\Constant\ConstantFloatType;
 use PHPStan\Type\Constant\ConstantIntegerType;
 use PHPStan\Type\Constant\ConstantStringType;
-use PHPStan\Type\Generic\GenericObjectType;
 use PHPStan\Type\Generic\TemplateMixedType;
 use PHPStan\Type\Generic\TemplateType;
 use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
 use PHPStan\Type\Traits\NonGenericTypeTrait;
 use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
-use function get_class;
-use function in_array;
 use function sprintf;
 
 /** @api */

From 9b1cc2afced679dda8263ce659e0f315f99b6da5 Mon Sep 17 00:00:00 2001
From: Markus Staab <markus.staab@redaxo.de>
Date: Mon, 14 Apr 2025 09:14:02 +0200
Subject: [PATCH 05/13] Update MixedTypeTest.php

---
 tests/PHPStan/Type/MixedTypeTest.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php
index a4b6f32b0e..c85667c732 100644
--- a/tests/PHPStan/Type/MixedTypeTest.php
+++ b/tests/PHPStan/Type/MixedTypeTest.php
@@ -1170,6 +1170,7 @@ public function testEquals(MixedType $mixedType, Type $typeToCompare, bool $expe
 		$this->assertSame(
 			$expectedResult,
 			$mixedType->equals($typeToCompare),
+			sprintf('%s -> equals(%s)', $mixedType->describe(VerbosityLevel::precise()), $typeToCompare->describe(VerbosityLevel::precise())),
 		);
 	}
 

From 888cb1192a0542f641cb78bd6fb028c9437279e9 Mon Sep 17 00:00:00 2001
From: Markus Staab <markus.staab@redaxo.de>
Date: Mon, 14 Apr 2025 09:29:33 +0200
Subject: [PATCH 06/13] Support arrays in fast-path

---
 src/Analyser/MutatingScope.php                   | 2 +-
 tests/PHPStan/Analyser/nsrt/conditional-vars.php | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php
index 051d15d960..d46ddac54e 100644
--- a/src/Analyser/MutatingScope.php
+++ b/src/Analyser/MutatingScope.php
@@ -4486,7 +4486,7 @@ public function addTypeToExpression(Expr $expr, Type $type): self
 
 		if ($originalExprType->equals($nativeType)) {
 			$newType = TypeCombinator::intersect($type, $originalExprType);
-			if ($newType->isConstantScalarValue()->yes() && $newType->equals($originalExprType)) {
+			if ($newType->isObject()->no() && $newType->equals($originalExprType)) {
 				// don't add the same type over and over again to improve performance
 				return $this;
 			}
diff --git a/tests/PHPStan/Analyser/nsrt/conditional-vars.php b/tests/PHPStan/Analyser/nsrt/conditional-vars.php
index 568c6a8b7f..467ca05aae 100644
--- a/tests/PHPStan/Analyser/nsrt/conditional-vars.php
+++ b/tests/PHPStan/Analyser/nsrt/conditional-vars.php
@@ -10,10 +10,10 @@ class HelloWorld
 	public function conditionalVarInTernary(array $innerHits): void
 	{
 		if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) {
-			assertType('non-empty-array', $innerHits);
+			assertType('non-empty-array<mixed>', $innerHits);
 			$x = array_key_exists('nearest_premise', $innerHits)
 				? assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits)
-				: assertType('non-empty-array', $innerHits);
+				: assertType('non-empty-array<mixed>', $innerHits);
 
 			assertType('non-empty-array', $innerHits);
 		}
@@ -24,11 +24,11 @@ public function conditionalVarInTernary(array $innerHits): void
 	public function conditionalVarInIf(array $innerHits): void
 	{
 		if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) {
-			assertType('non-empty-array', $innerHits);
+			assertType('non-empty-array<mixed>', $innerHits);
 			if (array_key_exists('nearest_premise', $innerHits)) {
 				assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits);
 			} else {
-				assertType('non-empty-array', $innerHits);
+				assertType('non-empty-array<mixed>', $innerHits);
 			}
 
 			assertType('non-empty-array', $innerHits);

From 3e91a6ec3f3d1bad3d9cc26f3fb88f3669b08751 Mon Sep 17 00:00:00 2001
From: Markus Staab <markus.staab@redaxo.de>
Date: Tue, 15 Apr 2025 16:06:30 +0200
Subject: [PATCH 07/13] Update conditional-vars.php

---
 tests/PHPStan/Analyser/nsrt/conditional-vars.php | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/tests/PHPStan/Analyser/nsrt/conditional-vars.php b/tests/PHPStan/Analyser/nsrt/conditional-vars.php
index 467ca05aae..568c6a8b7f 100644
--- a/tests/PHPStan/Analyser/nsrt/conditional-vars.php
+++ b/tests/PHPStan/Analyser/nsrt/conditional-vars.php
@@ -10,10 +10,10 @@ class HelloWorld
 	public function conditionalVarInTernary(array $innerHits): void
 	{
 		if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) {
-			assertType('non-empty-array<mixed>', $innerHits);
+			assertType('non-empty-array', $innerHits);
 			$x = array_key_exists('nearest_premise', $innerHits)
 				? assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits)
-				: assertType('non-empty-array<mixed>', $innerHits);
+				: assertType('non-empty-array', $innerHits);
 
 			assertType('non-empty-array', $innerHits);
 		}
@@ -24,11 +24,11 @@ public function conditionalVarInTernary(array $innerHits): void
 	public function conditionalVarInIf(array $innerHits): void
 	{
 		if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) {
-			assertType('non-empty-array<mixed>', $innerHits);
+			assertType('non-empty-array', $innerHits);
 			if (array_key_exists('nearest_premise', $innerHits)) {
 				assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits);
 			} else {
-				assertType('non-empty-array<mixed>', $innerHits);
+				assertType('non-empty-array', $innerHits);
 			}
 
 			assertType('non-empty-array', $innerHits);

From 4b40700ac1edafe81b47b5704dbbcd88f5a72000 Mon Sep 17 00:00:00 2001
From: Markus Staab <markus.staab@redaxo.de>
Date: Tue, 13 May 2025 16:19:16 +0200
Subject: [PATCH 08/13] fix

---
 src/Type/MixedType.php                             | 7 ++-----
 tests/PHPStan/Generics/TemplateTypeFactoryTest.php | 7 ++++++-
 2 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php
index 485c7c5270..75f8972bc1 100644
--- a/src/Type/MixedType.php
+++ b/src/Type/MixedType.php
@@ -37,6 +37,7 @@
 use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
 use PHPStan\Type\Traits\NonGenericTypeTrait;
 use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
+use function get_class;
 use function sprintf;
 
 /** @api */
@@ -310,11 +311,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope)
 
 	public function equals(Type $type): bool
 	{
-		if (!$type instanceof self) {
-			return false;
-		}
-
-		if ($type instanceof ErrorType) {
+		if (get_class($type) !== static::class) {
 			return false;
 		}
 
diff --git a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php
index 58af77cd3b..d044b4b469 100644
--- a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php
+++ b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php
@@ -55,7 +55,12 @@ public function dataCreate(): array
 					null,
 					TemplateTypeVariance::createInvariant(),
 				),
-				new MixedType(),
+				TemplateTypeFactory::create(
+					TemplateTypeScope::createWithFunction('a'),
+					'U',
+					null,
+					TemplateTypeVariance::createInvariant(),
+				),
 			],
 			[
 				new UnionType([

From 9dcc4e5dab222ad79657d4dc8ec6f7417b671852 Mon Sep 17 00:00:00 2001
From: Markus Staab <markus.staab@redaxo.de>
Date: Wed, 14 May 2025 09:16:39 +0200
Subject: [PATCH 09/13] Update MutatingScope.php

---
 src/Analyser/MutatingScope.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php
index d46ddac54e..0a4e85ab55 100644
--- a/src/Analyser/MutatingScope.php
+++ b/src/Analyser/MutatingScope.php
@@ -4486,7 +4486,7 @@ public function addTypeToExpression(Expr $expr, Type $type): self
 
 		if ($originalExprType->equals($nativeType)) {
 			$newType = TypeCombinator::intersect($type, $originalExprType);
-			if ($newType->isObject()->no() && $newType->equals($originalExprType)) {
+			if (!$newType->isObject()->yes() && $newType->equals($originalExprType)) {
 				// don't add the same type over and over again to improve performance
 				return $this;
 			}

From e0b692d7c0163960c98f125750a0300e70594532 Mon Sep 17 00:00:00 2001
From: Markus Staab <markus.staab@redaxo.de>
Date: Wed, 14 May 2025 09:21:26 +0200
Subject: [PATCH 10/13] Update MutatingScope.php

---
 src/Analyser/MutatingScope.php | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php
index 0a4e85ab55..299df748d4 100644
--- a/src/Analyser/MutatingScope.php
+++ b/src/Analyser/MutatingScope.php
@@ -4486,8 +4486,9 @@ public function addTypeToExpression(Expr $expr, Type $type): self
 
 		if ($originalExprType->equals($nativeType)) {
 			$newType = TypeCombinator::intersect($type, $originalExprType);
-			if (!$newType->isObject()->yes() && $newType->equals($originalExprType)) {
-				// don't add the same type over and over again to improve performance
+			if ($newType->isObject()->no() && $newType->equals($originalExprType)) {
+				// don't add the same type over and over again to improve performance.
+				// objects can get narrowed even though ObjectType->equal() will return true (e.g. via impicit "final" via new())
 				return $this;
 			}
 			return $this->specifyExpressionType($expr, $newType, $newType, TrinaryLogic::createYes());

From b9e54116410ebbe046befe20ce02b205e7b6e30b Mon Sep 17 00:00:00 2001
From: Markus Staab <markus.staab@redaxo.de>
Date: Wed, 14 May 2025 09:27:30 +0200
Subject: [PATCH 11/13] Update MutatingScope.php

---
 src/Analyser/MutatingScope.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php
index 299df748d4..12a4d46a42 100644
--- a/src/Analyser/MutatingScope.php
+++ b/src/Analyser/MutatingScope.php
@@ -4488,7 +4488,7 @@ public function addTypeToExpression(Expr $expr, Type $type): self
 			$newType = TypeCombinator::intersect($type, $originalExprType);
 			if ($newType->isObject()->no() && $newType->equals($originalExprType)) {
 				// don't add the same type over and over again to improve performance.
-				// objects can get narrowed even though ObjectType->equal() will return true (e.g. via impicit "final" via new())
+				// objects can get narrowed even though ObjectType->equal() will return true (e.g. via implicit "final" via new())
 				return $this;
 			}
 			return $this->specifyExpressionType($expr, $newType, $newType, TrinaryLogic::createYes());

From d2deae68e4b33cf17f903ebfce9b5bbdc745ca1e Mon Sep 17 00:00:00 2001
From: Markus Staab <markus.staab@redaxo.de>
Date: Wed, 14 May 2025 09:27:47 +0200
Subject: [PATCH 12/13] Update MutatingScope.php

---
 src/Analyser/MutatingScope.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php
index 12a4d46a42..cda23689c3 100644
--- a/src/Analyser/MutatingScope.php
+++ b/src/Analyser/MutatingScope.php
@@ -4488,7 +4488,7 @@ public function addTypeToExpression(Expr $expr, Type $type): self
 			$newType = TypeCombinator::intersect($type, $originalExprType);
 			if ($newType->isObject()->no() && $newType->equals($originalExprType)) {
 				// don't add the same type over and over again to improve performance.
-				// objects can get narrowed even though ObjectType->equal() will return true (e.g. via implicit "final" via new())
+				// objects can get narrowed even though ObjectType->equal() will return true (e.g. via implicit "final" via new Foo())
 				return $this;
 			}
 			return $this->specifyExpressionType($expr, $newType, $newType, TrinaryLogic::createYes());

From 9bfeae2f3e01a2fd52d38ec9b6e17c35f6c7b374 Mon Sep 17 00:00:00 2001
From: Markus Staab <maggus.staab@googlemail.com>
Date: Fri, 16 May 2025 15:05:07 +0200
Subject: [PATCH 13/13] Discard changes to src/Analyser/MutatingScope.php

---
 src/Analyser/MutatingScope.php | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php
index cda23689c3..051d15d960 100644
--- a/src/Analyser/MutatingScope.php
+++ b/src/Analyser/MutatingScope.php
@@ -4486,9 +4486,8 @@ public function addTypeToExpression(Expr $expr, Type $type): self
 
 		if ($originalExprType->equals($nativeType)) {
 			$newType = TypeCombinator::intersect($type, $originalExprType);
-			if ($newType->isObject()->no() && $newType->equals($originalExprType)) {
-				// don't add the same type over and over again to improve performance.
-				// objects can get narrowed even though ObjectType->equal() will return true (e.g. via implicit "final" via new Foo())
+			if ($newType->isConstantScalarValue()->yes() && $newType->equals($originalExprType)) {
+				// don't add the same type over and over again to improve performance
 				return $this;
 			}
 			return $this->specifyExpressionType($expr, $newType, $newType, TrinaryLogic::createYes());