Open gmazzap opened 2 years ago
I found these snippets:
This is basically due to the lack of automatic upcasting of templates (see also #7675).
You can either add an extra function or force set the iterator type to work around the issue for now: https://psalm.dev/r/4090f9a7ad
Return types are one of the few places where automatic upcasting should be relatively easy to implement, so hopefully it's not too difficult if I get around to it sometime in the next month or two.
I found these snippets:
Thank you @AndrolGenhald
Right now I'm using @var
docboloc where I consume the value. Like:
/** @var Value<ArrayIterator> $it */
$it = test($num);
so I don't have to declare a function for this.
But the issue is bigger than that, especially when you add assertions into the mix.
If I have a template type Foo|Bar
and I somehow assert the template type is Bar
Psalm is going to complain if the value is not exactly Bar
, but one the Bar
sub-types.
In short, I can get around it, but if templates would get upcasting that will be a huge improvement, imo.
In my case, I made it think by using @var
that I am actually returning a generic interface - just to make sure this was not the problem. Looks like it isn't: below please find my error, formatted for readability.
The declared return type
Inpsyde\PayoneerSdk\Api\Command\Error\InteractionErrorInterface
<
E:Inpsyde\PayoneerSdk\Api\Command\Error\InteractionErrorFactory
as Inpsyde\PayoneerSdk\Api\Command\Exception\InteractionExceptionInterface
>
for
Inpsyde\PayoneerSdk\Api\Command\Error\InteractionErrorFactory::createInteractionError
is incorrect, got
Inpsyde\PayoneerSdk\Api\Command\Error\InteractionErrorInterface
<
Inpsyde\PayoneerSdk\Api\Command\Exception\InteractionExceptionInterface
>
The expected and inferred types appear to be exactly the same. What gives? Seems like a trivial case, but not working.
I confirm that the InteractionErrorFactory
(concrete implementation) declares it's own generic type and uses it to implement the interface with a generic type, like this, in order to use it for a ctor argument:
// InteractionErrorFactory
/**
* @template E of InteractionExceptionInterface
* @implements InteractionErrorFactoryInterface<E>
*/
// InteractionErrorFactory
/**
* @template E of InteractionExceptionInterface
*/
I'm no expert in Psalm internals, but doesn't like an issue with template letter case to me.
@XedinUnknown This is a separate issue, and I think it's actually a bug in your code. It's a bit hard to tell without the full source, but I'm guessing you have something like this. The problem is that this allows you to have the type FooFactory<Bar>
, but it will still return Foo
.
I found these snippets:
@AndrolGenhald, that looks like my issue! But what's wrong with the snippet you wrote? 🤔
https://psalm.dev/r/d39f0af14f
Even if the code can only construct FooFactory<Foo>
, FooFactory<Bar>
is still a valid type. In this case you probably want to either remove the template and have FooFactory
implement FooFactoryInterface<Foo>
or create the template class somehow by using class-string<T>
or closure(): T
or something like that.
I found these snippets:
@AndrolGenhald, sorry, I didn't quite understand what you wrote.
FooFactory<Bar>
is a valid type. But you are not using it anywhere in your snippet. What of it?Bar
there because this is what you told Psalm the factory is of. But my issue does not happen around instantiation or consumption of the factory: at this point it is not consumed anywhere yet.closure
or class-string
?
- Yes,
FooFactory<Bar>
is a valid type. But you are not using it anywhere in your snippet. What of it?
If you use a template Psalm still checks to make sure your code is valid for all possible types of that template, regardless of whether they're used in your code, regardless of whether they can be used. If you have a template that can only ever be Foo
it serves no purpose, remove it and use Foo
instead.
- Your latest snippet reports issues. I'm not sure how it fixed the problem. Psalm reports
Bar
there because this is what you told Psalm the factory is of. But my issue does not happen around instantiation or consumption of the factory: at this point it is not consumed anywhere yet.
It was meant to illustrate the issue, not fix it. Psalm will make sure your code is correct for all possible template types regardless of whether they are instantiated. If you were writing a library you wouldn't want a bug to slip by because you never instantiated a specific template.
- Where would I use the templated
closure
orclass-string
?
https://psalm.dev/r/2cfc76eb6b https://psalm.dev/r/225c764d41
I found these snippets:
Thanks for the explanation, @AndrolGenhald! I hadn't quite understood, but @Biont figured it out.
In the method where I return the "erroneous" type, I add this var typehint with PHPDoc:
/** @var InteractionErrorInterface<E> */
I see why this solves the problem: this confirms that the returned value is correct for all possible permutations. Before that, the returned value was too narrow: it was saying that it has to return X<B>
. and I was returning A
of which B
is a subtype.
But I don't understand why the problem is there in the first place: X<A>
obeys X<B>
where B of A
. Seems like a bug to me. Especially since the problem still persisted after I told Psalm that the returned type is exactly what it says it is expecting.
But I don't understand why the problem is there in the first place:
X<A>
obeysX<B>
whereB of A
. Seems like a bug to me.
@XedinUnknown this is what this issue is about. So far, in Psalm, templated types are "invariant". If you declare X<A>
it expects exactly X<A>
and not X<instance of A>
.
And yes, this is labeled as a bug already by maintainers.
Great, thanks @gmazzap! I came to the right place then, and it's not a problem with my code. Good to know!
But I don't understand why the problem is there in the first place:
X<A>
obeysX<B>
whereB of A
. Seems like a bug to me. Especially since the problem still persisted after I told Psalm that the returned type is exactly what it says it is expecting.
That sounds backwards, you can't return X<Parent>
where X<Child>
is expected. If you mean you want to return X<Child>
where X<Parent>
is expected then yes, that is what this issue is about. X<Child>
is not a subtype of X<Parent>
unless the template is declared as covariant, so Psalm doesn't allow it.
Returning X<Child>
instead of X<Parent>
is safe but it requires Psalm to support automatic upcasting of the template, which hasn't been implemented yet.
@AndrolGenhald, if B of A
, that means B
includes A
. So, yes, the problem seems to be the invariance, rather than the type hierarchy. But yes, I get that this is not yet supported, although I don't know enough to understand why, so you're probably right.
Great, I look forward to seeing a solution!
Actually this is a bit more complicated than I was thinking, even for return types: https://3v4l.org/lbVCZ
An automatic upcast can only be allowed when there isn't some sort of type constraint. I thought maybe ensuring it's a local variable would work, but you can have stuff like this which is also illegal. Unfortunately this is difficult to check and probably won't happen any time soon.
The correct way to annotate the code in initial snippet is to use @template-covariant
: https://psalm.dev/r/52f99d05a1
I found these snippets:
Will try @template-covariant
next time. Does it support a superset of @template
? If so, why would I need a separate tag for that, especially given that it seems to just add more support for generic types which I guess we already expect to work that way with @template
. I haven't found much docs for it, aside from it appearing in an example for @psalm-yield
.
Here's the article on that: https://psalm.dev/docs/annotating_code/templated_annotations/#template-covariance
It doesn't just add support, it adds some restriction on what you can do with your now covariant templated class so you need to be aware of what's going on
Worked for me here.
Now I tried it in another project, and it gives really confiusing errors like this:
ERROR: InvalidReturnType - modules/dummyjson/src/Transform/ProductHydrator.php:44:16 - The declared return type 'Out:DigitalSilk\DummyJson\Transform\ProductHydrator as mixed' for DigitalSilk\DummyJson\Transform\ProductHydrator::transform is incorrect, got 'DigitalSilk\DummyJson\Data\Product'
Here, I even tried to declare the generic type as mixed
, and yet it doesn't understand that Product
is a subtype of mixed
(let alone of object
, ProductInterface
, or even Product
). It's as if template covariance is altogether not working. This is Psalm 5.x-dev
.
The only way I could get it to work is to declare the type of the returned value explicitly with @var
. Here's an example that illustrates the problem. If you add /** @var Out */
on line 258, the issue goes away. Somehow, it is unable to infer that Product
is a subtype of ProductInterface
(or of mixed
even), unless you explicitly tell it so.
Consider the simplest templated class:
now consider the function:
Of course, both
ArrayIterator
andEmptyIterator
implementIterator
, so in theory there's nothing wrong with the function. Still what I get is:The only way to have no issues with the function is:
But declaring type like this is much more complicated.
Even because the following won't work either:
Value<ArrayIterator>|Value<EmptyIterator>
(without the two templated types forArrayIterator
)Value<ArrayIterator<int, int>|EmptyIterator>
(using the uniton type for the templated type). To be noted that with scalar types, e.g.Value<string|int>
works.The strictness of templated values even go further.
See things like:
Fails with:
So
int
does not suffice, I've to useint<1, 100>
; andstring
does not suffice, I've to usenon-empty-string
.I understand supprt for such strictness, but forcing it does not seem correct. With these strictnesss declaring generics is much more difficult, and generic as a feature is much less usable.
See https://psalm.dev/r/a427924638