Skip to content

Fix #6777: Bogus write-only error when appending to an array-like object in private class property#5102

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

Fix #6777: Bogus write-only error when appending to an array-like object in private class property#5102
phpstan-bot wants to merge 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ryr3pbw

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When appending to an ArrayAccess object stored in a private class property (e.g. $this->array[] = $s), PHPStan incorrectly reported "Property is never read, only written." This is a false positive because appending to an ArrayAccess object is semantically $this->array->offsetSet(null, $s) — a method call that reads the property.

Changes

  • Modified src/Node/ClassStatementsGatherer.php: When handling a PropertyAssignNode, check if the assigned expression is a SetOffsetValueTypeExpr or SetExistingOffsetValueTypeExpr (indicating array dim fetch assignment) and the property type is an ArrayAccess object. If so, also record a PropertyRead.
  • Updated existing WriteToCollection test expectations in tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php to reflect the fix.
  • Added regression test in tests/PHPStan/Rules/DeadCode/data/bug-6777.php.

Root cause

In ClassStatementsGatherer, when NodeScopeResolver processes $this->array[] = $s on an ArrayAccess property, it emits a PropertyAssignNode which was unconditionally recorded as a PropertyWrite. Meanwhile, the PropertyFetch for $this->array was processed in expression-assign mode, causing ClassStatementsGatherer to skip recording a PropertyRead. This resulted in the property being tracked as "only written" even though it was being read to call offsetSet() on the object.

The fix detects when a property assignment comes from an array dim fetch operation on an ArrayAccess object and also records a PropertyRead, since the object must be read from the property to invoke the method.

Test

Added tests/PHPStan/Rules/DeadCode/data/bug-6777.php which reproduces the original issue: a class with a private ArrayObject property that is only appended to via $this->array[] = $s. The test expects no errors.

Fixes phpstan/phpstan#6777

- In ClassStatementsGatherer, also record a PropertyRead when a PropertyAssignNode
  is for an ArrayAccess property via array dim fetch (e.g. $this->array[] = $s),
  because this is semantically $this->array->offsetSet() which reads the property
- New regression test in tests/PHPStan/Rules/DeadCode/data/bug-6777.php
- Updated existing WriteToCollection test expectations

Closes phpstan/phpstan#6777
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

This doesn't only affect ArrayAccess objects, but also objects like Threaded which support array operations without implementing the interface. So the check

(new ObjectType(\ArrayAccess::class))->isSuperTypeOf($propertyType)->yes()

is not correct

@VincentLanglet VincentLanglet self-assigned this Mar 28, 2026
Objects like Threaded support array operations without implementing
ArrayAccess. Use isObject() instead of checking for ArrayAccess
specifically, so that appending to any object property via array
syntax is correctly treated as a read.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

All 13,936 tests pass, static analysis clean. The fix has been committed and pushed.

Summary of the change: Replaced the ArrayAccess-specific check with $propertyType->isObject()->yes(), so that appending to any object property via array syntax ($this->prop[] = $val) is correctly treated as a property read. This covers objects like Threaded that support array operations without implementing ArrayAccess.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

  • Add a test with \ArrayObject<int, string>|array<int, string>. (Maybe object, maybe array)
  • Also add a test with Threaded class since it does not implements ArrayAccess
  • Also add a test with \ArrayObject<int, string>|string (Maybe object)

…ccess objects

- Simplify condition to `!$propertyType->isObject()->no()` so that any type
  that *might* be an object triggers a PropertyRead when appending via `[]`
- Add test cases for Threaded (non-ArrayAccess object), ArrayObject|array
  union, and ArrayObject|string union

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

The background make phpstan also completed successfully — confirms static analysis is clean. Everything's pushed.

@VincentLanglet VincentLanglet requested a review from staabm March 28, 2026 21:14
public function __construct(private \ArrayObject $array){}

public function send(string $s) : void{
$this->array[] = $s;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add a similar test with assign by reference

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Background task confirmed — make tests completed successfully as well. Everything was already verified and pushed.

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