Skip to content

Commit 1488f0f

Browse files
committed
Invalidate static expressions when a non-static expression is called
1 parent 5ed2f90 commit 1488f0f

File tree

4 files changed

+135
-1
lines changed

4 files changed

+135
-1
lines changed

src/Analyser/MutatingScope.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3643,6 +3643,67 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se
36433643
);
36443644
}
36453645

3646+
public function invalidateStaticMembers(string $className): self
3647+
{
3648+
if (!$this->reflectionProvider->hasClass($className)) {
3649+
return $this;
3650+
}
3651+
3652+
$classReflection = $this->reflectionProvider->getClass($className);
3653+
$classNamesToInvalidate = [strtolower($className)];
3654+
foreach ($classReflection->getParents() as $parentClass) {
3655+
$classNamesToInvalidate[] = strtolower($parentClass->getName());
3656+
}
3657+
3658+
$expressionTypes = $this->expressionTypes;
3659+
$nativeExpressionTypes = $this->nativeExpressionTypes;
3660+
$invalidated = false;
3661+
$nodeFinder = new NodeFinder();
3662+
foreach ($expressionTypes as $exprString => $exprTypeHolder) {
3663+
$expr = $exprTypeHolder->getExpr();
3664+
$found = $nodeFinder->findFirst([$expr], static function (Node $node) use ($classNamesToInvalidate): bool {
3665+
if (!$node instanceof Expr\StaticCall && !$node instanceof Expr\StaticPropertyFetch) {
3666+
return false;
3667+
}
3668+
if (!$node->class instanceof Name || !$node->class->isFullyQualified()) {
3669+
return false;
3670+
}
3671+
3672+
return in_array($node->class->toLowerString(), $classNamesToInvalidate, true);
3673+
});
3674+
if ($found === null) {
3675+
continue;
3676+
}
3677+
3678+
unset($expressionTypes[$exprString]);
3679+
unset($nativeExpressionTypes[$exprString]);
3680+
$invalidated = true;
3681+
}
3682+
3683+
if (!$invalidated) {
3684+
return $this;
3685+
}
3686+
3687+
return $this->scopeFactory->create(
3688+
$this->context,
3689+
$this->isDeclareStrictTypes(),
3690+
$this->getFunction(),
3691+
$this->getNamespace(),
3692+
$expressionTypes,
3693+
$nativeExpressionTypes,
3694+
$this->conditionalExpressions,
3695+
$this->inClosureBindScopeClasses,
3696+
$this->anonymousFunctionReflection,
3697+
$this->inFirstLevelStatement,
3698+
$this->currentlyAssignedExpressions,
3699+
$this->currentlyAllowedUndefinedExpressions,
3700+
[],
3701+
$this->afterExtractCall,
3702+
$this->parentScope,
3703+
$this->nativeTypesPromoted,
3704+
);
3705+
}
3706+
36463707
private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self
36473708
{
36483709
if ($this->hasExpressionType($expr)->no()) {

src/Analyser/NodeScopeResolver.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3213,9 +3213,16 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
32133213
$scope = $result->getScope();
32143214

32153215
if ($methodReflection !== null) {
3216-
if ($methodReflection->getName() === '__construct' || $methodReflection->hasSideEffects()->yes()) {
3216+
$hasSideEffects = $methodReflection->hasSideEffects()->yes();
3217+
if ($hasSideEffects || $methodReflection->getName() === '__construct') {
32173218
$this->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
32183219
$scope = $scope->invalidateExpression($normalizedExpr->var, true, $methodReflection->getDeclaringClass());
3220+
if ($hasSideEffects) {
3221+
$classNames = $scope->getType($normalizedExpr->var)->getObjectClassNames();
3222+
foreach ($classNames as $className) {
3223+
$scope = $scope->invalidateStaticMembers($className);
3224+
}
3225+
}
32193226
}
32203227
if ($parametersAcceptor !== null && !$methodReflection->isStatic()) {
32213228
$selfOutType = $methodReflection->getSelfOutType();

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,4 +1056,9 @@ public function testBug11609(): void
10561056
]);
10571057
}
10581058

1059+
public function testBug13416(): void
1060+
{
1061+
$this->analyse([__DIR__ . '/data/bug-13416.php'], []);
1062+
}
1063+
10591064
}
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)