Skip to content

Detect private promoted properties whose only reads are self-writes via new self()#5599

Merged
ondrejmirtes merged 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-hmfsst6
May 5, 2026
Merged

Detect private promoted properties whose only reads are self-writes via new self()#5599
ondrejmirtes merged 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-hmfsst6

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Detects when a private promoted property is only ever "read" in order to pass its value to the same constructor parameter of the same class (e.g. return new self($this->test)). In such cases, the property is effectively only writing to itself and should be reported as "never read, only written."

Changes

  • Modified src/Rules/DeadCode/UnusedPrivatePropertyRule.php:
    • Added a hasTrueRead tracking field to distinguish genuine reads from self-writes
    • After processing all property usages, promoted properties with no true reads have their read flag cleared
    • Added isPropertySelfWrite() helper that uses Scope::getFunctionCallStackWithParameters() to check if a read occurs inside a constructor call of the same class, targeting the same-named parameter
  • Added tests/PHPStan/Rules/DeadCode/data/bug-14573.php with comprehensive test cases
  • Added test method testBug14573 to UnusedPrivatePropertyRuleTest
  • Added 3 baseline entries for newly detected issues in PHPStan's own code

Root cause

The UnusedPrivatePropertyRule previously treated any PropertyRead as evidence that a property is used. However, when a promoted property's only reads are passing its value to new self(...) / new static(...) / new ClassName(...) at the same parameter position, the property is effectively circular — it's only written from constructor and read to write to another instance's constructor. The fix uses the scope's function call stack to identify these self-write reads and exclude them from the "has been read" determination.

Test

The regression test covers:

  • Basic case: single promoted property passed to new self() (reported)
  • Multiple properties where one has a true read and one doesn't (only the dead one reported)
  • Both properties only self-written (both reported)
  • Swapped arguments: new self($this->b, $this->a) (NOT reported - different parameter)
  • Property with additional genuine read (NOT reported)
  • new static() (reported)
  • new ClassName() with explicit class name (reported)
  • Named arguments: new self(b: $this->b, a: $this->a) (reported)
  • Named arguments swapped: new self(b: $this->a, a: $this->b) (NOT reported)
  • Property passed to different class constructor (NOT reported)
  • Multiple self-write call sites (reported)
  • Mixed self-write and true read (NOT reported)
  • Property passed to non-constructor method (NOT reported)

Analogous cases probed and found not applicable:

  • Static properties: cannot be promoted, so N/A
  • Non-promoted properties: the issue specifically scopes to promoted properties where constructor assignment is guaranteed without inspecting the body

Fixes phpstan/phpstan#14573

…ia `new self()`

- Track whether each property read is a "true read" vs a "self-write" (property
  passed to the same constructor parameter of the same class)
- After processing all usages, if a promoted property's only reads are self-writes,
  mark it as unread so the "never read, only written" error is reported
- Handles new self(), new static(), and new ClassName() for the same class
- Named arguments are correctly matched by parameter name
- Properties passed to different parameters or different classes are not affected
- Add baseline entries for 3 new findings in PHPStan's own code
Copy link
Copy Markdown
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Instead of baselining the PHPStan errors, please actually fix them by removing the promoted properties.

phpstan-bot and others added 2 commits May 5, 2026 08:41
…dPropertyPrototypeReflection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…elper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. I addressed the review by removing the unused promoted properties instead of baselining them:

  1. IntersectionTypeUnresolvedPropertyPrototypeReflection::$propertyName and UnionTypeUnresolvedPropertyPrototypeReflection::$propertyName — removed from constructors and updated all call sites in IntersectionType.php, UnionType.php, and ObjectType.php.

  2. ImpossibleCheckTypeHelper::$universalObjectCratesClasses — removed from constructor and doNotTreatPhpDocTypesAsCertain(), also removed from TypeSpecifyingFunctionsDynamicReturnTypeExtension which only held it to pass here, and updated all 16 test files.

Both make phpstan and make tests pass cleanly.

Copy link
Copy Markdown
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Make sure there are no edits to the baseline file. You've left an empty newline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Removed the stray blank line from phpstan-baseline.neon so the baseline file has zero net changes from this PR. Both make phpstan and the tests pass cleanly.

@ondrejmirtes ondrejmirtes merged commit cd037c5 into phpstan:2.1.x May 5, 2026
658 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-hmfsst6 branch May 5, 2026 09:24
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