I'm attempting to use @template with callable like this:
/**
* @template T
* @param callable(): (T|Throwable) $get_resource
* @param callable(T): Result $action
* @return callable(): Result
*/
function runner($get_resource, $action) {
return function() use ($get_resource, $action) {
$resource = $get_resource();
if ( $resource instanceof Throwable ) {
throw $resource;
}
return $action( $resource );
};
}
In use, I expect that if the $get_resource's T does not match $actions T then a type error will be raised.
If no namespace is used, then an InvalidArgument is correctly raised:
/**
* @param OtherThing $thing
* @return Result
*/
function execute( $thing ) {
return new class() implements Result {};
}
runner(
fn() => new Thing(),
'execute'
);
The error as I expect:
ERROR: InvalidArgument - 30:1 - Type OtherThing should be a subtype of Thing
If a namespace is used, the error disappears.
I would expect the same error:
ERROR: InvalidArgument - 32:1 - Type Hello\OtherThing should be a subtype of Hello\Thing
But there is no error.
I found these snippets:
https://psalm.dev/r/640d53013d
<?php
interface Result {}
class Thing {}
class OtherThing {}
/**
* @template T
* @param callable(): (T|Throwable) $get_resource
* @param callable(T): Result $action
* @return callable(): Result
*/
function runner($get_resource, $action) {
return function() use ($get_resource, $action) {
$resource = $get_resource();
if ( $resource instanceof Throwable ) {
throw $resource;
}
return $action( $resource );
};
}
/**
* @param OtherThing $thing
*/
function execute( $thing ) {
return new class() implements Result {};
}
runner(
fn() => new Thing(),
'execute'
);
Psalm output (using commit 41e6ef6):
INFO: UnusedParam - 26:19 - Param $thing is never referenced in this method
INFO: MissingReturnType - 26:10 - Method execute does not have a return type, expecting _var_www_vhosts_psalm_dev_httpdocs_src____src_somefile_php_27_550
ERROR: InvalidArgument - 30:1 - Type OtherThing should be a subtype of Thing
https://psalm.dev/r/bcbd96e22f
<?php
namespace Hello;
interface Result {}
class Thing {}
class OtherThing {}
/**
* @template T
* @param callable(): (T|\Throwable) $get_resource
* @param callable(T): Result $action
* @return callable(): Result
*/
function runner($get_resource, $action) {
return function() use ($get_resource, $action) {
$resource = $get_resource();
if ( $resource instanceof \Throwable ) {
throw $resource;
}
return $action( $resource );
};
}
/**
* @param OtherThing $thing
* @return Result
*/
function execute( $thing ) {
return new class() implements Result {};
}
runner(
fn() => new Thing(),
'Hello\execute'
);
Psalm output (using commit 41e6ef6):
INFO: UnusedParam - 29:19 - Param $thing is never referenced in this method
In your second example, you refer to the function as Hello\execute from within Hello namespace - so you're actually calling Hello\Hello\execute which does not exist: https://3v4l.org/9WQE8
When this is fixed, Psalm correctly reports the issue: https://psalm.dev/r/fd7e115eaa
I found these snippets:
https://psalm.dev/r/fd7e115eaa
<?php
namespace Hello;
interface Result {}
class Thing {}
class OtherThing {}
/**
* @template T
* @param callable(): (T|\Throwable) $get_resource
* @param callable(T): Result $action
* @return callable(): Result
*/
function runner($get_resource, $action) {
return function() use ($get_resource, $action) {
$resource = $get_resource();
if ( $resource instanceof \Throwable ) {
throw $resource;
}
return $action( $resource );
};
}
/**
* @param OtherThing $thing
* @return Result
*/
function execute( $thing ) {
return new class() implements Result {};
}
runner(
fn() => new Thing(),
'execute'
);
Psalm output (using commit 41e6ef6):
INFO: UnusedParam - 29:19 - Param $thing is never referenced in this method
ERROR: InvalidArgument - 33:1 - Type Hello\OtherThing should be a subtype of Hello\Thing
Also the failure is limited to functions from the calling namespace, functions from other namespaces are resolved properly: https://psalm.dev/r/26c638224c
I found these snippets:
https://psalm.dev/r/26c638224c
<?php
namespace NS;
function runner(callable $_action): void {}
function execute(): void {}
runner('OtherNS\execute');
Psalm output (using commit 41e6ef6):
ERROR: UndefinedFunction - 9:8 - Function OtherNS\execute does not exist
So to recap:
1) If Hello\execute is a function that doesn’t exist, I would expect Psalm to fail it it’s used as a callable.
2) Using \Hello\execute should fail with InvalidArgument but doesn’t, it behaves the same way: https://psalm.dev/r/de3c0df2f1
I found these snippets:
https://psalm.dev/r/de3c0df2f1
<?php
namespace Hello;
interface Result {}
class Thing {}
class OtherThing {}
/**
* @template T
* @param callable(): (T|\Throwable) $get_resource
* @param callable(T): Result $action
* @return callable(): Result
*/
function runner($get_resource, $action) {
return function() use ($get_resource, $action) {
$resource = $get_resource();
if ( $resource instanceof \Throwable ) {
throw $resource;
}
return $action( $resource );
};
}
/**
* @param OtherThing $thing
* @return Result
*/
function execute( $thing ) {
return new class() implements Result {};
}
runner(
fn() => new Thing(),
'\Hello\execute'
);
Psalm output (using commit 41e6ef6):
INFO: UnusedParam - 29:19 - Param $thing is never referenced in this method
Here's a sample that shows the same problem even when the function is in a different namespace as the string callable:
I found these snippets:
https://psalm.dev/r/44f133751c
<?php
namespace Hello;
interface Result {}
class Thing {}
class OtherThing {}
/**
* @template T
* @param callable(): (T|\Throwable) $get_resource
* @param callable(T): Result $action
* @return callable(): Result
*/
function runner($get_resource, $action) {
return function() use ($get_resource, $action) {
$resource = $get_resource();
if ( $resource instanceof \Throwable ) {
throw $resource;
}
return $action( $resource );
};
}
namespace Other;
/**
* @param \Hello\OtherThing $thing
* @return \Hello\Result
*/
function execute( $thing ) {
return new class() implements \Hello\Result {};
}
namespace Third;
\Hello\runner(
fn() => new \Hello\Thing(),
'\Other\execute'
);
Psalm output (using commit 41e6ef6):
INFO: UnusedParam - 31:19 - Param $thing is never referenced in this method
Is there a way to test the changes?
I attempted to install psalm/phar=dev-master but it fails to find the failing scenario still. From what I can tell it _does_ include the fix from a3ae2a7.
Step 0) composer require --dev psalm/phar=dev-master
Step 1) create non-namespace code:
<?php
/**
* @template T
* @param callable():T $get_resource
* @param callable(T):void $action
* @return callable():void
*/
function execute( $get_resource, $action ) {
return function() use ( $get_resource, $action ) {
$resource = $get_resource();
$action( $resource );
};
}
class ThingA {}
class ThingB {}
execute(
fn() => new ThingA(),
'execute_thingb'
);
/**
* @param ThingB $thing
* @return void
*/
function execute_thingb( $thing) {
}
Note that it successfully detects that the return value for T does not match the @param type of execute_thingb because ThingA and ThingB are not compatible types for T.
./vendor/bin/psalm.phar src
Scanning files...
Analyzing files...
â–‘Eâ–‘
ERROR: InvalidArgument - src/other.php:19:1 - Type ThingB should be a subtype of ThingA (see https://psalm.dev/004)
execute(
fn() => new ThingA(),
'execute_thingb'
);
------------------------------
1 errors found
------------------------------
Step 2) change the file to use a namespace:
diff --git a/src/other.php b/src/other.php
index 76b4a3b..468dae7 100644
--- a/src/other.php
+++ b/src/other.php
@@ -1,4 +1,5 @@
<?php
+namespace Foo;
/**
* @template T
@@ -18,7 +19,7 @@ class ThingB {}
execute(
fn() => new ThingA(),
- 'execute_thingb'
+ '\Foo\execute_thingb'
);
/**
Step 3) Run psalm again:
./vendor/bin/psalm.phar src
Scanning files...
Analyzing files...
â–‘â–‘â–‘
------------------------------
No errors found!
------------------------------
The same error is present, but Psalm failed to find it.
To make sure I'm using the correct psalm binary, if I keep the namespace but remove the fully qualified string callable name, Psalm correctly reports the UndefinedFunction error as introduced by a3ae2a7.
./vendor/bin/psalm.phar src
Scanning files...
Analyzing files...
â–‘Eâ–‘
ERROR: UndefinedFunction - src/other.php:22:2 - Function execute_thingb does not exist (see https://psalm.dev/021)
'execute_thingb'
------------------------------
1 errors found
------------------------------
Same problem when there are two different namespaces:
diff --git a/src/other.php b/src/other.php
index 76b4a3b..1f8e8a3 100644
--- a/src/other.php
+++ b/src/other.php
@@ -1,4 +1,5 @@
<?php
+namespace Foo;
/**
* @template T
@@ -13,12 +14,14 @@ function execute( $get_resource, $action ) {
};
}
+namespace Bar;
+
class ThingA {}
class ThingB {}
-execute(
+\Foo\execute(
fn() => new ThingA(),
- 'execute_thingb'
+ '\Bar\execute_thingb'
);
/**
./vendor/bin/psalm.phar src
Scanning files...
Analyzing files...
â–‘â–‘â–‘
------------------------------
No errors found!
------------------------------
Checks took 5.87 seconds and used 417.671MB of memory
Psalm was able to infer types for 99.3421% of the codebase
composer require --dev psalm/phar=dev-master
composer require --dev vimeo/psalm:dev-master should be the actual latest, without any potential phar issues.