Skip to content

Add support for RBS signature comments #2236

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

Merged
merged 10 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ PATH
bundler (>= 2.2.25)
netrc (>= 0.11.0)
parallel (>= 1.21.0)
rbi (~> 0.2)
rbi (>= 0.3.1)
require-hooks (>= 0.2.2)
sorbet-static-and-runtime (>= 0.5.11087)
spoom (>= 1.2.0)
spoom (>= 1.7.0)
thor (>= 1.2.0)
yard-sorbet

Expand Down Expand Up @@ -284,12 +285,13 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rbi (0.3.2)
rbi (0.3.3)
prism (~> 1.0)
rbs (>= 3.4.4)
sorbet-runtime (>= 0.5.9204)
rbs (3.9.2)
rbs (4.0.0.dev.4)
logger
prism (>= 1.3.0)
rdoc (6.13.1)
psych (>= 4.0.0)
redis (5.4.0)
Expand All @@ -299,6 +301,7 @@ GEM
regexp_parser (2.10.0)
reline (0.6.1)
io-console (~> 0.5)
require-hooks (0.2.2)
rexml (3.4.1)
rubocop (1.75.3)
json (~> 2.3)
Expand Down Expand Up @@ -338,19 +341,21 @@ GEM
rack (>= 2.2.4)
redis-client (>= 0.22.2)
smart_properties (1.17.0)
sorbet (0.5.12109)
sorbet-static (= 0.5.12109)
sorbet-runtime (0.5.12109)
sorbet-static (0.5.12109-aarch64-linux)
sorbet-static (0.5.12109-universal-darwin)
sorbet-static (0.5.12109-x86_64-linux)
sorbet-static-and-runtime (0.5.12109)
sorbet (= 0.5.12109)
sorbet-runtime (= 0.5.12109)
spoom (1.6.1)
sorbet (0.5.12119)
sorbet-static (= 0.5.12119)
sorbet-runtime (0.5.12119)
sorbet-static (0.5.12119-aarch64-linux)
sorbet-static (0.5.12119-universal-darwin)
sorbet-static (0.5.12119-x86_64-linux)
sorbet-static-and-runtime (0.5.12119)
sorbet (= 0.5.12119)
sorbet-runtime (= 0.5.12119)
spoom (1.7.0)
erubi (>= 1.10.0)
prism (>= 0.28.0)
rbi (>= 0.2.3)
rbi (>= 0.3.3)
rbs (>= 4.0.0.dev.4)
rexml (>= 3.2.6)
sorbet-static-and-runtime (>= 0.5.10187)
thor (>= 0.19.2)
sprockets (4.2.2)
Expand Down
2 changes: 2 additions & 0 deletions lib/tapioca/gem/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ class ConstantFound < Event
#: String
attr_reader :symbol

# @without_runtime
#: BasicObject
attr_reader :constant

# @without_runtime
#: (String symbol, BasicObject constant) -> void
def initialize(symbol, constant)
super()
Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/gem/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def push_symbol(symbol)
@events << Gem::SymbolFound.new(symbol)
end

# @without_runtime
#: (String symbol, BasicObject constant) -> void
def push_constant(symbol, constant)
@events << Gem::ConstantFound.new(symbol, constant)
Expand Down Expand Up @@ -222,6 +223,7 @@ def compile_foreign_constant(symbol, constant)
push_foreign_scope(symbol, constant, scope)
end

# @without_runtime
#: (String symbol, BasicObject constant) -> void
def compile_constant(symbol, constant)
case constant
Expand Down Expand Up @@ -257,6 +259,7 @@ def compile_alias(name, constant)
@root << node
end

# @without_runtime
#: (String name, BasicObject value) -> void
def compile_object(name, value)
return if seen?(name)
Expand Down Expand Up @@ -371,6 +374,7 @@ def skip_symbol?(name)
symbol_in_payload?(name) && !@bootstrap_symbols.include?(name)
end

# @without_runtime
#: (String name, top constant) -> bool
def skip_constant?(name, constant)
return true if name.strip.empty?
Expand All @@ -392,6 +396,7 @@ def skip_alias?(name, constant)
false
end

# @without_runtime
#: (String name, BasicObject constant) -> bool
def skip_object?(name, constant)
return true if symbol_in_payload?(name)
Expand Down
6 changes: 5 additions & 1 deletion lib/tapioca/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
require "tapioca/runtime/reflection"
require "tapioca/runtime/trackers"

# The rewriter needs to be loaded very early so RBS comments within Tapioca itself are rewritten
require "spoom"
require "tapioca/rbs/rewriter"
# ^ Do not change the order of these requires

