From e3e9895557a29ecb58c9e4c29162bc44c6650e2b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:33:25 +0000 Subject: [PATCH 1/5] Fix phpstan/phpstan#11705: variable type forgotten for array dim fetch after narrowing - In MutatingScope::resolveType(), when an ArrayDimFetch has a stored expression type and its dim has been narrowed to a constant scalar value, recompute the offset value type from the current array and dim types - Only replace the stored type when the recomputed type is strictly more specific - New regression test in tests/PHPStan/Analyser/nsrt/bug-11705.php - Root cause: array_key_exists with non-constant key stored a broad type for the ArrayDimFetch expression, which was not updated when the key was later narrowed (e.g., in a switch case) --- src/Analyser/MutatingScope.php | 19 +++++++++++++- tests/PHPStan/Analyser/nsrt/bug-11705.php | 31 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11705.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 03348429ff..2bd61beb13 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1043,7 +1043,24 @@ private function resolveType(string $exprString, Expr $node): Type && !$node instanceof Expr\ArrowFunction && $this->hasExpressionType($node)->yes() ) { - return $this->expressionTypes[$exprString]->getType(); + $type = $this->expressionTypes[$exprString]->getType(); + + if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) { + $dimType = $this->getType($node->dim); + if ($dimType->isConstantScalarValue()->yes()) { + $arrayType = $this->getType($node->var); + $offsetValueType = $arrayType->getOffsetValueType($dimType); + if ( + !$offsetValueType instanceof ErrorType + && $type->isSuperTypeOf($offsetValueType)->yes() + && !$offsetValueType->isSuperTypeOf($type)->yes() + ) { + $type = $offsetValueType; + } + } + } + + return $type; } /** @var ExprHandler $exprHandler */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-11705.php b/tests/PHPStan/Analyser/nsrt/bug-11705.php new file mode 100644 index 0000000000..5e29e27fc0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11705.php @@ -0,0 +1,31 @@ +} $theInput + * @param array $theTags + */ +function example(array $theInput, array $theTags): void +{ + foreach ($theTags as $tag) { + if (!array_key_exists($tag, $theInput)) { + continue; + } + switch ($tag) { + case 'name': + assertType("'name'", $tag); + assertType('string', $theInput[$tag]); + if ($tag === 'name') { + echo "Of course it is..."; + } + assertType("'name'", $tag); + assertType('string', $theInput[$tag]); + break; + default: + // fall out + } + } +} From 84f9914f0fa09eba5dd4a7c13adbd1e3fde5809b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 12 Mar 2026 08:44:35 +0000 Subject: [PATCH 2/5] Fix phpstan/phpstan#11705: move fix from MutatingScope to ArrayKeyExistsFunctionTypeSpecifyingExtension The previous fix in MutatingScope::resolveType() was a workaround that recomputed ArrayDimFetch types when the dim was narrowed. The root cause is in ArrayKeyExistsFunctionTypeSpecifyingExtension which stores the full getIterableValueType() for $arr[$key] when the key is non-constant. This stored type becomes stale when the key is later narrowed (e.g., in a switch/case), blocking the correct type from being computed. The fix: for constant arrays (shaped arrays) without optional keys, skip storing the dim fetch value type. The type will instead be computed fresh from the array type and the narrowed key type, giving the correct specific type. For arrays with optional keys, the stored type is still needed to prevent false "offset might not exist" errors. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 19 +----------------- ...yExistsFunctionTypeSpecifyingExtension.php | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 2bd61beb13..03348429ff 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1043,24 +1043,7 @@ private function resolveType(string $exprString, Expr $node): Type && !$node instanceof Expr\ArrowFunction && $this->hasExpressionType($node)->yes() ) { - $type = $this->expressionTypes[$exprString]->getType(); - - if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) { - $dimType = $this->getType($node->dim); - if ($dimType->isConstantScalarValue()->yes()) { - $arrayType = $this->getType($node->var); - $offsetValueType = $arrayType->getOffsetValueType($dimType); - if ( - !$offsetValueType instanceof ErrorType - && $type->isSuperTypeOf($offsetValueType)->yes() - && !$offsetValueType->isSuperTypeOf($type)->yes() - ) { - $type = $offsetValueType; - } - } - } - - return $type; + return $this->expressionTypes[$exprString]->getType(); } /** @var ExprHandler $exprHandler */ diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index ae9afb3005..716df6cd2a 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -23,6 +23,7 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; use function in_array; @@ -54,6 +55,21 @@ public function isFunctionSupported( && !$context->null(); } + private function shouldStoreArrayDimFetchType(Type $arrayType): bool + { + if (!$arrayType->isConstantArray()->yes()) { + return true; + } + + foreach ($arrayType->getConstantArrays() as $constantArray) { + if (count($constantArray->getOptionalKeys()) > 0) { + return true; + } + } + + return false; + } + public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, @@ -109,6 +125,10 @@ public function specifyTypes( $key, ); + if (!$this->shouldStoreArrayDimFetchType($arrayType)) { + return $specifiedTypes->setRootExpr(new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT')))); + } + return $specifiedTypes->unionWith($this->typeSpecifier->create( $arrayDimFetch, $arrayType->getIterableValueType(), From 2603236b4862ff5efcb5a6d5d3864b797a36b1fb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 12 Mar 2026 11:09:06 +0100 Subject: [PATCH 3/5] added failling test --- tests/PHPStan/Analyser/nsrt/bug-11705b.php | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11705b.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-11705b.php b/tests/PHPStan/Analyser/nsrt/bug-11705b.php new file mode 100644 index 0000000000..406d3d0856 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11705b.php @@ -0,0 +1,31 @@ +, 'optional'?: string} $theInput + * @param array $theTags + */ +function example(array $theInput, array $theTags): void +{ + foreach ($theTags as $tag) { + if (!array_key_exists($tag, $theInput)) { + continue; + } + switch ($tag) { + case 'name': + assertType("'name'", $tag); + assertType('string', $theInput[$tag]); + if ($tag === 'name') { + echo "Of course it is..."; + } + assertType("'name'", $tag); + assertType('string', $theInput[$tag]); + break; + default: + // fall out + } + } +} From 75cffd0740b8402b458fd841d0560f7243c5cc48 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 12 Mar 2026 10:23:58 +0000 Subject: [PATCH 4/5] Fix phpstan/phpstan#11705: recompute ArrayDimFetch type in MutatingScope when key is narrowed Revert the ArrayKeyExistsFunctionTypeSpecifyingExtension approach (shouldStoreArrayDimFetchType) which caused regressions in bug-11276 and bug-13526 by not storing the value type needed by the NonexistentOffset rule. Instead, fix in MutatingScope::resolveType(): when an ArrayDimFetch has a stored expression type and its dim has been narrowed to a constant scalar, recompute the offset value type and use it if strictly more specific than the stored type. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 19 +++++++++++++++++- ...yExistsFunctionTypeSpecifyingExtension.php | 20 ------------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 03348429ff..2bd61beb13 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1043,7 +1043,24 @@ private function resolveType(string $exprString, Expr $node): Type && !$node instanceof Expr\ArrowFunction && $this->hasExpressionType($node)->yes() ) { - return $this->expressionTypes[$exprString]->getType(); + $type = $this->expressionTypes[$exprString]->getType(); + + if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) { + $dimType = $this->getType($node->dim); + if ($dimType->isConstantScalarValue()->yes()) { + $arrayType = $this->getType($node->var); + $offsetValueType = $arrayType->getOffsetValueType($dimType); + if ( + !$offsetValueType instanceof ErrorType + && $type->isSuperTypeOf($offsetValueType)->yes() + && !$offsetValueType->isSuperTypeOf($type)->yes() + ) { + $type = $offsetValueType; + } + } + } + + return $type; } /** @var ExprHandler $exprHandler */ diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index 716df6cd2a..ae9afb3005 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -23,7 +23,6 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; -use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; use function in_array; @@ -55,21 +54,6 @@ public function isFunctionSupported( && !$context->null(); } - private function shouldStoreArrayDimFetchType(Type $arrayType): bool - { - if (!$arrayType->isConstantArray()->yes()) { - return true; - } - - foreach ($arrayType->getConstantArrays() as $constantArray) { - if (count($constantArray->getOptionalKeys()) > 0) { - return true; - } - } - - return false; - } - public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, @@ -125,10 +109,6 @@ public function specifyTypes( $key, ); - if (!$this->shouldStoreArrayDimFetchType($arrayType)) { - return $specifiedTypes->setRootExpr(new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT')))); - } - return $specifiedTypes->unionWith($this->typeSpecifier->create( $arrayDimFetch, $arrayType->getIterableValueType(), From 9932f2af24b1648f7b6d673e9cba7e056e04c508 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 13 Mar 2026 07:37:58 +0100 Subject: [PATCH 5/5] Update bug-11705b.php --- tests/PHPStan/Analyser/nsrt/bug-11705b.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11705b.php b/tests/PHPStan/Analyser/nsrt/bug-11705b.php index 406d3d0856..5363504de8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11705b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11705b.php @@ -1,6 +1,6 @@