From 1b40e73f5da5f8db817482eb9dd72f470908cec3 Mon Sep 17 00:00:00 2001 From: Zeeshan Khan Date: Thu, 18 Jun 2026 23:24:28 -0700 Subject: [PATCH 1/2] Handle payload superclass redefinition errors Co-authored-by: Cursor --- lib/tapioca/helpers/rbi_files_helper.rb | 74 ++++++++++++++++++++++++- spec/tapioca/cli/gem_spec.rb | 55 ++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/lib/tapioca/helpers/rbi_files_helper.rb b/lib/tapioca/helpers/rbi_files_helper.rb index b84e81832..4c10ecd3a 100644 --- a/lib/tapioca/helpers/rbi_files_helper.rb +++ b/lib/tapioca/helpers/rbi_files_helper.rb @@ -86,7 +86,20 @@ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err || "") - if errors.empty? + payload_superclass_errors = T.let([], T::Array[Spoom::Sorbet::Errors::Error]) + if auto_strictness + payload_superclass_res = sorbet( + "--no-config", + "--error-url-base=#{error_url_base}", + dsl_dir, + gem_dir, + ) + payload_superclass_errors = Spoom::Sorbet::Errors::Parser + .parse_string(payload_superclass_res.err || "") + .select { |error| error.code == 5012 } + end + + if errors.empty? && payload_superclass_errors.empty? say(" No errors found\n\n", [:green, :bold]) return @@ -132,6 +145,7 @@ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], if auto_strictness redef_errors = errors.select { |error| error.code == 4010 } update_gem_rbis_strictnesses(redef_errors, gem_dir) + update_sorbet_config_for_payload_superclass_redefinitions(payload_superclass_errors) end Kernel.raise Tapioca::Error, error_messages.join("\n") if parse_errors.any? @@ -258,6 +272,64 @@ def extract_methods_and_attrs(nodes) ) end + SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG = + "--suppress-payload-superclass-redefinition-for" #: String + + #: (Array[Spoom::Sorbet::Errors::Error] errors) -> void + def update_sorbet_config_for_payload_superclass_redefinitions(errors) + errors + .filter_map { |error| payload_superclass_constant_from_error(error) } + .uniq + .each { |constant| add_payload_superclass_suppression_to_config(constant) } + end + + #: (Spoom::Sorbet::Errors::Error error) -> String? + def payload_superclass_constant_from_error(error) + if error.message =~ /Parent of class `([^`]+)` redefined/ + return T.must(Regexp.last_match(1)) + end + + error.more.each do |line| + if line =~ /--suppress-payload-superclass-redefinition-for=([^\s`]+)/ + return T.must(Regexp.last_match(1)) + end + end + + nil + end + + #: (String constant) -> void + def add_payload_superclass_suppression_to_config(constant) + flag = "#{SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG}=#{constant}" + config_path = Tapioca::SORBET_CONFIG_FILE + config = File.exist?(config_path) ? File.read(config_path) : "" + added = !config.lines(chomp: true).include?(flag) + + if added + FileUtils.mkdir_p(File.dirname(config_path)) + if config.empty? + File.write(config_path, "#{flag}\n") + else + suffix = config.end_with?("\n") ? "" : "\n" + File.write(config_path, "#{config}#{suffix}#{flag}\n") + end + end + + if added + say( + "\n Added `#{flag}` to sorbet/config (payload superclass of `#{constant}` was redefined)", + [:yellow, :bold], + ) + else + say( + "\n Payload superclass of `#{constant}` was redefined; `#{flag}` is already in sorbet/config", + [:yellow, :bold], + ) + end + + say("\n") + end + #: (Array[Spoom::Sorbet::Errors::Error] errors, String gem_dir) -> void def update_gem_rbis_strictnesses(errors, gem_dir) files = [] diff --git a/spec/tapioca/cli/gem_spec.rb b/spec/tapioca/cli/gem_spec.rb index b79145e4a..c278c15c1 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1875,6 +1875,61 @@ def foo; end @project.remove!("sorbet/rbi/shims/foo.rbi") end + + it "must add a payload superclass redefinition suppression to sorbet/config" do + config = @project.read("sorbet/config") + @project.write!( + "sorbet/config", + "#{config.rstrip}\n--suppress-payload-superclass-redefinition-for=Net::IMAP::CommandData\n", + ) + + @project.write!("sorbet/rbi/gems/bar@0.3.0.rbi", <<~RBI) + # typed: true + + module Bar + end + + class Net::IMAP::Literal < ::String + end + RBI + + result = @project.tapioca("gem foo") + + assert_stdout_includes(result, <<~OUT) + Checking generated RBI files... Done + + + Added `--suppress-payload-superclass-redefinition-for=Net::IMAP::Literal` to sorbet/config (payload superclass of `Net::IMAP::Literal` was redefined) + OUT + + assert_empty_stderr(result) + assert_success_status(result) + + config = @project.read("sorbet/config") + assert_equal( + 1, + config.lines(chomp: true).count("--suppress-payload-superclass-redefinition-for=Net::IMAP::Literal"), + ) + assert_equal( + 1, + config.lines(chomp: true).count("--suppress-payload-superclass-redefinition-for=Net::IMAP::CommandData"), + ) + + result = @project.tapioca("gem foo") + + assert_stdout_includes(result, <<~OUT) + Payload superclass of `Net::IMAP::Literal` was redefined; `--suppress-payload-superclass-redefinition-for=Net::IMAP::Literal` is already in sorbet/config + OUT + + config = @project.read("sorbet/config") + assert_equal( + 1, + config.lines(chomp: true).count("--suppress-payload-superclass-redefinition-for=Net::IMAP::Literal"), + ) + + assert_empty_stderr(result) + assert_success_status(result) + end end describe "sanity" do From 3117c0767da7618f0e8d89d6238309038db44a9d Mon Sep 17 00:00:00 2001 From: Zeeshan Khan Date: Wed, 24 Jun 2026 13:23:33 -0700 Subject: [PATCH 2/2] Address PR review feedback for payload superclass handling. Use a single Sorbet resolver pass, detect payload redefinitions from error.more hints instead of code 5012, and add a regression test for non-payload superclass conflicts. Co-authored-by: Cursor --- lib/tapioca/helpers/rbi_files_helper.rb | 42 ++++++++----------------- spec/tapioca/cli/gem_spec.rb | 26 +++++++++++++++ 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/lib/tapioca/helpers/rbi_files_helper.rb b/lib/tapioca/helpers/rbi_files_helper.rb index 4c10ecd3a..70309b8a6 100644 --- a/lib/tapioca/helpers/rbi_files_helper.rb +++ b/lib/tapioca/helpers/rbi_files_helper.rb @@ -78,7 +78,7 @@ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], res = sorbet( "--no-config", "--error-url-base=#{error_url_base}", - "--stop-after namer", + "--stop-after resolver", dsl_dir, gem_dir, ) @@ -86,20 +86,7 @@ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err || "") - payload_superclass_errors = T.let([], T::Array[Spoom::Sorbet::Errors::Error]) - if auto_strictness - payload_superclass_res = sorbet( - "--no-config", - "--error-url-base=#{error_url_base}", - dsl_dir, - gem_dir, - ) - payload_superclass_errors = Spoom::Sorbet::Errors::Parser - .parse_string(payload_superclass_res.err || "") - .select { |error| error.code == 5012 } - end - - if errors.empty? && payload_superclass_errors.empty? + if errors.empty? say(" No errors found\n\n", [:green, :bold]) return @@ -145,6 +132,10 @@ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], if auto_strictness redef_errors = errors.select { |error| error.code == 4010 } update_gem_rbis_strictnesses(redef_errors, gem_dir) + + payload_superclass_errors = errors.select do |error| + error.more.any? { |line| line.include?(SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG) } + end update_sorbet_config_for_payload_superclass_redefinitions(payload_superclass_errors) end @@ -285,10 +276,6 @@ def update_sorbet_config_for_payload_superclass_redefinitions(errors) #: (Spoom::Sorbet::Errors::Error error) -> String? def payload_superclass_constant_from_error(error) - if error.message =~ /Parent of class `([^`]+)` redefined/ - return T.must(Regexp.last_match(1)) - end - error.more.each do |line| if line =~ /--suppress-payload-superclass-redefinition-for=([^\s`]+)/ return T.must(Regexp.last_match(1)) @@ -303,9 +290,14 @@ def add_payload_superclass_suppression_to_config(constant) flag = "#{SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG}=#{constant}" config_path = Tapioca::SORBET_CONFIG_FILE config = File.exist?(config_path) ? File.read(config_path) : "" - added = !config.lines(chomp: true).include?(flag) + flag_already_present = config.lines(chomp: true).include?(flag) - if added + if flag_already_present + say( + "\n Payload superclass of `#{constant}` was redefined; `#{flag}` is already in sorbet/config", + [:yellow, :bold], + ) + else FileUtils.mkdir_p(File.dirname(config_path)) if config.empty? File.write(config_path, "#{flag}\n") @@ -313,18 +305,10 @@ def add_payload_superclass_suppression_to_config(constant) suffix = config.end_with?("\n") ? "" : "\n" File.write(config_path, "#{config}#{suffix}#{flag}\n") end - end - - if added say( "\n Added `#{flag}` to sorbet/config (payload superclass of `#{constant}` was redefined)", [:yellow, :bold], ) - else - say( - "\n Payload superclass of `#{constant}` was redefined; `#{flag}` is already in sorbet/config", - [:yellow, :bold], - ) end say("\n") diff --git a/spec/tapioca/cli/gem_spec.rb b/spec/tapioca/cli/gem_spec.rb index c278c15c1..57706f645 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1930,6 +1930,32 @@ class Net::IMAP::Literal < ::String assert_empty_stderr(result) assert_success_status(result) end + + it "must not add a payload suppression for non-payload superclass redefinitions" do + @project.write!("sorbet/rbi/dsl/non_payload_superclass_conflict.rbi", <<~RBI) + # typed: true + + class NonPayloadSuperclassConflict < ::Object + end + RBI + + @project.write!("sorbet/rbi/gems/bar@0.3.0.rbi", <<~RBI) + # typed: true + + class NonPayloadSuperclassConflict < ::String + end + RBI + + result = @project.tapioca("gem foo") + + refute_includes( + @project.read("sorbet/config"), + "--suppress-payload-superclass-redefinition-for=NonPayloadSuperclassConflict", + ) + + assert_empty_stderr(result) + assert_success_status(result) + end end describe "sanity" do