Skip to content

Commit 8f9490e

Browse files
committed
Support writing to array in foreach with value-by-ref
1 parent 0eb7dff commit 8f9490e

File tree

9 files changed

+216
-11
lines changed

9 files changed

+216
-11
lines changed

src/Analyser/MutatingScope.php

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
3434
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
3535
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
36+
use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
3637
use PHPStan\Node\Expr\NativeTypeExpr;
3738
use PHPStan\Node\Expr\OriginalForeachKeyExpr;
3839
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
@@ -3929,18 +3930,39 @@ public function enterMatch(Expr\Match_ $expr): self
39293930
return $this->assignExpression($condExpr, $type, $nativeType);
39303931
}
39313932

3932-
public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName): self
3933+
public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName, bool $valueByRef): self
39333934
{
39343935
$iterateeType = $originalScope->getType($iteratee);
39353936
$nativeIterateeType = $originalScope->getNativeType($iteratee);
3937+
$valueType = $originalScope->getIterableValueType($iterateeType);
3938+
$nativeValueType = $originalScope->getIterableValueType($nativeIterateeType);
39363939
$scope = $this->assignVariable(
39373940
$valueName,
3938-
$originalScope->getIterableValueType($iterateeType),
3939-
$originalScope->getIterableValueType($nativeIterateeType),
3941+
$valueType,
3942+
$nativeValueType,
39403943
TrinaryLogic::createYes(),
39413944
);
3945+
if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
3946+
$scope = $scope->assignExpression(
3947+
new IntertwinedVariableByReferenceWithExpr($valueName, $iteratee, new SetOffsetValueTypeExpr(
3948+
$iteratee,
3949+
new GetIterableKeyTypeExpr($iteratee),
3950+
new Variable($valueName),
3951+
)),
3952+
$valueType,
3953+
$nativeValueType,
3954+
);
3955+
}
39423956
if ($keyName !== null) {
39433957
$scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName);
3958+
3959+
if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
3960+
$scope = $scope->assignExpression(
3961+
new IntertwinedVariableByReferenceWithExpr($valueName, new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), new Variable($valueName)),
3962+
$valueType,
3963+
$nativeValueType,
3964+
);
3965+
}
39443966
}
39453967

39463968
return $scope;
@@ -4142,13 +4164,38 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
41424164
$scope->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($node, $nativeType, $certainty);
41434165
}
41444166

4145-
$parameterOriginalValueExprString = $this->getNodeKey(new ParameterVariableOriginalValueExpr($variableName));
4146-
unset($scope->expressionTypes[$parameterOriginalValueExprString]);
4147-
unset($scope->nativeExpressionTypes[$parameterOriginalValueExprString]);
4167+
foreach ($scope->expressionTypes as $expressionType) {
4168+
if (!$expressionType->getExpr() instanceof IntertwinedVariableByReferenceWithExpr) {
4169+
continue;
4170+
}
4171+
if (!$expressionType->getCertainty()->yes()) {
4172+
continue;
4173+
}
4174+
if ($expressionType->getExpr()->getVariableName() !== $variableName) {
4175+
continue;
4176+
}
4177+
4178+
$has = $scope->hasExpressionType($expressionType->getExpr()->getExpr());
4179+
if (
4180+
$expressionType->getExpr()->getExpr() instanceof Variable
4181+
&& is_string($expressionType->getExpr()->getExpr()->name)
4182+
&& !$has->no()
4183+
) {
4184+
$scope = $scope->assignVariable(
4185+
$expressionType->getExpr()->getExpr()->name,
4186+
$scope->getType($expressionType->getExpr()->getAssignedExpr()),
4187+
$scope->getNativeType($expressionType->getExpr()->getAssignedExpr()),
4188+
$has,
4189+
);
4190+
} else {
4191+
$scope = $scope->assignExpression(
4192+
$expressionType->getExpr()->getExpr(),
4193+
$scope->getType($expressionType->getExpr()->getAssignedExpr()),
4194+
$scope->getNativeType($expressionType->getExpr()->getAssignedExpr()),
4195+
);
4196+
}
41484197

4149-
$originalForeachKeyExpr = $this->getNodeKey(new OriginalForeachKeyExpr($variableName));
4150-
unset($scope->expressionTypes[$originalForeachKeyExpr]);
4151-
unset($scope->nativeExpressionTypes[$originalForeachKeyExpr]);
4198+
}
41524199

41534200
return $scope;
41544201
}

