Fix #12253: assign.readOnlyProperty#5061
Open
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
Open
Conversation
…led from constructor When a constructor calls a helper method that assigns a readonly property and also calls other methods, the property was incorrectly reported as "already assigned". Two root causes: 1. In getMethodsCalledFromConstructor(), non-__construct methods that called other methods had all constructor-initialized properties marked as Yes. This was intended only for additional constructors (like setUp) but incorrectly applied to regular helper methods. Fixed by tracking which methods are original constructors vs discovered helper methods. 2. The additionalAssigns check used scope's hasExpressionType() for PropertyInitializationExpr, but this expression leaks from the constructor scope into all non-static method scopes via rememberConstructorScope(). For non-constructor methods, the scope check always returns Yes even for the first assignment. Fixed by only consulting the scope for __construct itself, and relying on the initializedPropertiesMap for other methods. Fixes phpstan/phpstan#12253
288ca28 to
9ec0cbd
Compare
VincentLanglet
approved these changes
Mar 26, 2026
staabm
reviewed
Mar 26, 2026
src/Node/ClassPropertiesNode.php
Outdated
| if (array_key_exists($propertyName, $initializedPropertiesMap)) { | ||
| $hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName))); | ||
| $hasInitialization = $initializedPropertiesMap[$propertyName]; | ||
| if (strtolower($function->getName()) === '__construct') { |
Contributor
There was a problem hiding this comment.
when seeing such comparison it reminds me that we have a NEON config additionalConstructors. is this somehow related/should be taken into account?
Collaborator
Author
There was a problem hiding this comment.
All background tasks confirmed successful. The fix is committed and pushed.
The scope check for PropertyInitializationExpr was restricted to only __construct, but additional constructors (configured via additionalConstructors or AdditionalConstructorsExtension) should also consult the scope since PropertyInitializationExpr from __construct correctly reflects promoted property initialization state for them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
staabm
reviewed
Mar 26, 2026
| @@ -0,0 +1,87 @@ | |||
| <?php // lint >= 8.4 | |||
|
|
|||
| declare(strict_types = 1); | |||
Contributor
There was a problem hiding this comment.
add another test with the same problem, but 1 additionalConstructors configured
Collaborator
Author
There was a problem hiding this comment.
You're out of extra usage · resets 5pm (UTC)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes phpstan/phpstan#12253
Reproducer
Root causes
Two issues in
ClassPropertiesNode::getUninitializedProperties():getMethodsCalledFromConstructor()over-eagerly marks properties as initialized: When a non-__constructmethod (likeparseValidation) calls another method (likevalidationValue), all properties initialized in the constructor were marked asYesfor that method. This logic was intended only for additional constructors configured viaConstructorsHelper(e.g.,setUp), but it applied to all non-__constructmethods. Fixed by passing and checking the list of original constructors.Scope pollution for
PropertyInitializationExpr: TheadditionalAssignscheck consulted$usageScope->hasExpressionType(PropertyInitializationExpr), butPropertyInitializationExprtypes leak from the post-constructor class scope into all non-static method scopes viarememberConstructorScope()+enterFunctionLike($preserveConstructorScope=true). For non-constructor methods, this always reports the property as already initialized even on first assignment. Fixed by only consulting the scope for__constructitself, relying on$initializedPropertiesMapfor helper methods.Changes
src/Node/ClassPropertiesNode.php: Added$originalConstructorsparameter togetMethodsCalledFromConstructor()and restricted both the constructor-property-initialization logic and the scope check to appropriate methodstests/PHPStan/Rules/Properties/data/bug-12253.php: Regression testtests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php: Test method for the regression test