diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 51995ac6657..ae7f7d61ad0 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -155,6 +155,31 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex ); $scope = $result->getScope(); + if ( + $expr instanceof AssignRef + && $expr->var instanceof Variable + && is_string($expr->var->name) + && $expr->expr instanceof ArrayDimFetch + ) { + $rootExpr = $expr->expr; + while ($rootExpr instanceof ArrayDimFetch) { + $rootExpr = $rootExpr->var; + } + + if ($rootExpr instanceof Variable && is_string($rootExpr->name)) { + $varName = $expr->var->name; + $type = $scope->getType($expr->var); + $nativeType = $scope->getNativeType($expr->var); + + // When $varName is assigned, update the ArrayDimFetch expression + $scope = $scope->assignExpression( + new IntertwinedVariableByReferenceWithExpr($varName, $expr->expr, new Variable($varName)), + $type, + $nativeType, + ); + } + } + if ( $expr instanceof AssignRef && $expr->var instanceof Variable diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a43ba35dda5..b27eef5856d 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2623,17 +2623,80 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp if ($targetRootVar !== null && in_array($targetRootVar, $intertwinedPropagatedFrom, true)) { continue; } - $scope = $scope->assignExpression( - $expressionType->getExpr()->getExpr(), - $scope->getType($expressionType->getExpr()->getAssignedExpr()), - $scope->getNativeType($expressionType->getExpr()->getAssignedExpr()), - ); + $targetExpr = $expressionType->getExpr()->getExpr(); + $newType = $scope->getType($expressionType->getExpr()->getAssignedExpr()); + $newNativeType = $scope->getNativeType($expressionType->getExpr()->getAssignedExpr()); + + if ($targetExpr instanceof Expr\ArrayDimFetch && $targetRootVar !== null && $this->hasConstantDimInChain($targetExpr)) { + $scope = $this->propagateRefTypeToArrayDimFetch($scope, $targetExpr, $newType, $newNativeType, $intertwinedPropagatedFrom, $variableName); + } else { + $scope = $scope->assignExpression( + $targetExpr, + $newType, + $newNativeType, + ); + } } } return $scope; } + private function hasConstantDimInChain(Expr\ArrayDimFetch $dimFetch): bool + { + $current = $dimFetch; + while ($current instanceof Expr\ArrayDimFetch) { + if ($current->dim !== null) { + $dimType = $this->getType($current->dim)->toArrayKey(); + if ($dimType->isConstantScalarValue()->yes()) { + return true; + } + } + $current = $current->var; + } + + return false; + } + + /** + * @param list $intertwinedPropagatedFrom + */ + private function propagateRefTypeToArrayDimFetch( + self $scope, + Expr\ArrayDimFetch $targetExpr, + Type $newType, + Type $newNativeType, + array $intertwinedPropagatedFrom, + string $variableName, + ): self + { + $expr = $targetExpr; + $type = $newType; + $nativeType = $newNativeType; + + while ($expr instanceof Expr\ArrayDimFetch) { + if ($expr->dim === null) { + return $scope->assignExpression($targetExpr, $newType, $newNativeType); + } + $dimType = $scope->getType($expr->dim)->toArrayKey(); + $type = $scope->getType($expr->var)->setOffsetValueType($dimType, $type); + $nativeType = $scope->getNativeType($expr->var)->setOffsetValueType($dimType, $nativeType); + $expr = $expr->var; + } + + if ($expr instanceof Variable && is_string($expr->name)) { + return $scope->assignVariable( + $expr->name, + $type, + $nativeType, + TrinaryLogic::createYes(), + array_merge($intertwinedPropagatedFrom, [$variableName]), + ); + } + + return $scope->assignExpression($targetExpr, $newType, $newNativeType); + } + private function isDimFetchPathReachable(self $scope, Expr\ArrayDimFetch $dimFetch): bool { if ($dimFetch->dim === null) { diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index cbb3ed25a77..6d41fc1a0a9 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1226,4 +1226,10 @@ public function testBug12063(): void $this->analyse([__DIR__ . '/data/bug-12063.php'], []); } + public function testBug14449(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-14449.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14449.php b/tests/PHPStan/Rules/Comparison/data/bug-14449.php new file mode 100644 index 00000000000..5e8450a8338 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14449.php @@ -0,0 +1,36 @@ + + */ +function DataGenerator() : \Generator +{ + yield [ 'id' => 1 ] ; + yield [ 'id' => 1 ] ; +} + +function () : void { + $results = []; + + $generator = DataGenerator(); + foreach ( $generator as $data ) + { + $id = $data['id']; + if ( !array_key_exists($id, $results ) ) + { + $results[$id] = []; + } + if ( !array_key_exists('data',$results[$id]) ) + { + $results[$id]['data'] = []; + } + + $resultData = &$results[$id]['data']; + if ( !array_key_exists('id', $results[$id]['data']) ) + { + $resultData['id'] = $id; + } + } +};