Skip to content
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
61 changes: 61 additions & 0 deletions lib/tapioca/gem/listeners/methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion sorbet/rbi/shims/sorbet.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 82 additions & 2 deletions spec/tapioca/gem/pipeline_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Morriar marked this conversation as resolved.
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading