Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
10 changes: 8 additions & 2 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1040,4 +1040,9 @@ public function testBug11609(): void
]);
}

public function testBug13416(): void
{
$this->analyse([__DIR__ . '/data/bug-13416.php'], []);
}

}
61 changes: 61 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-13416.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php declare(strict_types=1);

namespace Bug13416;

class MyRecord {
/** @var array<int, self> */
private static array $storage = [];

/** @phpstan-impure */
public function insert(): void {
self::$storage[] = $this;
}

/**
* @return array<int, self>
* @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'
);
}
}
Loading