FooBarWidget / sorbet-rspec

Sorbet typechecking support for RSpec
3 stars 0 forks source link

Infer (or specify) return type from let block and use that in generated Let method sig #1

Open marknuzz opened 4 days ago

marknuzz commented 4 days ago

This is a great tool, I didn't know about it until now. It took me a couple hours to set up but I can say you've done a great job with it.

It would be great if we could specify types for the let blocks so that referencing those will get you the type originally returned. It can infer the type, but should also let you specify the type as well. I don't know if either of them will require a monkey patch to rspec itself though, or an alternative method to let or let! that takes an extra argument for the type.

marknuzz commented 3 days ago

I have something crude as a proof of concept for setting the type of a let method. I am not sure if this is the right approach though, it is slow, and as the DSL compilers run with RAILS_ENV=development, I don't think it's a good idea to use outright. If there was a way to inject a method sig to the generated method, that could be better. Or maybe the method source can be analyzed in some manner similar to what sorbet-runtime does, to extract the intended return type which can be set explicitly.

modules.each do |mod|
  scope = root.create_module(T.must(mod.name))
  let_defs_module = ::RSpec::Core::MemoizedHelpers.module_for(constant)

  methods_types = T.let({}, T::Hash[Symbol, String])

  return_value = T.unsafe(nil)
  ::RSpec::Mocks.with_temporary_scope do
    direct_public_instance_methods_for(mod).each do |method_name|
      let_method = let_defs_module.instance_method(method_name)
      bound_method = let_method.bind(constant.new)
      return_value = bound_method.call
      methods_types[method_name] = return_value.class.to_s
    rescue StandardError, NotImplementedError
      methods_types[method_name] = 'T.untyped'
    end
  end

  direct_public_instance_methods_for(mod).each do |method_name|
    method_def = mod.instance_method(method_name)
    scope.create_method(
      method_def.name.to_s,
      parameters: compile_method_parameters_to_rbi(method_def),
      return_type: methods_types[method_name],
      class_method: false
    )
  end
marknuzz commented 3 days ago

Here's my work in progress for improving it to allow for sigs to be defined on let blocks. It's hacky and needs to be cleaned up but it seems like it will work, and I've been wanting to write an AST processor for awhile now. I was able to build a signature dynamically and will be able to identify it by location on the AST, then after matching the two, use Tapioca's sanitize_signature_types(signature.return_type.to_s) and duplicate the signature's return type correctly in the RBI.

https://gist.github.com/marknuzz/c7598efa664fe944d790e16464280cd1