diff --git a/lib/tapioca/helpers/rbi_files_helper.rb b/lib/tapioca/helpers/rbi_files_helper.rb index b84e81832..1e3224571 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, ) @@ -131,10 +131,25 @@ 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_gem_rbis_strictnesses(redef_errors, gem_dir) if redef_errors.any? + + payload_superclass_errors = errors.select do |error| + error.more.any? { |line| line.include?(SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG) } + end + if payload_superclass_errors.any? + update_sorbet_config_for_payload_superclass_redefinitions(payload_superclass_errors) + end end Kernel.raise Tapioca::Error, error_messages.join("\n") if parse_errors.any? + + unhandled_errors = errors.reject do |error| + auto_fixable_validation_error?(error, gem_dir: gem_dir, dsl_dir: dsl_dir) + end + + if unhandled_errors.empty? + say(" No errors found\n\n", [:green, :bold]) + end end private @@ -258,6 +273,67 @@ 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) + error.more.each do |line| + if line =~ /--suppress-payload-superclass-redefinition-for=([^\s`]+)/ + return T.must(Regexp.last_match(1)) + end + end + + nil + end + + #: (Spoom::Sorbet::Errors::Error error, gem_dir: String, dsl_dir: String) -> bool + def auto_fixable_validation_error?(error, gem_dir:, dsl_dir:) + return true if error.code == 4010 + return true if error.more.any? { |line| line.include?(SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG) } + + return false if Dir.exist?(gem_dir) && !Dir.glob("#{gem_dir}/**/*.rbi").empty? + + [5002, 5067].include?(error.code) && T.must(error.file).start_with?(dsl_dir) + 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) : "" + flag_already_present = config.lines(chomp: true).include?(flag) + + if flag_already_present + say( + "\n Payload superclass of `#{constant}` was redefined; `#{flag}` is already in sorbet/config\n", + [:yellow, :bold], + ) + return + end + + 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 + say( + "\n Added `#{flag}` to sorbet/config (payload superclass of `#{constant}` was redefined)", + [:yellow, :bold], + ) + 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..57706f645 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1875,6 +1875,87 @@ 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 + + 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