diff --git a/src/Analyser/ImpurePoint.php b/src/Analyser/ImpurePoint.php index 59fccfac36..e87e8866b0 100644 --- a/src/Analyser/ImpurePoint.php +++ b/src/Analyser/ImpurePoint.php @@ -6,7 +6,7 @@ use PHPStan\Node\VirtualNode; /** - * @phpstan-type ImpurePointIdentifier = 'echo'|'die'|'exit'|'propertyAssign'|'propertyUnset'|'methodCall'|'new'|'functionCall'|'include'|'require'|'print'|'eval'|'superglobal'|'yield'|'yieldFrom'|'static'|'global'|'betweenPhpTags' + * @phpstan-type ImpurePointIdentifier = 'echo'|'die'|'exit'|'propertyAssign'|'propertyAssignByRef'|'propertyUnset'|'methodCall'|'new'|'functionCall'|'include'|'require'|'print'|'eval'|'superglobal'|'yield'|'yieldFrom'|'static'|'global'|'betweenPhpTags' * @api */ class ImpurePoint diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b0ab0ae213..74abe12d13 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2135,7 +2135,23 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop $nodeCallback, $context, function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult { + $impurePoints = []; if ($expr instanceof AssignRef) { + $referencedExpr = $expr->expr; + while ($referencedExpr instanceof ArrayDimFetch) { + $referencedExpr = $referencedExpr->var; + } + + if ($referencedExpr instanceof PropertyFetch || $referencedExpr instanceof StaticPropertyFetch) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'propertyAssignByRef', + 'property assignment by reference', + false, + ); + } + $scope = $scope->enterExpressionAssign($expr->expr); } @@ -2150,7 +2166,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); - $impurePoints = $result->getImpurePoints(); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); if ($expr instanceof AssignRef) { diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index 07141e4ad7..a3b7495691 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -157,4 +157,14 @@ public function testPureConstructor(): void ]); } + public function testImpureAssignRef(): void + { + $this->analyse([__DIR__ . '/data/impure-assign-ref.php'], [ + [ + 'Possibly impure property assignment by reference in pure method ImpureAssignRef\HelloWorld::bar6().', + 49, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php b/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php new file mode 100644 index 0000000000..7aca03a4a4 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php @@ -0,0 +1,63 @@ += 7.4 + +namespace ImpureAssignRef; + +class HelloWorld +{ + public int $value = 0; + /** @var array */ + public array $arr = []; + /** @var array */ + public array $objectArr = []; + public static int $staticValue = 0; + /** @var array */ + public static array $staticArr = []; + + private function bar1(): void + { + $value = &$this->value; + $value = 1; + } + + private function bar2(): void + { + $value = &$this->arr[0]; + $value = 1; + } + + private function bar3(): void + { + $value = &self::$staticValue; + $value = 1; + } + + private function bar4(): void + { + $value = &self::$staticArr[0]; + $value = 1; + } + + private function bar5(self $other): void + { + $value = &$other->value; + $value = 1; + } + + /** @phpstan-pure */ + private function bar6(): int + { + $value = &$this->objectArr[0]->foo; + + return 1; + } + + public function foo(): void + { + $this->bar1(); + $this->bar2(); + $this->bar3(); + $this->bar4(); + $this->bar5(new self()); + $this->bar6(); + } +}