diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 21fd76cb6b..a0ca68ad81 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4601,6 +4601,67 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se ); } + public function invalidateStaticMembers(string $className): self + { + if (!$this->reflectionProvider->hasClass($className)) { + return $this; + } + + $classReflection = $this->reflectionProvider->getClass($className); + $classNamesToInvalidate = [strtolower($className)]; + foreach ($classReflection->getParents() as $parentClass) { + $classNamesToInvalidate[] = strtolower($parentClass->getName()); + } + + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $invalidated = false; + $nodeFinder = new NodeFinder(); + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $expr = $exprTypeHolder->getExpr(); + $found = $nodeFinder->findFirst([$expr], static function (Node $node) use ($classNamesToInvalidate): bool { + if (!$node instanceof Expr\StaticCall && !$node instanceof Expr\StaticPropertyFetch) { + return false; + } + if (!$node->class instanceof Name || !$node->class->isFullyQualified()) { + return false; + } + + return in_array($node->class->toLowerString(), $classNamesToInvalidate, true); + }); + if ($found === null) { + continue; + } + + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + if (!$invalidated) { + return $this; + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + [], + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self { if ($this->hasExpressionType($expr)->no()) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 603035e2d1..22abb89aef 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2922,10 +2922,16 @@ static function (): void { $scope = $result->getScope(); if ($methodReflection !== null) { - $hasSideEffects = $methodReflection->hasSideEffects(); - if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') { + $hasSideEffects = $methodReflection->hasSideEffects()->yes(); + if ($hasSideEffects || $methodReflection->getName() === '__construct') { $nodeCallback(new InvalidateExprNode($expr->var), $scope); $scope = $scope->invalidateExpression($expr->var, true); + if ($hasSideEffects) { + $classNames = $scope->getType($expr->var)->getObjectClassNames(); + foreach ($classNames as $className) { + $scope = $scope->invalidateStaticMembers($className); + } + } } if ($parametersAcceptor !== null && !$methodReflection->isStatic()) { $selfOutType = $methodReflection->getSelfOutType(); diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 75529afc48..73cbd9188c 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1040,4 +1040,9 @@ public function testBug11609(): void ]); } + public function testBug13416(): void + { + $this->analyse([__DIR__ . '/data/bug-13416.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13416.php b/tests/PHPStan/Rules/Comparison/data/bug-13416.php new file mode 100644 index 0000000000..007eb5dfbc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13416.php @@ -0,0 +1,61 @@ + */ + private static array $storage = []; + + /** @phpstan-impure */ + public function insert(): void { + self::$storage[] = $this; + } + + /** + * @return array + * @phpstan-impure + */ + public static function find(): array { + return self::$storage; + } +} + +class AnotherRecord extends MyRecord {} + +class PHPStanMinimalBug { + public function testMinimalBug(): void { + $msg1 = new MyRecord(); + $msg1->insert(); + + assert( + count(MyRecord::find()) === 1, + 'should have 1 record initially' + ); + + $msg2 = new MyRecord(); + $msg2->insert(); + + assert( + count(MyRecord::find()) === 2, + 'should have 2 messages after adding one' + ); + } + + public function testMinimalBugChildClass(): void { + $msg1 = new AnotherRecord(); + $msg1->insert(); + + assert( + count(MyRecord::find()) === 1, + 'should have 1 record initially' + ); + + $msg2 = new AnotherRecord(); + $msg2->insert(); + + assert( + count(MyRecord::find()) === 2, + 'should have 2 messages after adding one' + ); + } +}