Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/tapioca/dsl/helpers/graphql_type_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def type_for(type, ignore_nilable_wrapper: false, prepare_method: nil)
signature = Runtime::Reflection.signature_of(method)
return_type = signature&.return_type

valid_return_type?(return_type) ? return_type.to_s : "T.untyped"
# Wrap as non-nilable for required arguments. `coerce_input` supports both
# required and optional; optional arguments are re-wrapped below based on `type.non_null?`
valid_return_type?(return_type) ? RBIHelper.as_non_nilable_type(return_type.to_s) : "T.untyped"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give context on why we can assume coerce_input can't return nil?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coerce_input has to handle both required and optional arguments, and for an optional argument it still gets called with an input of nil. So when we have a required argument the signature should no longer have a T.nilable(...) since we know the output won't be nil.

Copy link
Copy Markdown
Contributor

@KaanOzkan KaanOzkan May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's capture this in a comment since there's quite a bit going on, something like

# Wrap as non-nilable for required arguments. `coerce_input` supports both
# required and optional; optional arguments are re-wrapped below based on `type.non_null?`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, I've updated it with the comment as suggested.

when GraphQL::Schema::InputObject.singleton_class
type_for_constant(unwrapped_type)
when Module
Expand Down
39 changes: 39 additions & 0 deletions spec/tapioca/dsl/compilers/graphql_mutation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,45 @@ def resolve(loaded_argument:, loaded_arguments:, custom_name:, optional_loaded_a
assert_equal(expected, rbi_for(:CreateComment))
end

it "generates correct RBI for custom scalars whose coerce_input returns a nilable type" do
add_ruby_file("create_comment.rb", <<~RUBY)
class CustomScalar; end

class NilableScalarType < GraphQL::Schema::Scalar
class << self
extend T::Sig

sig { params(value: T.untyped, context: GraphQL::Query::Context).returns(T.nilable(CustomScalar)) }
def coerce_input(value, context)
return nil if value.nil?
CustomScalar.new
end
end
end

class CreateComment < GraphQL::Schema::Mutation
argument :required_scalar, NilableScalarType, required: true
argument :required_scalar_array, [NilableScalarType], required: true
argument :optional_scalar, NilableScalarType, required: false

def resolve(required_scalar:, required_scalar_array:, optional_scalar: nil)
# ...
end
end
RUBY

expected = <<~RBI
# typed: strong

class CreateComment
sig { params(required_scalar: ::CustomScalar, required_scalar_array: T::Array[::CustomScalar], optional_scalar: T.nilable(::CustomScalar)).returns(T.untyped) }
def resolve(required_scalar:, required_scalar_array:, optional_scalar: T.unsafe(nil)); end
end
RBI

assert_equal(expected, rbi_for(:CreateComment))
end

it "generates correct RBI for custom scalars with return types" do
add_ruby_file("create_comment.rb", <<~RUBY)
class CustomScalar; end
Expand Down
Loading