From b12c794babcacbfaee5e9d578262eaa3e21cef92 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 c78ce49de8..a8b64d88bf 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 3e844e94341c09e0b87b4e623424a58812b2c234 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 a8b64d88bf..a83d63b0da 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 4ff1b665cd8ffbc2b73a8208324f7bad78f92e60 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 a83d63b0da..736b98bbb4 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 f1b626138f3b230c51de0afca146318439579d41 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 736b98bbb4..af852b1e7f 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 11ec2ee3a3383736e5029e0f974ddb16a2b4c5b1 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 af852b1e7f..a801484daa 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 8ad03611c94a364d1082ed8ff6714514aed36237 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 a801484daa..7c2e03655a 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 e19db84333914b5953fae6fd73573b7807a8246f 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 7c2e03655a..d8e727de6d 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 886c69e088cf35124e206b1b82d82c39d0610df2 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 d8e727de6d..5f9123a74c 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 3c21b376c0f5dc7cfd124129d160ac680647d8ec 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 5f9123a74c..5526251132 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 9386e6b329f7ca2ee2150e393907f8b564803419 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 5526251132..6080853624 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; }