Psalm: Type inference on methods returning $this

Created on 1 Oct 2020  ·  9Comments  ·  Source: vimeo/psalm

When using chained method calls returning $this, an error is raised for "UndefinedMethod".

Samples:

It also won't use return $this to infer the return type if no return type is provided.

tl;dr: It works if phpdoc is provided, but it won't use return $this to clarify.

All 9 comments

I found these snippets:


https://psalm.dev/r/3d454e34c2

<?php

abstract class Foo {
    public function do(): self
    {
        echo 'do' . PHP_EOL;
        return $this;
    }
}

class Bar extends Foo {
    public function something(): self
    {
        echo 'something' . PHP_EOL;
        return $this;
    }
}

$example = new Bar();
$example->something()->do();
$example->do()->something();
Psalm output (using commit c23406f):

ERROR: UndefinedMethod - 21:17 - Method Foo::something does not exist


https://psalm.dev/r/23a3e1ee01

<?php

abstract class Foo {
    public function do(): Foo
    {
        echo 'do' . PHP_EOL;
        return $this;
    }
}

class Bar extends Foo {
    public function something(): Bar
    {
        echo 'something' . PHP_EOL;
        return $this;
    }
}

$example = new Bar();
$example->something()->do();
$example->do()->something();
Psalm output (using commit c23406f):

ERROR: UndefinedMethod - 21:17 - Method Foo::something does not exist


https://psalm.dev/r/bda78a0789

<?php

abstract class Foo {
    /**
     * @return $this
     */
    public function do(): self
    {
        echo 'do' . PHP_EOL;
        return $this;
    }
}

class Bar extends Foo {
    /**
     * @return $this
     */
    public function something(): self
    {
        echo 'something' . PHP_EOL;
        return $this;
    }
}

$example = new Bar();
$example->something()->do();
$example->do()->something();
Psalm output (using commit c23406f):

No issues!


https://psalm.dev/r/86e41520e7

<?php

abstract class Foo {
    /**
     * @return static
     */
    public function do(): self
    {
        echo 'do' . PHP_EOL;
        return $this;
    }
}

class Bar extends Foo {
    /**
     * @return static
     */
    public function something(): self
    {
        echo 'something' . PHP_EOL;
        return $this;
    }
}

$example = new Bar();
$example->something()->do();
$example->do()->something();
Psalm output (using commit c23406f):

No issues!

Can you elaborate on why you think it should work differently?

I'll do my best!

  • The code works (https://3v4l.org/Lc73L) and isn't something ludicrous
  • PhpStorm infers it successfully
  • This is common in libraries using fluent patterns. In my case, this was prompted by issues raised when using specifically https://github.com/spatie/icalendar-generator
  • It works with the phpdoc specifying what's essentially the same as the return (/** @return $this */ - or with static), so having to redundantly provide the PhpDoc from what could be inferred seems... inefficient? And it appears that it _can_ be understood.

EDIT: In fairness, phpstan also doesn't infer it.

If Psalm inferred the return type as $this here: https://psalm.dev/r/392e337d4d it would have to consider this: https://psalm.dev/r/fbc0ad66a6 an LSP violation, and there's clearly none. Returning $this in C::ugh() is an implementation detail. Since this would introduce ambiguity I don't think this is feasible, nor, in fact, desirable.

Edit: fixed second snippet

I found these snippets:


https://psalm.dev/r/392e337d4d

<?php

class C {
    public function ugh(): static {
        return $this;
    }
}
Psalm output (using commit c23406f):

No issues!


https://psalm.dev/r/fbc0ad66a6

<?php

class C {
    public function ugh(): static {
        return $this;
    }
}

class D extends C {
    public function ugh(): static {
        return clone $this;
    }
}
Psalm output (using commit c23406f):

ERROR: MethodSignatureMismatch - 10:5 - Method D::ugh with return type 'D' is different to return type 'C' of inherited method C::ugh

Yeah, Psalm has followed the rule that it infers types only inside functions, and I don't think that's worth violating. PHP 8 supports a static return type (which Psalm will support fully) to help with this particular use-case.

I must admit I hadn't considered PHP8's changes, which do solve the issue.

Thanks to both of you for your quick replies, it's much appreciated!

So as solution (until PHP8), in any of our code (not that we have much fluent code tbh) if that arises our best bet would be to use phpdoc with @return static.

If I may end with a quick question: any tip you could suggest that could be used in consumer code (ie when consuming an external package) to solve that?

  • I tried using a .phpstorm.meta.php file to type-hint as 'static', without any effect. (IIRC they're taken into account!?)
  • Adding to baseline to ignore the issue is kinda dirty.
  • The only solution I came up with was to break down the fluent calls in step-by-step....

....or PR into that external package to add the static @return annotation 🤷‍♂️

@tdtm you can use stub files https://psalm.dev/docs/running_psalm/plugins/authoring_plugins/#stub-files

Or send PR to package :)

Thanks @andrew-demb. I'll check with that packages' maintainers if they'll accept a PR for it.

Was this page helpful?
0 / 5 - 0 ratings