diff --git a/lib/tapioca/helpers/rbi_files_helper.rb b/lib/tapioca/helpers/rbi_files_helper.rb index b84e81832..fabe82cb2 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, ) @@ -129,12 +129,16 @@ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], ERR end - if auto_strictness - redef_errors = errors.select { |error| error.code == 4010 } - update_gem_rbis_strictnesses(redef_errors, gem_dir) - end + handled_errors = apply_validation_fixes(errors, gem_dir: gem_dir, auto_strictness: auto_strictness) Kernel.raise Tapioca::Error, error_messages.join("\n") if parse_errors.any? + + unhandled_errors = errors - handled_errors + unhandled_errors.reject! { |error| ignored_validation_error?(error, gem_dir: gem_dir, dsl_dir: dsl_dir) } + + if unhandled_errors.empty? + say(" No errors found\n\n", [:green, :bold]) + end end private @@ -258,6 +262,93 @@ 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 + + #: ( + #| Array[Spoom::Sorbet::Errors::Error] errors, + #| gem_dir: String, + #| auto_strictness: bool, + #| ) -> Array[Spoom::Sorbet::Errors::Error] + def apply_validation_fixes(errors, gem_dir:, auto_strictness:) + handled_errors = [] #: Array[Spoom::Sorbet::Errors::Error] + + if auto_strictness + redef_errors = errors.select { |error| error.code == 4010 } + update_gem_rbis_strictnesses(redef_errors, gem_dir) if redef_errors.any? + handled_errors.concat(redef_errors) + end + + # Automatically fix payload superclass redefinition errors. + payload_superclass_errors = errors.select { |error| payload_superclass_error?(error) } + if payload_superclass_errors.any? + update_sorbet_config_for_payload_superclass_redefinitions(payload_superclass_errors) + end + handled_errors.concat(payload_superclass_errors) + + handled_errors + end + + #: (Spoom::Sorbet::Errors::Error error) -> bool + def payload_superclass_error?(error) + error.more.any? { |line| line.include?(SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG) } + end + + #: (Spoom::Sorbet::Errors::Error error, gem_dir: String, dsl_dir: String) -> bool + def ignored_validation_error?(error, gem_dir:, dsl_dir:) + 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..61484ad51 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1875,6 +1875,91 @@ 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").lines.reject do |line| + [ + "--suppress-payload-superclass-redefinition-for=Net::IMAP::Literal", + "--suppress-payload-superclass-redefinition-for=Net::IMAP::CommandData", + ].include?(line.chomp) + end.join + @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, + "Added `--suppress-payload-superclass-redefinition-for=Net::IMAP::Literal` to sorbet/config " \ + "(payload superclass of `Net::IMAP::Literal` was redefined)", + ) + + 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