Skip to content

Commit 0624342

Browse files
committed
Bleeding edge - AssignToByRefExprFromForeachRule (level 1)
1 parent a2ebe49 commit 0624342

File tree

10 files changed

+236
-0
lines changed

10 files changed

+236
-0
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ parameters:
1313
reportTooWideBool: true
1414
rawMessageInBaseline: true
1515
reportNestedTooWideType: false
16+
assignToByRefForeachExpr: true

conf/config.level1.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ parameters:
1010
autowiredAttributeServices:
1111
# registers rules with #[RegisteredRule] attribute
1212
level: 1
13+
14+
conditionalTags:
15+
PHPStan\Rules\Variables\AssignToByRefExprFromForeachRule:
16+
phpstan.rules.rule: %featureToggles.assignToByRefForeachExpr%
17+
18+
services:
19+
-
20+
class: PHPStan\Rules\Variables\AssignToByRefExprFromForeachRule

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ parameters:
3737
reportTooWideBool: false
3838
rawMessageInBaseline: false
3939
reportNestedTooWideType: false
40+
assignToByRefForeachExpr: false
4041
fileExtensions:
4142
- php
4243
checkAdvancedIsset: false

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ parametersSchema:
4040
reportTooWideBool: bool()
4141
rawMessageInBaseline: bool()
4242
reportNestedTooWideType: bool()
43+
assignToByRefForeachExpr: bool()
4344
])
4445
fileExtensions: listOf(string())
4546
checkAdvancedIsset: bool()

src/Analyser/NodeScopeResolver.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
use PHPStan\Node\ExecutionEndNode;
8787
use PHPStan\Node\Expr\AlwaysRememberedExpr;
8888
use PHPStan\Node\Expr\ExistingArrayDimFetch;
89+
use PHPStan\Node\Expr\ForeachValueByRefExpr;
8990
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
9091
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
9192
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
@@ -1207,6 +1208,14 @@ private function processStmtNode(
12071208
$originalScope = $scope;
12081209
$bodyScope = $scope;
12091210

1211+
if ($stmt->keyVar instanceof Variable) {
1212+
$nodeCallback(new VariableAssignNode($stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr)), $originalScope);
1213+
}
1214+
1215+
if ($stmt->valueVar instanceof Variable) {
1216+
$nodeCallback(new VariableAssignNode($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)), $originalScope);
1217+
}
1218+
12101219
if ($context->isTopLevel()) {
12111220
$originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope;
12121221
$bodyScope = $this->enterForeach($originalScope, $originalScope, $stmt, $nodeCallback);
@@ -1269,6 +1278,9 @@ private function processStmtNode(
12691278
if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) {
12701279
$throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr);
12711280
}
1281+
if ($stmt->byRef) {
1282+
$finalScope = $finalScope->assignExpression(new ForeachValueByRefExpr($stmt->valueVar), new MixedType(), new MixedType());
1283+
}
12721284

12731285
return new StatementResult(
12741286
$finalScope,
@@ -1926,6 +1938,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch
19261938
$scope = $scope->invalidateExpression($var);
19271939
}
19281940

