From 5cbf11be45e6d7be1ab20ce729551e30b02015e1 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:59:17 +0000 Subject: [PATCH 1/3] Fix phpstan/phpstan#13473: Take into account Property Hooks in __construct() - Property set hooks should not assume the property is initialized, since the hook runs at assignment time when the property may not yet have been set - Invalidate PropertyInitializationExpr for the hooked property when entering a set hook scope - New regression test in tests/PHPStan/Rules/Variables/IssetRuleTest.php --- src/Analyser/MutatingScope.php | 8 ++++++- .../PHPStan/Rules/Variables/IssetRuleTest.php | 8 +++++++ .../Rules/Variables/data/bug-13473.php | 24 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-13473.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a43ba35dda5..0997fb2217e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1574,7 +1574,7 @@ public function enterPropertyHook( $realParameterTypes = $this->getRealParameterTypes($hook); - return $this->enterFunctionLike( + $scope = $this->enterFunctionLike( new PhpMethodFromParserNodeReflection( $this->getClassReflection(), $hook, @@ -1606,6 +1606,12 @@ public function enterPropertyHook( ), true, ); + + if ($hookName === 'set') { + $scope = $scope->invalidateExpression(new PropertyInitializationExpr($propertyName)); + } + + return $scope; } private function transformStaticType(Type $type): Type diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index fd841e49b16..ac4a49fe051 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -526,6 +526,14 @@ public function testBug9503(): void $this->analyse([__DIR__ . '/data/bug-9503.php'], []); } + #[RequiresPhp('>= 8.4')] + public function testBug13473(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-13473.php'], []); + } + public function testBug14393(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-13473.php b/tests/PHPStan/Rules/Variables/data/bug-13473.php new file mode 100644 index 00000000000..e4ab49046a6 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13473.php @@ -0,0 +1,24 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug13473; + +class Foo { + private(set) int $bar { + get => $this->bar; + set(int $bar) { + if (isset($this->bar)) { + throw new \Exception('bar is set'); + } + $this->bar = $bar; + } + } + + public function __construct(int $bar) + { + $this->bar = $bar; + } +} + +$foo = new Foo(10); From 7a6c50aab90c9972f90f724ddc899ab1836b85db Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 6 Apr 2026 19:28:17 +0000 Subject: [PATCH 2/3] Add test for property with default value in set hook isset check When a property has a default value (e.g. $bar = 1), it is always initialized, so isset() in a set hook should still report an error. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Rules/Variables/IssetRuleTest.php | 7 ++++++- .../PHPStan/Rules/Variables/data/bug-13473.php | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index ac4a49fe051..ec04c01b7e9 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -531,7 +531,12 @@ public function testBug13473(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-13473.php'], []); + $this->analyse([__DIR__ . '/data/bug-13473.php'], [ + [ + 'Property Bug13473\Bar::$bar (int) in isset() is not nullable.', + 30, + ], + ]); } public function testBug14393(): void diff --git a/tests/PHPStan/Rules/Variables/data/bug-13473.php b/tests/PHPStan/Rules/Variables/data/bug-13473.php index e4ab49046a6..558228d1d2b 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-13473.php +++ b/tests/PHPStan/Rules/Variables/data/bug-13473.php @@ -22,3 +22,20 @@ public function __construct(int $bar) } $foo = new Foo(10); + +class Bar { + private(set) int $bar = 1 { + get => $this->bar; + set(int $bar) { + if (isset($this->bar)) { + throw new \Exception('bar is set'); + } + $this->bar = $bar; + } + } + + public function __construct(int $bar) + { + $this->bar = $bar; + } +} From be75095c4bd5aba364e59b77d72d97ff950ee4e1 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 6 Apr 2026 19:41:08 +0000 Subject: [PATCH 3/3] Keep property as initialized in set hook when it has a default value When a property has a default value (e.g. `int $bar = 1`), it is always initialized even during __construct(). Only invalidate the property initialization in set hooks for properties without default values. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 2 +- tests/PHPStan/Rules/Variables/IssetRuleTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 0997fb2217e..b82d5c1119b 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1607,7 +1607,7 @@ public function enterPropertyHook( true, ); - if ($hookName === 'set') { + if ($hookName === 'set' && !$this->getClassReflection()->getNativeProperty($propertyName)->getNativeReflection()->hasDefaultValue()) { $scope = $scope->invalidateExpression(new PropertyInitializationExpr($propertyName)); } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index ec04c01b7e9..c2a29dd0116 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -533,7 +533,7 @@ public function testBug13473(): void $this->analyse([__DIR__ . '/data/bug-13473.php'], [ [ - 'Property Bug13473\Bar::$bar (int) in isset() is not nullable.', + 'Property Bug13473\Bar::$bar in isset() is not nullable nor uninitialized.', 30, ], ]);