-
Notifications
You must be signed in to change notification settings - Fork 139
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
Add support for RBS signature comments #2236
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Alexandre Terrasa <[email protected]>
Signed-off-by: Alexandre Terrasa <[email protected]>
Signed-off-by: Alexandre Terrasa <[email protected]>
Signed-off-by: Alexandre Terrasa <[email protected]>
Signed-off-by: Alexandre Terrasa <[email protected]> Co-authored-by: Ufuk Kayserilioglu <[email protected]>
Signed-off-by: Alexandre Terrasa <[email protected]> Co-authored-by: Kaan Ozkan <[email protected]>
lib/tapioca/internal.rb
Outdated
@@ -1,6 +1,11 @@ | |||
# typed: strict | |||
# frozen_string_literal: true | |||
|
|||
# The rewriter needs to be loaded very early so RBS comments within Tapioca itself are rewritten | |||
require "spoom" | |||
require "tapioca/rbs/rewriter" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need this to happen super early so we also rewrite the RBS comments we find inside Tapioca
@@ -160,7 +160,7 @@ def with_rails_application(&blk) | |||
Rails.app_class = Rails.application = rails_application | |||
end | |||
|
|||
#: -> Array[singleton(Rails::Engine)] | |||
T::Sig::WithoutRuntime.sig { returns(T::Array[T.class_of(Rails::Engine)]) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we re-execute the signatures, having a reference to Rails
is a problem. For now I changed them back to a T::Sig::WithoutRuntime
. We can add a @without-runtime
annotation later.
|
||
# We need to include `T::Sig` very early to make sure that the `sig` method is available since gems using RBS comments | ||
# are unlikely to include `T::Sig` in their own classes. | ||
Module.include(T::Sig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@KaanOzkan Is there any adverse effect to do this in the context of the LSP addon?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it should be fine. Rewriting itself could be an issue with the addon, I'll test it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems okay on a first glance, not blocking.
|
||
RubyNext::Language.include_patterns.clear | ||
RubyNext::Language.include_patterns << "**/*.rb" | ||
RubyNext::Language.rewriters = [Tapioca::RBS::Rewriter] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we're replacing any list of rewriters that may have been set
#: (String source) -> String | ||
def rewrite(source) | ||
# Do not try to parse files that don't have RBS comments | ||
return source unless source =~ /^\s*#\s*typed: (ignore|false|true|strict|strong|__STDLIB_INTERNAL)/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parsing all the files is a major slow down. We escape early if the file is not likely to contain Sorbet signatures.
Note that I'd like for us to move away from having typed: X
in our files, so maybe we should be matching on #:
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I'm thinking about it and the more I think it's a mistake. I already got confused while writing tests because I forgot the sigil. Matching on #:
seems safer and future proof 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would gain significant performance improvement for tapioca gem
with arguments if we could rewrite only the gems we are generating RBIs for. It would help with the performance loss of the early return when we match #:
instead.
sig { returns(::String) } | ||
def foo; end | ||
|
||
def foo=(_arg0); end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that writer were already untyped before this PR.
2b9454e
to
c116258
Compare
@@ -37,7 +37,7 @@ def override_require_false(exclude:, &blk) | |||
end | |||
end | |||
|
|||
#: -> untyped | |||
sig { returns(T.untyped).checked(:never) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are some comments now sigs? Is it so that tapioca's own RBIs are generated correctly?
Will it error in CI if someone wrote a RBS comment that was supposed to be a sig?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's only for the checked(:never)
and Sig::WithoutRuntime
because now that we re-inject them they get executed. This will only be required for the sigs in Tapioca we do not want to run at runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I missed the checked(:never)
calls. I think this is fine for now, we can look at speeding it up later.
Locally I reverted some of these sigs to comments but I don't get any test failures. I'm wary that we'll forget to use sigs in this case and only encounter the issue after we release/merge. Is there a way we can run into this failure in CI and create a check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CI should fail if any of this method gets reverted to one checked at runtime. Try with the BasicObject
test for example: bin/test -n "/can handle Basic/"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah. I tested it with RBI generation which strangely succeeds but CI fails. Thanks.
|
||
# We need to include `T::Sig` very early to make sure that the `sig` method is available since gems using RBS comments | ||
# are unlikely to include `T::Sig` in their own classes. | ||
Module.include(T::Sig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it should be fine. Rewriting itself could be an issue with the addon, I'll test it.
#: (String source) -> String | ||
def rewrite(source) | ||
# Do not try to parse files that don't have RBS comments | ||
return source unless source =~ /^\s*#\s*typed: (ignore|false|true|strict|strong|__STDLIB_INTERNAL)/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would gain significant performance improvement for tapioca gem
with arguments if we could rewrite only the gems we are generating RBIs for. It would help with the performance loss of the early return when we match #:
instead.
Yeah it's a good idea, though we would need to register the rewriter much later so we can have access to the list of gems which means no signatures for Tapioca itself. If we want to go this route I think it would mean exporting the RBI for Tapioca itself with This would also solve the issue of the signatures I had to revert to |
Signed-off-by: Alexandre Terrasa <[email protected]>
Signed-off-by: Alexandre Terrasa <[email protected]>
Signed-off-by: Alexandre Terrasa <[email protected]>
…ame way Signed-off-by: Alexandre Terrasa <[email protected]>
8151094
to
5995622
Compare
This would be great. I am incredibly excited to see RBS comments fully embraced by sorbet/tapioca. My team is closely following your progress in that matter. Good luck and cheers 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach we're taking here is to do a full rewriter pass to translate RBS -> RBI and then letting Tapioca do its thing with no functional changes.
Would it be possible to change Tapioca to handle the RBS comments directly instead? For example, when we find a method, we could search its comments, find an RBS signature and then build a signature entry for it.
If this suggestion is feasible, I think (without actually benchmarking anything) we may be able to achieve better performance on top of avoiding the ruby-next
dependency.
What do you think? Am I missing context?
This was the idea with our first prototype to solve this issue. First we need to access the comments which we can't from a pure runtime perspective and we need to rely on reparsing the file (indexer, yard, whatever). Once we have the comment there is the issue of resolving constants. Consider this: module Foo
class Bar; end
class Baz
#: -> Bar
def foo; end
end
end The way Tapioca is currently built, it creates a flattened view of classes so if we were to just translate the sig the result would look like this: module Foo; end
class Foo::Bar; end
class Foo::Baz
sig { returns(Bar) } # error: Unresolved constant `Bar`
def foo; end
end Since the code is already loaded, we can use the runtime to resolve the constants, something like this would work for example: const = eval <<~RB
module Foo
class Baz
Bar
end
end
RB Finally with this information we can write the final sig. And that would work but it wouldn't solve the issue of DSL compilers that look for the method signature at runtime. So what we could do at this point is use the same approach but instead of generating the sig as a RBI string, we could call into One interesting thing about the load time rewriter we use here is that we could in the future extract it as a standalone feature to re-enable runtime type checks in tests for example. Imagine using RBS comments, you could just require |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for explaining the other considerations. I think using the indexer to detect the RBS signatures would be nice, but it seems like it would be a much more involved change that expands simply adopting it.
I would rather us move forward with solving the problem and then iterate on improving the performance / designing a longer term solution
RubyNext::Language.include_patterns.clear | ||
RubyNext::Language.include_patterns << "**/*.rb" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do these need to be cleared first? Does it include something other than Ruby files by default?
We could do
RubyNext::Language.include_patterns.clear | |
RubyNext::Language.include_patterns << "**/*.rb" | |
RubyNext::Language.include_patterns = ["**/*.rb"] |
or just append if it's already empty.
module RBS | ||
# Transpiles RBS comments back into Sorbet's `sig{}` blocks | ||
class Rewriter < RubyNext::Language::Rewriters::Text | ||
NAME = "rbs_rewriter" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, is this used by ruby-next
?
Motivation
Before this PR, when generating RBI files for gems using RBS comments, Tapioca was unable to generate signatures.
This PR introduce a preprocessing of all loaded files that rewrites the RBS comments into actual Sorbet signatures blocks.
Thus a file looking like this:
Will be translated into this at load time:
This will enable
sorbet-runtime
to properly see the signature and wrap the method as it would do normally.Then Tapioca will be able to pick up the signature and generate properly types RBIs for gems using RBS comment.
Implementation
We rely on
ruby-next
for the rewriting. While I not convinced by the gem itself, it does work. We could potentially rewrite the mechanism with an ad-hoc implementation later.Tests
See automated tests.