1941+
$scope = $scope->invalidateExpression(new ForeachValueByRefExpr($var));
19291942
}
19301943
} elseif ($stmt instanceof Node\Stmt\Use_) {
19311944
$hasYield = false;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 ForeachValueByRefExpr extends Expr implements VirtualNode
10+
{
11+
12+
public function __construct(private Expr $expr)
13+
{
14+
parent::__construct([]);
15+
}
16+
17+
public function getExpr(): Expr
18+
{
19+
return $this->expr;
20+
}
21+
22+
#[Override]
23+
public function getType(): string
24+
{
25+
return 'PHPStan_Node_ForeachValueByRefExpr';
26+
}
27+
28+
/**
29+
* @return string[]
30+
*/
31+
#[Override]
32+
public function getSubNodeNames(): array
33+
{
34+
return [];
35+
}
36+
37+
}

src/Node/Printer/Printer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\DependencyInjection\AutowiredService;
77
use PHPStan\Node\Expr\AlwaysRememberedExpr;
88
use PHPStan\Node\Expr\ExistingArrayDimFetch;
9+
use PHPStan\Node\Expr\ForeachValueByRefExpr;
910
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
1011
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
1112
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
@@ -88,6 +89,11 @@ protected function pPHPStan_Node_PropertyInitializationExpr(PropertyInitializati
8889
return sprintf('__phpstanPropertyInitialization(%s)', $expr->getPropertyName());
8990
}
9091

92+
protected function pPHPStan_Node_ForeachValueByRefExpr(ForeachValueByRefExpr $expr): string // phpcs:ignore
93+
{
94+
return sprintf('__phpstanForeachValueByRef(%s)', $this->p($expr->getExpr()));
95+
}
96+
9197
protected function pPHPStan_Node_ParameterVariableOriginalValueExpr(ParameterVariableOriginalValueExpr $expr): string // phpcs:ignore
9298
{
9399
return sprintf('__phpstanParameterVariableOriginalValue(%s)', $expr->getVariableName());
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Variables;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeAbstract;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\Expr\ForeachValueByRefExpr;
9+
use PHPStan\Node\Printer\ExprPrinter;
10+
use PHPStan\Node\PropertyAssignNode;
11+
use PHPStan\Node\VariableAssignNode;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use function sprintf;
15+
16+
/**
17+
* @implements Rule<NodeAbstract>
18+
*/
19+
final class AssignToByRefExprFromForeachRule implements Rule
20+
{
21+
22+
public function __construct(private ExprPrinter $printer)
23+
{
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return NodeAbstract::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
if ($node instanceof VariableAssignNode) {
34+
$expr = $node->getVariable();
35+
} elseif ($node instanceof PropertyAssignNode) {
36+
$expr = $node->getPropertyFetch();
37+
} else {
38+
return [];
39+
}
40+
41+
if ($scope->hasExpressionType(new ForeachValueByRefExpr($expr))->no()) {
42+
return [];
43+
}
44+
45+
return [
46+
RuleErrorBuilder::message(sprintf(
47+
'Assign to %s overwrites the last element from array.',
48+
$this->printer->printExpr($expr),
49+
))->identifier('assign.byRefForeachExpr')
50+
->tip('Unset it right after foreach to avoid this problem.')->build(),
51+
];
52+
}
53+
54+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Variables;
4+
5+
use PHPStan\Node\Printer\ExprPrinter;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<AssignToByRefExprFromForeachRule>
11+
*/
12+
class AssignToByRefExprFromForeachRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new AssignToByRefExprFromForeachRule(
18+
self::getContainer()->getByType(ExprPrinter::class),
19+
);
20+
}
21+
22+
public function testRule(): void
23+
{
24+
$this->analyse([__DIR__ . '/data/assign-to-by-ref-expr-from-foreach.php'], [
25+
[
26+
'Assign to $item overwrites the last element from array.',
27+
26,
28+
'Unset it right after foreach to avoid this problem.',
29+
],
30+
[
31+
'Assign to $item overwrites the last element from array.',
32+
50,
33+
'Unset it right after foreach to avoid this problem.',
34+
],
35+
[
36+
'Assign to $item overwrites the last element from array.',
37+
51,
38+
'Unset it right after foreach to avoid this problem.',
39+
],
40+
[
41+
'Assign to $item overwrites the last element from array.',
42+
62,
43+
'Unset it right after foreach to avoid this problem.',
44+
],
45+
]);
46+
}
47+
48+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace AssignToByRefExprFromForeach;
4+
5+
class Foo
6+
{
7+
8+
/** @param list<array<string, mixed>> $input */
9+
public function sayHello(array $input): void
10+
{
11+
foreach ($input as &$item) {
12+
$item = 2;
13+
}
14+
unset($item);
15+
16+
$item = 'foo';
17+
}
18+
19+
/** @param list<array<string, mixed>> $input */
20+
public function sayHello2(array $input): void
21+
{
22+
foreach ($input as &$item) {
23+
$item = 2;
24+
}
25+
26+
$item = 'foo';
27+
}
28+
29+
public function doFoo(): void
30+
{
31+
$array = [0, 1, 2, 3];
32+
foreach ($array as &$item) {
33+
$item = 2;
34+
}
35+
36+
unset($item);
37+
38+
foreach ($array as $item) {
39+
40+
}
41+
}
42+
43+
public function doFoo2(): void
44+
{
45+
$array = [0, 1, 2, 3];
46+
foreach ($array as &$item) {
47+
$item = 2;
48+
}
49+
50+
foreach ($array as $item) {
51+
$item = 2;
52+
}
53+
}
54+
55+
public function doFoo3(): void
56+
{
57+
$array = [0, 1, 2, 3];
58+
foreach ($array as &$item) {
59+
60+
}
61+
62+
foreach ($array as $item) {
63+
64+
}
65+
}
66+
67+
}

0 commit comments

Comments
 (0)