-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Compiler: correctly check abstract methods #7956
Conversation
I think this could be enough (the return type of the implemented abstract method might not be mandatory, just deduce the type from method body and check it against abstract def return type, if it's given - that would be the perfect Crystal way): abstract class Foo
abstract def foo : String
end
class Bar < Foo
def foo
"aaa"
end
end but I understand, that it's hard to implement - by your comments, you have not method bodies in the time of this check - is it even possible? I also don't like "Overrides" annotation (I don't like annotations at all, it's not Java) - is it even documented somewhere? But mainly - thanks for your efforts! Can't wait for comments from others. |
I think doing it this without forcing a return type annotation is possible, it's just that the implementation will be a bit messier (I have to inject a fake return type that also carries which abstract def it's related to, so that it can be shown in an error, and we have no AST node for that) and I also prefer an explicit return type annotation to be in the source code for clarity, and eventually an |
I think the worse case is this: abstract class Foo
abstract def foo : Nil
end
class Bar < Foo
def foo
1
end
end
# Huh??
Bar.new.foo # => nil The compiler will copy the In that case, yeah, we could force an explicit return type so that it's clearer... but since we'll be doing that for at least that case, I'd like it to be done for every case, which is more uniform and clear. |
So, why just not keep returning exactly what is returned (instead of enforced nil): ...
Bar.new.foo # => 1 Any bad consequences? |
Because for example |
In fact I'd like there to be more and more type annotations over time. The feature about being able to omit types in method arguments is most of the time an anti-feature in my opinion. It's good for generic code, but most code is not generic. |
@asterite would you mind rebasing on master and squashing the fixup commits so this can be cleanly merged? |
Let's tackle this in a future commit. It's tricky because of type parameters but it can be done.
It could, but we can leave the implementation for later (or better: make an Indexable comparable to any other Indexable)
This is in case we got something wrong in the algorithm, so people can still compile their code, or they don't want to bother upgrading their code just yet.
@RX14 Done! I think the 6 commits that are left are good on their own. |
Thank you, Ary <3 |
I guess this change is fine. But it's a potential source for annoying errors coming up in 0.30.0 when you have a large code base implementing abstract methods. It only shows one error at a time so you'll either need a fix-error-repeat cycle until all are fixed or find a different way to identify all malicious methods. It's good to see that it can be disabled. With Crinja it actually showed me that the type restrictions on the abstract methods were wrong (which is getting fixed), but if they had been right, there would have been dozens of implementations that need a return type. |
I think we can do it as warnings. My main issue with warnings is that they are opt-in, meaning that almost nobody will really take a look at them when upgrading. Then when we make the warning an error it will cause the same issue, code will break. So before making this a warning I'd like warnings to be opt-out. |
The thing is that this is going to break quite a lot of code. And it can be difficult to fix when the missing return types are in shard dependencies. This will need fixes and updated versions for each of them. And this takes time. I've created PR's against some shards to fix this, but this is probably pretty common. This breaking change has just come into the nightly build yesterday and the release is coming up in the next days. That's hardly enough time to properly identify and update breaking code. We should give maintainers more time (until 0.31.0) to do this without their code suddenly breaking. (At the very least, the |
You are right. Let's make it a warning and let's take our time to fix them in the next release. We should mention this prominently in the changelog and the release blog. I'll send a PR later today. @bcardiff what do you think? |
I agree. Either mention the flag or be more conservative with the feature. We could make it opt-in for this release. |
fix for abstract method change in crystal-lang/crystal#7956
Fixes #3546
Fixes #6085
Fixes #6762
This PR does a few things.
Return types are checked 🎉
Return types of abstract methods are now checked in subtypes.
It took me a long time to implement this because I didn't know how to resolve this issue: the abstract method check runs after the first compiler pass where all types and methods are defined, but before method bodies are typed (and so, actual method return types are computed). So by the time we could check whether an abstract method was correctly implemented (the return type matches) a different error might have been triggered.
Also, if you have something like this:
what would the error be? It will be something like "Method Bar#foo must return String because it implements the abstract method Foo#foo which returns String", which is a mouthful of words. But also looking at the code it's not clear that
Bar#foo
must return a String, because that code and the abstract method are far away (probably in a different file).One way to solve this, which is what this PR does, is to force subtypes to include an explicit return type if the abstract method has one. Once this is required we can check whether the return type matches without actually typing the body. Actually checking whether the body complies to the explicit return type always comes next, at some point.
This is backwards incompatible because for existing code you will be required to add explicit return type annotations. However, I think this is good: it solves the problem I mention above where it wasn't clear that
Bar#foo
was supposed to returnString
, because now that will be explicitly written. (well, the connection with the abstract method is still missing, and for that maybe I'd like to make anOverrides
annotation mandatory, at least for this case... but not a part of this PR or discussion).So for instance, in the above code you will now get this error (featuring the super nice error formatting made by @martimatix:
Then let's say you go ahead and modify it to this:
You get this error:
Changing
Int32
toString
and fixing the body compiles as expected.(the error messages are still long, but I think the resulting code is clearer and more explicit)
Also note that the error message says it must return the base type or a subtype of it. For example, this is valid:
The code above is fine. After all,
Child
is aParent
so if you didn't know whether you have aFoo
or aBar
you still can treat it as aParent
(and this is how other statically typed languages with subtyping work).Finally, the third commit in this PR adds some explicit return type annotations to comply with this change. I was able to fix it in a few minutes, so even though this is a breaking change it should be forward to fix it. And if not, I also added a flag,
skip_abstract_def_check
, which will skip this check altogether. This is useful in case you can't upgrade right now, or in case I got something wrong in the check algorithm.I also fixed a couple of incorrect things:
Random#next_u
there was noUInt
type (and some Random instances returnUInt32
, someUInt64
, someUInt8
... maybe this should be unified, I don't know... but not part of this PR or issue)Socket::Server
(or something like that) the return type was incorrectDeque
includedComparable
but never implemented<=>
: I decided to remove the inclusion ofComparable
because I think we can move that toIndexable
, but I'd like to do it in a different commit (and nothing is lost here anyway because it could never be used as aComparable
).Abstract methods are now correctly checked for generic types! 🎉
So for example this works fine:
The compiler knows that the
T
inFoo
inBaz
is anInt32
so it checks against that. Kind of obvious, I know, but it wasn't that straight-forward to implement. And for return types it will suggest the correct (instantiated) type.And this also works for generic modules. So previously these were all skipped (because the implementation wasn't correct, so it was turned off... though for generic classes it was turned on but it worked incorrectly! see #6762). So this code used to compile:
But now it correctly gives an error: