Sorbet: Typing an attr_writer raises an exception at runtime

Created on 9 Jul 2019  Â·  3Comments  Â·  Source: sorbet/sorbet

Input

→ View on sorbet.run

# 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

Observed output

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>'

Expected behavior

No runtime error.


bug

All 3 comments

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:

  1. cover this case and not be likely to match other cases
  2. be fast enough to check so that it does not add overhead.

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/

Was this page helpful?
0 / 5 - 0 ratings