Skip to content

Commit ca25ff8

Browse files
committed
Infer attr_accessor writer signatures
1 parent a8794c0 commit ca25ff8

3 files changed

Lines changed: 125 additions & 1 deletion

File tree

lib/tapioca/gem/listeners/methods.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public
6969

7070
begin
7171
signature = signature_of!(method)
72+
signature ||= inferred_attr_writer_signature(method, constant)
7273
method = signature.method if signature #: UnboundMethod
7374

7475
case @pipeline.method_definition_in_gem(method.name, constant)
@@ -192,6 +193,60 @@ def method_names_by_visibility(mod)
192193
}
193194
end
194195

196+
#: (UnboundMethod method, Module[top] constant) -> untyped
197+
def inferred_attr_writer_signature(method, constant)
198+
reader_method = attr_reader_for_writer(method, constant)
199+
return unless reader_method
200+
201+
reader_signature = signature_of(reader_method)
202+
return unless reader_signature
203+
204+
build_attr_writer_signature(method, reader_method, reader_signature)
205+
end
206+
207+
#: (UnboundMethod method, Module[top] constant) -> UnboundMethod?
208+
def attr_reader_for_writer(method, constant)
209+
method_name = method.name.to_s
210+
return unless method_name.end_with?("=")
211+
return unless method.parameters == [[:req]]
212+
213+
reader_method = constant.instance_method(method_name.delete_suffix("=").to_sym)
214+
return unless method_owned_by_constant?(reader_method, constant)
215+
216+
reader_method
217+
rescue NameError
218+
nil
219+
end
220+
221+
#: (UnboundMethod writer_method, UnboundMethod reader_method, untyped reader_signature) -> untyped
222+
def build_attr_writer_signature(writer_method, reader_method, reader_signature)
223+
return unless reader_signature.arg_types.empty?
224+
return unless reader_signature.kwarg_types.empty?
225+
return if reader_signature.rest_type
226+
return if reader_signature.keyrest_type
227+
return if reader_signature.block_type
228+
return unless same_source_location?(writer_method, reader_signature.method)
229+
230+
T::Private::Methods::Signature.new(
231+
method: writer_method,
232+
method_name: writer_method.name,
233+
raw_arg_types: { reader_method.name => reader_signature.return_type },
234+
raw_return_type: T::Private::Types::Void::Private::INSTANCE,
235+
bind: nil,
236+
mode: reader_signature.mode,
237+
check_level: reader_signature.check_level,
238+
on_failure: reader_signature.on_failure,
239+
override_allow_incompatible: reader_signature.override_allow_incompatible,
240+
defined_raw: reader_signature.defined_raw,
241+
)
242+
end
243+
244+
#: (UnboundMethod method, UnboundMethod other_method) -> bool
245+
def same_source_location?(method, other_method)
246+
source_location = method.source_location
247+
!!source_location && source_location == other_method.source_location
248+
end
249+
195250
#: (Module[top] constant, String method_name) -> bool
196251
def struct_method?(constant, method_name)
197252
return false unless T::Props::ClassMethods === constant

sorbet/rbi/shims/sorbet.rbi

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,21 @@ module T::Private
6060
def finalized=(finalized); end
6161
end
6262

63-
class Signature; end
63+
class Signature
64+
def initialize(
65+
method:,
66+
method_name:,
67+
raw_arg_types:,
68+
raw_return_type:,
69+
bind:,
70+
mode:,
71+
check_level:,
72+
on_failure:,
73+
parameters: T.unsafe(nil),
74+
override_allow_incompatible: T.unsafe(nil),
75+
defined_raw: T.unsafe(nil)
76+
); end
77+
end
6478
end
6579

6680
module DeclState

spec/tapioca/gem/pipeline_spec.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,61 @@ def foo; end
19331933
assert_equal(output, compile)
19341934
end
19351935

1936+
it "compiles attr_reader/attr_writer/attr_accessor with signatures" do
1937+
add_ruby_file("bar.rb", <<~RUBY)
1938+
class Bar
1939+
sig { returns(String) }
1940+
attr_reader(:a)
1941+
1942+
sig { returns(Integer) }
1943+
attr_accessor(:b)
1944+
1945+
sig { params(c: Symbol).void }
1946+
attr_writer(:c)
1947+
end
1948+
RUBY
1949+
1950+
output = template(<<~RBI)
1951+
class Bar
1952+
sig { returns(::String) }
1953+
def a; end
1954+
1955+
sig { returns(::Integer) }
1956+
def b; end
1957+
1958+
sig { params(b: ::Integer).void }
1959+
def b=(b); end
1960+
1961+
sig { params(c: ::Symbol).void }
1962+
def c=(c); end
1963+
end
1964+
RBI
1965+
1966+
assert_equal(output, compile)
1967+
end
1968+
1969+
it "does not infer attr_accessor writer signatures for manually defined writers" do
1970+
add_ruby_file("bar.rb", <<~RUBY)
1971+
class Bar
1972+
sig { returns(Integer) }
1973+
def foo; end
1974+
1975+
def foo=(foo); end
1976+
end
1977+
RUBY
1978+
1979+
output = template(<<~RBI)
1980+
class Bar
1981+
sig { returns(::Integer) }
1982+
def foo; end
1983+
1984+
def foo=(foo); end
1985+
end
1986+
RBI
1987+
1988+
assert_equal(output, compile)
1989+
end
1990+
19361991
it "ignores methods with invalid names" do
19371992
add_ruby_file("bar.rb", <<~RUBY)
19381993
class Bar

0 commit comments

Comments
 (0)