vimeo / psalm

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

A way to use signature of another method as a type #8716

Open weirdan opened 1 year ago

weirdan commented 1 year ago

Rationale

Some methods may have signatures based on other methods. One particular example is Laravel's Dispatchable trait, where methods have signatures based on constructor signature.

It would be great if we had a way to specify something like this:

<?php declare(strict_types=1);

trait Creatable 
{
   /** @param method-args<static::__construct> $args */
   public static function create(...$args): static
   {
      return new static(...$args);
   }

   /** @param method-args<static::__construct> $args */
   public static function createIf(bool $condition, ...$args): static|null
   {
      return $condition ? new static(...$args) : null;
   }
}

final class A
{
   use Creatable;
   public function __construct(private int $param) {}
}

final class B
{
   use Creatable; 
   public function __construct(private string $param) {}
}

$a = A::create(2); // valid
$b = B::createIf(true === true, "42"); // valid

$aa = A::createIf(true, "42"); // invalid
$bb = B::create(2); // invalid

https://psalm.dev/r/bd50e7e47e

Prior art

Parameters<T>, ConstructorParameters<T> and ReturnType<T> in Typescript.

Typescript, however, builds those types on function signature types + generics + infer: https://github.com/microsoft/TypeScript/blob/a3092c798ad9f165b0f7cba964a2c7b976cd30d0/lib/lib.es5.d.ts#L1601

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

I found these snippets:

https://psalm.dev/r/bd50e7e47e ```php $args */ public static function create(...$args): static { return new static(...$args); } /** @param method-args $args */ public static function createIf(bool $condition, ...$args): static|null { return $condition ? new static(...$args) : null; } } final class A { use Creatable; public function __construct(private int $param) {} } final class B { use Creatable; public function __construct(private string $param) {} } $a = A::create(2); // valid $b = B::createIf(true === true, "42"); // valid $aa = A::createIf(true, "42"); // invalid $bb = B::create(2); // invalid ``` ``` Psalm output (using commit 12f33fa): ERROR: InvalidArgument - 30:16 - Argument 1 of A::create expects method-args, but 2 provided ERROR: InvalidArgument - 31:33 - Argument 2 of B::createIf expects method-args, but '42' provided ERROR: InvalidArgument - 33:25 - Argument 2 of A::createIf expects method-args, but '42' provided ERROR: InvalidArgument - 34:17 - Argument 1 of B::create expects method-args, but 2 provided INFO: UnusedVariable - 30:1 - $a is never referenced or the value is not used INFO: UnusedVariable - 31:1 - $b is never referenced or the value is not used INFO: UnusedVariable - 33:1 - $aa is never referenced or the value is not used INFO: UnusedVariable - 34:1 - $bb is never referenced or the value is not used ERROR: UndefinedDocblockClass - 5:15 - Docblock-defined class, interface or enum named method-args does not exist ERROR: UndefinedConstant - 5:15 - Constant A::__construct is not defined INFO: MixedInferredReturnType - 6:45 - Could not verify return type 'A' for Creatable::create ERROR: UndefinedDocblockClass - 11:15 - Docblock-defined class, interface or enum named method-args does not exist ERROR: UndefinedConstant - 11:15 - Constant A::__construct is not defined INFO: MixedInferredReturnType - 12:64 - Could not verify return type 'A|null' for Creatable::createIf ERROR: UndefinedConstant - 5:15 - Constant B::__construct is not defined INFO: MixedInferredReturnType - 6:45 - Could not verify return type 'B' for Creatable::create ERROR: UndefinedConstant - 11:15 - Constant B::__construct is not defined INFO: MixedInferredReturnType - 12:64 - Could not verify return type 'B|null' for Creatable::createIf ```
weirdan commented 1 year ago

Could be extended to functions and callable, and include return types:

/**
 * @param function-args<$func> $args
 * @return function-return<$func>
 */
function call_user_func(callable $func, ...$args) { ... }