soutaro / rbs-inline

Inline RBS type declaration
MIT License
250 stars 8 forks source link

Proposal: `@rbs return: infer!` and `#: infer!` #152

Open KieranP opened 5 days ago

KieranP commented 5 days ago

I'm building an app on mruby and wanted to add Array#index_by.

Ruby:

class Array < Object
  def index_by
    result = {}
    each { |elem| result[yield(elem)] = elem }
    result
  end
end

RBS:

class Array[unchecked out Elem] < Object
  def index_by: () { (Elem) -> (String | Symbol) } -> Hash[String | Symbol, Elem]
end

Then utilizing it like so:

class Items
  TYPES = [
    Shield,
    Sword
  ].index_by do |v|
    v.name.to_s.demodulize
  end
end

Now, BEFORE the rbs files are generated, the VSCode steep extension is correctly inferring the type as:

::Hash[(::String | ::Symbol), (singleton(::Items::Shield) | singleton(::Items::Sword))]

But when I run the rbs-inline generator, the RBS file gets TYPES: untyped because I don't have a RBS comment, and then VSCode reports it as untyped, and shows errors.

Now, I could add #: ::Hash[(::String | ::Symbol), (singleton(::Items::Shield) | singleton(::Items::Sword))], but it seems redundant, because each time I add a value to the array, I now need to add it to the RBS also.

Consider also this:

class Item
  NAME = 'Kieran'.freeze

  def format_name
    NAME.split('').join
  end
end

rbs-inline currently outputs this:

class Item
  NAME: untyped

  def format_name: () -> untyped
end

But steep auto infers these just fine. rbs-inline should use this when generating rbs files for variables/methods withou return types.

class Item
  NAME: ::String

  def format_name: () -> ::String
end

I'd like to propose that rbs-inline use steeps correct auto-detected type when generating RBS files for values it can infer the type from. It could do this automatically or if you want it to be defined, I propose a @rbs return: infer! and #: infer!

class Item
  NAME = 'Kieran'.freeze #: infer!

  # @rbs return: infer!
  def format_name
    NAME.split('').join
  end
end

If you use infer! and it's not actually able to be inferred, it should throw an error.

ParadoxV5 commented 5 days ago

+1 but as a Steep feature, for rbs-inline – which will become part of RBS itself soon™ – musn’t have dependency on a 3rd-party type checker, but the other way around is open.

BTW, Enumerable#group_by

[1, 2, '3', ?4, 0x5].group_by(&:class) #=> {Integer=>[1, 2, 5], String=>["3", "4"]}

And String#chars/String#each_char too

KieranP commented 5 days ago

@ParadoxV5

Re: rbs-inline needing to work independent from Steep, then I suggest a # @rbs ignore! or similar which makes rbs-inline ignore adding it to the RBS files so that Steep can continue to infer it correct. Because at the moment, without a RBS comment, rbs-inline will throw everything in with untyped, which overrides any auto-inference steep does.

Re: group_by, not quite the same thing. index_by is designed to only have one value for each key, not an array. See Rails docs: https://api.rubyonrails.org/classes/Enumerable.html#method-i-index_by

ParadoxV5 commented 4 days ago

Re: rbs-inline needing to work independent from Steep, then I suggest a # @rbs ignore! or similar which makes rbs-inline ignore adding it to the RBS files so that Steep can continue to infer it correct.

Does @rbs skip suffice?

Re: group_by, not quite the same thing. index_by is designed to only have one value for each key, not an array.

Whoops, sorry, I didn’t read carefully. For that, idiomatic pure-Ruby would be TYPES.to_h {|v| [demodulize(v.name), v] }.

KieranP commented 4 days ago

Does @rbs skip suffice?

That does stop rbs-inline from putting untyped in the RBS file, but now Steep complains:

Cannot find the declaration of constant: `TYPES`(Ruby::UnknownConstant)

Seems there is no good path here. Issues if I leave it out, issues if I leave it in :-(

Whoops, sorry, I didn’t read carefully. For that, idiomatic pure-Ruby would be TYPES.to_h {|v| [demodulize(v.name), v] }.

Thanks for the suggestion. I'll do that so I can remove index_by