From 164c280b68c95de1fd97c79a18453cf7b01b38b4 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Wed, 24 Jun 2026 23:27:22 +0200 Subject: [PATCH 1/2] Sort enum-case unions by class and case name instead of describe() UnionTypeHelper::sortTypes runs on every UnionType construction and sorts enum-case members through describe(VerbosityLevel), the documented "never sort via describe()" anti-pattern. It is the dominant cost of sorting a large-enum union, which a match/switch over the enum re-sorts once per arm. Compare enum cases by className.'::'.caseName instead - the same key IntersectionType::getFiniteTypes() already uses. That is the describe(typeOnly) string for an enum case (enums are never generic), so the sort order is identical, without the per-comparison VerbosityLevel dispatch and sprintf. The instanceof EnumCaseObjectType gets a baseline entry alongside the two existing ones for that rule (the same internal type-system usage). Co-Authored-By: Claude Opus 4.8 (1M context) --- phpstan-baseline.neon | 6 ++++++ src/Type/UnionTypeHelper.php | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2c3e0d16c3c..d1a31cf0bff 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1773,6 +1773,12 @@ parameters: count: 2 path: src/Type/UnionTypeHelper.php + - + rawMessage: 'Doing instanceof PHPStan\Type\Enum\EnumCaseObjectType is error-prone and deprecated. Use Type::getEnumCases() instead.' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionTypeHelper.php + - rawMessage: 'Doing instanceof PHPStan\Type\IntegerType is error-prone and deprecated. Use Type::isInteger() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index ba9eba259e3..a028d9ffb03 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -7,6 +7,7 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use function count; use function strcasecmp; use function usort; @@ -121,6 +122,18 @@ public static function sortTypes(array $types): array return self::compareStrings($a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())); } + // Enum cases would otherwise fall through to describe(), the documented "never sort via + // describe()" anti-pattern and the dominant cost of sorting a large-enum union (re-sorted + // per arm of a match/switch). className.'::'.caseName is the describe(typeOnly) string for + // an enum case (enums are never generic), so the order is identical without the + // VerbosityLevel dispatch and sprintf. Same key as IntersectionType::getFiniteTypes(). + if ($a instanceof EnumCaseObjectType && $b instanceof EnumCaseObjectType) { + return self::compareStrings( + $a->getClassName() . '::' . $a->getEnumCaseName(), + $b->getClassName() . '::' . $b->getEnumCaseName(), + ); + } + return self::compareStrings($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); }); return $types; From 43d909fb1ff8363c40a56877eb709bd9da35c149 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 25 Jun 2026 10:25:08 +0200 Subject: [PATCH 2/2] cheap checks first --- src/Type/UnionTypeHelper.php | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index a028d9ffb03..02fa454dcb6 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -97,6 +97,20 @@ public static function sortTypes(array $types): array return self::compareStrings($a->getValue(), $b->getValue()); } + if ($a instanceof EnumCaseObjectType && $b instanceof EnumCaseObjectType) { + return self::compareStrings( + $a->getClassName() . '::' . $a->getEnumCaseName(), + $b->getClassName() . '::' . $b->getEnumCaseName(), + ); + } + + if ( + ($a instanceof CallableType || $a instanceof ClosureType) + && ($b instanceof CallableType || $b instanceof ClosureType) + ) { + return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); + } + if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) { if ($a->isIterableAtLeastOnce()->no()) { if ($b->isIterableAtLeastOnce()->no()) { @@ -111,29 +125,10 @@ public static function sortTypes(array $types): array return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); } - if ( - ($a instanceof CallableType || $a instanceof ClosureType) - && ($b instanceof CallableType || $b instanceof ClosureType) - ) { - return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); - } - if ($a->isString()->yes() && $b->isString()->yes()) { return self::compareStrings($a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())); } - // Enum cases would otherwise fall through to describe(), the documented "never sort via - // describe()" anti-pattern and the dominant cost of sorting a large-enum union (re-sorted - // per arm of a match/switch). className.'::'.caseName is the describe(typeOnly) string for - // an enum case (enums are never generic), so the order is identical without the - // VerbosityLevel dispatch and sprintf. Same key as IntersectionType::getFiniteTypes(). - if ($a instanceof EnumCaseObjectType && $b instanceof EnumCaseObjectType) { - return self::compareStrings( - $a->getClassName() . '::' . $a->getEnumCaseName(), - $b->getClassName() . '::' . $b->getEnumCaseName(), - ); - } - return self::compareStrings($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); }); return $types;