From 4f11780a4cc593ad91f92a1302ea34a9ec595205 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Mar 2026 14:24:11 +0100 Subject: [PATCH 01/11] "Cannot re-assign $this." false-positive --- .../Variables/InvalidVariableAssignRule.php | 9 ++++ .../InvalidVariableAssignRuleTest.php | 10 +++++ .../Rules/Variables/data/bug-14352.php | 45 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-14352.php diff --git a/src/Rules/Variables/InvalidVariableAssignRule.php b/src/Rules/Variables/InvalidVariableAssignRule.php index b70a3b6f67..e00c3eff96 100644 --- a/src/Rules/Variables/InvalidVariableAssignRule.php +++ b/src/Rules/Variables/InvalidVariableAssignRule.php @@ -2,12 +2,14 @@ namespace PHPStan\Rules\Variables; +use ArrayAccess; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\VariableAssignNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\ObjectType; use function is_string; /** @@ -30,6 +32,13 @@ public function processNode(Node $node, Scope $scope): array } if ($variable->name === 'this') { + $expr = $node->getAssignedExpr(); + $type = $scope->getType($expr); + + if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($type)->yes()) { + return []; + } + return [ RuleErrorBuilder::message('Cannot re-assign $this.') ->identifier('assign.this') diff --git a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php index 06febdd1d0..caa89a9dec 100644 --- a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php +++ b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php @@ -50,6 +50,16 @@ public function testBug3585(): void ]); } + public function testBug14352(): void + { + $this->analyse([__DIR__ . '/data/bug-14352.php'], [ + [ + 'Cannot re-assign $this.', + 35, + ], + ]); + } + public function testBug14349(): void { $this->analyse([__DIR__ . '/data/bug-14349.php'], [ diff --git a/tests/PHPStan/Rules/Variables/data/bug-14352.php b/tests/PHPStan/Rules/Variables/data/bug-14352.php new file mode 100644 index 0000000000..dd718fe884 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-14352.php @@ -0,0 +1,45 @@ + Date: Mon, 23 Mar 2026 14:49:38 +0100 Subject: [PATCH 02/11] Update InvalidVariableAssignRuleTest.php --- tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php index caa89a9dec..ec35a4f84d 100644 --- a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php +++ b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -50,6 +51,7 @@ public function testBug3585(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug14352(): void { $this->analyse([__DIR__ . '/data/bug-14352.php'], [ From bbf04d15c42d617ac67c2e5ec0aaa5afcdaaac67 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Mar 2026 15:07:06 +0100 Subject: [PATCH 03/11] test $this=$value when $value is ArrayAccess --- .../Rules/Variables/InvalidVariableAssignRuleTest.php | 6 +++++- tests/PHPStan/Rules/Variables/data/bug-14352.php | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php index ec35a4f84d..b34dbb5da2 100644 --- a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php +++ b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php @@ -57,7 +57,11 @@ public function testBug14352(): void $this->analyse([__DIR__ . '/data/bug-14352.php'], [ [ 'Cannot re-assign $this.', - 35, + 13, + ], + [ + 'Cannot re-assign $this.', + 37, ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14352.php b/tests/PHPStan/Rules/Variables/data/bug-14352.php index dd718fe884..3e7f9b9e6a 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14352.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14352.php @@ -8,7 +8,9 @@ class TestArrayAccess implements ArrayAccess { public function doFoo(string $key, string $value): void { - $this[$key] = $value; + $this[$key] = $value; // fine because ArrayAccess + + $this = $value; // should still fail } public function offsetExists(mixed $offset): bool From 97e496599ea746375bc2b5142cda40baa9208c32 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Mar 2026 15:09:35 +0100 Subject: [PATCH 04/11] fix lint --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 02b17ed80e..5fdb666448 100644 --- a/Makefile +++ b/Makefile @@ -134,6 +134,7 @@ lint: --exclude tests/PHPStan/Rules/EnumCases/data/bug-14252.php \ --exclude tests/PHPStan/Rules/Functions/data/bug-14241.php \ --exclude tests/PHPStan/Rules/Variables/data/bug-14349.php \ + --exclude tests/PHPStan/Rules/Variables/data/bug-14352.php \ src tests install-paratest: From 8d2b7d432fbfc538160c30d279a6206d3660a980 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 23 Mar 2026 14:07:14 +0000 Subject: [PATCH 05/11] Use dimFetch flag instead of checking assigned expression type for $this reassignment $this = $value should still be reported even when $value implements ArrayAccess. The fix now tracks whether the VariableAssignNode originated from a dimension-fetch assignment ($this[$key] = $value) vs a direct assignment ($this = $value). Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/AssignHandler.php | 6 ++--- src/Node/VariableAssignNode.php | 6 +++++ .../Variables/InvalidVariableAssignRule.php | 5 +--- .../InvalidVariableAssignRuleTest.php | 8 +++++++ .../Rules/Variables/data/bug-14352.php | 24 +++++++++++++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index df779e633e..3019bd9c55 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -463,7 +463,7 @@ public function processAssignVar( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite), true), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { @@ -480,7 +480,7 @@ public function processAssignVar( } } else { if ($var instanceof Variable) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr, true), $scopeBeforeAssignEval, $storage); } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { @@ -799,7 +799,7 @@ public function processAssignVar( } if ($var instanceof Variable && is_string($var->name)) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr, true), $scope, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php index 678d5bb348..35bb4a0771 100644 --- a/src/Node/VariableAssignNode.php +++ b/src/Node/VariableAssignNode.php @@ -12,6 +12,7 @@ final class VariableAssignNode extends NodeAbstract implements VirtualNode public function __construct( private Expr\Variable $variable, private Expr $assignedExpr, + private bool $dimFetch = false, ) { parent::__construct($variable->getAttributes()); @@ -27,6 +28,11 @@ public function getAssignedExpr(): Expr return $this->assignedExpr; } + public function isDimFetch(): bool + { + return $this->dimFetch; + } + #[Override] public function getType(): string { diff --git a/src/Rules/Variables/InvalidVariableAssignRule.php b/src/Rules/Variables/InvalidVariableAssignRule.php index e00c3eff96..f9d3894bb7 100644 --- a/src/Rules/Variables/InvalidVariableAssignRule.php +++ b/src/Rules/Variables/InvalidVariableAssignRule.php @@ -32,10 +32,7 @@ public function processNode(Node $node, Scope $scope): array } if ($variable->name === 'this') { - $expr = $node->getAssignedExpr(); - $type = $scope->getType($expr); - - if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($type)->yes()) { + if ($node->isDimFetch() && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($variable))->yes()) { return []; } diff --git a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php index b34dbb5da2..857af68ca1 100644 --- a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php +++ b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php @@ -63,6 +63,14 @@ public function testBug14352(): void 'Cannot re-assign $this.', 37, ], + [ + 'Cannot re-assign $this.', + 61, + ], + [ + 'Cannot re-assign $this.', + 69, + ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14352.php b/tests/PHPStan/Rules/Variables/data/bug-14352.php index 3e7f9b9e6a..a786ecc830 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14352.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14352.php @@ -30,6 +30,30 @@ public function offsetUnset(mixed $offset): void } } +class TestArrayAccessReassign implements ArrayAccess +{ + public function doFoo(self $other): void + { + $this = $other; + } + + public function offsetExists(mixed $offset): bool + { + } + + public function offsetGet(mixed $offset): mixed + { + } + + public function offsetSet(mixed $offset, mixed $value): void + { + } + + public function offsetUnset(mixed $offset): void + { + } +} + final class FinalTestPlain { public function doFoo(string $key, string $value): void From 2e8875d6ffadbebd580ac9cbb814faac1b93622b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Mar 2026 15:11:31 +0100 Subject: [PATCH 06/11] Revert "Use dimFetch flag instead of checking assigned expression type for $this reassignment" This reverts commit 8d2b7d432fbfc538160c30d279a6206d3660a980. --- src/Analyser/ExprHandler/AssignHandler.php | 6 ++--- src/Node/VariableAssignNode.php | 6 ----- .../Variables/InvalidVariableAssignRule.php | 5 +++- .../InvalidVariableAssignRuleTest.php | 8 ------- .../Rules/Variables/data/bug-14352.php | 24 ------------------- 5 files changed, 7 insertions(+), 42 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 3019bd9c55..df779e633e 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -463,7 +463,7 @@ public function processAssignVar( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite), true), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { @@ -480,7 +480,7 @@ public function processAssignVar( } } else { if ($var instanceof Variable) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr, true), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage); } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { @@ -799,7 +799,7 @@ public function processAssignVar( } if ($var instanceof Variable && is_string($var->name)) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr, true), $scope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php index 35bb4a0771..678d5bb348 100644 --- a/src/Node/VariableAssignNode.php +++ b/src/Node/VariableAssignNode.php @@ -12,7 +12,6 @@ final class VariableAssignNode extends NodeAbstract implements VirtualNode public function __construct( private Expr\Variable $variable, private Expr $assignedExpr, - private bool $dimFetch = false, ) { parent::__construct($variable->getAttributes()); @@ -28,11 +27,6 @@ public function getAssignedExpr(): Expr return $this->assignedExpr; } - public function isDimFetch(): bool - { - return $this->dimFetch; - } - #[Override] public function getType(): string { diff --git a/src/Rules/Variables/InvalidVariableAssignRule.php b/src/Rules/Variables/InvalidVariableAssignRule.php index f9d3894bb7..e00c3eff96 100644 --- a/src/Rules/Variables/InvalidVariableAssignRule.php +++ b/src/Rules/Variables/InvalidVariableAssignRule.php @@ -32,7 +32,10 @@ public function processNode(Node $node, Scope $scope): array } if ($variable->name === 'this') { - if ($node->isDimFetch() && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($variable))->yes()) { + $expr = $node->getAssignedExpr(); + $type = $scope->getType($expr); + + if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($type)->yes()) { return []; } diff --git a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php index 857af68ca1..b34dbb5da2 100644 --- a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php +++ b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php @@ -63,14 +63,6 @@ public function testBug14352(): void 'Cannot re-assign $this.', 37, ], - [ - 'Cannot re-assign $this.', - 61, - ], - [ - 'Cannot re-assign $this.', - 69, - ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14352.php b/tests/PHPStan/Rules/Variables/data/bug-14352.php index a786ecc830..3e7f9b9e6a 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14352.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14352.php @@ -30,30 +30,6 @@ public function offsetUnset(mixed $offset): void } } -class TestArrayAccessReassign implements ArrayAccess -{ - public function doFoo(self $other): void - { - $this = $other; - } - - public function offsetExists(mixed $offset): bool - { - } - - public function offsetGet(mixed $offset): mixed - { - } - - public function offsetSet(mixed $offset, mixed $value): void - { - } - - public function offsetUnset(mixed $offset): void - { - } -} - final class FinalTestPlain { public function doFoo(string $key, string $value): void From 5c5e3c71b334fce5a386f77ae720171464e4817a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 23 Mar 2026 14:27:36 +0000 Subject: [PATCH 07/11] Use dimFetch flag and check $this type with !->no() for ArrayAccess reassignment Instead of checking the assigned expression type, use a dimFetch flag on VariableAssignNode to distinguish $this[$key] = $value from $this = $value. Check $this type with !->no() so non-final classes that might implement ArrayAccess through subclasses are not falsely reported. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/AssignHandler.php | 6 ++--- src/Node/VariableAssignNode.php | 6 +++++ .../Variables/InvalidVariableAssignRule.php | 5 +--- .../InvalidVariableAssignRuleTest.php | 4 ++++ .../Rules/Variables/data/bug-14352.php | 24 +++++++++++++++++++ 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index df779e633e..3019bd9c55 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -463,7 +463,7 @@ public function processAssignVar( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite), true), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { @@ -480,7 +480,7 @@ public function processAssignVar( } } else { if ($var instanceof Variable) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr, true), $scopeBeforeAssignEval, $storage); } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { @@ -799,7 +799,7 @@ public function processAssignVar( } if ($var instanceof Variable && is_string($var->name)) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr, true), $scope, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php index 678d5bb348..35bb4a0771 100644 --- a/src/Node/VariableAssignNode.php +++ b/src/Node/VariableAssignNode.php @@ -12,6 +12,7 @@ final class VariableAssignNode extends NodeAbstract implements VirtualNode public function __construct( private Expr\Variable $variable, private Expr $assignedExpr, + private bool $dimFetch = false, ) { parent::__construct($variable->getAttributes()); @@ -27,6 +28,11 @@ public function getAssignedExpr(): Expr return $this->assignedExpr; } + public function isDimFetch(): bool + { + return $this->dimFetch; + } + #[Override] public function getType(): string { diff --git a/src/Rules/Variables/InvalidVariableAssignRule.php b/src/Rules/Variables/InvalidVariableAssignRule.php index e00c3eff96..0a775cdac3 100644 --- a/src/Rules/Variables/InvalidVariableAssignRule.php +++ b/src/Rules/Variables/InvalidVariableAssignRule.php @@ -32,10 +32,7 @@ public function processNode(Node $node, Scope $scope): array } if ($variable->name === 'this') { - $expr = $node->getAssignedExpr(); - $type = $scope->getType($expr); - - if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($type)->yes()) { + if ($node->isDimFetch() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($variable))->no()) { return []; } diff --git a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php index b34dbb5da2..6292134111 100644 --- a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php +++ b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php @@ -63,6 +63,10 @@ public function testBug14352(): void 'Cannot re-assign $this.', 37, ], + [ + 'Cannot re-assign $this.', + 61, + ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14352.php b/tests/PHPStan/Rules/Variables/data/bug-14352.php index 3e7f9b9e6a..b0bc60f13e 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14352.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14352.php @@ -30,6 +30,30 @@ public function offsetUnset(mixed $offset): void } } +class TestArrayAccessReassign implements ArrayAccess +{ + public function doFoo(self $other): void + { + $this = $other; // should still fail + } + + public function offsetExists(mixed $offset): bool + { + } + + public function offsetGet(mixed $offset): mixed + { + } + + public function offsetSet(mixed $offset, mixed $value): void + { + } + + public function offsetUnset(mixed $offset): void + { + } +} + final class FinalTestPlain { public function doFoo(string $key, string $value): void From ff7af6bff434db8a5b4956d69813cc610c53f407 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Mar 2026 15:45:00 +0100 Subject: [PATCH 08/11] Revert "Use dimFetch flag and check $this type with !->no() for ArrayAccess reassignment" This reverts commit 5c5e3c71b334fce5a386f77ae720171464e4817a. --- src/Analyser/ExprHandler/AssignHandler.php | 6 ++--- src/Node/VariableAssignNode.php | 6 ----- .../Variables/InvalidVariableAssignRule.php | 5 +++- .../InvalidVariableAssignRuleTest.php | 4 ---- .../Rules/Variables/data/bug-14352.php | 24 ------------------- 5 files changed, 7 insertions(+), 38 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 3019bd9c55..df779e633e 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -463,7 +463,7 @@ public function processAssignVar( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite), true), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { @@ -480,7 +480,7 @@ public function processAssignVar( } } else { if ($var instanceof Variable) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr, true), $scopeBeforeAssignEval, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage); } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { @@ -799,7 +799,7 @@ public function processAssignVar( } if ($var instanceof Variable && is_string($var->name)) { - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr, true), $scope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php index 35bb4a0771..678d5bb348 100644 --- a/src/Node/VariableAssignNode.php +++ b/src/Node/VariableAssignNode.php @@ -12,7 +12,6 @@ final class VariableAssignNode extends NodeAbstract implements VirtualNode public function __construct( private Expr\Variable $variable, private Expr $assignedExpr, - private bool $dimFetch = false, ) { parent::__construct($variable->getAttributes()); @@ -28,11 +27,6 @@ public function getAssignedExpr(): Expr return $this->assignedExpr; } - public function isDimFetch(): bool - { - return $this->dimFetch; - } - #[Override] public function getType(): string { diff --git a/src/Rules/Variables/InvalidVariableAssignRule.php b/src/Rules/Variables/InvalidVariableAssignRule.php index 0a775cdac3..e00c3eff96 100644 --- a/src/Rules/Variables/InvalidVariableAssignRule.php +++ b/src/Rules/Variables/InvalidVariableAssignRule.php @@ -32,7 +32,10 @@ public function processNode(Node $node, Scope $scope): array } if ($variable->name === 'this') { - if ($node->isDimFetch() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($variable))->no()) { + $expr = $node->getAssignedExpr(); + $type = $scope->getType($expr); + + if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($type)->yes()) { return []; } diff --git a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php index 6292134111..b34dbb5da2 100644 --- a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php +++ b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php @@ -63,10 +63,6 @@ public function testBug14352(): void 'Cannot re-assign $this.', 37, ], - [ - 'Cannot re-assign $this.', - 61, - ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14352.php b/tests/PHPStan/Rules/Variables/data/bug-14352.php index b0bc60f13e..3e7f9b9e6a 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14352.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14352.php @@ -30,30 +30,6 @@ public function offsetUnset(mixed $offset): void } } -class TestArrayAccessReassign implements ArrayAccess -{ - public function doFoo(self $other): void - { - $this = $other; // should still fail - } - - public function offsetExists(mixed $offset): bool - { - } - - public function offsetGet(mixed $offset): mixed - { - } - - public function offsetSet(mixed $offset, mixed $value): void - { - } - - public function offsetUnset(mixed $offset): void - { - } -} - final class FinalTestPlain { public function doFoo(string $key, string $value): void From 12e4ef59c245c60be0492b552e9a46c598f28c99 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Mar 2026 15:56:07 +0100 Subject: [PATCH 09/11] fix --- .../Variables/InvalidVariableAssignRule.php | 9 ++++---- .../InvalidVariableAssignRuleTest.php | 22 +++++++++++++++++++ .../Rules/Variables/data/bug-14352.php | 17 ++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/Rules/Variables/InvalidVariableAssignRule.php b/src/Rules/Variables/InvalidVariableAssignRule.php index e00c3eff96..8225090001 100644 --- a/src/Rules/Variables/InvalidVariableAssignRule.php +++ b/src/Rules/Variables/InvalidVariableAssignRule.php @@ -32,11 +32,12 @@ public function processNode(Node $node, Scope $scope): array } if ($variable->name === 'this') { - $expr = $node->getAssignedExpr(); - $type = $scope->getType($expr); + if ($scope->isInClass()) { + $classReflection = $scope->getClassReflection(); - if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($type)->yes()) { - return []; + if ($classReflection->is(ArrayAccess::class)) { + return []; + } } return [ diff --git a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php index b34dbb5da2..77cbf2c1b4 100644 --- a/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php +++ b/tests/PHPStan/Rules/Variables/InvalidVariableAssignRuleTest.php @@ -55,14 +55,36 @@ public function testBug3585(): void public function testBug14352(): void { $this->analyse([__DIR__ . '/data/bug-14352.php'], [ + /* [ 'Cannot re-assign $this.', 13, ], + */ [ 'Cannot re-assign $this.', 37, ], + [ + 'Cannot re-assign $this.', + 39, + ], + [ + 'Cannot re-assign $this.', + 47, + ], + [ + 'Cannot re-assign $this.', + 49, + ], + [ + 'Cannot re-assign $this.', + 57, + ], + [ + 'Cannot re-assign $this.', + 63, + ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14352.php b/tests/PHPStan/Rules/Variables/data/bug-14352.php index 3e7f9b9e6a..60f16d6544 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14352.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14352.php @@ -35,6 +35,8 @@ final class FinalTestPlain public function doFoo(string $key, string $value): void { $this[$key] = $value; + + $this = $value; } } @@ -43,5 +45,20 @@ class TestPlain public function doFoo(string $key, string $value): void { $this[$key] = $value; + + $this = $value; + } +} + +class TestStatic +{ + static public function doFoo(string $value): void + { + $this = $value; } } + +function doFoo(string $value): void +{ + $this = $value; +} From 272e2a9fd8d232215daf7a880933bcccd2b3511f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Mar 2026 15:56:39 +0100 Subject: [PATCH 10/11] Update InvalidVariableAssignRule.php --- src/Rules/Variables/InvalidVariableAssignRule.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Rules/Variables/InvalidVariableAssignRule.php b/src/Rules/Variables/InvalidVariableAssignRule.php index 8225090001..6167050617 100644 --- a/src/Rules/Variables/InvalidVariableAssignRule.php +++ b/src/Rules/Variables/InvalidVariableAssignRule.php @@ -9,7 +9,6 @@ use PHPStan\Node\VariableAssignNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectType; use function is_string; /** From 5f16ff142f8dd5d06cded82393f3a170e6beac95 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Mar 2026 16:01:52 +0100 Subject: [PATCH 11/11] Update InvalidVariableAssignRule.php --- src/Rules/Variables/InvalidVariableAssignRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/Variables/InvalidVariableAssignRule.php b/src/Rules/Variables/InvalidVariableAssignRule.php index 6167050617..aeaa6e3511 100644 --- a/src/Rules/Variables/InvalidVariableAssignRule.php +++ b/src/Rules/Variables/InvalidVariableAssignRule.php @@ -34,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array if ($scope->isInClass()) { $classReflection = $scope->getClassReflection(); - if ($classReflection->is(ArrayAccess::class)) { + if ($classReflection->implementsInterface(ArrayAccess::class)) { return []; } }