From 7d1213255cb794e13de213f69144a7a80b674c26 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Dec 2025 10:57:27 +0100 Subject: [PATCH 01/10] Preserve already resolved types after scope re-creation --- src/Analyser/MutatingScope.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 9eeb9e6bec..a3ec2b38b2 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4358,6 +4358,7 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $this->parentScope, $this->nativeTypesPromoted, ); + $scope->resolvedTypes = $this->preserveResolvedTypes($expressionTypes); if ($expr instanceof AlwaysRememberedExpr) { return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); @@ -4406,6 +4407,26 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType()); } + /** + * @param array $expressionTypes + * + * @return array + */ + private function preserveResolvedTypes(array $expressionTypes): array + { + $preservedTypes = $this->resolvedTypes; + foreach($preservedTypes as $exprStringToInvalidate => $resolvedType) { + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + if (str_contains($exprStringToInvalidate, $exprString)) { + unset ($preservedTypes[$exprStringToInvalidate]); + continue 2; + } + } + } + + return $preservedTypes; + } + public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false): self { $expressionTypes = $this->expressionTypes; From 077c3a78c89e36a737579a0a918f38e6b919565a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Dec 2025 11:08:24 +0100 Subject: [PATCH 02/10] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a3ec2b38b2..d666958329 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -862,9 +862,9 @@ public function getType(Expr $node): Type $key = $this->getNodeKey($node); if (!array_key_exists($key, $this->resolvedTypes)) { - $this->resolvedTypes[$key] = ExpressionTypeHolder::createYes($node, TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node))); + $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node)); } - return $this->resolvedTypes[$key]->getType(); + return $this->resolvedTypes[$key]; } public function getScopeType(Expr $expr): Type From 51ec3f5c44ed89191cf44264a55380ffaafe5bfe Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Dec 2025 11:03:10 +0100 Subject: [PATCH 03/10] Don't store scalar values in resolvedTypes --- src/Analyser/MutatingScope.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index d666958329..d6ff84a7f2 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -797,6 +797,19 @@ public function getAnonymousFunctionReturnType(): ?Type /** @api */ public function getType(Expr $node): Type { + if ($node instanceof Node\Scalar\Int_) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); + } elseif ($node instanceof String_) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); + } elseif ($node instanceof Node\Scalar\Float_) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); + } elseif ($node instanceof ConstFetch) { + $loweredConstName = strtolower($node->name->toString()); + if (in_array($loweredConstName, ['true', 'false', 'null'], true)) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); + } + } + if ($node instanceof GetIterableKeyTypeExpr) { return $this->getIterableKeyType($this->getType($node->getExpr())); } @@ -1295,11 +1308,7 @@ private function resolveType(string $exprString, Expr $node): Type }); } - if ($node instanceof Node\Scalar\Int_) { - return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); - } elseif ($node instanceof String_) { - return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); - } elseif ($node instanceof Node\Scalar\InterpolatedString) { + if ($node instanceof Node\Scalar\InterpolatedString) { $resultType = null; foreach ($node->parts as $part) { if ($part instanceof InterpolatedStringPart) { @@ -1316,8 +1325,6 @@ private function resolveType(string $exprString, Expr $node): Type } return $resultType ?? new ConstantStringType(''); - } elseif ($node instanceof Node\Scalar\Float_) { - return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { if ($node instanceof FuncCall && $node->name instanceof Expr) { $callableType = $this->getType($node->name); From 08fca1d3f3939443c71d0e49d023448025e98ddf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Dec 2025 11:23:47 +0100 Subject: [PATCH 04/10] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index d6ff84a7f2..fcd744f03e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -803,6 +803,8 @@ public function getType(Expr $node): Type return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Node\Scalar\Float_) { return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); + } elseif ($node instanceof Expr\UnaryMinus && $node->expr instanceof Node\Scalar) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof ConstFetch) { $loweredConstName = strtolower($node->name->toString()); if (in_array($loweredConstName, ['true', 'false', 'null'], true)) { From 90c615ac313387202d801b4c8e3db579033538fd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Dec 2025 11:26:03 +0100 Subject: [PATCH 05/10] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index fcd744f03e..1d2b49fe82 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2048,16 +2048,6 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu } if ($node instanceof ConstFetch) { - $constName = (string) $node->name; - $loweredConstName = strtolower($constName); - if ($loweredConstName === 'true') { - return new ConstantBooleanType(true); - } elseif ($loweredConstName === 'false') { - return new ConstantBooleanType(false); - } elseif ($loweredConstName === 'null') { - return new NullType(); - } - $namespacedName = null; if (!$node->name->isFullyQualified() && $this->getNamespace() !== null) { $namespacedName = new FullyQualified([$this->getNamespace(), $node->name->toString()]); From eaab61107db7e2ee31529f18592b8f3549db4487 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Dec 2025 11:36:44 +0100 Subject: [PATCH 06/10] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 1d2b49fe82..c0d5b1f7b0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4357,7 +4357,7 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $this->parentScope, $this->nativeTypesPromoted, ); - $scope->resolvedTypes = $this->preserveResolvedTypes($expressionTypes); + $scope->resolvedTypes = $this->preserveResolvedTypes([$exprString]); if ($expr instanceof AlwaysRememberedExpr) { return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); @@ -4407,15 +4407,15 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN } /** - * @param array $expressionTypes + * @param array $changedExpressions * * @return array */ - private function preserveResolvedTypes(array $expressionTypes): array + private function preserveResolvedTypes(array $changedExpressions): array { $preservedTypes = $this->resolvedTypes; foreach($preservedTypes as $exprStringToInvalidate => $resolvedType) { - foreach ($expressionTypes as $exprString => $exprTypeHolder) { + foreach ($changedExpressions as $exprString) { if (str_contains($exprStringToInvalidate, $exprString)) { unset ($preservedTypes[$exprStringToInvalidate]); continue 2; From c3d0a18430212745ae45e833a0d01594191fdcd5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Dec 2025 11:44:53 +0100 Subject: [PATCH 07/10] 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 c0d5b1f7b0..8808a52e3b 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4409,7 +4409,7 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN /** * @param array $changedExpressions * - * @return array + * @return array */ private function preserveResolvedTypes(array $changedExpressions): array { From 3c9c8254c8715f89e8648cccc9ba78141d9325d8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Dec 2025 10:14:08 +0100 Subject: [PATCH 08/10] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8808a52e3b..ae29762759 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -877,9 +877,9 @@ public function getType(Expr $node): Type $key = $this->getNodeKey($node); if (!array_key_exists($key, $this->resolvedTypes)) { - $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node)); + $this->resolvedTypes[$key] = ExpressionTypeHolder::createYes($node, TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node))); } - return $this->resolvedTypes[$key]; + return $this->resolvedTypes[$key]->getType(); } public function getScopeType(Expr $expr): Type @@ -4357,7 +4357,7 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $this->parentScope, $this->nativeTypesPromoted, ); - $scope->resolvedTypes = $this->preserveResolvedTypes([$exprString]); + $scope->resolvedTypes = $this->preserveResolvedTypes([$exprString => $expr]); if ($expr instanceof AlwaysRememberedExpr) { return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); @@ -4407,19 +4407,23 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN } /** - * @param array $changedExpressions + * @param array $changedExpressions * - * @return array + * @return array */ private function preserveResolvedTypes(array $changedExpressions): array { $preservedTypes = $this->resolvedTypes; - foreach($preservedTypes as $exprStringToInvalidate => $resolvedType) { - foreach ($changedExpressions as $exprString) { - if (str_contains($exprStringToInvalidate, $exprString)) { - unset ($preservedTypes[$exprStringToInvalidate]); - continue 2; + foreach($preservedTypes as $exprString => $exprTypeHolder) { + $exprExpr = $exprTypeHolder->getExpr(); + + foreach ($changedExpressions as $exprStringToInvalidate => $expressionToInvalidate) { + if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, true)) { + continue; } + + unset($preservedTypes[$exprString]); + continue 2; } } From 1547053d0ec3080f61609e1a3f0d1f612c68bf28 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Dec 2025 10:30:32 +0100 Subject: [PATCH 09/10] 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 ae29762759..b721c00dc5 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4418,7 +4418,7 @@ private function preserveResolvedTypes(array $changedExpressions): array $exprExpr = $exprTypeHolder->getExpr(); foreach ($changedExpressions as $exprStringToInvalidate => $expressionToInvalidate) { - if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, true)) { + if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr)) { continue; } From 9ef4757dda5dbb619352d71e0d752380a0117dc9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Dec 2025 11:02:07 +0100 Subject: [PATCH 10/10] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b721c00dc5..f4291a1c58 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -10,6 +10,7 @@ use PhpParser\Node\ComplexType; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\BinaryOp; use PhpParser\Node\Expr\Cast\Unset_; use PhpParser\Node\Expr\ConstFetch; @@ -4418,6 +4419,11 @@ private function preserveResolvedTypes(array $changedExpressions): array $exprExpr = $exprTypeHolder->getExpr(); foreach ($changedExpressions as $exprStringToInvalidate => $expressionToInvalidate) { + while ($expressionToInvalidate instanceof Expr\ArrayDimFetch) { + $expressionToInvalidate = $expressionToInvalidate->var; + $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); + } + if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr)) { continue; }