Psalm: How to annotate @return type depending on argument count [func_num_args()]

Created on 30 Apr 2020  Â·  13Comments  Â·  Source: vimeo/psalm

Laravel has a response() helper function:

  • response() returns a ResponseFactory object
  • response('Hello world') returns a Response object

Does Psalm currently support annotating this @return type depending on the argument count? Conditional types don't seem to support this.

Here's a derived example:
https://psalm.dev/r/927336a62d

Most helpful comment

I made the solution a little more conservative – $argc already has a meaning, so I used func_num_args() instead:

(func_num_args() is 0 ? int : string)

All 13 comments

I found these snippets:


https://psalm.dev/r/927336a62d

<?php

/**
 * @param string $content
 * @return string|bool
 */
function mirror($content = '') {
    if (func_num_args() === 0) {
        return false;
    }

    return $content;
}

$str = mirror('x');
echo strlen($str);
Psalm output (using commit 21f4dee):

ERROR: InvalidScalarArgument - 16:13 - Argument 1 of strlen expects string, bool|string provided

Here's a closer example:

https://psalm.dev/r/bd04a5ea2a

I found these snippets:


https://psalm.dev/r/bd04a5ea2a

<?php

class Response {
    private string $content;

    public function __construct(string $content) {
        $this->content = $content;
    }

    public function getContent(): string
    {
        return $this->content;
    }   
}

class ResponseFactory {
    public function json(array $data): Response {
        $content = json_encode($data);
        return new Response($content);
    }
}

/**
 * @return Response|ResponseFactory
 */
function response(string $content = '') {
    if (func_num_args() === 0) {
        return new ResponseFactory();
    }

    return new Response($content);
}

echo response()
    ->json(['foo' => 'bar'])
    ->getContent();
Psalm output (using commit 21f4dee):

ERROR: PossiblyUndefinedMethod - 35:7 - Method Response::json does not exist

INFO: MixedMethodCall - 36:7 - Cannot determine the type of the object on the left hand side of this expression

INFO: MixedArgument - 34:6 - Argument 1 of echo cannot be mixed, expecting string

You don't need argument count if you can ignore the case of return('') not returning the factory as return type then may depend on the parameter type (literal empty string in this case): https://psalm.dev/r/306cc1e212

I found these snippets:


https://psalm.dev/r/306cc1e212

<?php

class Response {
    private string $content;

    public function __construct(string $content) {
        $this->content = $content;
    }

    public function getContent(): string
    {
        return $this->content;
    }   
}

class ResponseFactory {
    public function json(array $data): Response {
        $content = json_encode($data);
        return new Response($content);
    }
}

/**
 * @return Response|ResponseFactory
 * @template T of string
 * @psalm-param T $content
 * @psalm-return (T is '' ? ResponseFactory : Response)
 */
function response(string $content = '') {
    if (func_num_args() === 0) {
        return new ResponseFactory();
    }

    return new Response($content);
}

echo response()
    ->json(['foo' => 'bar'])
    ->getContent();

echo response('html')->getContent();
Psalm output (using commit 21f4dee):

No issues!

You don't need argument count if you can ignore the case of return('') not returning the factory as return type then may depend on the parameter type (literal empty string in this case): https://psalm.dev/r/306cc1e212

Good idea, but unfortunately that workaround only works for explicit strings, not for string parameters:
https://psalm.dev/r/cd6d14ee01

I found these snippets:


https://psalm.dev/r/306cc1e212

<?php

class Response {
    private string $content;

    public function __construct(string $content) {
        $this->content = $content;
    }

    public function getContent(): string
    {
        return $this->content;
    }   
}

class ResponseFactory {
    public function json(array $data): Response {
        $content = json_encode($data);
        return new Response($content);
    }
}

/**
 * @return Response|ResponseFactory
 * @template T of string
 * @psalm-param T $content
 * @psalm-return (T is '' ? ResponseFactory : Response)
 */
function response(string $content = '') {
    if (func_num_args() === 0) {
        return new ResponseFactory();
    }

    return new Response($content);
}

echo response()
    ->json(['foo' => 'bar'])
    ->getContent();

echo response('html')->getContent();
Psalm output (using commit 21f4dee):

No issues!


https://psalm.dev/r/cd6d14ee01

<?php

class Response {
    private string $content;

    public function __construct(string $content) {
        $this->content = $content;
    }

    public function getContent(): string
    {
        return $this->content;
    }   
}

class ResponseFactory {
    public function json(array $data): Response {
        $content = json_encode($data);
        return new Response($content);
    }
}

/**
 * @return Response|ResponseFactory
 * @template T of string
 * @psalm-param T $content
 * @psalm-return (T is '' ? ResponseFactory : Response)
 */
function response(string $content = '') {
    if (func_num_args() === 0) {
        return new ResponseFactory();
    }

    return new Response($content);
}

function stringResponse(string $content): Response {
    return response($content);
}
Psalm output (using commit 21f4dee):

ERROR: InvalidReturnStatement - 38:12 - The inferred type 'Response|ResponseFactory' does not match the declared return type 'Response' for stringResponse

ERROR: InvalidReturnType - 37:43 - The declared return type 'Response' for stringResponse is incorrect, got 'Response|ResponseFactory'

@caugner I assume this is in a stub file then yeah? I think you might be able to get away with declaring the parameter string|null, and then checking if it's a string.

I did something similar for laravel factories https://github.com/mr-feek/psalm-plugin-laravel/pull/1/files

Would love to have you PR whatever you have so far against the laravel plugin and we can get it over the finish line!

HMMMMMM

What if conditional return types supported

($argc > 1 ? string : int)

That wouldn't be too hard to add to the parser, I don't think

HMMMMMM

What if conditional return types supported

($argc > 1 ? string : int)

That wouldn't be too hard to add to the parser, I don't think

Sounds like a good solution to me!

I made the solution a little more conservative – $argc already has a meaning, so I used func_num_args() instead:

(func_num_args() is 0 ? int : string)

@muglug Thank you for this great feature and the rapid implementation!

Awesome, this will be helpful for me too! Thanks @muglug

Was this page helpful?
0 / 5 - 0 ratings