Skip to content

Fix #12253: assign.readOnlyProperty#5061

Open
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-fvdtxi4
Open

Fix #12253: assign.readOnlyProperty#5061
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-fvdtxi4

Conversation

@phpstan-bot
Copy link
Collaborator

@phpstan-bot phpstan-bot commented Feb 27, 2026

Fixes phpstan/phpstan#12253

Reproducer

class Payload
{
    private(set) readonly array $validation;

    public function __construct(private readonly stdClass $payload)
    {
        $this->parseValidation();
    }

    private function parseValidation(): void
    {
        // ... builds $validations array, calls $this->validationValue() ...
        $this->validation = $validations; // FALSE POSITIVE: "already assigned"
    }
}

Root causes

Two issues in ClassPropertiesNode::getUninitializedProperties():

  1. getMethodsCalledFromConstructor() over-eagerly marks properties as initialized: When a non-__construct method (like parseValidation) calls another method (like validationValue), all properties initialized in the constructor were marked as Yes for that method. This logic was intended only for additional constructors configured via ConstructorsHelper (e.g., setUp), but it applied to all non-__construct methods. Fixed by passing and checking the list of original constructors.

  2. Scope pollution for PropertyInitializationExpr: The additionalAssigns check consulted $usageScope->hasExpressionType(PropertyInitializationExpr), but PropertyInitializationExpr types leak from the post-constructor class scope into all non-static method scopes via rememberConstructorScope() + 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 __construct itself, relying on $initializedPropertiesMap for helper methods.

Changes

  • src/Node/ClassPropertiesNode.php: Added $originalConstructors parameter to getMethodsCalledFromConstructor() and restricted both the constructor-property-initialization logic and the scope check to appropriate methods
  • tests/PHPStan/Rules/Properties/data/bug-12253.php: Regression test
  • tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php: Test method for the regression test

…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
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-fvdtxi4 branch from 288ca28 to 9ec0cbd Compare March 25, 2026 20:29
@VincentLanglet VincentLanglet self-assigned this Mar 25, 2026
@VincentLanglet VincentLanglet requested a review from staabm March 26, 2026 07:06
if (array_key_exists($propertyName, $initializedPropertiesMap)) {
$hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
$hasInitialization = $initializedPropertiesMap[$propertyName];
if (strtolower($function->getName()) === '__construct') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when seeing such comparison it reminds me that we have a NEON config additionalConstructors. is this somehow related/should be taken into account?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@@ -0,0 +1,87 @@
<?php // lint >= 8.4

declare(strict_types = 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add another test with the same problem, but 1 additionalConstructors configured

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're out of extra usage · resets 5pm (UTC)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants