From 4c68de7fbf475689fc984d4ea8f3e3d3dc410153 Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Fri, 10 Jul 2026 11:18:33 -0700 Subject: [PATCH] Fix double-wrapped array type for list arguments with a typed prepare method GraphqlMutation/GraphqlInputObject would generate T::Array[T::Array[X]] for a list argument whose `prepare:` method has a sig, because the prepare method's return type (already the fully-coerced list value) was wrapped in T::Array[...] a second time. --- .../dsl/helpers/graphql_type_helper.rb | 7 +- .../compilers/graphql_input_object_spec.rb | 33 +++++++++ .../dsl/compilers/graphql_mutation_spec.rb | 71 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/lib/tapioca/dsl/helpers/graphql_type_helper.rb b/lib/tapioca/dsl/helpers/graphql_type_helper.rb index a81886b42..63ea2304a 100644 --- a/lib/tapioca/dsl/helpers/graphql_type_helper.rb +++ b/lib/tapioca/dsl/helpers/graphql_type_helper.rb @@ -91,15 +91,20 @@ def type_for(type, ignore_nilable_wrapper: false, prepare_method: nil) "T.untyped" end + prepared = false if prepare_method prepare_signature = Runtime::Reflection.signature_of(prepare_method) prepare_return_type = prepare_signature&.return_type if valid_return_type?(prepare_return_type) parsed_type = prepare_return_type&.to_s + prepared = true end end - if type.list? + # A `prepare` method receives (and returns) the argument's fully coerced value, list + # wrapper included, so its return type already accounts for `type.list?` and must not be + # wrapped again. + if type.list? && !prepared parsed_type = "T::Array[#{parsed_type}]" end diff --git a/spec/tapioca/dsl/compilers/graphql_input_object_spec.rb b/spec/tapioca/dsl/compilers/graphql_input_object_spec.rb index 918dabfaf..6a629a644 100644 --- a/spec/tapioca/dsl/compilers/graphql_input_object_spec.rb +++ b/spec/tapioca/dsl/compilers/graphql_input_object_spec.rb @@ -111,6 +111,39 @@ def post_id; end assert_equal(expected, rbi_for(:CreateCommentInput)) end + it "generates correct RBI for a list argument with a prepare method returning the same list type" do + add_ruby_file("create_comment_input.rb", <<~RUBY) + class CreateCommentInput < GraphQL::Schema::InputObject + extend T::Sig + + class << self + extend T::Sig + sig { params(tags: T::Array[String], _context: T.untyped).returns(T::Array[String]) } + def prepare_tags(tags, _context) + tags.map(&:downcase) + end + end + + argument :tags, [String], "Tags for the comment", prepare: :prepare_tags, required: true + + def resolve(tags:) + # ... + end + end + RUBY + + expected = <<~RBI + # typed: strong + + class CreateCommentInput + sig { returns(T::Array[::String]) } + def tags; end + end + RBI + + assert_equal(expected, rbi_for(:CreateCommentInput)) + end + it "doesn't fail when input object is anonymous" do add_ruby_file("create_comment_input.rb", <<~RUBY) class CreateCommentInput < GraphQL::Schema::InputObject diff --git a/spec/tapioca/dsl/compilers/graphql_mutation_spec.rb b/spec/tapioca/dsl/compilers/graphql_mutation_spec.rb index 4850770d1..85b2e3f2b 100644 --- a/spec/tapioca/dsl/compilers/graphql_mutation_spec.rb +++ b/spec/tapioca/dsl/compilers/graphql_mutation_spec.rb @@ -224,6 +224,77 @@ def resolve(min:, max:, other:, proc:); end assert_equal(expected, rbi_for(:CreateComment)) end + it "generates correct RBI for a list argument with a prepare method returning the same list type" do + add_ruby_file("create_comment.rb", <<~RUBY) + class CreateComment < GraphQL::Schema::Mutation + extend T::Sig + + class << self + extend T::Sig + sig { params(tags: T::Array[String], _context: T.untyped).returns(T::Array[String]) } + def prepare_tags(tags, _context) + tags.map(&:downcase) + end + end + + argument :tags, [String], "Tags for the comment", prepare: :prepare_tags + + def resolve(tags:) + # ... + end + end + RUBY + + expected = <<~RBI + # typed: strong + + class CreateComment + sig { params(tags: T::Array[::String]).returns(T.untyped) } + def resolve(tags:); end + end + RBI + + assert_equal(expected, rbi_for(:CreateComment)) + end + + it "generates correct RBI for a list argument with a prepare method that has no valid return type" do + add_ruby_file("create_comment.rb", <<~RUBY) + class CreateComment < GraphQL::Schema::Mutation + extend T::Sig + + class << self + extend T::Sig + sig { params(tags: T::Array[String], _context: T.untyped).void } + def prepare_tags_void(tags, _context) + tags.map(&:downcase) + end + + def prepare_tags_untyped(tags, _context) + tags.map(&:downcase) + end + end + + argument :tags, [String], "Tags for the comment", prepare: :prepare_tags_void + argument :other_tags, [String], "Other tags for the comment", prepare: :prepare_tags_untyped + + def resolve(tags:, other_tags:) + # ... + end + end + RUBY + + expected = <<~RBI + # typed: strong + + class CreateComment + sig { params(tags: T::Array[::String], other_tags: T::Array[::String]).returns(T.untyped) } + def resolve(tags:, other_tags:); end + end + RBI + + assert_equal(expected, rbi_for(:CreateComment)) + end + it "generates correct RBI arguments with a prepare method on the argument class" do add_ruby_file("create_comment.rb", <<~RUBY) class CommentInput < GraphQL::Schema::InputObject