diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6a9b163271..8dbbd7ae53 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2542,10 +2542,8 @@ public function enterMatch(Expr\Match_ $expr, Type $condType, Type $condNativeTy return $this->assignExpression($condExpr, $type, $nativeType); } - public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName, bool $valueByRef): self + public function enterForeach(self $originalScope, Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $valueName, ?string $keyName, bool $valueByRef): self { - $iterateeType = $originalScope->getType($iteratee); - $nativeIterateeType = $originalScope->getNativeType($iteratee); $valueType = $originalScope->getIterableValueType($iterateeType); $nativeValueType = $originalScope->getIterableValueType($nativeIterateeType); $scope = $this->assignVariable( @@ -2574,7 +2572,7 @@ public function enterForeach(self $originalScope, Expr $iteratee, string $valueN ); } if ($keyName !== null) { - $scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName); + $scope = $scope->enterForeachKey($originalScope, $iteratee, $iterateeType, $nativeIterateeType, $keyName); if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) { $scope = $scope->assignExpression( @@ -2588,11 +2586,8 @@ public function enterForeach(self $originalScope, Expr $iteratee, string $valueN return $scope; } - public function enterForeachKey(self $originalScope, Expr $iteratee, string $keyName): self + public function enterForeachKey(self $originalScope, Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $keyName): self { - $iterateeType = $originalScope->getType($iteratee); - $nativeIterateeType = $originalScope->getNativeType($iteratee); - $keyType = $originalScope->getIterableKeyType($iterateeType); $nativeKeyType = $originalScope->getIterableKeyType($nativeIterateeType); @@ -2847,8 +2842,13 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp } } - $assignedType = $scope->getType($assignedExpr); - $assignedNativeType = $scope->getNativeType($assignedExpr); + // Resolve the byref slot's new value directly from the just-assigned + // root variable's type, instead of re-evaluating the (stale) $assignedExpr + // node via Scope::getType(): the stored ArrayDimFetch result captured the + // array variable before it existed, so re-reading it would only resolve + // through the asking scope. We already hold the authoritative value here. + $assignedType = $this->resolveIntertwinedAssignedType($scope, $type, $assignedExpr, $variableName, false); + $assignedNativeType = $this->resolveIntertwinedAssignedType($scope, $nativeType, $assignedExpr, $variableName, true); $has = $scope->hasExpressionType($expressionType->getExpr()->getExpr()); if ( @@ -2862,14 +2862,15 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp } if ($unionWithOld) { $targetVarNode = new Variable($targetVarName); + $rootVarNode = new Variable($variableName); $assignedType = TypeCombinator::union( $assignedType, - $this->getType($assignedExpr), + $this->resolveIntertwinedAssignedType($this, $this->getType($rootVarNode), $assignedExpr, $variableName, false), $scope->getType($targetVarNode), ); $assignedNativeType = TypeCombinator::union( $assignedNativeType, - $this->getNativeType($assignedExpr), + $this->resolveIntertwinedAssignedType($this, $this->getNativeType($rootVarNode), $assignedExpr, $variableName, true), $scope->getNativeType($targetVarNode), ); } @@ -2896,6 +2897,37 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp return $scope; } + /** + * Resolves the type of a byref slot expression (rooted at $rootVariableName) + * from $rootType - the type just assigned to that root variable - by walking + * the offsets, without re-evaluating the stored $assignedExpr node via + * Scope::getType(). + */ + private function resolveIntertwinedAssignedType(self $scope, Type $rootType, Expr $assignedExpr, string $rootVariableName, bool $native): Type + { + if ($assignedExpr instanceof Variable && is_string($assignedExpr->name) && $assignedExpr->name === $rootVariableName) { + return $rootType; + } + + if ($assignedExpr instanceof Expr\ArrayDimFetch && $assignedExpr->dim !== null) { + return $this->resolveIntertwinedAssignedType($scope, $rootType, $assignedExpr->var, $rootVariableName, $native) + ->getOffsetValueType($scope->getType($assignedExpr->dim)); + } + + if ($assignedExpr instanceof SetExistingOffsetValueTypeExpr) { + // foreach-byref slot: the iteratee with its key offset set to the value + // variable's new type ($rootType is exactly that value - the expr's + // getValue()). + $iterateeType = $native + ? $scope->getNativeType($assignedExpr->getVar()) + : $scope->getType($assignedExpr->getVar()); + + return $iterateeType->setExistingOffsetValueType($scope->getType($assignedExpr->getDim()), $rootType); + } + + throw new ShouldNotHappenException(); + } + private function isDimFetchPathReachable(self $scope, Expr\ArrayDimFetch $dimFetch): bool { if ($dimFetch->dim === null) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c9028fcc91..aefe7850be 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1482,25 +1482,27 @@ public function processStmtNode( $this->callNodeCallback($nodeCallback, new InForeachNode($stmt), $scope, $storage); $originalScope = $scope; $bodyScope = $scope; + $foreachIterateeType = $originalScope->getType($stmt->expr); + $foreachNativeIterateeType = $originalScope->getNativeType($stmt->expr); if ($stmt->keyVar instanceof Variable) { $keyTypeExpr = new NativeTypeExpr( - $originalScope->getIterableKeyType($originalScope->getType($stmt->expr)), - $originalScope->getIterableKeyType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableKeyType($foreachIterateeType), + $originalScope->getIterableKeyType($foreachNativeIterateeType), ); $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, $keyTypeExpr), $originalScope, $storage); } if ($stmt->valueVar instanceof Variable) { $valueTypeExpr = new NativeTypeExpr( - $originalScope->getIterableValueType($originalScope->getType($stmt->expr)), - $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableValueType($foreachIterateeType), + $originalScope->getIterableValueType($foreachNativeIterateeType), ); $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, $valueTypeExpr), $originalScope, $storage); } elseif ($stmt->valueVar instanceof List_) { $virtualAssign = new Assign($stmt->valueVar, new NativeTypeExpr( - $originalScope->getIterableValueType($originalScope->getType($stmt->expr)), - $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableValueType($foreachIterateeType), + $originalScope->getIterableValueType($foreachNativeIterateeType), )); $virtualAssign->setAttributes($stmt->valueVar->getAttributes()); $this->callNodeCallback($nodeCallback, $virtualAssign, $scope, $storage); @@ -1514,19 +1516,21 @@ public function processStmtNode( $storage = $originalStorage->duplicate(); $originalScope = $iterateeScope; - $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context); + $foreachIterateeType = $originalScope->getType($stmt->expr); + $foreachNativeIterateeType = $originalScope->getNativeType($stmt->expr); + $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context, $foreachIterateeType, $foreachNativeIterateeType); if ($unrolledResult !== null) { $bodyScope = $unrolledResult['bodyScope']; $unrolledEndScope = $unrolledResult['endScope']; $unrolledTotalKeys = $unrolledResult['totalKeys']; } else { - $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $count = 0; do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($iterateeScope); $storage = $originalStorage->duplicate(); - $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { @@ -1546,7 +1550,7 @@ public function processStmtNode( $bodyScope = $bodyScope->mergeWith($iterateeScope); $storage = $originalStorage; - $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $finalPassContext = $unrolledTotalKeys !== null ? $context->enterUnrolledForeach($unrolledTotalKeys) : $context; $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $finalPassContext)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); @@ -4520,6 +4524,8 @@ private function tryProcessUnrolledConstantArrayForeach( MutatingScope $originalScope, ExpressionResultStorage $originalStorage, StatementContext $context, + Type $iterateeType, + Type $nativeIterateeType, ): ?array { if ($stmt->byRef) { @@ -4532,7 +4538,6 @@ private function tryProcessUnrolledConstantArrayForeach( return null; } - $iterateeType = $originalScope->getType($stmt->expr); if (!$iterateeType->isConstantArray()->yes()) { return null; } @@ -4558,7 +4563,6 @@ private function tryProcessUnrolledConstantArrayForeach( return null; } - $nativeIterateeType = $originalScope->getNativeType($stmt->expr); $nativeConstantArrays = $nativeIterateeType->getConstantArrays(); $matchedNativeArrays = count($nativeConstantArrays) === count($constantArrays) ? $nativeConstantArrays : null; @@ -4694,7 +4698,7 @@ private function tryProcessUnrolledConstantArrayForeach( $prevLoopScope = $loopScope; $iterStorage = $originalStorage->duplicate(); $iterBodyScope = $loopScope->mergeWith($endScope); - $iterBodyScope = $this->enterForeach($iterBodyScope, $iterStorage, $originalScope, $stmt, new NoopNodeCallback()); + $iterBodyScope = $this->enterForeach($iterBodyScope, $iterStorage, $originalScope, $stmt, $iterateeType, $nativeIterateeType, new NoopNodeCallback()); $iterBodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $iterBodyScope, $iterStorage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $loopScope = $iterBodyScopeResult->getScope(); foreach ($iterBodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { @@ -4753,13 +4757,12 @@ private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $ite /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, callable $nodeCallback): MutatingScope + private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, Type $iterateeType, Type $nativeIterateeType, callable $nodeCallback): MutatingScope { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $iterateeType = $originalScope->getType($stmt->expr); if ( ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) @@ -4768,6 +4771,8 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $scope = $scope->enterForeach( $originalScope, $stmt->expr, + $iterateeType, + $nativeIterateeType, $stmt->valueVar->name, $keyVarName, $stmt->byRef, @@ -4784,7 +4789,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $stmt->valueVar, new NativeTypeExpr( $originalScope->getIterableValueType($iterateeType), - $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableValueType($nativeIterateeType), ), $nodeCallback, )->getScope(); @@ -4792,7 +4797,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto if ( $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ) { - $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $stmt->keyVar->name); + $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $iterateeType, $nativeIterateeType, $stmt->keyVar->name); $vars[] = $stmt->keyVar->name; } elseif ($stmt->keyVar !== null) { $scope = $this->processVirtualAssign( @@ -4802,7 +4807,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $stmt->keyVar, new NativeTypeExpr( $originalScope->getIterableKeyType($iterateeType), - $originalScope->getIterableKeyType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableKeyType($nativeIterateeType), ), $nodeCallback, )->getScope();