Skip to content
Open
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
73 changes: 58 additions & 15 deletions lib/tapioca/dsl/compilers/active_record_delegated_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module Compilers
# include GeneratedDelegatedTypeMethods
#
# module GeneratedDelegatedTypeMethods
# sig { params(args: T.untyped).returns(T.any(Message, Comment)) }
# sig { params(args: T.untyped).returns(T.any(::Message, ::Comment)) }
# def build_entryable(*args); end
#
# sig { returns(Class) }
Expand All @@ -45,7 +45,7 @@ module Compilers
# sig { returns(T::Boolean) }
# def message?; end
#
# sig { returns(T.nilable(Message)) }
# sig { returns(T.nilable(::Message)) }
# def message; end
#
# sig { returns(T.nilable(Integer)) }
Expand All @@ -54,7 +54,7 @@ module Compilers
# sig { returns(T::Boolean) }
# def comment?; end
#
# sig { returns(T.nilable(Comment)) }
# sig { returns(T.nilable(::Comment)) }
# def comment; end
#
# sig { returns(T.nilable(Integer)) }
Expand All @@ -77,8 +77,9 @@ def decorate
constant.__tapioca_delegated_types.each do |role, data|
types = data.fetch(:types)
options = data.fetch(:options, {})
populate_role_accessors(mod, role, types)
populate_type_helpers(mod, role, types, options)
type_pairs = types.map { |type| [type, qualified_type_name(type, role)] }
populate_role_accessors(mod, role, type_pairs)
populate_type_helpers(mod, role, type_pairs, options)
end
end

Expand All @@ -96,8 +97,8 @@ def gather_constants

private

#: (RBI::Scope mod, Symbol role, Array[String] types) -> void
def populate_role_accessors(mod, role, types)
#: (RBI::Scope mod, Symbol role, Array[[String, String]] type_pairs) -> void
def populate_role_accessors(mod, role, type_pairs)
mod.create_method(
"#{role}_name",
parameters: [],
Expand All @@ -113,19 +114,19 @@ def populate_role_accessors(mod, role, types)
mod.create_method(
"build_#{role}",
parameters: [create_rest_param("args", type: "T.untyped")],
return_type: types.size == 1 ? types.first : "T.any(#{types.join(", ")})",
return_type: build_return_type(type_pairs),
)
end

#: (RBI::Scope mod, Symbol role, Array[String] types, Hash[Symbol, untyped] options) -> void
def populate_type_helpers(mod, role, types, options)
types.each do |type|
populate_type_helper(mod, role, type, options)
#: (RBI::Scope mod, Symbol role, Array[[String, String]] type_pairs, Hash[Symbol, untyped] options) -> void
def populate_type_helpers(mod, role, type_pairs, options)
type_pairs.each do |type, qualified_type|
populate_type_helper(mod, role, type, qualified_type, options)
end
end

#: (RBI::Scope mod, Symbol role, String type, Hash[Symbol, untyped] options) -> void
def populate_type_helper(mod, role, type, options)
#: (RBI::Scope mod, Symbol role, String type, String qualified_type, Hash[Symbol, untyped] options) -> void
def populate_type_helper(mod, role, type, qualified_type, options)
singular = type.tableize.tr("/", "_").singularize
query = "#{singular}?"
primary_key = options[:primary_key] || "id"
Expand All @@ -142,7 +143,7 @@ def populate_type_helper(mod, role, type, options)
mod.create_method(
singular,
parameters: [],
return_type: "T.nilable(#{type})",
return_type: "T.nilable(#{qualified_type})",
)

mod.create_method(
Expand All @@ -151,6 +152,48 @@ def populate_type_helper(mod, role, type, options)
return_type: as_nilable_type(getter_type),
)
end

# Collapses to `T.untyped` if any member is `T.untyped`, since `T.any(::Foo, T.untyped)`
# is equivalent to `T.untyped` in Sorbet and the per-type error has already been recorded.
#: (Array[[String, String]] type_pairs) -> String
def build_return_type(type_pairs)
qualified_types = type_pairs.map { |_, qualified_type| qualified_type }
if qualified_types.include?("T.untyped")
"T.untyped"
elsif qualified_types.size == 1
qualified_types.fetch(0)
else
"T.any(#{qualified_types.join(", ")})"
end
end

# Resolves a delegated type entry to a fully-qualified constant name. The strings passed
# to `delegated_type(..., types: %w[...])` are written verbatim into the generated RBI,
# but the surrounding `class A::B::C` scope omits `A` and `A::B` from Sorbet's lexical
# nesting, so a bare `D` reference fails to resolve to `A::B::D` even when that constant
# exists. `compute_type` is `ActiveRecord::Base`'s own (private) namespace-walking lookup
# — the same one Rails uses for STI and polymorphic associations — so it resolves both
# bare and fully-qualified names. When the constant can't be resolved (NameError) or its
# qualified name can't be derived (anonymous class) we record a compiler error and emit
# `T.untyped`, which both surfaces the problem and keeps the generated RBI type-checkable.
#: (String type, Symbol role) -> String
def qualified_type_name(type, role)
klass = constant.send(:compute_type, type)
qualified_name = qualified_name_of(klass)
return qualified_name if qualified_name

add_unresolvable_type_error(type, role)
rescue NameError, LoadError
add_unresolvable_type_error(type, role)
end

#: (String type, Symbol role) -> String
def add_unresolvable_type_error(type, role)
add_error(<<~MSG.strip)
Cannot generate delegated_type `#{role}` on `#{constant}` since the type `#{type}` could not be resolved.
MSG
"T.untyped"
end
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions manual/compiler_activerecorddelegatedtypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Entry
include GeneratedDelegatedTypeMethods

module GeneratedDelegatedTypeMethods
sig { params(args: T.untyped).returns(T.any(Message, Comment)) }
sig { params(args: T.untyped).returns(T.any(::Message, ::Comment)) }
def build_entryable(*args); end

sig { returns(Class) }
Expand All @@ -36,7 +36,7 @@ class Entry
sig { returns(T::Boolean) }
def message?; end

sig { returns(T.nilable(Message)) }
sig { returns(T.nilable(::Message)) }
def message; end

sig { returns(T.nilable(Integer)) }
Expand All @@ -45,7 +45,7 @@ class Entry
sig { returns(T::Boolean) }
def comment?; end

sig { returns(T.nilable(Comment)) }
sig { returns(T.nilable(::Comment)) }
def comment; end

sig { returns(T.nilable(Integer)) }
Expand Down
Loading
Loading