From 4b9ca6e63bade6d86932d05a18c94cbfc66917f5 Mon Sep 17 00:00:00 2001 From: Haseeb Nazir <36381672+iamhaseebn@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:19:11 -0400 Subject: [PATCH] Support generic ActiveJob return types --- lib/tapioca/dsl/compilers/active_job.rb | 24 ++++++++++- spec/tapioca/dsl/compilers/active_job_spec.rb | 43 +++++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/lib/tapioca/dsl/compilers/active_job.rb b/lib/tapioca/dsl/compilers/active_job.rb index 6698f1f21..8513463ad 100644 --- a/lib/tapioca/dsl/compilers/active_job.rb +++ b/lib/tapioca/dsl/compilers/active_job.rb @@ -47,7 +47,7 @@ def decorate root.create_path(constant) do |job| method = constant.instance_method(:perform) - constant_name = name_of(constant) + constant_name = type_name_of(constant) #: as !nil parameters = compile_method_parameters_to_rbi(method) return_type = compile_method_return_type_to_rbi(method) @@ -69,7 +69,27 @@ def decorate private - #: (Array[RBI::TypedParam] parameters, String? constant_name) -> Array[RBI::TypedParam] + # Resolves a constant into a valid Sorbet type reference, + # applying `T.untyped` for any unfixed generic type variables. + # + # @example + # type_name_of(StandardJob) # => "::StandardJob" + # type_name_of(GenericJob) # => "::SomeModule::GenericJob[T.untyped]" + #: (Module[top] constant) -> String? + def type_name_of(constant) + type_name = qualified_name_of(constant) + return type_name if !type_name || type_name.end_with?("]") + + type_variables = Runtime::GenericTypeRegistry.lookup_type_variables(constant) + return type_name unless type_variables + + type_variables = type_variables.reject(&:fixed?) + return type_name if type_variables.empty? + + "#{type_name}[#{type_variables.map { "T.untyped" }.join(", ")}]" + end + + #: (Array[RBI::TypedParam] parameters, String constant_name) -> Array[RBI::TypedParam] def perform_later_parameters(parameters, constant_name) if ::Gem::Requirement.new(">= 7.0").satisfied_by?(::ActiveJob.gem_version) parameters.reject! { |typed_param| RBI::BlockParam === typed_param.param } diff --git a/spec/tapioca/dsl/compilers/active_job_spec.rb b/spec/tapioca/dsl/compilers/active_job_spec.rb index aa47ab272..752ce9758 100644 --- a/spec/tapioca/dsl/compilers/active_job_spec.rb +++ b/spec/tapioca/dsl/compilers/active_job_spec.rb @@ -73,7 +73,7 @@ def perform(user_id) class NotifyJob class << self - sig { params(user_id: T.untyped, block: T.nilable(T.proc.params(job: NotifyJob).void)).returns(T.any(NotifyJob, FalseClass)) } + sig { params(user_id: T.untyped, block: T.nilable(T.proc.params(job: ::NotifyJob).void)).returns(T.any(::NotifyJob, FalseClass)) } def perform_later(user_id, &block); end sig { params(user_id: T.untyped).returns(T.untyped) } @@ -100,7 +100,7 @@ def perform(user_id) class NotifyJob class << self - sig { params(user_id: ::Integer, block: T.nilable(T.proc.params(job: NotifyJob).void)).returns(T.any(NotifyJob, FalseClass)) } + sig { params(user_id: ::Integer, block: T.nilable(T.proc.params(job: ::NotifyJob).void)).returns(T.any(::NotifyJob, FalseClass)) } def perform_later(user_id, &block); end sig { params(user_id: ::Integer).void } @@ -111,6 +111,41 @@ def perform_now(user_id); end assert_equal(expected, rbi_for(:NotifyJob)) end + it "generates correct RBI file for a generic job" do + add_ruby_file("job.rb", <<~RUBY) + require "active_record" + + class GenericJob < ActiveJob::Base + extend T::Sig + extend T::Generic + + Input = type_member + Fixed = type_member { { fixed: String } } + Output = type_member { { upper: ActiveRecord::Base } } + + sig { params(value: Input).returns(Output) } + def perform(value) + raise NotImplementedError + end + end + RUBY + + expected = template(<<~RBI) + # typed: strong + + class GenericJob + class << self + sig { params(value: Input, block: T.nilable(T.proc.params(job: ::GenericJob[T.untyped, T.untyped]).void)).returns(T.any(::GenericJob[T.untyped, T.untyped], FalseClass)) } + def perform_later(value, &block); end + + sig { params(value: Input).returns(Output) } + def perform_now(value); end + end + end + RBI + assert_equal(expected, rbi_for(:GenericJob)) + end + it "generates correct RBI file for subclass with block argument" do add_ruby_file("job.rb", <<~RUBY) class NotifyJob < ActiveJob::Base @@ -125,7 +160,7 @@ def perform(user_id, &blk) class NotifyJob class << self - sig { params(user_id: T.untyped, block: T.nilable(T.proc.params(job: NotifyJob).void)).returns(T.any(NotifyJob, FalseClass)) } + sig { params(user_id: T.untyped, block: T.nilable(T.proc.params(job: ::NotifyJob).void)).returns(T.any(::NotifyJob, FalseClass)) } def perform_later(user_id, &block); end sig { params(user_id: T.untyped).returns(T.untyped) } @@ -153,7 +188,7 @@ def perform(user_id) class NotifyJob class << self - sig { params(user_id: ::Integer, block: T.nilable(T.proc.params(job: NotifyJob).void)).returns(T.any(NotifyJob, FalseClass)) } + sig { params(user_id: ::Integer, block: T.nilable(T.proc.params(job: ::NotifyJob).void)).returns(T.any(::NotifyJob, FalseClass)) } def perform_later(user_id, &block); end sig { params(user_id: ::Integer).void }