Skip to content

Commit 27085c5

Browse files
committed
Foreach can append to the array but it does not change the number of iterations
Closes phpstan/phpstan#8924
1 parent 2df1a59 commit 27085c5

File tree

4 files changed

+40
-23
lines changed

4 files changed

+40
-23
lines changed

src/Analyser/MutatingScope.php

+13-13
Original file line numberDiff line numberDiff line change
@@ -3121,37 +3121,37 @@ public function enterMatch(Expr\Match_ $expr): self
31213121
return $this->assignExpression($condExpr, $type, $nativeType);
31223122
}
31233123

3124-
public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName): self
3124+
public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName): self
31253125
{
3126-
$iterateeType = $this->getType($iteratee);
3127-
$nativeIterateeType = $this->getNativeType($iteratee);
3126+
$iterateeType = $originalScope->getType($iteratee);
3127+
$nativeIterateeType = $originalScope->getNativeType($iteratee);
31283128
$scope = $this->assignVariable(
31293129
$valueName,
3130-
$this->getIterableValueType($iterateeType),
3131-
$this->getIterableValueType($nativeIterateeType),
3130+
$originalScope->getIterableValueType($iterateeType),
3131+
$originalScope->getIterableValueType($nativeIterateeType),
31323132
);
31333133
if ($keyName !== null) {
3134-
$scope = $scope->enterForeachKey($iteratee, $keyName);
3134+
$scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName);
31353135
}
31363136

31373137
return $scope;
31383138
}
31393139

3140-
public function enterForeachKey(Expr $iteratee, string $keyName): self
3140+
public function enterForeachKey(self $originalScope, Expr $iteratee, string $keyName): self
31413141
{
3142-
$iterateeType = $this->getType($iteratee);
3143-
$nativeIterateeType = $this->getNativeType($iteratee);
3142+
$iterateeType = $originalScope->getType($iteratee);
3143+
$nativeIterateeType = $originalScope->getNativeType($iteratee);
31443144
$scope = $this->assignVariable(
31453145
$keyName,
3146-
$this->getIterableKeyType($iterateeType),
3147-
$this->getIterableKeyType($nativeIterateeType),
3146+
$originalScope->getIterableKeyType($iterateeType),
3147+
$originalScope->getIterableKeyType($nativeIterateeType),
31483148
);
31493149

31503150
if ($iterateeType->isArray()->yes()) {
31513151
$scope = $scope->assignExpression(
31523152
new Expr\ArrayDimFetch($iteratee, new Variable($keyName)),
3153-
$this->getIterableValueType($iterateeType),
3154-
$this->getIterableValueType($nativeIterateeType),
3153+
$originalScope->getIterableValueType($iterateeType),
3154+
$originalScope->getIterableValueType($nativeIterateeType),
31553155
);
31563156
}
31573157

src/Analyser/NodeScopeResolver.php

+11-9
Original file line numberDiff line numberDiff line change
@@ -835,20 +835,21 @@ private function processStmtNode(
835835
$stmt->expr,
836836
new Array_([]),
837837
);
838-
$inForeachScope = $scope;
839838
if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
840-
$inForeachScope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
839+
$scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
841840
}
842-
$nodeCallback(new InForeachNode($stmt), $inForeachScope);
841+
$nodeCallback(new InForeachNode($stmt), $scope);
842+
$originalScope = $scope;
843843
$bodyScope = $scope;
844844

845845
if ($context->isTopLevel()) {
846-
$bodyScope = $this->polluteScopeWithAlwaysIterableForeach ? $this->enterForeach($scope->filterByTruthyValue($arrayComparisonExpr), $stmt) : $this->enterForeach($scope, $stmt);
846+
$originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope;
847+
$bodyScope = $this->enterForeach($originalScope, $originalScope, $stmt);
847848
$count = 0;
848849
do {
849850
$prevScope = $bodyScope;
850851
$bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
851-
$bodyScope = $this->enterForeach($bodyScope, $stmt);
852+
$bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt);
852853
$bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
853854
}, $context->enterDeep())->filterOutLoopExitPoints();
854855
$bodyScope = $bodyScopeResult->getScope();
@@ -867,7 +868,7 @@ private function processStmtNode(
867868
}
868869

869870
$bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
870-
$bodyScope = $this->enterForeach($bodyScope, $stmt);
871+
$bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt);
871872
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
872873
$finalScope = $finalScopeResult->getScope();
873874
foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
@@ -4142,12 +4143,12 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames
41424143
return $scope;
41434144
}
41444145

4145-
private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingScope
4146+
private function enterForeach(MutatingScope $scope, MutatingScope $originalScope, Foreach_ $stmt): MutatingScope
41464147
{
41474148
if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
41484149
$scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
41494150
}
4150-
$iterateeType = $scope->getType($stmt->expr);
4151+
$iterateeType = $originalScope->getType($stmt->expr);
41514152
if (
41524153
($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))
41534154
&& ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
@@ -4157,6 +4158,7 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco
41574158
$keyVarName = $stmt->keyVar->name;
41584159
}
41594160
$scope = $scope->enterForeach(
4161+
$originalScope,
41604162
$stmt->expr,
41614163
$stmt->valueVar->name,
41624164
$keyVarName,
@@ -4180,7 +4182,7 @@ static function (): void {
41804182
if (
41814183
$stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
41824184
) {
4183-
$scope = $scope->enterForeachKey($stmt->expr, $stmt->keyVar->name);
4185+
$scope = $scope->enterForeachKey($originalScope, $stmt->expr, $stmt->keyVar->name);
41844186
$vars[] = $stmt->keyVar->name;
41854187
} elseif ($stmt->keyVar !== null) {
41864188
$scope = $this->processAssignVar(

tests/PHPStan/Analyser/data/bug-4504.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public function sayHello($models): void
1414
assertType('Bug4504TypeInference\A', $v);
1515
}
1616

17-
assertType('array{}|Iterator<mixed, Bug4504TypeInference\A>', $models);
17+
assertType('Iterator<mixed, Bug4504TypeInference\A>', $models);
1818
}
1919

2020
}

tests/PHPStan/Analyser/data/bug-8924.php

+15
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,18 @@ function foo(array $array): void {
1313
$array = null;
1414
}
1515
}
16+
17+
function makeValidNumbers(): array
18+
{
19+
$validNumbers = [1, 2];
20+
foreach ($validNumbers as $k => $v) {
21+
assertType("non-empty-list<-2|-1|1|2|' 1'|' 2'>", $validNumbers);
22+
assertType('0|1', $k);
23+
assertType('1|2', $v);
24+
$validNumbers[] = -$v;
25+
$validNumbers[] = ' ' . (string)$v;
26+
assertType("non-empty-list<-2|-1|1|2|' 1'|' 2'>", $validNumbers);
27+
}
28+
29+
return $validNumbers;
30+
}

0 commit comments

Comments
 (0)