Skip to content

Commit 72cd601

Browse files
phpstan-botclaude
andcommitted
Fix isset() false positive for methods called from constructor
When a method that assigns a property is called from the constructor, PHPStan merges the property initialization state back into the constructor scope. This caused a false positive when the same method used isset() to check if the property was already initialized, because the preserved constructor scope incorrectly marked the property as initialized when entering the method. The fix removes PropertyInitializationExpr entries for properties that a method itself initializes, when the method was called from the constructor. This generalizes the property hook fix to also work for regular methods. Also extracts exitPropertyInitialization() on MutatingScope to share the logic between property hooks and regular methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c6b5dd5 commit 72cd601

File tree

4 files changed

+53
-3
lines changed

4 files changed

+53
-3
lines changed

src/Analyser/MutatingScope.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,9 +1608,7 @@ public function enterPropertyHook(
16081608
);
16091609

16101610
if ($hookName === 'set') {
1611-
$initExprKey = $this->getNodeKey(new PropertyInitializationExpr($propertyName));
1612-
unset($scope->expressionTypes[$initExprKey]);
1613-
unset($scope->nativeExpressionTypes[$initExprKey]);
1611+
$scope->exitPropertyInitialization($propertyName);
16141612
}
16151613

16161614
return $scope;
@@ -2840,6 +2838,15 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN
28402838
return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType());
28412839
}
28422840

2841+
public function exitPropertyInitialization(string $propertyName): self
2842+
{
2843+
$initExprKey = $this->getNodeKey(new PropertyInitializationExpr($propertyName));
2844+
unset($this->expressionTypes[$initExprKey]);
2845+
unset($this->nativeExpressionTypes[$initExprKey]);
2846+
2847+
return $this;
2848+
}
2849+
28432850
public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): self
28442851
{
28452852
$expressionTypes = $this->expressionTypes;

src/Analyser/NodeScopeResolver.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,20 @@ public function processStmtNode(
712712

713713
$classReflection = $scope->getClassReflection();
714714

715+
if (!$isConstructor && !$stmt->isStatic()) {
716+
$stackName = sprintf('%s::%s', $classReflection->getName(), $stmt->name->toString());
717+
$calledMethodScope = $this->calledMethodResults[$stackName] ?? null;
718+
if ($calledMethodScope !== null) {
719+
foreach ($calledMethodScope->expressionTypes as $typeHolder) {
720+
$expr = $typeHolder->getExpr();
721+
if (!$expr instanceof PropertyInitializationExpr) {
722+
continue;
723+
}
724+
$methodScope = $methodScope->exitPropertyInitialization($expr->getPropertyName());
725+
}
726+
}
727+
}
728+
715729
if ($isConstructor) {
716730
foreach ($stmt->params as $param) {
717731
if ($param->flags === 0 && $param->hooks === []) {

tests/PHPStan/Rules/Variables/IssetRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,4 +570,11 @@ public function testBug13473(): void
570570
$this->analyse([__DIR__ . '/data/bug-13473.php'], []);
571571
}
572572

573+
public function testBug13473Method(): void
574+
{
575+
$this->treatPhpDocTypesAsCertain = true;
576+
577+
$this->analyse([__DIR__ . '/data/bug-13473-method.php'], []);
578+
}
579+
573580
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13473Method;
6+
7+
class Foo {
8+
private int $bar;
9+
10+
public function __construct(int $bar)
11+
{
12+
$this->setBar($bar);
13+
}
14+
15+
public function setBar(int $bar): void
16+
{
17+
if (isset($this->bar)) {
18+
throw new \Exception('bar is set');
19+
}
20+
$this->bar = $bar;
21+
}
22+
}

0 commit comments

Comments
 (0)