soutaro / steep

Static type checker for Ruby
MIT License
1.36k stars 83 forks source link

`steep check` raises errors for Forwardable #216

Open chloelbn opened 4 years ago

chloelbn commented 4 years ago

When using the Forwardable extension it seems like steep check doesn't recognize the def_delegators method and then the expected usage of Forwardable inside the class

Ruby Class

    class Client
      extend Forwardable

      def_delegators :@transporter, :read, :write

      def initialize(search_config, opts = {})
        ...
        @transporter = Transport::Transport.new(@config, requester)
      end

[...]
      def get_task_status(index_name, task_id, opts = {})
        res = read(:GET, path_encode('/1/indexes/%s/task/%s', index_name, task_id), {}, opts)
        get_option(res, 'status')
      end
end

Raised errors

lib/algolia/search_client.rb:13:6: NoMethodError: type=singleton(::Client), method=def_delegators (def_delegators :@transporter, :read, :write)
lib/algolia/search_client.rb:81:14: NoMethodError: type=::Client, method=read (read(:GET, path_encode('/1/indexes/%s/task/%s', index_name, task_id), {}, opts))
soutaro commented 4 years ago

Hello @chloelbn, Thank you for opening this issue.

I think it is the intended behavior. I'm not 100% sure about these. Let me know if it's not correct.

Steep says there is no def_delegators method.

I guess extend Forwardable is missing in the Client declaration in the .rbs file.

class Client
  extend Forwardable

  def get_task_status(String, String task_id, ?opts: Hash[untyped, untyped]) -> void
end

Steep cannot reason the type (even existence) of the read and write method defined by def_delegators call.

The method definitions without def syntax are also complicated. Because the tool doesn't know the implementation of def_delegators. It doesn't know what the method will do for the class.

Steep (RBS) wants you to write them in RBS files.

class Client
  extend Forwardable

  def get_task_status(String, String task_id, ?opts: Hash[untyped, untyped]) -> void

  def read: (Symbol, String, untyped, untyped opts) -> Response

  def write: () -> Response
end

Why is it so redundant?

It is the design of RBS. The files contain everything type checkers need. RBS is the place everything is written down, and users and tools can understand your library with it.

I know meta programming doesn't work well with this design... but I haven't found a better way for it.