Skip to content

Commit

Permalink
Refactor for clarity
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenprater committed Jan 10, 2025
1 parent ca9ea0e commit 5d5aaea
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 46 deletions.
157 changes: 111 additions & 46 deletions lib/tapioca/dsl/compilers/delegate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ module Compilers
# Delegates that _themselves_ return a `T.untyped` value will not be generated in the RBI file, since Sorbet
# already generates a `T.untyped` return by default
#

class UntypedDelegate < StandardError; end

class Delegate < Compiler
extend T::Sig

Expand All @@ -59,7 +62,7 @@ def gather_constants
return [] unless defined?(Tapioca::Dsl::Compilers::Extensions::Module)

all_classes.grep(Tapioca::Dsl::Compilers::Extensions::Module).select do |c|
T.unsafe(c).__tapioca_delegated_methods.any?
T.unsafe(c).__tapioca_delegated_methods.any?
end
end
end
Expand All @@ -72,63 +75,125 @@ def decorate
# We don't handle delegations to instance, class and global variables
next if delegated_method[:to].start_with?("@", "$")

constant_target = if delegated_method[:to] == :class
constant
elsif delegated_method[:to].start_with?(/[A-Z]/)
target_klass = constantize(delegated_method[:to], namespace: constant)
next unless Module === target_klass

target_klass
delegate_sig = if delegated_method[:to] == :class || delegated_method[:to].start_with?(/[A-Z]/)
ClassMethodDelegate.new(constant, method, **delegated_method.except(:methods))
else
false
DelegateMethod.new(constant, method, **delegated_method.except(:methods))
end

sig = if constant_target
signature_of(constant_target.singleton_method(method))
else
signature_of(constant.instance_method(delegated_method[:to]))
end
parameters = compile_method_parameters_to_rbi(delegate_sig.method_def)
return_type = delegate_sig.maybe_wrap_return(
compile_method_return_type_to_rbi(
delegate_sig.method_def,
),
)

next unless sig
klass.create_method(
delegate_sig.method_name,
parameters:,
return_type:,
class_method: false,
visibility: delegate_sig.visibility,
)

delegate_klass = if delegated_method[:allow_nil]
sig.return_type.unwrap_nilable.raw_type
else
sig.return_type.raw_type
end
# if the target of the delegate is a constant, but not resolvable, we skip it,
# or if the target of the delegate is a method, but the return type of that method
# doesn't hold a signature for this the method we're looking for, we skip it.
rescue UntypedDelegate
next
end
end
end
end

next if delegate_klass == T.untyped
class DelegateMethod
include RBIHelper
include Runtime::Reflection
extend Runtime::Reflection

visibility = if delegated_method[:private]
RBI::Private.new
else
RBI::Public.new
end
attr_reader :klass, :method, :target, :allow_nil, :prefix, :private

method_def = if constant_target
constant_target.singleton_method(method)
else
delegate_klass.instance_method(method)
end
def initialize(klass, method, to:, allow_nil: false, prefix: nil, private: false)
@klass = klass
@method = method
@target = to
@allow_nil = allow_nil
@prefix = prefix
@private = private
end

method_name = [delegated_method[:prefix], method.name.to_s].compact.join("_")
def target_klass
@target_klass ||= klass
end

method_return_type = if delegated_method[:allow_nil]
non_nilable = compile_method_return_type_to_rbi(method_def)
"T.nilable(#{non_nilable})"
else
compile_method_return_type_to_rbi(method_def)
end
def method_def
@method_def ||= target_return_type.instance_method(method)
end

klass.create_method(
method_name,
parameters: compile_method_parameters_to_rbi(method_def),
return_type: method_return_type,
class_method: false,
visibility: visibility,
)
end
def method_name
if prefix == true
[@target, method.name.to_s].compact.join("_")
else
[prefix, method.name.to_s].compact.join("_")
end
end

def visibility
private ? RBI::Private.new : RBI::Public.new
end

def target_method
target_klass.instance_method(target)
end

def maybe_wrap_return(type)
allow_nil ? "T.nilable(#{type})" : type
end

private

def target_method_signature
signature_of(target_method)
end

def target_return_type
return @target_return_type if defined?(@target_return_type)

@target_return_type = if allow_nil
target_method_signature.return_type.unwrap_nilable.raw_type
else
target_method_signature.return_type.raw_type
end

raise UntypedDelegate if @target_return_type == T.untyped

@target_return_type
end
end

class ClassMethodDelegate < DelegateMethod
def target_klass
return @target_klass if defined?(@target_klass)

@target_klass = if target == :class
@klass
elsif target.start_with?(/[A-Z]/)
constantize(target)
end

raise UntypedDelegate if @target_klass.nil?
raise UntypedDelegate if @target_klass == UNDEFINED_CONSTANT
raise UntypedDelegate unless Module === @target_klass

@target_klass
end

def method_def
@method_def ||= target_klass.singleton_method(method)
end

def target_method
target_klass.singleton_method(method)
end
end
end
Expand Down
14 changes: 14 additions & 0 deletions spec/tapioca/dsl/compilers/delegate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,33 @@ class Target
def string_method; end
end
class OtherTarget
extend T::Sig
sig { params(step: Integer).returns(Integer) }
def echo_integer(step); step; end
end
class Delegate
extend T::Sig
sig { returns(Target) }
attr_reader :target
sig { returns(OtherTarget) }
attr_reader :other_target
delegate :string_method, to: :target, prefix: "target"
delegate :echo_integer, to: :other_target, prefix: true
end
RUBY

expected = <<~RBI
# typed: strong
class Delegate
sig { params(step: ::Integer).returns(::Integer) }
def other_target_echo_integer(step); end
sig { returns(::String) }
def target_string_method; end
end
Expand Down

0 comments on commit 5d5aaea

Please sign in to comment.