vimeo / psalm

A static analysis tool for finding errors in PHP applications
https://psalm.dev
MIT License
5.55k stars 661 forks source link

Strange behaviour combining inheritance and constrained templates #6781

Open marcosh opened 2 years ago

marcosh commented 2 years ago

This snippet of code (https://psalm.dev/r/cad42fc0c5) produces a strange error message which disappears as soon as I remove the constraint from the template or any item in the inheritance chain of interfaces.

psalm-github-bot[bot] commented 2 years ago

I found these snippets:

https://psalm.dev/r/cad42fc0c5 ```php */ interface DefaultFunctor extends HK1 { /** * @template B * @param callable(A): B $f * @return HK1 */ public function map(callable $f): HK1; } /** * @template T of Brand * @template A * @extends HK1 */ interface DefaultFoldable extends HK1 { } /** * @template T of Brand * @template A * @extends DefaultFunctor * @extends DefaultFoldable */ interface DefaultTraversable extends DefaultFunctor, DefaultFoldable { } final class EitherBrand implements Brand { } /** * @template A * @implements DefaultFunctor * @implements DefaultTraversable */ final class Either implements DefaultFunctor, DefaultTraversable { /** * @template B * @param callable(A): B $f * @return Either implements DefaultFunctor extends HK1 */ public function map(callable $f): Either { return new self(); } } ``` ``` Psalm output (using commit 8c33b21): ERROR: LessSpecificImplementedReturnType - 63:16 - The inherited return type 'HK1' for DefaultFunctor::map is more specific than the implemented return type for Either::map 'Either' ```
orklah commented 2 years ago

I don't think @return Either<B> implements DefaultFunctor<EitherBrand, B> extends HK1<EitherBrand, B> has any meaning in Psalm. everything from implements is just seen as a comment in the return.

Also, this seems to work: https://psalm.dev/r/4dd5c2ef98

Maybe a template covariance issue?

psalm-github-bot[bot] commented 2 years ago

I found these snippets:

https://psalm.dev/r/4dd5c2ef98 ```php */ interface DefaultFunctor extends HK1 { /** * @template B * @param callable(A): B $f * @return HK1 */ public function map(callable $f): HK1; } /** * @template T of Brand * @template A * @extends HK1 */ interface DefaultFoldable extends HK1 { } /** * @template T of Brand * @template A * @extends DefaultFunctor * @extends DefaultFoldable */ interface DefaultTraversable extends DefaultFunctor, DefaultFoldable { } final class EitherBrand implements Brand { } /** * @template A * @implements DefaultFunctor * @implements DefaultTraversable */ final class Either implements DefaultFunctor, DefaultTraversable { /** * @template B * @param callable(A): B $f * @return Either implements DefaultFunctor extends HK1 */ public function map(callable $f): Either { return new self(); } } ``` ``` Psalm output (using commit 76bb8bc): No issues! ```
marcosh commented 2 years ago

I don't think @return Either<B> implements DefaultFunctor<EitherBrand, B> extends HK1<EitherBrand, B> has any meaning in Psalm. everything from implements is just seen as a comment in the return.

Sure, that was not meant to read by Psalm, but by humans, to clarify why the raised issue should actually not be there

Also, this seems to work: https://psalm.dev/r/4dd5c2ef98

Using EitherBrand here is important because it says that those functions could be used only with instances of Either and not of any other class being an instance of Functor and Traversable

Maybe a template covariance issue?

I tried adding template-covariance annotations on every template, but it does not seem to solve the issue, see https://psalm.dev/r/491d084795

psalm-github-bot[bot] commented 2 years ago

I found these snippets:

https://psalm.dev/r/4dd5c2ef98 ```php */ interface DefaultFunctor extends HK1 { /** * @template B * @param callable(A): B $f * @return HK1 */ public function map(callable $f): HK1; } /** * @template T of Brand * @template A * @extends HK1 */ interface DefaultFoldable extends HK1 { } /** * @template T of Brand * @template A * @extends DefaultFunctor * @extends DefaultFoldable */ interface DefaultTraversable extends DefaultFunctor, DefaultFoldable { } final class EitherBrand implements Brand { } /** * @template A * @implements DefaultFunctor * @implements DefaultTraversable */ final class Either implements DefaultFunctor, DefaultTraversable { /** * @template B * @param callable(A): B $f * @return Either implements DefaultFunctor extends HK1 */ public function map(callable $f): Either { return new self(); } } ``` ``` Psalm output (using commit 19ae9e8): No issues! ```
https://psalm.dev/r/491d084795 ```php * @psalm-immutable */ interface DefaultFunctor extends HK1 { /** * @template B * @param callable(A): B $f * @return HK1 */ public function map(callable $f): HK1; } /** * @template-covariant T of Brand * @template-covariant A * @extends HK1 * @psalm-immutable */ interface DefaultFoldable extends HK1 { } /** * @template-covariant T of Brand * @template-covariant A * @extends DefaultFunctor * @extends DefaultFoldable * @psalm-immutable */ interface DefaultTraversable extends DefaultFunctor, DefaultFoldable { } final class EitherBrand implements Brand { } /** * @template-covariant A * @implements DefaultFunctor * @implements DefaultTraversable * @psalm-immutable */ final class Either implements DefaultFunctor, DefaultTraversable { /** * @template B * @param callable(A): B $f * @return Either implements DefaultFunctor extends HK1 */ public function map(callable $f): Either { return new self(); } } ``` ``` Psalm output (using commit 19ae9e8): ERROR: LessSpecificImplementedReturnType - 68:16 - The inherited return type 'HK1' for DefaultFunctor::map is more specific than the implemented return type for Either::map 'Either' ```