vimeo / psalm

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

Templates on static methods yet again #7975

Open someniatko opened 2 years ago

someniatko commented 2 years ago

I try to design something like this: https://psalm.dev/r/4cf10dedf9

However, as stated by @orklah in https://github.com/vimeo/psalm/issues/7507#issuecomment-1024437536,

This is on purpose. Psalm made a design choice that class-level templates and static properties/method should not mix.

Is there a way to design the interface in such a way Psalm won't complain?

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

I found these snippets:

https://psalm.dev/r/4cf10dedf9 ```php */ final class User implements PersistableEntityInterface { public function __construct( private readonly string $id, private readonly string $name, ) {} public function name(): string { return strtoupper($this->name); } public function saveState(): UserState { return new UserState($this->id, $this->name); } public static function restoreState(mixed $state): self { return new self($state->id, $state->name); } } class UserState { public function __construct( public readonly string $id, public readonly string $name, ) {} } class UserRepository { /** @var array */ private array $users = []; public function getById(string $id): User { return User::restoreState($this->users[$id] ?? throw new \RuntimeException); } public function save(User $user): void { $userState = $user->saveState(); $this->users[$userState->id] = $userState; } } ``` ``` Psalm output (using commit f960d71): ERROR: UndefinedDocblockClass - 16:15 - Docblock-defined class, interface or enum named T does not exist INFO: MixedInferredReturnType - 39:56 - Could not verify return type 'User&PersistableEntityInterface' for User::restoreState ERROR: InvalidArgument - 60:35 - Argument 1 of User::restoreState expects T, UserState provided ```
ZebulanStanphill commented 2 years ago

I was trying to setup Psalm on a codebase that's already using PHPStan (I figured it would be good to check things with both tools), and this is one of the main things blocking me from doing so. I have several abstract classes that define template types used in the params of their static methods. For (an abridged) example:

/**
 * @template T of Table
 * @template R of array<non-empty-string, mixed>
 */
abstract class Model {
    /**
     * @phpstan-param R $row
     *
     * @phpstan-return self<T, R>
     */
    abstract public static function fromRow(array $row): self;
}

The param type of the fromRow method causes Psalm to complain Docblock-defined class, interface or enum named My\Namespace\R does not exist.

PatchRanger commented 1 year ago

I confirm the issue and join the request of how to do templating for static properties. The minimal case of reproduction is attached below. https://psalm.dev/r/9a4c417e38

psalm-github-bot[bot] commented 1 year ago

I found these snippets:

https://psalm.dev/r/9a4c417e38 ```php , mixed> */ private static array $classMap = []; } ``` ``` Psalm output (using commit a82e7fc): ERROR: UndefinedDocblockClass - 9:19 - Docblock-defined class, interface or enum named T does not exist ```
someniatko commented 1 year ago

@orklah Do you think introducing something like @static-template or @template-static may solve the issue? (and @template-static-implements, @template-static-extends)

orklah commented 1 year ago

Possibly. Frankly, this is kinda over my head. It's a design choice that was made before I started actively contributing and I would have no idea how to change that meaningfully

discordier commented 1 month ago

This also affects static properties as seen in https://psalm.dev/r/047cddc9ac.

Having a static template annotation won't help when we have mixed usage I suppose (storing values from static context but using them in instanced context).

psalm-github-bot[bot] commented 1 month ago

I found these snippets:

https://psalm.dev/r/047cddc9ac ```php */ private static array $values = []; /** @param T $arg */ private static function failsButShouldNot($arg): void {} // All fine for non static usage. /** * This works as expected * @var array */ private array $values2 = []; /** @param T $arg */ private function worksAsExpected($arg): void { if ($arg === null); } } final readonly class Bar { /** @use FooTrait */ use FooTrait; } ``` ``` Psalm output (using commit 03ee02c): ERROR: UndefinedDocblockClass - 13:20 - Docblock-defined class, interface or enum named T does not exist ERROR: UndefinedDocblockClass - 15:16 - Docblock-defined class, interface or enum named T does not exist ```