Skip to content

Commit aaeff92

Browse files
committed
Invalidate static expressions when a non-static expression is called
1 parent 54a5bd6 commit aaeff92

File tree

4 files changed

+135
-2
lines changed

4 files changed

+135
-2
lines changed

src/Analyser/MutatingScope.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4601,6 +4601,67 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se
46014601
);
46024602
}
46034603

4604+
public function invalidateStaticMembers(string $className): self
4605+
{
4606+
if (!$this->reflectionProvider->hasClass($className)) {
4607+
return $this;
4608+
}
4609+
4610+
$classReflection = $this->reflectionProvider->getClass($className);
4611+
$classNamesToInvalidate = [strtolower($className)];
4612+
foreach ($classReflection->getParents() as $parentClass) {
4613+
$classNamesToInvalidate[] = strtolower($parentClass->getName());
4614+
}
4615+
4616+
$expressionTypes = $this->expressionTypes;
4617+
$nativeExpressionTypes = $this->nativeExpressionTypes;
4618+
$invalidated = false;
4619+
$nodeFinder = new NodeFinder();
4620+
foreach ($expressionTypes as $exprString => $exprTypeHolder) {
4621+
$expr = $exprTypeHolder->getExpr();
4622+
$found = $nodeFinder->findFirst([$expr], static function (Node $node) use ($classNamesToInvalidate): bool {
4623+
if (!$node instanceof Expr\StaticCall && !$node instanceof Expr\StaticPropertyFetch) {
4624+
return false;
4625+
}
4626+
if (!$node->class instanceof Name || !$node->class->isFullyQualified()) {
4627+
return false;
4628+
}
4629+
4630+
return in_array($node->class->toLowerString(), $classNamesToInvalidate, true);
4631+
});
4632+
if ($found === null) {
4633+
continue;
4634+
}
4635+
4636+
unset($expressionTypes[$exprString]);
4637+
unset($nativeExpressionTypes[$exprString]);
4638+
$invalidated = true;
4639+
}
4640+
4641+
if (!$invalidated) {
4642+
return $this;
4643+
}
4644+
4645+
return $this->scopeFactory->create(
4646+
$this->context,
4647+
$this->isDeclareStrictTypes(),
4648+
$this->getFunction(),
4649+
$this->getNamespace(),
4650+
$expressionTypes,
4651+
$nativeExpressionTypes,
4652+
$this->conditionalExpressions,
4653+
$this->inClosureBindScopeClasses,
4654+
$this->anonymousFunctionReflection,
4655+
$this->inFirstLevelStatement,
4656+
$this->currentlyAssignedExpressions,
4657+
$this->currentlyAllowedUndefinedExpressions,
4658+
[],
4659+
$this->afterExtractCall,
4660+
$this->parentScope,
4661+
$this->nativeTypesPromoted,
4662+
);
4663+
}
4664+
46044665
private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self
46054666
{
46064667
if ($this->hasExpressionType($expr)->no()) {

src/Analyser/NodeScopeResolver.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2922,10 +2922,16 @@ static function (): void {
29222922
$scope = $result->getScope();
29232923

29242924
if ($methodReflection !== null) {
2925-
$hasSideEffects = $methodReflection->hasSideEffects();
2926-
if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') {
2925+
$hasSideEffects = $methodReflection->hasSideEffects()->yes();
2926+
if ($hasSideEffects || $methodReflection->getName() === '__construct') {
29272927
$nodeCallback(new InvalidateExprNode($expr->var), $scope);
29282928
$scope = $scope->invalidateExpression($expr->var, true);
2929+
if ($hasSideEffects) {
2930+
$classNames = $scope->getType($expr->var)->getObjectClassNames();
2931+
foreach ($classNames as $className) {
2932+
$scope = $scope->invalidateStaticMembers($className);
2933+
}
2934+
}
29292935
}
29302936
if ($parametersAcceptor !== null && !$methodReflection->isStatic()) {
29312937
$selfOutType = $methodReflection->getSelfOutType();

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,4 +1040,9 @@ public function testBug11609(): void
10401040
]);
10411041
}
10421042

1043+
public function testBug13416(): void
1044+
{
1045+
$this->analyse([__DIR__ . '/data/bug-13416.php'], []);
1046+
}
1047+
10431048
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug13416;
4+
5+
class MyRecord {
6+
/** @var array<int, self> */
7+
private static array $storage = [];
8+
9+
/** @phpstan-impure */
10+
public function insert(): void {
11+
self::$storage[] = $this;
12+
}
13+
14+
/**
15+
* @return array<int, self>
16+
* @phpstan-impure
17+
*/
18+
public static function find(): array {
19+
return self::$storage;
20+
}
21+
}
22+
23+
class AnotherRecord extends MyRecord {}
24+
25+
class PHPStanMinimalBug {
26+
public function testMinimalBug(): void {
27+
$msg1 = new MyRecord();
28+
$msg1->insert();
29+
30+
assert(
31+
count(MyRecord::find()) === 1,
32+
'should have 1 record initially'
33+
);
34+
35+
$msg2 = new MyRecord();
36+
$msg2->insert();
37+
38+
assert(
39+
count(MyRecord::find()) === 2,
40+
'should have 2 messages after adding one'
41+
);
42+
}
43+
44+
public function testMinimalBugChildClass(): void {
45+
$msg1 = new AnotherRecord();
46+
$msg1->insert();
47+
48+
assert(
49+
count(MyRecord::find()) === 1,
50+
'should have 1 record initially'
51+
);
52+
53+
$msg2 = new AnotherRecord();
54+
$msg2->insert();
55+
56+
assert(
57+
count(MyRecord::find()) === 2,
58+
'should have 2 messages after adding one'
59+
);
60+
}
61+
}

0 commit comments

Comments
 (0)