Skip to content

Commit 7fe129a

Browse files
committed
Invalidate static expressions when a non-static expression is called
1 parent 386da24 commit 7fe129a

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
@@ -3608,6 +3608,67 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se
36083608
);
36093609
}
36103610

3611+
public function invalidateStaticMembers(string $className): self
3612+
{
3613+
if (!$this->reflectionProvider->hasClass($className)) {
3614+
return $this;
3615+
}
3616+
3617+
$classReflection = $this->reflectionProvider->getClass($className);
3618+
$classNamesToInvalidate = [strtolower($className)];
3619+
foreach ($classReflection->getParents() as $parentClass) {
3620+
$classNamesToInvalidate[] = strtolower($parentClass->getName());
3621+
}
3622+
3623+
$expressionTypes = $this->expressionTypes;
3624+
$nativeExpressionTypes = $this->nativeExpressionTypes;
3625+
$invalidated = false;
3626+
$nodeFinder = new NodeFinder();
3627+
foreach ($expressionTypes as $exprString => $exprTypeHolder) {
3628+
$expr = $exprTypeHolder->getExpr();
3629+
$found = $nodeFinder->findFirst([$expr], static function (Node $node) use ($classNamesToInvalidate): bool {
3630+
if (!$node instanceof Expr\StaticCall && !$node instanceof Expr\StaticPropertyFetch) {
3631+
return false;
3632+
}
3633+
if (!$node->class instanceof Name || !$node->class->isFullyQualified()) {
3634+
return false;
3635+
}
3636+
3637+
return in_array($node->class->toLowerString(), $classNamesToInvalidate, true);
3638+
});
3639+
if ($found === null) {
3640+
continue;
3641+
}
3642+
3643+
unset($expressionTypes[$exprString]);
3644+
unset($nativeExpressionTypes[$exprString]);
3645+
$invalidated = true;
3646+
}
3647+
3648+
if (!$invalidated) {
3649+
return $this;
3650+
}
3651+
3652+
return $this->scopeFactory->create(
3653+
$this->context,
3654+
$this->isDeclareStrictTypes(),
3655+
$this->getFunction(),
3656+
$this->getNamespace(),
3657+
$expressionTypes,
3658+
$nativeExpressionTypes,
3659+
$this->conditionalExpressions,
3660+
$this->inClosureBindScopeClasses,
3661+
$this->anonymousFunctionReflection,
3662+
$this->inFirstLevelStatement,
3663+
$this->currentlyAssignedExpressions,
3664+
$this->currentlyAllowedUndefinedExpressions,
3665+
[],
3666+
$this->afterExtractCall,
3667+
$this->parentScope,
3668+
$this->nativeTypesPromoted,
3669+
);
3670+
}
3671+
36113672
private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self
36123673
{
36133674
if ($this->hasExpressionType($expr)->no()) {

src/Analyser/NodeScopeResolver.php

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

31753175
if ($methodReflection !== null) {
3176-
if ($methodReflection->getName() === '__construct' || $methodReflection->hasSideEffects()->yes()) {
3176+
$hasSideEffects = $methodReflection->hasSideEffects()->yes();
3177+
if ($hasSideEffects || $methodReflection->getName() === '__construct') {
31773178
$this->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
31783179
$scope = $scope->invalidateExpression($normalizedExpr->var, true);
3180+
if ($hasSideEffects) {
3181+
$classNames = $scope->getType($normalizedExpr->var)->getObjectClassNames();
3182+
foreach ($classNames as $className) {
3183+
$scope = $scope->invalidateStaticMembers($className);
3184+
}
3185+
}
31793186
}
31803187
if ($parametersAcceptor !== null && !$methodReflection->isStatic()) {
31813188
$selfOutType = $methodReflection->getSelfOutType();

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,4 +1045,9 @@ public function testBug11609(): void
10451045
]);
10461046
}
10471047

1048+
public function testBug13416(): void
1049+
{
1050+
$this->analyse([__DIR__ . '/data/bug-13416.php'], []);
1051+
}
1052+
10481053
}
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)