diff --git a/lib/tapioca/runtime/reflection.rb b/lib/tapioca/runtime/reflection.rb index cb0c4350b..3518c5169 100644 --- a/lib/tapioca/runtime/reflection.rb +++ b/lib/tapioca/runtime/reflection.rb @@ -125,7 +125,23 @@ def qualified_name_of(constant) #: ((UnboundMethod | Method) method) -> untyped def signature_of!(method) - T::Utils.signature_for_method(method) + signature = T::Utils.signature_for_method(method) + return signature if signature + + # Walk the super method chain to evaluate pending signatures that may have been hidden by `prepend`. + # During that walk, Sorbet may register a signature on the requested wrapper, so check it again. + # See https://github.com/Shopify/tapioca/issues/2710 for details. + current_method = method.super_method #: (UnboundMethod | Method)? + while current_method + T::Utils.signature_for_method(current_method) + + evaluated_signature = T::Utils.signature_for_method(method) + return evaluated_signature if evaluated_signature + + current_method = current_method.super_method + end + + nil rescue LoadError, StandardError Kernel.raise SignatureBlockError end diff --git a/spec/tapioca/cli/dsl_spec.rb b/spec/tapioca/cli/dsl_spec.rb index 246dbbe33..136141ed8 100644 --- a/spec/tapioca/cli/dsl_spec.rb +++ b/spec/tapioca/cli/dsl_spec.rb @@ -801,6 +801,61 @@ def find(value); end assert_success_status(result) end + it "finds lazy signatures hidden by prepended methods" do + @project.write!("lib/wrapped_method.rb", <<~RB) + # typed: strict + + module MethodWrapper + def call(*args) + super + end + end + + class WrappedMethod + #: (String) -> String + def call(value) + value + end + + prepend MethodWrapper + end + RB + + @project.write!("lib/compilers/wrapped_method_compiler.rb", <<~RB) + require "wrapped_method" + + class WrappedMethodCompiler < Tapioca::Dsl::Compiler + extend T::Sig + extend T::Generic + + ConstantType = type_member { { fixed: T.class_of(::WrappedMethod) } } + + sig { override.void } + def decorate + root.create_path(constant) do |klass| + create_method_from_def(klass, constant.instance_method(:call)) + end + end + + sig { override.returns(T::Enumerable[T::Module[T.anything]]) } + def self.gather_constants + [::WrappedMethod] + end + end + RB + + result = @project.tapioca("dsl WrappedMethod --only WrappedMethodCompiler") + + assert_empty_stderr(result) + assert_project_file_includes("sorbet/rbi/dsl/wrapped_method.rbi", <<~RBI) + class WrappedMethod + sig { params(value: ::String).returns(::String) } + def call(value); end + end + RBI + assert_success_status(result) + end + it "raises when the host calls Bootsnap.setup under TAPIOCA_RBS_CACHE=1" do @project.write!("lib/post.rb", <<~RB) require "bootsnap" diff --git a/spec/tapioca/runtime/reflection_spec.rb b/spec/tapioca/runtime/reflection_spec.rb index 95edd51c3..3e6b4b5f3 100644 --- a/spec/tapioca/runtime/reflection_spec.rb +++ b/spec/tapioca/runtime/reflection_spec.rb @@ -82,6 +82,21 @@ def unknown_method end end + module LazySignatureWrapper + def wrapped_method(...) + super + end + end + + class LazySignatureFoo + #: (String) -> String + def wrapped_method(value) + value + end + + prepend LazySignatureWrapper + end + class ReflectionSpec < Minitest::Spec describe Tapioca::Runtime::Reflection do it "might return the wrong results without Reflection helpers" do @@ -157,6 +172,38 @@ class ReflectionSpec < Minitest::Spec assert_nil(Runtime::Reflection.signature_of(method)) end + it "finds a lazy signature registered on a prepended wrapper" do + method = LazySignatureFoo.instance_method(:wrapped_method) + + signature = Runtime::Reflection.signature_of(method) + + refute_nil(signature) + assert_equal(LazySignatureWrapper, method.owner) + assert_equal(String, signature.arg_types.first.last.raw_type) + assert_equal(String, signature.return_type.raw_type) + end + + it "does not use a lazy signature from an ordinary super method" do + parent = Class.new + parent.class_eval <<~RUBY + extend T::Sig + + sig { params(value: String).returns(String) } + def overridden_method(value) + value + end + RUBY + child = Class.new(parent) + child.class_eval <<~RUBY + def overridden_method(value, suffix) + "\#{value}\#{suffix}" + end + RUBY + method = child.instance_method(:overridden_method) + + assert_nil(Runtime::Reflection.signature_of(method)) + end + it "returns nil when a signature block raises an exception" do method = SignatureFoo.instance_method(:bad_method) assert_nil(Runtime::Reflection.signature_of(method))