# typed: strict
require 'sorbet-runtime'
class A
extend T::Sig
sig {void}
def initialize
@foo = T.let(0, Integer)
end
sig {params(foo:Integer).returns(Integer)}
attr_writer :foo
sig {void}
def dbg
puts @foo
end
end
a = A.new
a.foo = T.let(10, Integer)
a.dbg
No errors during type-checking, but at runtime:
<home>/sorbet/gems/sorbet-runtime/lib/types/private/methods/signature.rb:65:in `initialize': The declaration for `foo=` is missing parameter(s): (RuntimeError)
from <home>/sorbet/gems/sorbet-runtime/lib/types/private/methods/_methods.rb:219:in `new'
from <home>/sorbet/gems/sorbet-runtime/lib/types/private/methods/_methods.rb:219:in `build_sig'
from <home>/sorbet/gems/sorbet-runtime/lib/types/private/methods/_methods.rb:188:in `run_sig'
from <home>/sorbet/gems/sorbet-runtime/lib/types/private/methods/_methods.rb:119:in `block in _on_method_added'
from <home>/sorbet/gems/sorbet-runtime/lib/types/private/methods/_methods.rb:282:in `run_sig_block_for_key'
from <home>/sorbet/gems/sorbet-runtime/lib/types/private/methods/_methods.rb:272:in `run_sig_block_for_method'
from <home>/sorbet/gems/sorbet-runtime/lib/types/private/methods/_methods.rb:143:in `block in _on_method_added'
from test.rb:24:in `<main>'
No runtime error.
The problem is that sorbet runtime depends on reflection to check for missing / extra parameters in signatures here: https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/private/methods/signature.rb#L60, but Ruby doesn't expose the parameter name for attr_writer, resulting in a nil parameter name.
A workaround for now would be to forgo use of attr_writer and do:
# typed: strict
class Foo
extend(T::Sig)
sig { void }
def initialize
@bar = T.let("initial_value", String)
end
# Here, Ruby correctly returns [[:req, :value]] instead of just [[:req]] when doing
# method(:bar=).parameters, but if it was attr_writer, Ruby doesn't tell you the
# name of the param :(
sig { params(value: String).returns(String) }
def bar=(value)
@bar = value
end
end
I have an idea of how we can fix this and I would like to run it by the team to get feedback before I dive in to an implementation.
As @JPDuchesne points out, the issue stems from the fact that for an attr_writer (or attr_accessor for that matter) generated foo= writer method, the single parameter is "nameless". If we can special case this, then we can make this problem go away.
The special casing should:
My thinking is that while we are extracting names from parameters, if
(a) parameters.size == 1 and
(b) parameters.first.name.nil? and
(c) method_name[-1] == "=",
then we can assume that this is a writer method and make param_names = [method_name[0...-1]].
The check should fail really fast for most unrelated methods, since most will not have 1 params and of ones that do majority will have names for the params and if it happens that they are C-defined functions their method name will most probably not end with a =.
Thoughts?
@paracycle That approach works for me. I looked into this a while back and came to essentially the same solution, thought it was gross, and decided to not do it. But I can't think of any better way to do it, so if you're down to implement it, that'd be great. Please add tests in gems/sorbet-runtime/test/