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