Skip to content

Report indirect modification of readonly properties through array dim fetch chains#5489

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-xt2d9hw
Open

Report indirect modification of readonly properties through array dim fetch chains#5489
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-xt2d9hw

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHP prevents indirect modification of readonly properties even when the modification target is a property on an object accessed through the readonly array. For example, $this->readonlyArr[$key]->brand[$key2] = value throws "Cannot indirectly modify readonly property" at runtime, but PHPStan did not report this error. The existing ReadOnlyPropertyAssignRule only checked the directly assigned property (brand), not intermediate readonly properties in the assignment chain.

Changes

  • Added new ReadOnlyPropertyIndirectModificationRule in src/Rules/Properties/ReadOnlyPropertyIndirectModificationRule.php
    • Registered at level 3, same as existing readonly property rules
    • Processes PropertyAssignNode and walks up the var chain from the assigned PropertyFetch
    • Detects ArrayDimFetchPropertyFetch patterns where the property is readonly
    • Checks both native readonly and @readonly by PHPDoc properties
    • Skips ArrayAccess implementations (their [] access is via offsetGet/offsetSet methods)
    • Respects isAllowedPrivateMutation() for @readonly properties
    • Reports errors even inside constructors, since indirect modifications are always illegal in PHP

Root cause

The AssignHandler unwinds ArrayDimFetch nodes to find the base variable for creating PropertyAssignNode. For $this->readonlyArr[$key]->brand[$key2] = value, it unwinds the outer [$key2] and finds PropertyFetch(brand) as the base — emitting a PropertyAssignNode for brand, not for readonlyArr. Since brand is not readonly, no error was reported. The intermediate ArrayDimFetch(PropertyFetch(readonlyArr), $key) was never checked for readonly status.

Analogous cases probed

All of these are covered by the new rule and tests:

  • Chain through readonly array in constructor (the reported issue) — $this->readonlyArr[$key]->prop[$key2] = value
  • Chain through readonly array outside constructor — same pattern in a regular method
  • Direct property assignment through readonly array$this->readonlyArr[$key]->prop = value
  • Nested array dim fetch$this->readonlyArr[$key1][$key2]->prop[$key3] = value
  • Compound assignment through readonly array$this->readonlyArr[$key]->prop .= 'x'
  • Deeper object chains$this->readonlyArr[$key]->obj->prop = value
  • @readonly by PHPDoc property — same pattern with @readonly annotation
  • @readonly class — readonly class with array property

Cases verified as already correct (no false positives):

  • Non-readonly property$this->normalArr[$key]->prop = value — no error
  • ArrayAccess property$this->readonlyArrayObject[$key]->prop = value — no error
  • Readonly object property$this->readonlyObj->prop = value — no error (objects use reference semantics)

Test

  • Added tests/PHPStan/Rules/Properties/data/bug-14481.php with comprehensive test cases
  • Added tests/PHPStan/Rules/Properties/ReadOnlyPropertyIndirectModificationRuleTest.php
  • All existing tests continue to pass

Fixes phpstan/phpstan#14481

… fetch chains

- Add new ReadOnlyPropertyIndirectModificationRule that processes PropertyAssignNode
- Walk up the var chain from the assigned PropertyFetch to detect intermediate
  ArrayDimFetch on readonly (native or @readonly) properties
- Skip properties implementing ArrayAccess (offsetSet is a method call, not array modification)
- Handle both native readonly and @readonly by PHPDoc properties
- Respect isAllowedPrivateMutation() for @readonly properties
- Cover analogous cases: deeper chains, compound assignments, nested array dim fetches
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.

2 participants