[DeadCode] Use PHPStan flow analysis for property initialization in RemoveDefaultValueFromAssignedPropertyRector#8210
Open
TomasVotruba wants to merge 3 commits into
Open
Conversation
…emoveDefaultValueFromAssignedPropertyRector Decorate every ClassMethod with the scope merged from all its execution ends and return statements, so rules can ask PHPStan whether a property is initialized on every path out of the constructor. This replaces the hand-rolled "any return bails out" heuristic and covers if/elseif/else, switch, loops, try/catch and closures for free.
…scope lazily The scope resolver now only stores the raw MethodReturnStatementsNode on the ClassMethod. Merging the exit point scopes happens in the new analyzer, on demand, so methods no rule asks about cost nothing.
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.
Follow-up to #8209 (comment) — @staabm suggested basing the rule on PHPStan's own uninitialized-property mechanics instead of hand-rolled AST scanning.
New service
Rector\NodeAnalyzer\AlwaysInitializedPropertyAnalyzeranswers one question, for any class method:Under the hood it merges the scope of every exit point of that method and asks PHPStan whether the property is initialized there — the same query PHPStan's own
ClassPropertiesNode::collectUninitializedProperties()uses:It is not tied to constructors, so any rule that today guesses at "is this property assigned" can use it.
ConstructorAssignDetectorhas 8 other consumers that inherit the same blind spots this replaces — migrating them is a separate change.Wiring:
PHPStanNodeScopeResolveralready receives every virtual node PHPStan emits, it just dropped them. It now picks upMethodReturnStatementsNodeand stores it on the matchingClassMethodunderAttributeKey::METHOD_RETURN_STATEMENTS_NODE. PHPStan does not expose theClassMethodon that virtual node, but it copies the method's attributes onto it, so the start file position pairs them back up.Rule change
The
Return_-scanning heuristic added in #8209 is gone — PHPStan handles it structurally, plus everything the heuristic could not.Early
returnno longer blocks the whole constructor, only paths that actually skip the assign:final class ReturnAfterAssign { - private ?string $name = null; + private ?string $name; public function __construct(string $name, bool $shouldReturn = false) { $this->name = $name; if ($shouldReturn) { return; } $this->name = strtoupper($name); } }if/elseif/else,switchwithdefault, and a throwing branch are all understood:final class AssignInIfElseIfElse { - private ?string $name = null; + private ?string $name; public function __construct(int $type) { if ($type === 1) { $this->name = 'one'; } elseif ($type === 2) { $this->name = 'two'; } else { $this->name = 'many'; } } }Still skipped: assign inside
foreach(loop may not run), insidetryonly, inside a closure that is never invoked, and an assign that reads the default first ($this->count = $this->count + $extra).Bug fixed on the way
Offset writes were treated as full assigns, so this used to produce a fatal
must not be accessed before initialization:It was hit by
Rector\Privatization\Guard\ParentClassMagicCallGuardwhen running Rector on this repo itself. Now skipped.Performance
Kept for the record. All runs:
bin/rector process --dry-run --no-progress-bar --no-diffs --clear-cache,withoutParallel(), only this rule registered, PHP 8.4.23. Branches alternated every repetition so machine drift hits both sides equally; 6 repetitions per side, median reported.Corpora:
scale1000— 1000 files, one class each, one property with a default plus a constructor that assigns it (the case asked about: this rule firing 1000 times)methods— same 1000 classes, each with 10 extra methods that never touch a propertyreal code— this repo's ownsrc/+rules/, 1317 filesTime is a wash. Memory grows because the exit point scopes stay reachable after analysis.
Two earlier variants were measured and rejected:
Eager merging costs real time on method-heavy code. Storing the raw node for every method costs a lot of memory, because it pins every exit point scope. Doing both — decorate only methods that contain a
$this->...assign, and merge on demand — keeps each within noise on real code.Tests
tests/NodeAnalyzer/AlwaysInitializedPropertyAnalyzerTest.php— 6 cases against the service directly, viaTestingParserso the nodes carry real PHPStan scopes