Account for subclasses in get_parent_class() of a $this/static value#6048
Conversation
staabm
left a comment
There was a problem hiding this comment.
needs a rule test showing the rule error in the origin reproducer no longer occurs
c4258ce to
d814e38
Compare
|
do we have similar problems with other functions, e.g. |
|
Added tests asserting I also looked at unifying the three: |
| assertType('\'ParentClass\\\\Foo\'', get_parent_class(self::class)); | ||
| assertType('\'ParentClass\\\\Foo\'|class-string<ParentClass\Bar>', get_parent_class(static::class)); |
There was a problem hiding this comment.
not sure it makes sense that inside the trait we assume we know the class/all classes this trait might be used by. @VincentLanglet wdyt?
There was a problem hiding this comment.
That's the classic behavior of the trait, everything is computed in the context of the class which use the trait. I'm not sure it's easy to change
There was a problem hiding this comment.
ok, was not sure tbh. I though this is true from within the class using the trait, but not the trait itself. I might missremember.
in case you think it is fine, we move ahead.
There was a problem hiding this comment.
I think you're right, looking at
https://phpstan.org/r/e81f1d9e-19da-42b4-b576-d52cd54be556
We should have a similar behavior than
assertType('class-string|false', get_parent_class($this));
(which already work for get_parent_class(static::class))
VincentLanglet
left a comment
There was a problem hiding this comment.
It should be class-string|false in a trait.
| assertType('\'ParentClass\\\\Foo\'', get_parent_class(self::class)); | ||
| assertType('\'ParentClass\\\\Foo\'|class-string<ParentClass\Bar>', get_parent_class(static::class)); |
There was a problem hiding this comment.
I think you're right, looking at
https://phpstan.org/r/e81f1d9e-19da-42b4-b576-d52cd54be556
We should have a similar behavior than
assertType('class-string|false', get_parent_class($this));
(which already work for get_parent_class(static::class))
| assertType('class-string|false', get_parent_class()); | ||
| assertType('class-string|false', get_parent_class($this)); | ||
| // self::class / static::class resolve against the using class (Bar). | ||
| assertType('\'ParentClass\\\\Foo\'', get_parent_class(self::class)); |
There was a problem hiding this comment.
What about self::class ?
There was a problem hiding this comment.
self::class isn't late static bound — inside the trait it's already the using class as a constant class-string ('ParentClass\Bar'), which is indistinguishable from writing Bar::class. So the extension sees the very same ConstantString in both cases and there's nothing to be conservative about; making it class-string|false would mean special-casing the syntax and would contradict get_parent_class(Bar::class) returning 'ParentClass\Foo'.
The guard applies to $this / static::class because those carry the subclass uncertainty. I added assertions covering all three (plus Bar::class side by side) to make this explicit.
|
I don't think it's the same kind of thing. Two consequences:
Making it conservative would mean not resolving
|
VincentLanglet
left a comment
There was a problem hiding this comment.
I think we could add a test with @final phpdoc ? Which should be considered as final too.
get_parent_class($this) inside a non-final class returned the declared parent (or false when there was none), treating $this as exactly the current class. Through late static binding $this can be an instance of a subclass, so for a direct child the parent is the class itself and for a deeper descendant it is a subclass. The return type now also includes class-string<Class> in that case, which removes a false-positive identical.alwaysFalse on get_parent_class($this) === self::class. Final classes and non-$this/static arguments (typed parameters, new X()) keep the exact parent type. Closes phpstan/phpstan#14951
…lways false Covers the reported identical.alwaysFalse false positive at the rule level, in addition to the type-inference assertions.
…union Addresses review: avoid calling TypeCombinator::union() inside the loop and use a plain foreach for readability.
They already return class-string<static(...)>/class-string<$this(...)>, so they keep the subclass possibility instead of pinning to the current class.
A class-string argument names an exact class, so it is not subject to the subclass widening: get_parent_class(self::class) stays false / the exact parent.
static::class names the late-static-bound class, so it carries the same subclass possibility as $this. Unwrap the class-string<static(...)> and reuse the object handling, giving class-string<Class>|<parent> instead of the wide class-string|false. self::class / X::class name an exact class and keep the exact parent.
…t method $this stays deferred to the general type via the existing in-trait guard; self::class and static::class resolve against the using class the same way as outside a trait.
… a trait The in-trait guard already bails out for $this. Apply it to static::class as well, so a trait does not resolve a late-static-bound argument against one particular using class. Outside a trait static::class keeps the narrowed type.
self::class is not late static bound: inside a trait it is already the using class as a constant class-string, indistinguishable from writing Bar::class, so it keeps the exact parent while $this / static::class stay conservative.
getClassStringObjectType() returns an ErrorType for non-class-string types, so the isClassString()->yes() guard was redundant. Dropping it also removes an escaped TrinaryLogic mutant reported by mutation testing.
7393c1e to
035e89f
Compare
|
thank you @zonuexe |
Closes phpstan/phpstan#14951
get_parent_class($this)inside a non-final class returned the declared parent (orfalsewhen there was none), treating$thisas exactly the current class.Through late static binding,
$thiscan be an instance of a subclass, so for a direct child the parent is the class itself, and for a deeper descendant it is a subclass. The return type now also includesclass-string<Class>in that case — which removes a false-positiveidentical.alwaysFalseonget_parent_class($this) === self::class(the reported bug).Final classes and non-
$this/staticarguments (typed parameters,new X()) keep the exact parent type, soget_parent_class($typedParam)andget_parent_class(new B())are unchanged.Behaviour
Inside a non-final class
A(no parent), withclass B extends A:get_parent_class($this)inA→class-string<A>|false(wasfalse).get_parent_class($this)inB→'A'|class-string<B>(was'A').get_parent_class($this)in afinalclass → unchanged (exact parent /false).get_parent_class($typedParam),get_parent_class(new B()),get_parent_class(SomeClass::class)→ unchanged.The two updated assertions in
get-parent-class.phpreflect this corrected behaviour.Note
While checking whether phpstan/phpstan#14951 would interfere with the
self/parent/staticscope and member-visibility work in #4081 (Closure::bind()), it turned out the two are independent and this one could be fixed on its own — so it is submitted here as a separate PR.