Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions lib/spoom/cli/srb/tc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Tc < Thor
option :junit_output_path, type: :string, desc: "Output failures to XML file formatted for JUnit"
option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
option :ignore_errors, type: :string, desc: "Path to ignored errors file (default: sorbet/ignored_errors.cfg)"
def tc(*paths_to_select)
context = context_requiring_sorbet!
limit = options[:limit]
Expand All @@ -36,7 +37,16 @@ def tc(*paths_to_select)
junit_output_path = options[:junit_output_path]
sorbet = options[:sorbet]

unless limit || code || sort
ignore_errors_path = options[:ignore_errors]
ignore_errors_path ||= Spoom::Sorbet::Errors::DEFAULT_IGNORED_ERRORS_PATH if File.exist?(
File.join(context.absolute_path, Spoom::Sorbet::Errors::DEFAULT_IGNORED_ERRORS_PATH),
)
ignored_errors = if ignore_errors_path
path = File.expand_path(ignore_errors_path, context.absolute_path)
Spoom::Sorbet::Errors.parse_ignored_errors(path)
end

unless limit || code || sort || ignored_errors
result = T.unsafe(context).srb_tc(
*options[:sorbet_options].split(" "),
capture_err: false,
Expand Down Expand Up @@ -84,6 +94,32 @@ def tc(*paths_to_select)
end
end

ignored_count = 0
if ignored_errors
matched = Set.new #: Set[[Integer, String, Integer]]
active, ignored = errors.partition do |e|
err_code = e.code
err_file = e.file
err_line = e.line
if err_code && err_file && err_line && ignored_errors.include?([err_code, err_file, err_line])
matched << [err_code, err_file, err_line]
false
else
true
end
end
ignored_count = ignored.size
errors = active

stale = ignored_errors - matched
unless stale.empty?
stale.each do |entry|
say_error("Stale entry in ignore file: #{entry[0]}:#{entry[1]}:#{entry[2]}", status: nil)
end
exit(1)
end
end

errors = case sort
when SORT_CODE
Spoom::Sorbet::Errors.sort_errors_by_code(errors)
Expand All @@ -110,14 +146,20 @@ def tc(*paths_to_select)
end

if count
if errors_count == errors.size
parts = []
if errors_count != errors.size + ignored_count
parts << "#{errors.size} shown"
end
parts << "#{ignored_count} ignored" if ignored_count > 0
if parts.empty?
say_error("Errors: #{errors_count}", status: nil)
else
say_error("Errors: #{errors.size} shown, #{errors_count} total", status: nil)
parts << "#{errors_count} total"
say_error("Errors: #{parts.join(", ")}", status: nil)
end
end

exit(1)
exit(errors.empty? ? 0 : 1)
rescue Spoom::Sorbet::Error::Segfault => error
say_error(<<~ERR, status: nil)
#{red("!!! Sorbet exited with code #{error.result.exit_code} - SEGFAULT !!!")}
Expand Down
20 changes: 20 additions & 0 deletions lib/spoom/sorbet/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,32 @@ module Sorbet
module Errors
DEFAULT_ERROR_URL_BASE = "https://srb.help/"

DEFAULT_IGNORED_ERRORS_PATH = "sorbet/ignored_errors.cfg"

class << self
#: (Array[Error] errors) -> Array[Error]
def sort_errors_by_code(errors)
errors.sort_by { |e| [e.code, e.file, e.line, e.message] }
end

#: (String path) -> Set[[Integer, String, Integer]]
def parse_ignored_errors(path)
ignored = Set.new #: Set[[Integer, String, Integer]]
File.foreach(path) do |line|
line = line.strip
next if line.empty? || line.start_with?("#")

parts = line.split(":")
next if parts.size < 3

code = parts[0].to_i
file_line = parts[-1].to_i
file_path = parts[1..-2].join(":")
ignored << [code, file_path, file_line]
end
ignored
end

#: (Array[Error]) -> REXML::Document
def to_junit_xml(errors)
testsuite_element = REXML::Element.new("testsuite")
Expand Down
4 changes: 4 additions & 0 deletions rbi/spoom.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -2652,6 +2652,9 @@ class Spoom::Sorbet::Error::Segfault < ::Spoom::Sorbet::Error; end

module Spoom::Sorbet::Errors
class << self
sig { params(path: ::String).returns(T::Set[[::Integer, ::String, ::Integer]]) }
def parse_ignored_errors(path); end

sig { params(errors: T::Array[::Spoom::Sorbet::Errors::Error]).returns(T::Array[::Spoom::Sorbet::Errors::Error]) }
def sort_errors_by_code(errors); end

Expand All @@ -2661,6 +2664,7 @@ module Spoom::Sorbet::Errors
end

Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE = T.let(T.unsafe(nil), String)
Spoom::Sorbet::Errors::DEFAULT_IGNORED_ERRORS_PATH = T.let(T.unsafe(nil), String)

class Spoom::Sorbet::Errors::Error
include ::Comparable
Expand Down
77 changes: 77 additions & 0 deletions test/spoom/cli/srb/tc_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,83 @@ def test_display_sorbet_error
refute(result.status)
end

def test_ignore_errors_all_ignored
@project.write!("sorbet/ignored_errors.cfg", <<~CFG)
# Ignore all errors in errors/errors.rb
5002:errors/errors.rb:5
7003:errors/errors.rb:5
7004:errors/errors.rb:10
7003:errors/errors.rb:11
7004:errors/errors.rb:11
CFG
result = @project.spoom("srb tc --no-color")
assert_equal(<<~MSG, result.err)
Errors: 7 ignored, 7 total
MSG
assert(result.status)
end

def test_ignore_errors_partial
@project.write!("sorbet/ignored_errors.cfg", <<~CFG)
5002:errors/errors.rb:5
CFG
result = @project.spoom("srb tc --no-color")
assert_equal(<<~MSG, result.err)
7003 - errors/errors.rb:5: Method `params` does not exist on `T.class_of(Foo)`
7003 - errors/errors.rb:5: Method `sig` does not exist on `T.class_of(Foo)`
7004 - errors/errors.rb:10: Wrong number of arguments for constructor. Expected: `0`, got: `1`
7003 - errors/errors.rb:11: Method `c` does not exist on `T.class_of(<root>)`
7004 - errors/errors.rb:11: Too many arguments provided for method `Foo#foo`. Expected: `1`, got: `2`
Errors: 2 ignored, 7 total
MSG
refute(result.status)
end

def test_ignore_errors_with_custom_path
@project.write!("custom_ignores.cfg", <<~CFG)
5002:errors/errors.rb:5
7003:errors/errors.rb:5
7004:errors/errors.rb:10
7003:errors/errors.rb:11
7004:errors/errors.rb:11
CFG
result = @project.spoom("srb tc --no-color --ignore-errors=custom_ignores.cfg")
assert_equal(<<~MSG, result.err)
Errors: 7 ignored, 7 total
MSG
assert(result.status)
end

def test_ignore_errors_with_comments_and_blank_lines
@project.write!("sorbet/ignored_errors.cfg", <<~CFG)
# This is a comment

5002:errors/errors.rb:5

# Another comment
CFG
result = @project.spoom("srb tc --no-color")
assert_equal(<<~MSG, result.err)
7003 - errors/errors.rb:5: Method `params` does not exist on `T.class_of(Foo)`
7003 - errors/errors.rb:5: Method `sig` does not exist on `T.class_of(Foo)`
7004 - errors/errors.rb:10: Wrong number of arguments for constructor. Expected: `0`, got: `1`
7003 - errors/errors.rb:11: Method `c` does not exist on `T.class_of(<root>)`
7004 - errors/errors.rb:11: Too many arguments provided for method `Foo#foo`. Expected: `1`, got: `2`
Errors: 2 ignored, 7 total
MSG
refute(result.status)
end

def test_ignore_errors_stale_entry
@project.write!("sorbet/ignored_errors.cfg", <<~CFG)
5002:errors/errors.rb:5
9999:nonexistent.rb:1
CFG
result = @project.spoom("srb tc --no-color")
assert_includes(result.err, "Stale entry in ignore file: 9999:nonexistent.rb:1")
refute(result.status)
end

def test_deprecated_command_spoom_tc
@project.remove!("errors")
result = @project.spoom("tc --no-color")
Expand Down
Loading