Skip to content

Commit 2b10f49

Browse files
committed
Infer attr_accessor writer signatures
1 parent a8794c0 commit 2b10f49

3 files changed

Lines changed: 146 additions & 3 deletions

File tree

lib/tapioca/gem/listeners/methods.rb

Lines changed: 61 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,66 @@ 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 = T.let(constant.instance_method(method_name.delete_suffix("=").to_sym), UnboundMethod)
214+
reader_method = original_method(reader_method)
215+
return unless same_source_location?(method, reader_method)
216+
return unless method_owned_by_constant?(reader_method, constant)
217+
218+
reader_method
219+
rescue NameError
220+
nil
221+
end
222+
223+
#: (UnboundMethod writer_method, UnboundMethod reader_method, untyped reader_signature) -> untyped
224+
def build_attr_writer_signature(writer_method, reader_method, reader_signature)
225+
return unless reader_signature.arg_types.empty?
226+
return unless reader_signature.kwarg_types.empty?
227+
return if reader_signature.rest_type
228+
return if reader_signature.keyrest_type
229+
return if reader_signature.block_type
230+
231+
T::Private::Methods::Signature.new(
232+
method: writer_method,
233+
method_name: writer_method.name,
234+
raw_arg_types: { reader_method.name => reader_signature.return_type },
235+
raw_return_type: reader_signature.return_type,
236+
bind: nil,
237+
mode: reader_signature.mode,
238+
check_level: reader_signature.check_level,
239+
on_failure: reader_signature.on_failure,
240+
override_allow_incompatible: reader_signature.override_allow_incompatible,
241+
defined_raw: reader_signature.defined_raw,
242+
)
243+
end
244+
245+
#: (UnboundMethod method) -> UnboundMethod
246+
def original_method(method)
247+
T.let(signature_of(method)&.method || method, UnboundMethod)
248+
end
249+
250+
#: (UnboundMethod method, UnboundMethod other_method) -> bool
251+
def same_source_location?(method, other_method)
252+
source_location = method.source_location
253+
!!source_location && source_location == other_method.source_location
254+
end
255+
195256
#: (Module[top] constant, String method_name) -> bool
196257
def struct_method?(constant, method_name)
197258
return false unless T::Props::ClassMethods === constant

sorbet/rbi/shims/sorbet.rbi

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

63-
class Signature; end
63+
class Signature
64+
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
65+
end
6466
end
6567

6668
module DeclState

spec/tapioca/gem/pipeline_spec.rb

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,83 @@ 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).returns(::Integer) }
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 separate attr_readers and attr_writers" do
1970+
add_ruby_file("bar.rb", <<~RUBY)
1971+
class Bar
1972+
sig { returns(Integer) }
1973+
attr_reader(:foo)
1974+
1975+
attr_writer(:foo)
1976+
end
1977+
RUBY
1978+
1979+
output = template(<<~RBI)
1980+
class Bar
1981+
sig { returns(::Integer) }
1982+
def foo; end
1983+
1984+
def foo=(_arg0); end
1985+
end
1986+
RBI
1987+
1988+
assert_equal(output, compile)
1989+
end
1990+
1991+
it "does not infer attr_accessor writer signatures for manually defined writers" do
1992+
add_ruby_file("bar.rb", <<~RUBY)
1993+
class Bar
1994+
sig { returns(Integer) }
1995+
def foo; end
1996+
1997+
def foo=(foo); end
1998+
end
1999+
RUBY
2000+
2001+
output = template(<<~RBI)
2002+
class Bar
2003+
sig { returns(::Integer) }
2004+
def foo; end
2005+
2006+
def foo=(foo); end
2007+
end
2008+
RBI
2009+
2010+
assert_equal(output, compile)
2011+
end
2012+
19362013
it "ignores methods with invalid names" do
19372014
add_ruby_file("bar.rb", <<~RUBY)
19382015
class Bar
@@ -3407,7 +3484,8 @@ def baz=(baz); end
34073484
sig { returns(T::Array[::Integer]) }
34083485
def foo; end
34093486
3410-
def foo=(_arg0); end
3487+
sig { params(foo: T::Array[::Integer]).returns(T::Array[::Integer]) }
3488+
def foo=(foo); end
34113489
34123490
sig { override.returns(::Integer) }
34133491
def something; end
@@ -4703,7 +4781,9 @@ def bar(a, b:); end
47034781
sig { returns(::String) }
47044782
def foo; end
47054783
4706-
def foo=(_arg0); end
4784+
sig { params(foo: ::String).returns(::String) }
4785+
def foo=(foo); end
4786+
47074787
def qux; end
47084788
47094789
class << self

0 commit comments

Comments
 (0)