Normalize dynamic member names that resolve to a constant string to bareword form when building expression keys#5919
Conversation
…areword form when building expression keys
- MutatingScope::getNodeKey() now rewrites member-access names that resolve to a single constant string into their bareword form before the expression is printed into a key, so that `$obj->$n`, `$obj->{$n}`, `$obj->{'n'}` and `$obj->n` (and the static / method-call equivalents) share one expression key and the same tracked type.
- Added MutatingScope::normalizeConstantMemberNames()/normalizeConstantMemberName() which handle PropertyFetch, NullsafePropertyFetch, MethodCall, NullsafeMethodCall, StaticPropertyFetch and StaticCall, recursing into the access chain. Bareword names are emitted as VarLikeIdentifier (accepted by every member-access node constructor); non-bareword constant strings fall back to a String_ node so they match the existing literal-curly normalization in Printer.
- Extracted the bareword pattern into Printer::BAREWORD_NAME_REGEX and reused it in both Printer::pObjectProperty() and the new scope normalization.
- This fixes assignments through dynamic property names (`$this->$n = 6`) not updating the bareword type, and narrowing through dynamic names not carrying to the bareword form, for both instance and static properties as well as method calls.
VincentLanglet
left a comment
There was a problem hiding this comment.
What about $this->$foo with $foo behing an union of constant string ?
…a union-of-constant-strings member name Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
I've addressed VincentLanglet's review about What I foundThe reviewer's question exposed a real soundness bug on the write side. When assigning through a member name that resolves to a union of constant strings (e.g. $this->a = 3; // narrowed to 3
$name = $c ? 'a' : 'b'; // 'a'|'b'
$this->$name = 5;
// before: $this->a still reported as 3 ← unsound (might now be 5)
// after: $this->a reported as int ← soundThe original PR's The read side and condition narrowing were already sound (reading The fix
Verification
Committed and pushed to |
Summary
Accessing or assigning a member through a dynamic name that is in fact a constant string —
$this->{'v'},$this->{$n}or$this->$nwhere$n = 'v'— did not affect the analysed type of the directly-accessed member$this->v. The reported playground assigned5/6/7through the curly/variable forms but PHPStan kept inferring the type from the first direct assignment (4).The recently merged Printer normalization (#5916) already made the literal
$obj->{'n'}form share an expression key with$obj->n. This change extends the same normalization to any member name that the scope can resolve to a single constant string (variables, class constants, concatenations, ...), and to static properties / static calls.Changes
src/Analyser/MutatingScope.phpgetNodeKey()rewrites the node throughnormalizeConstantMemberNames()before printing the expression key.normalizeConstantMemberNames()walks the access chain andnormalizeConstantMemberName()resolves a name expression: when it is a single constant string, a bareword is emitted asVarLikeIdentifier, otherwise (non-bareword string) as aString_node so the existing literal normalization applies. HandlesPropertyFetch,NullsafePropertyFetch,MethodCall,NullsafeMethodCall,StaticPropertyFetchandStaticCall.src/Node/Printer/Printer.phpBAREWORD_NAME_REGEXconstant, reused by both the printer and the scope.Root cause
Expression types in
MutatingScopeare keyed by the pretty-printed expression.$this->vprints to$this->v, but$this->$n/$this->{$n}print to$this->$n, so a write to one key was invisible to a read of the other, and narrowing stored under one key was not retrieved under the other. The fix centralizes the normalization ingetNodeKey()(the single chokepoint used by reads viagetType(), writes viaspecifyExpressionType(), andinvalidateExpression()), so all syntactic spellings of a constant-named member collapse to the same key. Only the printed key is normalized; the original node is still used for reflection/type resolution.Test
tests/PHPStan/Analyser/nsrt/bug-7851.phpreproduces the issue's playground (instance property assigned through$this->{'v'},$this->{$n},$this->$n) and additionally covers the analogous constructs found while sweeping the family:self::${$s},Fixes phpstan/phpstan#7851