src/Analyser/NodeScopeResolver.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6699,6 +6699,7 @@ private function enterForeach(MutatingScope $scope, MutatingScope $originalScope
66996699
$stmt->expr,
67006700
$stmt->valueVar->name,
67016701
$keyVarName,
6702+
$stmt->byRef,
67026703
);
67036704
$vars = [$stmt->valueVar->name];
67046705
if ($keyVarName !== null) {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node\Expr;
4+
5+
use Override;
6+
use PhpParser\Node\Expr;
7+
use PHPStan\Node\VirtualNode;
8+
9+
final class IntertwinedVariableByReferenceWithExpr extends Expr implements VirtualNode
10+
{
11+
12+
public function __construct(private string $variableName, public Expr $expr, private Expr $assignedExpr)
13+
{
14+
parent::__construct([]);
15+
}
16+
17+
public function getVariableName(): string
18+
{
19+
return $this->variableName;
20+
}
21+
22+
public function getExpr(): Expr
23+
{
24+
return $this->expr;
25+
}
26+
27+
public function getAssignedExpr(): Expr
28+
{
29+
return $this->assignedExpr;
30+
}
31+
32+
#[Override]
33+
public function getType(): string
34+
{
35+
return 'PHPStan_Node_IntertwinedVariableByReferenceWithExpr';
36+
}
37+
38+
/**
39+
* @return string[]
40+
*/
41+
#[Override]
42+
public function getSubNodeNames(): array
43+
{
44+
return ['expr'];
45+
}
46+
47+
}

src/Node/Expr/OriginalForeachKeyExpr.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
final class OriginalForeachKeyExpr extends Expr implements VirtualNode
1010
{
1111

12+
public Expr\Variable $var;
13+
1214
public function __construct(private string $variableName)
1315
{
1416
parent::__construct([]);
17+
$this->var = new Expr\Variable($this->variableName);
1518
}
1619

1720
public function getVariableName(): string
@@ -31,7 +34,7 @@ public function getType(): string
3134
#[Override]
3235
public function getSubNodeNames(): array
3336
{
34-
return [];
37+
return ['var'];
3538
}
3639

3740
}

src/Node/Expr/ParameterVariableOriginalValueExpr.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
final class ParameterVariableOriginalValueExpr extends Expr implements VirtualNode
1010
{
1111

12+
public Expr\Variable $var;
13+
1214
public function __construct(private string $variableName)
1315
{
1416
parent::__construct([]);
17+
$this->var = new Expr\Variable($this->variableName);
1518
}
1619

1720
public function getVariableName(): string
@@ -31,7 +34,7 @@ public function getType(): string
3134
#[Override]
3235
public function getSubNodeNames(): array
3336
{
34-
return [];
37+
return ['var'];
3538
}
3639

3740
}

src/Node/Printer/Printer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
1111
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
1212
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
13+
use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
1314
use PHPStan\Node\Expr\NativeTypeExpr;
1415
use PHPStan\Node\Expr\OriginalForeachKeyExpr;
1516
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
@@ -105,6 +106,11 @@ protected function pPHPStan_Node_OriginalForeachKeyExpr(OriginalForeachKeyExpr $
105106
return sprintf('__phpstanOriginalForeachKey(%s)', $expr->getVariableName());
106107
}
107108

109+
protected function pPHPStan_Node_IntertwinedVariableByReferenceWithExpr(IntertwinedVariableByReferenceWithExpr $expr): string // phpcs:ignore
110+
{
111+
return sprintf('__phpstanIntertwinedVariableByReference(%s, %s, %s)', $expr->getVariableName(), $this->p($expr->getExpr()), $this->p($expr->getAssignedExpr()));
112+
}
113+
108114
protected function pPHPStan_Node_IssetExpr(IssetExpr $expr): string // phpcs:ignore
109115
{
110116
return sprintf('__phpstanIssetExpr(%s)', $this->p($expr->getExpr()));

tests/PHPStan/Analyser/nsrt/overwritten-arrays.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,59 @@ public function doFoo6(array $a): void
118118
assertType('array<int, 1|2|string>', $a);
119119
}
120120

121+
/**
122+
* @param array<int, string> $a
123+
*/
124+
public function doFoo7(array $a): void
125+
{
126+
foreach ($a as &$v) {
127+
$v = 1;
128+
}
129+
130+
assertType('array<int, 1|string>', $a); // could be array<int, 1>
131+
}
132+
133+
/**
134+
* @param array<int, string> $a
135+
*/
136+
public function doFoo8(array $a): void
137+
{
138+
foreach ($a as &$v) {
139+
if (rand(0, 1)) {
140+
$v = 1;
141+
}
142+
}
143+
144+
assertType('array<int, 1|string>', $a);
145+
}
146+
147+
/**
148+
* @param array<int, string> $a
149+
*/
150+
public function doFoo9(array $a): void
151+
{
152+
foreach ($a as $k => &$v) {
153+
$v = 1;
154+
assertType('non-empty-array<int, 1|string>', $a);
155+
assertType('1', $a[$k]);
156+
}
157+
158+
assertType('array<int, 1>', $a);
159+
}
160+
161+
/**
162+
* @param array<int, string> $a
163+
*/
164+
public function doFoo10(array $a): void
165+
{
166+
foreach ($a as $k => &$v) {
167+
$k++;
168+
$v = 1;
169+
assertType('non-empty-array<int, 1|string>', $a);
170+
assertType('1|string', $a[$k]);
171+
}
172+
173+
assertType('array<int, 1|string>', $a);
174+
}
175+
121176
}

tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,11 @@ public function testNestedTooWideType(): void
302302
]);
303303
}
304304

305+
public function testBug13676(): void
306+
{
307+
$this->reportTooWideBool = true;
308+
$this->reportNestedTooWideType = true;
309+
$this->analyse([__DIR__ . '/data/bug-13676.php'], []);
310+
}
311+
305312
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Bug13676;
4+
5+
class HelloWorld
6+
{
7+
/**
8+
* @param array<array<string, int>> $rows
9+
*
10+
* @return array<array<string, int|string>>
11+
*/
12+
private function prepareExpectedRows(array $rows): array
13+
{
14+
if (rand(0,1)) {
15+
return $rows;
16+
}
17+
18+
if (rand(0,1)) {
19+
foreach ($rows as &$row) {
20+
foreach ($row as &$value) {
21+
$value = (string) $value;
22+
}
23+
}
24+
}
25+
26+
if (rand(0,1)) {
27+
return $rows;
28+
}
29+
30+
foreach ($rows as &$row) {
31+
$row = array_change_key_case($row, CASE_UPPER);
32+
}
33+
34+
return $rows;
35+
}
36+
}

0 commit comments

Comments
 (0)