require "benchmark"
require "bundler"
require "erb"
Expand All @@ -32,7 +37,6 @@
require "tapioca/sorbet_ext/proc_bind_patch"
require "tapioca/runtime/generic_type_registry"

require "spoom"
require "tapioca/helpers/gem_helper"
require "tapioca/helpers/git_attributes"
require "tapioca/helpers/sorbet_helper"
Expand Down
2 changes: 2 additions & 0 deletions lib/tapioca/loaders/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def with_rails_application(&blk)
Rails.app_class = Rails.application = rails_application
end

# @without_runtime
#: -> Array[singleton(Rails::Engine)]
def engines
return [] unless defined?(Rails::Engine)
Expand Down Expand Up @@ -216,6 +217,7 @@ def require_helper(file)
# The `eager_load_paths` method still exists, but doesn't return all paths anymore and causes Tapioca to miss some
# engine paths. The following commit is the change:
# https://github.com/rails/rails/commit/ebfca905db14020589c22e6937382e6f8f687664
# @without_runtime
#: (singleton(Rails::Engine) engine) -> Array[String]
def eager_load_paths(engine)
config = engine.config
Expand Down
55 changes: 55 additions & 0 deletions lib/tapioca/rbs/rewriter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# typed: strict
# frozen_string_literal: true

require "require-hooks/setup"

# This code rewrites RBS comments back into Sorbet's signatures as the files are being loaded.
# This will allow `sorbet-runtime` to wrap the methods as if they were originally written with the `sig{}` blocks.
# This will in turn allow Tapioca to use this signatures to generate typed RBI files.

begin
# When in a `bootsnap` environment, files are loaded from the cache and won't trigger the `source_transform` method.
# The `require-hooks` gem comes with a `bootsnap` mode that will disable the `bootsnap/compile_cache/iseq` caching.
# Sadly, we're way to early in the boot process to use it as bootsnap won't be loaded yet and the `require-hooks`
# setup won't pick it up.
#
# As a workaround, if we can preemptively require `bootsnap` and `bootsnap/compile_cache/iseq` we manually override
# the `load_iseq` method to disable the caching mechanism.
#
# This will make the Rails app load slower but allows us to trigger the RBS -> RBI source transform.
require "bootsnap"
require "bootsnap/compile_cache/iseq"

module Bootsnap
module CompileCache
module ISeq
module InstructionSequenceMixin
#: (String) -> RubyVM::InstructionSequence
def load_iseq(path)
super
end
end
end
end
end
rescue LoadError
# Bootsnap is not in the bundle, we don't need to do anything.
end

# 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)
Copy link
Collaborator Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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.


# Trigger the source transformation for each Ruby file being loaded.
RequireHooks.source_transform(patterns: ["**/*.rb"]) do |path, source|
# The source is most likely nil since no `source_transform` hook was triggered before this one.
source ||= File.read(path)

# For performance reasons, we only rewrite files that use Sorbet.
if source =~ /^\s*#\s*typed: (ignore|false|true|strict|strong|__STDLIB_INTERNAL)/
Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(source, file: path)
end
rescue RBI::RBS::MethodTypeTranslator::Error
# If we can't translate the RBS comments back into Sorbet's signatures, we just skip the file.
source
end
4 changes: 3 additions & 1 deletion lib/tapioca/runtime/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ module Reflection
METHOD_METHOD = Kernel.instance_method(:method) #: UnboundMethod
UNDEFINED_CONSTANT = Module.new.freeze #: Module

REQUIRED_FROM_LABELS = ["<top (required)>", "<main>"].freeze #: Array[String]
REQUIRED_FROM_LABELS = ["<top (required)>", "<main>", "<compiled>"].freeze #: Array[String]

# @without_runtime
#: (BasicObject constant) -> bool
def constant_defined?(constant)
!UNDEFINED_CONSTANT.eql?(constant)
end

# @without_runtime
#: (String symbol, ?inherit: bool, ?namespace: Module) -> BasicObject
def constantize(symbol, inherit: false, namespace: Object)
namespace.const_get(symbol, inherit)
Expand Down
1 change: 1 addition & 0 deletions lib/tapioca/static/symbol_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def symbols_from_paths(paths)

private

# @without_runtime
#: -> Array[singleton(Rails::Engine)]
def engines
@engines ||= if Object.const_defined?("Rails::Engine")
Expand Down
19 changes: 11 additions & 8 deletions sorbet/rbi/gems/[email protected]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading