Skip to content

Fix Tapioca Compiler to Preserve Params for Empty FixedHash#699

Merged
beepubapu merged 2 commits into
mainfrom
bb-empty-hash-tapioca-compiler
Apr 28, 2026
Merged

Fix Tapioca Compiler to Preserve Params for Empty FixedHash#699
beepubapu merged 2 commits into
mainfrom
bb-empty-hash-tapioca-compiler

Conversation

@beepubapu

@beepubapu beepubapu commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

The tapioca DSL compiler for JobIteration explodes FixedHash types into keyword arguments on the generated perform/perform_now/perform_later methods. When the FixedHash is empty ({}),
hash_type.types.map returns [], and flat_map replaces the positional params parameter with nothing — generating zero-arg methods.

This means a job like:

sig { params(params: {}, cursor: T.untyped).returns(T::Array[T.untyped]) }
def build_enumerator(params, cursor:)

# generates:

# Before (broken)
def perform; end
def perform_now; end

Any call site using perform_now({}) then fails with Sorbet error 7004 (too many arguments).

Fix

When the FixedHash has zero keys, preserve the original positional parameter as-is instead of exploding to nothing:

  # After                                                                                                                                                                                                      
  sig { params(params: {}).void }
  def perform(params); end                                                                                                                                                                                       

The param stays required because the runtime method chain (perform(*params) → build_enumerator(*arguments, cursor:)) splats caller arguments directly — an RBI default does not exist at runtime, so perform_now() with zero args would raise ArgumentError in build_enumerator. Callers must pass perform_now({}).

Non-empty FixedHash explosion is unchanged — all existing behavior is preserved.

Also extracted expand_fixed_hash private method to keep block nesting within rubocop's 3-level limit (Metrics/BlockNesting).

Tests

  • Added test_generates_correct_rbi_file_for_job_with_empty_fixed_hash_parameter — verifies the fix
  • Added test_generates_correct_rbi_file_for_job_with_all_nilable_fixed_hash_keys — verifies all-optional keys still explode to optional kwargs (no regression)

@beepubapu beepubapu requested a review from a team as a code owner April 23, 2026 20:27
@beepubapu beepubapu force-pushed the bb-empty-hash-tapioca-compiler branch from c27df22 to def4c34 Compare April 23, 2026 20:29
@beepubapu beepubapu force-pushed the bb-empty-hash-tapioca-compiler branch from def4c34 to 887bdfe Compare April 24, 2026 13:50
@beepubapu

Copy link
Copy Markdown
Contributor Author

lib/tapioca/dsl/compilers/job_iteration.rb

Line Comment (line 89):

🐛 Bug: The new empty-FixedHash branch does not preserve the original parameter shape and always replaces the original parameter with create_opt_param(..., default: "{}"). For the case described in the PR, a method like def build_enumerator(params, cursor:) now generates RBI methods such as def perform(params = {}); end, def perform_now(params = {}); end, and def perform_later(params = {}); end. This makes Sorbet accept MyJob.perform_now() and MyJob.perform_later() with no arguments. However, the runtime still forwards only the provided arguments into build_enumerator via perform(*params) -> interruptible_perform(*params) -> build_enumerator(*arguments, cursor: cursor_position). A zero-argument call therefore becomes build_enumerator(cursor: ...), which raises because params remains a required positional argument. The runtime tests in the repository also reflect this contract by invoking such jobs as perform_now({}), not perform_now(). This branch also discards the original parameter kind and order: if the original empty fixed-hash parameter was required or keyword-based, it is still rewritten into an optional positional param, which can further drift from the real build_enumerator API. The result is an RBI that advertises call forms the runtime cannot safely handle, potentially causing runtime exceptions, failed async jobs, and retry churn.

Suggestion: Preserve the original typed_param for empty fixed hashes instead of always synthesizing a new optional positional param:

if hash_type.types.empty?
  typed_param
else
  hash_type.types.map do |key, value|
    if value.name.start_with?("T.nilable")
      create_kw_opt_param(key.to_s, type: value.to_s, default: "nil")
    else
      create_kw_param(key.to_s, type: value.to_s)
    end
  end
end

If special handling is needed for genuinely optional empty-hash params, branch on typed_param.param.class rather than unconditionally converting every empty fixed-hash param into RBI::OptParam.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes Tapioca’s JobIteration DSL compiler so that an empty T::Types::FixedHash parameter ({}) does not get “exploded away” into zero parameters when generating RBIs, preserving the original positional parameter instead.

Changes:

  • Preserve the original positional parameter when a FixedHash has zero keys (instead of flattening to an empty parameter list).
  • Extract FixedHash expansion logic into a dedicated private helper to reduce nesting.
  • Add regression tests covering empty FixedHash params and ensuring all-nilable FixedHash keys still expand to optional keyword args.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
lib/tapioca/dsl/compilers/job_iteration.rb Adds expand_fixed_hash helper and ensures empty FixedHash parameters remain as positional params in generated method signatures.
test/tapioca/dsl/compilers/job_iteration_test.rb Adds test coverage for empty FixedHash handling and all-nilable FixedHash key expansion behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@beepubapu beepubapu merged commit 1f12d57 into main Apr 28, 2026
31 checks passed
@beepubapu beepubapu deleted the bb-empty-hash-tapioca-compiler branch April 28, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants