Skip to content

Commit 44f377c

Browse files
committed
Check nullsafe on left side of =
1 parent 29f8938 commit 44f377c

File tree

5 files changed

+166
-0
lines changed

5 files changed

+166
-0
lines changed

build.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@
131131
<arg path="tests/PHPStan/Rules/Classes/data/duplicate-promoted-property.php"/>
132132
<arg value="--exclude"/>
133133
<arg path="tests/PHPStan/Rules/Properties/data/default-value-for-promoted-property.php"/>
134+
<arg value="--exclude"/>
135+
<arg path="tests/PHPStan/Rules/Operators/data/invalid-assign-var.php"/>
134136
<arg path="src" />
135137
<arg path="tests" />
136138
<arg path="compiler/src" />

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ rules:
3838
- PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule
3939
- PHPStan\Rules\Methods\ExistingClassesInTypehintsRule
4040
- PHPStan\Rules\Methods\MissingMethodImplementationRule
41+
- PHPStan\Rules\Operators\InvalidAssignVarRule
4142
- PHPStan\Rules\Properties\AccessPropertiesInAssignRule
4243
- PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule
4344
- PHPStan\Rules\Ternary\RequireParenthesesForNestedTernaryRule
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Operators;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\Assign;
8+
use PhpParser\Node\Expr\AssignOp;
9+
use PhpParser\Node\Expr\AssignRef;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
14+
/**
15+
* @implements Rule<Expr>
16+
*/
17+
class InvalidAssignVarRule implements Rule
18+
{
19+
20+
public function getNodeType(): string
21+
{
22+
return Expr::class;
23+
}
24+
25+
public function processNode(Node $node, Scope $scope): array
26+
{
27+
if (
28+
!$node instanceof Assign
29+
&& !$node instanceof AssignOp
30+
&& !$node instanceof AssignRef
31+
) {
32+
return [];
33+
}
34+
35+
if (!$this->containsNullSafe($node->var)) {
36+
return [];
37+
}
38+
39+
return [
40+
RuleErrorBuilder::message('Nullsafe operator cannot be on left side of assignment.')->nonIgnorable()->build(),
41+
];
42+
}
43+
44+
private function containsNullSafe(Expr $expr): bool
45+
{
46+
if (
47+
$expr instanceof Expr\NullsafePropertyFetch
48+
|| $expr instanceof Expr\NullsafeMethodCall
49+
) {
50+
return true;
51+
}
52+
53+
if ($expr instanceof Expr\ArrayDimFetch) {
54+
return $this->containsNullSafe($expr->var);
55+
}
56+
57+
if ($expr instanceof Expr\PropertyFetch) {
58+
return $this->containsNullSafe($expr->var);
59+
}
60+
61+
if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
62+
return $this->containsNullSafe($expr->class);
63+
}
64+
65+
if ($expr instanceof Expr\MethodCall) {
66+
return $this->containsNullSafe($expr->var);
67+
}
68+
69+
if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) {
70+
return $this->containsNullSafe($expr->class);
71+
}
72+
73+
if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) {
74+
foreach ($expr->items as $item) {
75+
if ($item === null) {
76+
continue;
77+
}
78+
79+
if ($item->key !== null && $this->containsNullSafe($item->key)) {
80+
return true;
81+
}
82+
83+
if ($this->containsNullSafe($item->value)) {
84+
return true;
85+
}
86+
}
87+
}
88+
89+
return false;
90+
}
91+
92+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Operators;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<InvalidAssignVarRule>
10+
*/
11+
class InvalidAssignVarRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new InvalidAssignVarRule();
17+
}
18+
19+
public function testRule(): void
20+
{
21+
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
22+
$this->markTestSkipped('Test requires PHP 8.0.');
23+
}
24+
25+
$this->analyse([__DIR__ . '/data/invalid-assign-var.php'], [
26+
[
27+
'Nullsafe operator cannot be on left side of assignment.',
28+
12,
29+
],
30+
[
31+
'Nullsafe operator cannot be on left side of assignment.',
32+
13,
33+
],
34+
[
35+
'Nullsafe operator cannot be on left side of assignment.',
36+
14,
37+
],
38+
[
39+
'Nullsafe operator cannot be on left side of assignment.',
40+
16,
41+
],
42+
[
43+
'Nullsafe operator cannot be on left side of assignment.',
44+
17,
45+
],
46+
]);
47+
}
48+
49+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace InvalidAssignVar;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(
9+
?\stdClass $a
10+
): void
11+
{
12+
$a?->foo = 'bar';
13+
$a?->foo->bar = 'bar';
14+
$a?->foo->bar['foo'] = 'bar';
15+
16+
[$a?->foo->bar] = 'test';
17+
[$a?->foo->bar => $b, $f?->foo->bar => $c] = 'test';
18+
19+
$c = 'foo';
20+
}
21+
22+
}

0 commit comments

Comments
 (0)