New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A bundle of generic variance enhancements #2075
Conversation
da6e74b
to
7b9d51a
Compare
I disagree with this one. Right, it doesn't make sense because you can't set it when talking about an instance like As an experiment, I'd accept this for final classes. Only in bleeding edge though.
👍
I agree, needs to be in bleedingEdge though only.
👍 Next time, please send at least 4 separate PRs 😂 And only one-by-one, to avoid conflicts, and to apply learnings from the previous PRs in further ones. |
Yeah, sorry about that. I had most of the code prototyped for the properties check, and then I kept bumping into all these edge cases that I thought needed to be solved beforehand. As I said, this PR was meant as an overview of the whole context, and I'm ok with rebasing any changes that come out of this and sending the PRs one by one. It seems that only the static bit is causing a stir, anyway :)
I'm not sure I understand. Could you give me an example of what you mean? I've taken some time for further research, and I must stand my ground. I'll try to build my case from a different perspective, though: properties. This is no longer a question of variance, it's more a question of implementation details. Consider the following code: /** @template T */
class Foo
{
/** @var T */
public static mixed $foo;
} Because PHPStan only does type erasure, there is a singular property at runtime: Foo<string>::$foo = '✌️';
functionThatAcceptsInt(Foo<int>::$foo); This must not be allowed. I believe we can agree on this one :) Looping back to static methods, I think template type references should be disallowed there too, for the sake of consistency: It's consistent for the end user: however obvious it might be, you don't need to explain to them why template types can be used in static methods, but not in static properties. People get the same error message in both cases. It's also consistent implementation-wise. There are languages such as C# that have actual compiled generics; this allows them to treat It's only on the level of instances where the type parameters make any actual distinction. It's the instances that are actually parameterized by them – one instance of Admittedly, if variance rules are properly enforced, I don't think having template type references in static methods could lead to any actual runtime errors. But they feel useless, most of all: I've established before that class-level template types cannot be used in static properties. What purpose is left for them in static methods if they can't be used to access state? Without this ability, static methods are essentially just class-scoped functions, and if functions need to be generic, they have to be generic themselves. Without this ability, Yes, this might be opinionated and perhaps it should only be part of some optional, stricter ruleset, but I really think it should be there somewhere. Sorry for the long read. Looking forward to your thoughts! |
I love these brainstormings! I imagine this is how Einstein wrote letters to his friends too :) (Of course I'm no Einstein, but it has a similar vibe, figuring out things together.) I thought about it and you're probably right. What I meant for static methods was that you can do So yeah, it doesn't make much sense. But we need to disable it only for bleedingEdge, because we need to see the feedback - most of it it's gonna be wrong usage, but maybe someone comes up with something valid. And I don't want to make everyone's builds red again :) What I'd love to see is for FileTypeMapper be modified, so that we don't need a new special rule for this, the existing rules should pick up that I had to fix some code so that So the fix for static methods should be similar - it shouldn't see And oh, I just realized something about "Explicitly disallow defining type parameters in constructor": I think it's valid to have @param callable(T): void $cb
@param T[] $items And BTW since we are cleaning up generics, what do you think about union vs. intersection in this example? @template T
@param T $a
@param T $b And when such a function is called with Currently it's a union, but I've heard that intersection might make sense too, and it's the case in some languages. Also, callable parameters complicate that. PHPStan doesn't union them because we need functions like And one more challenge for generics is: Should we generalize the type or not? :) When you have I'm more inclined now towards not generalizing (because it causes problems), but we need to solve a |
Me too :)
I love the idea!
My train of thought was that the constructor is part of the class's "signature" rather than a separate method because that's how it is in many other languages. But unlike many other languages, PHP doesn't have any special syntactical construct for constructors, so I guess you're right.
Yes, it seems that both can be seen in the wild, so probably both make sense. Personally, I lean toward the intersection. When authoring such a function, I would assume you expect the type of both parameters to be the same (or at least compatible) – otherwise, you would have explicitly used two template types. Intersection gives you that: if the types are compatible, you get their common denominator, so to speak, and if they're not, you get
It's an interesting problem, and I see you already have a suggested solution :) I'll read through the linked issue and see if any idea comes up. |
So, I've found some time to take a look at this again
It's fairly easy to alter the code so that static methods do not see class-level template types, but I must say I'm struggling with static properties. If I understand it correctly, they share the same NameScope with instance properties, and I'm not sure how to hide template types from one kind of properties and not the other – at least not on If it turns out too complex – or perhaps even if not –, I still can see some value in reporting a distinct error for this – if nothing else, it should be more easily google-able for people who encounter it, and it leaves space for a potential targeted hint or a link to an explanation if people are confused by this. |
Can you please share how you did it for static methods? So that I might have an easier job figuring out how to did it with static properties :) |
After #2064, I've had a few more eureka moments about generic variance. Honestly, I'm starting to feel like I'm fighting a hydra: I cut off one head and two more grow back 😅 as a result, I'm proposing several changes.
Sorry for not opening separate pull requests, but all these changes sort of build upon one another, and I believe they make the most sense as a whole package. I'm opening a draft PR so that you can take a look at the changes in their interconnectedness, and then I can split this commit by commit. It's 100% ready for review.
Disallow class template types in static methods
Other languages with generics such as Java, Kotlin, or TypeScript do not allow template type references in static members (with the obvious exception of a static method referencing its own template types). I think PHPStan should follow suit.
After all, static members are shared, even between various instances of the type constructor, while the type parameter is bound to its instances. In terms of (pseudo)code, you can't just call
Collection<Dog>::create()
. You make the factory itself generic and callCollection::create<Dog>()
instead.I hope this change should be relatively safe; I seriously doubt people are using class template types in static methods because the semantics of that are just so unclear. But if necessary, I believe it wouldn't be that difficult to add a feature toggle for this, even in the context of all the surrounding changes.
Do not check variance in constructor
Turns out you were almost right: there really should be a similar exception for constructor, except there isn't :) the check has so far behaved in the way I described in reply to your comment, but that way is indeed incorrect. I've changed the code so that the constructor is ignored as well.
This potentially relaxes the whole check: I imagine it might have previously discovered variance issues with promoted properties. But I consider it to be just a fortunate side effect rather than a feature. PHPStan should distinguish between the constructor parameter which should be omitted from the variance check, and the promoted property which should be included in the variance check just as any other property (more on that below).
Explicitly disallow defining type parameters in constructor
While methods are allowed to define their own type parameters and become generic, the constructor is a special case and should be exempt from this. I expect nobody is doing this, anyway, but I think it can't hurt to forbid it explicitly.
Check generic variance rules for properties
All these changes gradually build up towards the final one: generic variance check for properties.
Properties are treated as invariant, therefore this new rule will be most useful with
invarianceComposition
enabled (#2054). The only exception is native readonly properties: I believe we can safely consider them covariant because PHPStan already makes sure that they are initialized in the constructor and only read ever since.Similarly to methods, private properties are ignored and static properties are prevented from referencing template types altogether.
I've hidden this new rule behind a bleeding-edge feature toggle because I expect it might break a build or two for projects (although hopefully not that many since private properties are ignored).