diff --git a/Gemfile.lock b/Gemfile.lock index 0bc6a1f4b..3e7d71795 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,7 +18,7 @@ PATH bundler (>= 2.2.25) netrc (>= 0.11.0) parallel (>= 1.21.0) - rbi (>= 0.4.1) + rbi (>= 0.4.3) require-hooks (>= 0.2.2) rubydex (>= 0.1.0.beta10) sorbet-static-and-runtime (>= 0.6.12698) diff --git a/lib/tapioca/dsl/compiler.rb b/lib/tapioca/dsl/compiler.rb index 6d91ee477..08a3da615 100644 --- a/lib/tapioca/dsl/compiler.rb +++ b/lib/tapioca/dsl/compiler.rb @@ -173,32 +173,8 @@ def compile_method_parameters_to_rbi(method_def) parameters = method_def.parameters #: Array[[Symbol, Symbol?]] parameters.each_with_index.map do |(type, name), index| - fallback_arg_name = "_arg#{index}" - - sig_name = name ? name.to_s : fallback_arg_name - is_anonymous_parameter = anonymous_parameter_name?(type, sig_name) - sig_name = fallback_arg_name unless is_anonymous_parameter || valid_parameter_name?(sig_name) - param_name = is_anonymous_parameter ? nil : sig_name - method_type = T.must(method_types[index]) - - case type - when :req - create_param(sig_name, type: method_type) - when :opt - create_opt_param(sig_name, type: method_type, default: "T.unsafe(nil)") - when :rest - create_rest_param(param_name, type: method_type) - when :keyreq - create_kw_param(sig_name, type: method_type) - when :key - create_kw_opt_param(sig_name, type: method_type, default: "T.unsafe(nil)") - when :keyrest - create_kw_rest_param(param_name, type: method_type) - when :block - create_block_param(param_name, type: method_type) - else - raise "Unknown type `#{type}`." - end + parameter, = create_method_parameter(type, name&.to_s, index) + create_typed_param(parameter, T.must(method_types[index])) end end diff --git a/lib/tapioca/gem/listeners/methods.rb b/lib/tapioca/gem/listeners/methods.rb index db6a6c74f..e2cf99e04 100644 --- a/lib/tapioca/gem/listeners/methods.rb +++ b/lib/tapioca/gem/listeners/methods.rb @@ -99,10 +99,8 @@ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public parameters = method.parameters #: Array[[Symbol, Symbol?]] - sanitized_parameters = parameters.each_with_index.map do |(type, name), index| - fallback_arg_name = "_arg#{index}" - - sig_name = if name + compiled_parameters = parameters.each_with_index.map do |(type, name), index| + parameter_name = if name name.to_s else # For attr_writer methods, Sorbet signatures have the name @@ -119,21 +117,11 @@ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public signature.arg_types.size == 1 && method_name[-1] == "=" - if writer_method_with_sig - method_name.delete_suffix("=") - else - fallback_arg_name - end + method_name.delete_suffix("=") if writer_method_with_sig end - # Sanitize param names, except for anonymous splat, keyword splat, - # and block parameters. Ruby reflects those as `:*`, `:**`, and `:&`, - # and Sorbet signatures use the same names to store their types. - is_anonymous_parameter = anonymous_parameter_name?(type, sig_name) - sig_name = fallback_arg_name unless is_anonymous_parameter || valid_parameter_name?(sig_name) - param_name = is_anonymous_parameter ? nil : sig_name - - [type, param_name, sig_name] + parameter, signature_name = create_method_parameter(type, parameter_name, index) + [type, parameter, signature_name] end rbi_method = RBI::Method.new( @@ -142,26 +130,11 @@ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public visibility: visibility, ) - sanitized_parameters.each do |type, param_name, _sig_name| - case type - when :req - rbi_method << RBI::ReqParam.new(param_name) - when :opt - rbi_method << RBI::OptParam.new(param_name, "T.unsafe(nil)") - when :rest - rbi_method << RBI::RestParam.new(param_name) - when :keyreq - rbi_method << RBI::KwParam.new(param_name) - when :key - rbi_method << RBI::KwOptParam.new(param_name, "T.unsafe(nil)") - when :keyrest - rbi_method << RBI::KwRestParam.new(param_name) - when :block - rbi_method << RBI::BlockParam.new(param_name) - end + compiled_parameters.each do |_, parameter, _| + rbi_method << parameter end - parameters_for_signature = sanitized_parameters.map { |type, _param_name, sig_name| [type, sig_name] } + parameters_for_signature = compiled_parameters.map { |type, _, name| [type, name] } @pipeline.push_method(symbol_name, constant, method, rbi_method, signature, parameters_for_signature) tree << rbi_method end diff --git a/lib/tapioca/helpers/rbi_helper.rb b/lib/tapioca/helpers/rbi_helper.rb index 6655ff28a..65315e680 100644 --- a/lib/tapioca/helpers/rbi_helper.rb +++ b/lib/tapioca/helpers/rbi_helper.rb @@ -101,14 +101,45 @@ def extract_type_parameters(type_strings) type_strings.join(", ").scan(TYPE_PARAMETER_MATCHER).flatten.uniq end + #: (Symbol type, String? name, Integer index) -> [RBI::Param, String] + def create_method_parameter(type, name, index) + is_anonymous_parameter = name && anonymous_parameter_name?(type, name) + name = "_arg#{index}" unless is_anonymous_parameter || valid_parameter_name?(name) + name = T.must(name) + parameter_name = is_anonymous_parameter ? nil : name + + parameter = case type + when :req + RBI::ReqParam.new(name) + when :opt + RBI::OptParam.new(name, "T.unsafe(nil)") + when :rest + RBI::RestParam.new(parameter_name) + when :keyreq + RBI::KwParam.new(name) + when :key + RBI::KwOptParam.new(name, "T.unsafe(nil)") + when :keyrest + RBI::KwRestParam.new(parameter_name) + when :block + RBI::BlockParam.new(parameter_name) + when :nokey + RBI::NoKwParam.new + else + Kernel.raise "Unknown type `#{type}`." + end + + [parameter, name] + end + #: (String name) -> bool def valid_method_name?(name) Prism.parse_success?("def self.#{name}(a); end") end - #: (String name) -> bool + #: (String? name) -> bool def valid_parameter_name?(name) - Prism.parse_success?("def sentinel_method_name(#{name}:); end") + name ? Prism.parse_success?("def sentinel_method_name(#{name}:); end") : false end #: (Symbol type, String name) -> bool diff --git a/spec/tapioca/gem/pipeline_spec.rb b/spec/tapioca/gem/pipeline_spec.rb index a2f09fa84..188ebe683 100644 --- a/spec/tapioca/gem/pipeline_spec.rb +++ b/spec/tapioca/gem/pipeline_spec.rb @@ -1077,6 +1077,22 @@ def add(a, b:); end assert_equal(output, compile) end + it "compiles methods that reject keyword arguments" do + add_ruby_file("foo.rb", <<~RUBY) + class Foo + def no_kwargs(**nil); end + end + RUBY + + output = template(<<~RBI) + class Foo + def no_kwargs(**nil); end + end + RBI + + assert_equal(output, compile) + end + it "compiles default arguments" do add_ruby_file("foo.rb", <<~RUBY) class Foo diff --git a/spec/tapioca/helpers/rbi_helper_spec.rb b/spec/tapioca/helpers/rbi_helper_spec.rb index 3a9af1d31..bdcf60bad 100644 --- a/spec/tapioca/helpers/rbi_helper_spec.rb +++ b/spec/tapioca/helpers/rbi_helper_spec.rb @@ -75,6 +75,10 @@ class Tapioca::RBIHelperSpec < Minitest::Spec end end + it "rejects missing parameter names" do + refute(valid_parameter_name?(nil)) + end + it "rejects invalid parameter names" do [ "", diff --git a/tapioca.gemspec b/tapioca.gemspec index 846c421ae..67084e0d8 100644 --- a/tapioca.gemspec +++ b/tapioca.gemspec @@ -31,9 +31,9 @@ Gem::Specification.new do |spec| spec.add_dependency("sorbet-static-and-runtime", ">= 0.6.12698") spec.add_dependency("thor", ">= 1.2.0") - # Tapioca requires a specific minimum versions of RBI and Spoom - # to ensure that the RBS comments are translated correctly. - spec.add_dependency("rbi", ">= 0.4.1") + # Tapioca requires specific minimum versions of RBI and Spoom + # to support the APIs used when generating RBIs. + spec.add_dependency("rbi", ">= 0.4.3") spec.add_dependency("spoom", ">= 1.7.16") # We need this to be ported to the RBS 4.0 branch before we can remove this dependency: # https://github.com/ruby/rbs/pull/2601