From 15606b0c6a5bdba7b7f855f1eb620e3e6c3d1eba Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sun, 3 May 2026 11:41:28 +0000 Subject: [PATCH] Propagate `ArrayDimFetch` type narrowing to parent array in `BooleanAnd`/`BooleanOr` compound conditions - Add `propagateArrayDimFetchToParentArray()` in TypeSpecifier that enriches normalized SpecifiedTypes with parent array narrowing when a dim fetch on a variable is present but the variable itself has no specifier - Apply the propagation before `intersectWith()` in BooleanAnd falsy and BooleanOr truthy paths, where the intersection previously dropped non-overlapping dim-fetch vs variable specifiers - Use `HasOffsetValueType` to compute the narrowed parent array type - Restrict propagation to cases where the parent is a simple variable (not a nested dim fetch) to avoid cascading side effects - Covers `is_string`, `is_int`, `is_array`, `instanceof`, and strict comparisons on array offsets combined with `isset` via `&&` --- src/Analyser/TypeSpecifier.php | 62 +++++++++++++++++- tests/PHPStan/Analyser/nsrt/bug-14566.php | 80 +++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14566.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 9f7a86eb4a..06eb10dbfb 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -734,7 +734,13 @@ public function specifyTypesInCondition( $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); $rightScope = $scope->filterByTruthyValue($expr->left); $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); - $types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); + if ($context->true()) { + $types = $leftTypes->unionWith($rightTypes); + } else { + $leftNormalized = $this->propagateArrayDimFetchToParentArray($leftTypes->normalize($scope), $scope); + $rightNormalized = $this->propagateArrayDimFetchToParentArray($rightTypes->normalize($rightScope), $rightScope); + $types = $leftNormalized->intersectWith($rightNormalized); + } if ($context->false()) { $leftTypesForHolders = $leftTypes; $rightTypesForHolders = $rightTypes; @@ -788,7 +794,9 @@ public function specifyTypesInCondition( ) { $types = $leftTypes->normalize($scope); } else { - $types = $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); + $leftNormalized = $this->propagateArrayDimFetchToParentArray($leftTypes->normalize($scope), $scope); + $rightNormalized = $this->propagateArrayDimFetchToParentArray($rightTypes->normalize($rightScope), $rightScope); + $types = $leftNormalized->intersectWith($rightNormalized); $types = $this->augmentBooleanOrTruthyWithConditionalHolders($scope, $rightScope, $expr, $types); } } else { @@ -2076,6 +2084,56 @@ private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $sco return $types; } + private function propagateArrayDimFetchToParentArray(SpecifiedTypes $normalizedTypes, Scope $scope): SpecifiedTypes + { + $additionalSureTypes = []; + $sureTypes = $normalizedTypes->getSureTypes(); + foreach ($sureTypes as $exprString => [$expr, $type]) { + if ( + !$expr instanceof Expr\ArrayDimFetch + || $expr->dim === null + || !$expr->var instanceof Expr\Variable + ) { + continue; + } + $dimType = $scope->getType($expr->dim)->toArrayKey(); + $constantDimType = null; + if ($dimType instanceof ConstantIntegerType) { + $constantDimType = $dimType; + } else { + $constantStrings = $dimType->getConstantStrings(); + if (count($constantStrings) === 1) { + $constantDimType = $constantStrings[0]; + } + } + if ($constantDimType === null) { + continue; + } + $varExprString = $this->exprPrinter->printExpr($expr->var); + if (isset($sureTypes[$varExprString]) || isset($additionalSureTypes[$varExprString])) { + continue; + } + $varType = $scope->getType($expr->var); + if ($varType instanceof MixedType || $varType->isArray()->no()) { + continue; + } + $narrowedVarType = TypeCombinator::intersect($varType, new HasOffsetValueType($constantDimType, $type)); + if ($narrowedVarType instanceof NeverType) { + continue; + } + $additionalSureTypes[$varExprString] = [$expr->var, $narrowedVarType]; + } + + if ($additionalSureTypes === []) { + return $normalizedTypes; + } + + return new SpecifiedTypes( + $sureTypes + $additionalSureTypes, + $normalizedTypes->getSureNotTypes(), + ); + } + /** * @return array */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-14566.php b/tests/PHPStan/Analyser/nsrt/bug-14566.php new file mode 100644 index 0000000000..d4202a29e7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14566.php @@ -0,0 +1,80 @@ +