From 1ea42ff129be9d1da6455ee7cd58a9ec2ab3322a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 1 Feb 2026 12:16:42 +0100 Subject: [PATCH 1/4] MutatingScope: Prevent unnecessary scope re-creation --- src/Analyser/MutatingScope.php | 45 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4425f8aa52..e2bae64785 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3386,24 +3386,33 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $nativeTypes = $scope->nativeExpressionTypes; $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); - $scope = $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $expressionTypes, - $nativeTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->currentlyAllowedUndefinedExpressions, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, - ); + if ( + !isset($scope->expressionTypes[$exprString]) + || !isset($scope->nativeExpressionTypes[$exprString]) + || !$expressionTypes[$exprString]->equals($scope->expressionTypes[$exprString]) + || !$nativeTypes[$exprString]->equals($scope->nativeExpressionTypes[$exprString]) + ) { + $scope = $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + if ($expr instanceof AlwaysRememberedExpr) { return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); From 783effd70a4ce1cab5c4ca9fefbf30ea46b0adda Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 1 Feb 2026 12:18:39 +0100 Subject: [PATCH 2/4] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index e2bae64785..65a33b7ade 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3412,8 +3412,6 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, ); } - - if ($expr instanceof AlwaysRememberedExpr) { return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); } From e3135ddd73484723b647800210db816431cba12b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 2 Feb 2026 07:53:56 +0100 Subject: [PATCH 3/4] try re-using ExpressionTypeHolder --- src/Analyser/MutatingScope.php | 47 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 65a33b7ade..a9cce96ea0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3382,36 +3382,39 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $exprString = $this->getNodeKey($expr); $expressionTypes = $scope->expressionTypes; - $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty); + $newHolder = new ExpressionTypeHolder($expr, $type, $certainty); $nativeTypes = $scope->nativeExpressionTypes; - $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); + $newNativeHolder = new ExpressionTypeHolder($expr, $nativeType, $certainty); if ( !isset($scope->expressionTypes[$exprString]) || !isset($scope->nativeExpressionTypes[$exprString]) - || !$expressionTypes[$exprString]->equals($scope->expressionTypes[$exprString]) - || !$nativeTypes[$exprString]->equals($scope->nativeExpressionTypes[$exprString]) + || !$newHolder->equals($scope->expressionTypes[$exprString]) + || !$newNativeHolder->equals($scope->nativeExpressionTypes[$exprString]) ) { - $scope = $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $expressionTypes, - $nativeTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->currentlyAllowedUndefinedExpressions, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, - ); + $expressionTypes[$exprString] = $newHolder; + $nativeTypes[$exprString] = $newNativeHolder; } + $scope = $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + if ($expr instanceof AlwaysRememberedExpr) { return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); } From 3c580d556864ba4fa5e4acb77a45a6675fa9289c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 3 Feb 2026 07:38:53 +0100 Subject: [PATCH 4/4] fix --- src/Analyser/ExpressionTypeHolder.php | 27 ++++++++++++++++++- tests/PHPStan/Analyser/nsrt/bug-2911.php | 2 +- tests/PHPStan/Analyser/nsrt/superglobals.php | 2 +- .../CallToFunctionParametersRuleTest.php | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Analyser/ExpressionTypeHolder.php b/src/Analyser/ExpressionTypeHolder.php index 7477dbf3dc..649de05f88 100644 --- a/src/Analyser/ExpressionTypeHolder.php +++ b/src/Analyser/ExpressionTypeHolder.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr; use PHPStan\TrinaryLogic; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -47,7 +48,31 @@ public function equals(self $other): bool return false; } - return $this->type === $other->type || $this->type->equals($other->type); + if ($this->type === $other->type) { + return true; + } + + if ($this->type instanceof ObjectType && $other->type instanceof ObjectType) { + if (!$this->type->equals($other->type)) { + return false; + } + + $classReflection = $this->type->getClassReflection(); + $otherClassReflection = $other->type->getClassReflection(); + if ( + $classReflection !== null && $otherClassReflection !== null + && ( + $classReflection->hasFinalByKeywordOverride() + !== $otherClassReflection->hasFinalByKeywordOverride() + ) + ) { + return false; + } + + return true; + } + + return $this->type->equals($other->type); } public function and(self $other): self diff --git a/tests/PHPStan/Analyser/nsrt/bug-2911.php b/tests/PHPStan/Analyser/nsrt/bug-2911.php index 1b189148b3..b845c0189b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2911.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2911.php @@ -134,6 +134,6 @@ private function getResultSettings(array $settings): array function foo(array $array): void { $array['bar'] = 'string'; - assertType("non-empty-array&hasOffsetValue('bar', 'string')", $array); + assertType("non-empty-array&hasOffsetValue('bar', 'string')", $array); } } diff --git a/tests/PHPStan/Analyser/nsrt/superglobals.php b/tests/PHPStan/Analyser/nsrt/superglobals.php index a7be748b26..f636efffae 100644 --- a/tests/PHPStan/Analyser/nsrt/superglobals.php +++ b/tests/PHPStan/Analyser/nsrt/superglobals.php @@ -31,7 +31,7 @@ public function canBeOverwritten(): void public function canBePartlyOverwritten(): void { $GLOBALS['foo'] = 'foo'; - assertType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); + assertType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); assertNativeType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index bdd0d7c73a..a04a2af76b 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1299,7 +1299,7 @@ public function testBug2911(): void { $this->analyse([__DIR__ . '/data/bug-2911.php'], [ [ - 'Parameter #1 $array of function Bug2911\bar expects array{bar: string}, non-empty-array given.', + 'Parameter #1 $array of function Bug2911\bar expects array{bar: string}, non-empty-array given.', 23, ], ]);