diff --git a/lib/tapioca/gem/listeners/methods.rb b/lib/tapioca/gem/listeners/methods.rb index 776dd2b61..d75df394b 100644 --- a/lib/tapioca/gem/listeners/methods.rb +++ b/lib/tapioca/gem/listeners/methods.rb @@ -69,6 +69,7 @@ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public begin signature = signature_of!(method) + signature ||= inferred_attr_writer_signature(method, constant) method = signature.method if signature #: UnboundMethod case @pipeline.method_definition_in_gem(method.name, constant) @@ -192,6 +193,66 @@ def method_names_by_visibility(mod) } end + #: (UnboundMethod method, Module[top] constant) -> untyped + def inferred_attr_writer_signature(method, constant) + reader_method = attr_reader_for_writer(method, constant) + return unless reader_method + + reader_signature = signature_of(reader_method) + return unless reader_signature + + build_attr_writer_signature(method, reader_method, reader_signature) + end + + #: (UnboundMethod method, Module[top] constant) -> UnboundMethod? + def attr_reader_for_writer(method, constant) + method_name = method.name.to_s + return unless method_name.end_with?("=") + return unless method.parameters == [[:req]] + + reader_method = T.let(constant.instance_method(method_name.delete_suffix("=").to_sym), UnboundMethod) + reader_method = original_method(reader_method) + return unless same_source_location?(method, reader_method) + return unless method_owned_by_constant?(reader_method, constant) + + reader_method + rescue NameError + nil + end + + #: (UnboundMethod writer_method, UnboundMethod reader_method, untyped reader_signature) -> untyped + def build_attr_writer_signature(writer_method, reader_method, reader_signature) + return unless reader_signature.arg_types.empty? + return unless reader_signature.kwarg_types.empty? + return if reader_signature.rest_type + return if reader_signature.keyrest_type + return if reader_signature.block_type + + T::Private::Methods::Signature.new( + method: writer_method, + method_name: writer_method.name, + raw_arg_types: { reader_method.name => reader_signature.return_type }, + raw_return_type: reader_signature.return_type, + bind: nil, + mode: reader_signature.mode, + check_level: reader_signature.check_level, + on_failure: reader_signature.on_failure, + override_allow_incompatible: reader_signature.override_allow_incompatible, + defined_raw: reader_signature.defined_raw, + ) + end + + #: (UnboundMethod method) -> UnboundMethod + def original_method(method) + T.let(signature_of(method)&.method || method, UnboundMethod) + end + + #: (UnboundMethod method, UnboundMethod other_method) -> bool + def same_source_location?(method, other_method) + source_location = method.source_location + !!source_location && source_location == other_method.source_location + end + #: (Module[top] constant, String method_name) -> bool def struct_method?(constant, method_name) return false unless T::Props::ClassMethods === constant diff --git a/sorbet/rbi/shims/sorbet.rbi b/sorbet/rbi/shims/sorbet.rbi index e2c2bc83d..372920f75 100644 --- a/sorbet/rbi/shims/sorbet.rbi +++ b/sorbet/rbi/shims/sorbet.rbi @@ -60,7 +60,9 @@ module T::Private def finalized=(finalized); end end - class Signature; end + class Signature + def initialize(method:, method_name:, raw_arg_types:, raw_return_type:, bind:, mode:, check_level:, on_failure:, parameters: T.unsafe(nil), override_allow_incompatible: T.unsafe(nil), defined_raw: T.unsafe(nil)); end + end end module DeclState diff --git a/spec/tapioca/gem/pipeline_spec.rb b/spec/tapioca/gem/pipeline_spec.rb index d9354b4fe..307b47c54 100644 --- a/spec/tapioca/gem/pipeline_spec.rb +++ b/spec/tapioca/gem/pipeline_spec.rb @@ -1933,6 +1933,83 @@ def foo; end assert_equal(output, compile) end + it "compiles attr_reader/attr_writer/attr_accessor with signatures" do + add_ruby_file("bar.rb", <<~RUBY) + class Bar + sig { returns(String) } + attr_reader(:a) + + sig { returns(Integer) } + attr_accessor(:b) + + sig { params(c: Symbol).void } + attr_writer(:c) + end + RUBY + + output = template(<<~RBI) + class Bar + sig { returns(::String) } + def a; end + + sig { returns(::Integer) } + def b; end + + sig { params(b: ::Integer).returns(::Integer) } + def b=(b); end + + sig { params(c: ::Symbol).void } + def c=(c); end + end + RBI + + assert_equal(output, compile) + end + + it "does not infer attr_accessor writer signatures for separate attr_readers and attr_writers" do + add_ruby_file("bar.rb", <<~RUBY) + class Bar + sig { returns(Integer) } + attr_reader(:foo) + + attr_writer(:foo) + end + RUBY + + output = template(<<~RBI) + class Bar + sig { returns(::Integer) } + def foo; end + + def foo=(_arg0); end + end + RBI + + assert_equal(output, compile) + end + + it "does not infer attr_accessor writer signatures for manually defined writers" do + add_ruby_file("bar.rb", <<~RUBY) + class Bar + sig { returns(Integer) } + def foo; end + + def foo=(foo); end + end + RUBY + + output = template(<<~RBI) + class Bar + sig { returns(::Integer) } + def foo; end + + def foo=(foo); end + end + RBI + + assert_equal(output, compile) + end + it "ignores methods with invalid names" do add_ruby_file("bar.rb", <<~RUBY) class Bar @@ -3407,7 +3484,8 @@ def baz=(baz); end sig { returns(T::Array[::Integer]) } def foo; end - def foo=(_arg0); end + sig { params(foo: T::Array[::Integer]).returns(T::Array[::Integer]) } + def foo=(foo); end sig { override.returns(::Integer) } def something; end @@ -4703,7 +4781,9 @@ def bar(a, b:); end sig { returns(::String) } def foo; end - def foo=(_arg0); end + sig { params(foo: ::String).returns(::String) } + def foo=(foo); end + def qux; end class << self