psalm icon indicating copy to clipboard operation
psalm copied to clipboard

`self` and `static` return types don't work if parent class has a constrained template type

Open janopae opened this issue 1 year ago • 4 comments

Consider the following code: https://psalm.dev/r/c676544a45

It works as expected: The parent class get method returns static – therefore, when called on the child, we can be sure that the self return type is correct.

Now, let's constrain the template parameter on the parent class (but in a way where the child is still valid). Urgh! https://psalm.dev/r/6f35cf8914

Now Psalm complains that "The type 'ParentClass<object>' is more general than the declared return type 'ChildClass' for ChildClass::myGet".

We can't fix that by using static on the child return type: https://psalm.dev/r/091f9485a7

Seems like we can't fix it at all in our application code, since Psalm seems to override the type constraint over the actual known type of the template.

janopae avatar Dec 12 '23 12:12 janopae

I found these snippets:

https://psalm.dev/r/c676544a45
<?php

class A {}

/**
* @template T
*/
class ParentClass
{
	public function get(): static
    {
    	return $this;
    }
}

/**
* @template-extends ParentClass<A>
*/
final class ChildClass extends ParentClass
{
	public function myGet(): self
    {
    	return $this->get();
    } 
}
Psalm output (using commit a75d26a):

No issues!
https://psalm.dev/r/6f35cf8914
<?php

class A {}

/**
* @template T of object
*/
class ParentClass
{
	public function get(): static
    {
    	return $this;
    }
}

/**
* @template-extends ParentClass<A>
*/
final class ChildClass extends ParentClass
{
	public function myGet(): self
    {
    	return $this->get();
    } 
}
Psalm output (using commit a75d26a):

INFO: LessSpecificReturnStatement - 23:13 - The type 'ParentClass<object>' is more general than the declared return type 'ChildClass' for ChildClass::myGet

INFO: MoreSpecificReturnType - 21:27 - The declared return type 'ChildClass' for ChildClass::myGet is more specific than the inferred return type 'ParentClass<object>'
https://psalm.dev/r/091f9485a7
<?php

class A {}

/**
* @template T of object
*/
class ParentClass
{
	public function get(): static
    {
    	return $this;
    }
}

/**
* @template-extends ParentClass<A>
*/
final class ChildClass extends ParentClass
{
	public function myGet(): static
    {
    	return $this->get();
    } 
}
Psalm output (using commit a75d26a):

INFO: LessSpecificReturnStatement - 23:13 - The type 'ParentClass<object>' is more general than the declared return type 'ChildClass' for ChildClass::myGet

INFO: MoreSpecificReturnType - 21:27 - The declared return type 'ChildClass' for ChildClass::myGet is more specific than the inferred return type 'ParentClass<object>'

psalm-github-bot[bot] avatar Dec 12 '23 12:12 psalm-github-bot[bot]

The fix seems to be rather simple: https://psalm.dev/r/872aced924

weirdan avatar Feb 26 '24 23:02 weirdan

I found these snippets:

https://psalm.dev/r/872aced924
<?php

class A {}

/**
* @template T of object
*/
class ParentClass
{
    /** @return static<T> */
	public function get(): static
    {
    	return $this;
    }
}

/**
* @template-extends ParentClass<A>
*/
final class ChildClass extends ParentClass
{
	public function myGet(): self
    {
    	return $this->get();
    } 
}
Psalm output (using commit b940c7e):

No issues!

psalm-github-bot[bot] avatar Feb 26 '24 23:02 psalm-github-bot[bot]

Thanks, @weirdan! I didn't think of a solution like this, because in my case, the parent class lives in a library.

Do you think Psalm should change its behaviour in this regard? After all, the resolved static type doesn't even support template types, so static<T> doesn't make so much sense. Otherwise, feel free to close this issue.

janopae avatar Mar 06 '24 20:03 janopae