Fix phpstan/phpstan#13473: Take into account Property Hooks in __construct()#5443
Fix phpstan/phpstan#13473: Take into account Property Hooks in __construct()#5443phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
Conversation
- In property set hooks, the hooked property might not be initialized yet (e.g., when the set hook is triggered from __construct()) - Removed PropertyInitializationExpr for the hooked property from the scope when entering a set hook in MutatingScope::enterPropertyHook() - Added regression test in tests/PHPStan/Rules/Variables/data/bug-13473.php
VincentLanglet
left a comment
There was a problem hiding this comment.
Do not focus on PropertyHooks, this issue can be reproduced with a generic method called in the constructor
class Foo {
private int $bar;
public function __construct(int $bar)
{
$this->setBar($bar);
}
public function setBar(int $bar): void
{
if (isset($this->bar)) {
throw new \Exception('bar is set');
}
$this->bar = $bar;
}
}
Method called in the constructor should be considered with two different scope:
- The place when they are called in the constructor
- When they are called outside the 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>
|
All done. Here's a summary of what was done to address the review: Changes madeThe reviewer correctly identified that the class Foo {
private int $bar;
public function __construct(int $bar) {
$this->setBar($bar);
}
public function setBar(int $bar): void {
if (isset($this->bar)) { // false positive here
throw new \Exception('bar is set');
}
$this->bar = $bar;
}
}Root cause: When Fix (3 changes):
Verification: All 11,841 tests pass, |
VincentLanglet
left a comment
There was a problem hiding this comment.
I think not all PropertyInitializationExpr should be invalidate because the two tests should error with private int $bar = 1;.
What about adding a boolean to PropertyInitializationExpr in order to specify it was initialized in the constructor ?
…alues Properties with default values (e.g. `private int $bar = 1;`) are handled by `hasDefaultValue()` in IssetCheck, not by PropertyInitializationExpr. Removing PropertyInitializationExpr entries for methods called from the constructor does not affect properties with defaults — they are still correctly flagged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The commit has been pushed. Here's a summary of what was done: Response to reviewThe reviewer was concerned that removing all Analysis shows the current approach is already correct — no code changes were needed, only test additions:
Test additions: Added |
Summary
PHPStan incorrectly reported "Property Foo::$bar in isset() is not nullable nor uninitialized" inside a property set hook, even though the property might not yet be initialized when the hook is called (e.g., from
__construct()).Changes
MutatingScope::enterPropertyHook()insrc/Analyser/MutatingScope.phpto remove thePropertyInitializationExprfor the hooked property when entering asethooktests/PHPStan/Rules/Variables/data/bug-13473.phpand corresponding test method intests/PHPStan/Rules/Variables/IssetRuleTest.phpRoot cause
When PHPStan processes a class, the constructor is analyzed first. The constructor scope tracks which properties have been initialized via
PropertyInitializationExprentries. After the constructor finishes,rememberConstructorScope()preserves these entries in the class scope.When property hooks are subsequently analyzed,
enterPropertyHook()callsenterFunctionLike(reflection, preserveConstructorScope: true), which copies the class scope's expression types — including thePropertyInitializationExprentries from the constructor. This caused theIssetCheckrule to believe the property was already initialized inside the set hook, even though the set hook is the very mechanism by which the property gets initialized.The fix removes the
PropertyInitializationExprfor the hooked property when entering a set hook, since the set hook may be called before the property is initialized (first assignment from constructor). Other properties' initialization state is preserved correctly. TheSetNonVirtualPropertyHookAssignRulecontinues to work because it checks the final scope after the hook body runs (where$this->bar = $barre-adds the initialization marker).Test
Added a regression test reproducing the original issue: a class with a
private(set) int $barproperty that has a set hook checkingisset($this->bar)before assigning. PHPStan should not report any errors for this code.Fixes phpstan/phpstan#13473