From 875c9dab263d4086e78ea8bb2d43014dead0a9cb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 1 Feb 2026 11:22:18 +0100 Subject: [PATCH 1/5] NodeScopeResolver: Prevent repetitive union of static types --- src/Analyser/MutatingScope.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4425f8aa52..6ffb5855a3 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3349,15 +3349,22 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, if ($dimType->isInteger()->yes() || $dimType->isString()->yes()) { $exprVarType = $scope->getType($expr->var); if (!$exprVarType instanceof MixedType && !$exprVarType->isArray()->no()) { - $types = [ + $this->nonIntKeyOffsetValueType ??= TypeCombinator::union( new ArrayType(new MixedType(), new MixedType()), new ObjectType(ArrayAccess::class), new NullType(), - ]; + ); + if ($dimType->isInteger()->yes()) { - $types[] = new StringType(); + $this->intKeyOffsetValueType ??= TypeCombinator::union( + $this->nonIntKeyOffsetValueType, + new StringType(), + ); + + $offsetValueType = TypeCombinator::intersect($exprVarType, $this->intKeyOffsetValueType); + } else { + $offsetValueType = TypeCombinator::intersect($exprVarType, $this->nonIntKeyOffsetValueType); } - $offsetValueType = TypeCombinator::intersect($exprVarType, TypeCombinator::union(...$types)); if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { $offsetValueType = TypeCombinator::intersect( From f6e56a4f5d7189277a0d4f989b14477ec6f8cb85 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 1 Feb 2026 11:33:04 +0100 Subject: [PATCH 2/5] refactor --- src/Analyser/MutatingScope.php | 16 +++----------- src/Analyser/NodeScopeResolver.php | 19 ++--------------- src/Type/StaticTypeFactory.php | 34 ++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6ffb5855a3..16433bc5ba 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -125,6 +125,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; +use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; @@ -3349,21 +3350,10 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, if ($dimType->isInteger()->yes() || $dimType->isString()->yes()) { $exprVarType = $scope->getType($expr->var); if (!$exprVarType instanceof MixedType && !$exprVarType->isArray()->no()) { - $this->nonIntKeyOffsetValueType ??= TypeCombinator::union( - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(ArrayAccess::class), - new NullType(), - ); - if ($dimType->isInteger()->yes()) { - $this->intKeyOffsetValueType ??= TypeCombinator::union( - $this->nonIntKeyOffsetValueType, - new StringType(), - ); - - $offsetValueType = TypeCombinator::intersect($exprVarType, $this->intKeyOffsetValueType); + $offsetValueType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::getIntArrayKeyValueType()); } else { - $offsetValueType = TypeCombinator::intersect($exprVarType, $this->nonIntKeyOffsetValueType); + $offsetValueType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::getNonIntArrayKeyValueType()); } if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 54a92fce7c..9c15408f51 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -249,10 +249,6 @@ class NodeScopeResolver /** @var array */ private array $calledMethodResults = []; - private ?Type $nonIntKeyOffsetValueType = null; - - private ?Type $intKeyOffsetValueType = null; - /** * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls @@ -6610,21 +6606,10 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar !$offsetValueType instanceof MixedType && !$offsetValueType->isArray()->yes() ) { - $this->nonIntKeyOffsetValueType ??= TypeCombinator::union( - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(ArrayAccess::class), - new NullType(), - ); - if ($offsetType !== null && $offsetType->isInteger()->yes()) { - $this->intKeyOffsetValueType ??= TypeCombinator::union( - $this->nonIntKeyOffsetValueType, - new StringType(), - ); - - $offsetValueType = TypeCombinator::intersect($offsetValueType, $this->intKeyOffsetValueType); + $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::getIntArrayKeyValueType()); } else { - $offsetValueType = TypeCombinator::intersect($offsetValueType, $this->nonIntKeyOffsetValueType); + $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::getNonIntArrayKeyValueType()); } } diff --git a/src/Type/StaticTypeFactory.php b/src/Type/StaticTypeFactory.php index 93fe12d555..40509ec8d9 100644 --- a/src/Type/StaticTypeFactory.php +++ b/src/Type/StaticTypeFactory.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use ArrayAccess; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -16,7 +17,7 @@ public static function falsey(): Type static $falsey; if ($falsey === null) { - $falsey = new UnionType([ + $falsey = TypeCombinator::union( new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), @@ -24,7 +25,7 @@ public static function falsey(): Type new ConstantStringType(''), new ConstantStringType('0'), new ConstantArrayType([], []), - ]); + ); } return $falsey; @@ -41,4 +42,33 @@ public static function truthy(): Type return $truthy; } + public static function getNonIntArrayKeyValueType(): Type + { + static $nonIntArrayKeyType; + + if ($nonIntArrayKeyType === null) { + $nonIntArrayKeyType = TypeCombinator::union( + new ArrayType(new MixedType(), new MixedType()), + new ObjectType(ArrayAccess::class), + new NullType(), + ); + } + + return $nonIntArrayKeyType; + } + + public static function getIntArrayKeyValueType(): Type + { + static $intArrayKeyType; + + if ($intArrayKeyType === null) { + $intArrayKeyType = TypeCombinator::union( + self::getNonIntArrayKeyValueType(), + new StringType(), + ); + } + + return $intArrayKeyType; + } + } From 04a34d93223e8ea74bf0c160746184a6c35de9ce Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Feb 2026 13:57:27 +0100 Subject: [PATCH 3/5] rename --- src/Analyser/MutatingScope.php | 4 ++-- src/Analyser/NodeScopeResolver.php | 4 ++-- src/Type/StaticTypeFactory.php | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 16433bc5ba..00104045db 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3351,9 +3351,9 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $exprVarType = $scope->getType($expr->var); if (!$exprVarType instanceof MixedType && !$exprVarType->isArray()->no()) { if ($dimType->isInteger()->yes()) { - $offsetValueType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::getIntArrayKeyValueType()); + $offsetValueType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::intOffsetValueType()); } else { - $offsetValueType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::getNonIntArrayKeyValueType()); + $offsetValueType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::generalOffsetValueType()); } if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 9c15408f51..c7673d0257 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6607,9 +6607,9 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar && !$offsetValueType->isArray()->yes() ) { if ($offsetType !== null && $offsetType->isInteger()->yes()) { - $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::getIntArrayKeyValueType()); + $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::intOffsetValueType()); } else { - $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::getNonIntArrayKeyValueType()); + $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::generalOffsetValueType()); } } diff --git a/src/Type/StaticTypeFactory.php b/src/Type/StaticTypeFactory.php index 40509ec8d9..5a09c32024 100644 --- a/src/Type/StaticTypeFactory.php +++ b/src/Type/StaticTypeFactory.php @@ -42,7 +42,7 @@ public static function truthy(): Type return $truthy; } - public static function getNonIntArrayKeyValueType(): Type + public static function generalOffsetValueType(): Type { static $nonIntArrayKeyType; @@ -57,13 +57,13 @@ public static function getNonIntArrayKeyValueType(): Type return $nonIntArrayKeyType; } - public static function getIntArrayKeyValueType(): Type + public static function intOffsetValueType(): Type { static $intArrayKeyType; if ($intArrayKeyType === null) { $intArrayKeyType = TypeCombinator::union( - self::getNonIntArrayKeyValueType(), + self::generalOffsetValueType(), new StringType(), ); } From 375a4d86456c08f54d2022051b6ed172e7c8a2fb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Feb 2026 16:12:15 +0100 Subject: [PATCH 4/5] Update StaticTypeFactory.php --- src/Type/StaticTypeFactory.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Type/StaticTypeFactory.php b/src/Type/StaticTypeFactory.php index 5a09c32024..ce4b0d1cb3 100644 --- a/src/Type/StaticTypeFactory.php +++ b/src/Type/StaticTypeFactory.php @@ -44,31 +44,31 @@ public static function truthy(): Type public static function generalOffsetValueType(): Type { - static $nonIntArrayKeyType; + static $generalOffsetValueType; - if ($nonIntArrayKeyType === null) { - $nonIntArrayKeyType = TypeCombinator::union( + if ($generalOffsetValueType === null) { + $generalOffsetValueType = TypeCombinator::union( new ArrayType(new MixedType(), new MixedType()), new ObjectType(ArrayAccess::class), new NullType(), ); } - return $nonIntArrayKeyType; + return $generalOffsetValueType; } public static function intOffsetValueType(): Type { - static $intArrayKeyType; + static $intOffsetValueType; - if ($intArrayKeyType === null) { - $intArrayKeyType = TypeCombinator::union( + if ($intOffsetValueType === null) { + $intOffsetValueType = TypeCombinator::union( self::generalOffsetValueType(), new StringType(), ); } - return $intArrayKeyType; + return $intOffsetValueType; } } From bb49087894dc1701d595f16bf30039f1944206b9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Feb 2026 16:33:43 +0100 Subject: [PATCH 5/5] naming things --- src/Analyser/MutatingScope.php | 10 +++++----- src/Analyser/NodeScopeResolver.php | 4 ++-- src/Type/StaticTypeFactory.php | 22 +++++++++++----------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 00104045db..5af1a88189 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3351,21 +3351,21 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $exprVarType = $scope->getType($expr->var); if (!$exprVarType instanceof MixedType && !$exprVarType->isArray()->no()) { if ($dimType->isInteger()->yes()) { - $offsetValueType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::intOffsetValueType()); + $varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::intOffsetAccessibleType()); } else { - $offsetValueType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::generalOffsetValueType()); + $varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::generalOffsetAccessibleType()); } if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { - $offsetValueType = TypeCombinator::intersect( - $offsetValueType, + $varType = TypeCombinator::intersect( + $varType, new HasOffsetValueType($dimType, $type), ); } $scope = $scope->specifyExpressionType( $expr->var, - $offsetValueType, + $varType, $scope->getNativeType($expr->var), $certainty, ); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c7673d0257..8ec4836e4f 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6607,9 +6607,9 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar && !$offsetValueType->isArray()->yes() ) { if ($offsetType !== null && $offsetType->isInteger()->yes()) { - $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::intOffsetValueType()); + $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::intOffsetAccessibleType()); } else { - $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::generalOffsetValueType()); + $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::generalOffsetAccessibleType()); } } diff --git a/src/Type/StaticTypeFactory.php b/src/Type/StaticTypeFactory.php index ce4b0d1cb3..b727a386d6 100644 --- a/src/Type/StaticTypeFactory.php +++ b/src/Type/StaticTypeFactory.php @@ -42,33 +42,33 @@ public static function truthy(): Type return $truthy; } - public static function generalOffsetValueType(): Type + public static function generalOffsetAccessibleType(): Type { - static $generalOffsetValueType; + static $generalOffsetAccessible; - if ($generalOffsetValueType === null) { - $generalOffsetValueType = TypeCombinator::union( + if ($generalOffsetAccessible === null) { + $generalOffsetAccessible = TypeCombinator::union( new ArrayType(new MixedType(), new MixedType()), new ObjectType(ArrayAccess::class), new NullType(), ); } - return $generalOffsetValueType; + return $generalOffsetAccessible; } - public static function intOffsetValueType(): Type + public static function intOffsetAccessibleType(): Type { - static $intOffsetValueType; + static $intOffsetAccessible; - if ($intOffsetValueType === null) { - $intOffsetValueType = TypeCombinator::union( - self::generalOffsetValueType(), + if ($intOffsetAccessible === null) { + $intOffsetAccessible = TypeCombinator::union( + self::generalOffsetAccessibleType(), new StringType(), ); } - return $intOffsetValueType; + return $intOffsetAccessible; } }