Skip to content

Commit 95dfd76

Browse files
committed
Add --ignore-errors option to spoom srb tc
Allow ignoring known errors by matching error_code:file:line from an ignore file (default: sorbet/ignored_errors.cfg). Ignored errors are hidden from output, shown as a count in the summary, and don't cause a non-zero exit code.
1 parent 0f8330a commit 95dfd76

4 files changed

Lines changed: 147 additions & 4 deletions

File tree

lib/spoom/cli/srb/tc.rb

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Tc < Thor
2525
option :junit_output_path, type: :string, desc: "Output failures to XML file formatted for JUnit"
2626
option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
2727
option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
28+
option :ignore_errors, type: :string, desc: "Path to ignored errors file (default: sorbet/ignored_errors.cfg)"
2829
def tc(*paths_to_select)
2930
context = context_requiring_sorbet!
3031
limit = options[:limit]
@@ -36,7 +37,16 @@ def tc(*paths_to_select)
3637
junit_output_path = options[:junit_output_path]
3738
sorbet = options[:sorbet]
3839

39-
unless limit || code || sort
40+
ignore_errors_path = options[:ignore_errors]
41+
ignore_errors_path ||= Spoom::Sorbet::Errors::DEFAULT_IGNORED_ERRORS_PATH if File.exist?(
42+
File.join(context.absolute_path, Spoom::Sorbet::Errors::DEFAULT_IGNORED_ERRORS_PATH),
43+
)
44+
ignored_errors = if ignore_errors_path
45+
path = File.expand_path(ignore_errors_path, context.absolute_path)
46+
Spoom::Sorbet::Errors.parse_ignored_errors(path)
47+
end
48+
49+
unless limit || code || sort || ignored_errors
4050
result = T.unsafe(context).srb_tc(
4151
*options[:sorbet_options].split(" "),
4252
capture_err: false,
@@ -84,6 +94,32 @@ def tc(*paths_to_select)
8494
end
8595
end
8696

97+
ignored_count = 0
98+
if ignored_errors
99+
matched = Set.new #: Set[[Integer, String, Integer]]
100+
active, ignored = errors.partition do |e|
101+
err_code = e.code
102+
err_file = e.file
103+
err_line = e.line
104+
if err_code && err_file && err_line && ignored_errors.include?([err_code, err_file, err_line])
105+
matched << [err_code, err_file, err_line]
106+
false
107+
else
108+
true
109+
end
110+
end
111+
ignored_count = ignored.size
112+
errors = active
113+
114+
stale = ignored_errors - matched
115+
unless stale.empty?
116+
stale.each do |entry|
117+
say_error("Stale entry in ignore file: #{entry[0]}:#{entry[1]}:#{entry[2]}", status: nil)
118+
end
119+
exit(1)
120+
end
121+
end
122+
87123
errors = case sort
88124
when SORT_CODE
89125
Spoom::Sorbet::Errors.sort_errors_by_code(errors)
@@ -110,14 +146,20 @@ def tc(*paths_to_select)
110146
end
111147

112148
if count
113-
if errors_count == errors.size
149+
parts = []
150+
if errors_count != errors.size + ignored_count
151+
parts << "#{errors.size} shown"
152+
end
153+
parts << "#{ignored_count} ignored" if ignored_count > 0
154+
if parts.empty?
114155
say_error("Errors: #{errors_count}", status: nil)
115156
else
116-
say_error("Errors: #{errors.size} shown, #{errors_count} total", status: nil)
157+
parts << "#{errors_count} total"
158+
say_error("Errors: #{parts.join(", ")}", status: nil)
117159
end
118160
end
119161

120-
exit(1)
162+
exit(errors.empty? ? 0 : 1)
121163
rescue Spoom::Sorbet::Error::Segfault => error
122164
say_error(<<~ERR, status: nil)
123165
#{red("!!! Sorbet exited with code #{error.result.exit_code} - SEGFAULT !!!")}

lib/spoom/sorbet/errors.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,32 @@ module Sorbet
88
module Errors
99
DEFAULT_ERROR_URL_BASE = "https://srb.help/"
1010

11+
DEFAULT_IGNORED_ERRORS_PATH = "sorbet/ignored_errors.cfg"
12+
1113
class << self
1214
#: (Array[Error] errors) -> Array[Error]
1315
def sort_errors_by_code(errors)
1416
errors.sort_by { |e| [e.code, e.file, e.line, e.message] }
1517
end
1618

19+
#: (String path) -> Set[[Integer, String, Integer]]
20+
def parse_ignored_errors(path)
21+
ignored = Set.new #: Set[[Integer, String, Integer]]
22+
File.foreach(path) do |line|
23+
line = line.strip
24+
next if line.empty? || line.start_with?("#")
25+
26+
parts = line.split(":")
27+
next if parts.size < 3
28+
29+
code = parts[0].to_i
30+
file_line = parts[-1].to_i
31+
file_path = parts[1..-2].join(":")
32+
ignored << [code, file_path, file_line]
33+
end
34+
ignored
35+
end
36+
1737
#: (Array[Error]) -> REXML::Document
1838
def to_junit_xml(errors)
1939
testsuite_element = REXML::Element.new("testsuite")

rbi/spoom.rbi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,6 +2652,9 @@ class Spoom::Sorbet::Error::Segfault < ::Spoom::Sorbet::Error; end
26522652

26532653
module Spoom::Sorbet::Errors
26542654
class << self
2655+
sig { params(path: ::String).returns(T::Set[[::Integer, ::String, ::Integer]]) }
2656+
def parse_ignored_errors(path); end
2657+
26552658
sig { params(errors: T::Array[::Spoom::Sorbet::Errors::Error]).returns(T::Array[::Spoom::Sorbet::Errors::Error]) }
26562659
def sort_errors_by_code(errors); end
26572660

@@ -2661,6 +2664,7 @@ module Spoom::Sorbet::Errors
26612664
end
26622665

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

26652669
class Spoom::Sorbet::Errors::Error
26662670
include ::Comparable

test/spoom/cli/srb/tc_test.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,83 @@ def test_display_sorbet_error
461461
refute(result.status)
462462
end
463463

464+
def test_ignore_errors_all_ignored
465+
@project.write!("sorbet/ignored_errors.cfg", <<~CFG)
466+
# Ignore all errors in errors/errors.rb
467+
5002:errors/errors.rb:5
468+
7003:errors/errors.rb:5
469+
7004:errors/errors.rb:10
470+
7003:errors/errors.rb:11
471+
7004:errors/errors.rb:11
472+
CFG
473+
result = @project.spoom("srb tc --no-color")
474+
assert_equal(<<~MSG, result.err)
475+
Errors: 7 ignored, 7 total
476+
MSG
477+
assert(result.status)
478+
end
479+
480+
def test_ignore_errors_partial
481+
@project.write!("sorbet/ignored_errors.cfg", <<~CFG)
482+
5002:errors/errors.rb:5
483+
CFG
484+
result = @project.spoom("srb tc --no-color")
485+
assert_equal(<<~MSG, result.err)
486+
7003 - errors/errors.rb:5: Method `params` does not exist on `T.class_of(Foo)`
487+
7003 - errors/errors.rb:5: Method `sig` does not exist on `T.class_of(Foo)`
488+
7004 - errors/errors.rb:10: Wrong number of arguments for constructor. Expected: `0`, got: `1`
489+
7003 - errors/errors.rb:11: Method `c` does not exist on `T.class_of(<root>)`
490+
7004 - errors/errors.rb:11: Too many arguments provided for method `Foo#foo`. Expected: `1`, got: `2`
491+
Errors: 2 ignored, 7 total
492+
MSG
493+
refute(result.status)
494+
end
495+
496+
def test_ignore_errors_with_custom_path
497+
@project.write!("custom_ignores.cfg", <<~CFG)
498+
5002:errors/errors.rb:5
499+
7003:errors/errors.rb:5
500+
7004:errors/errors.rb:10
501+
7003:errors/errors.rb:11
502+
7004:errors/errors.rb:11
503+
CFG
504+
result = @project.spoom("srb tc --no-color --ignore-errors=custom_ignores.cfg")
505+
assert_equal(<<~MSG, result.err)
506+
Errors: 7 ignored, 7 total
507+
MSG
508+
assert(result.status)
509+
end
510+
511+
def test_ignore_errors_with_comments_and_blank_lines
512+
@project.write!("sorbet/ignored_errors.cfg", <<~CFG)
513+
# This is a comment
514+
515+
5002:errors/errors.rb:5
516+
517+
# Another comment
518+
CFG
519+
result = @project.spoom("srb tc --no-color")
520+
assert_equal(<<~MSG, result.err)
521+
7003 - errors/errors.rb:5: Method `params` does not exist on `T.class_of(Foo)`
522+
7003 - errors/errors.rb:5: Method `sig` does not exist on `T.class_of(Foo)`
523+
7004 - errors/errors.rb:10: Wrong number of arguments for constructor. Expected: `0`, got: `1`
524+
7003 - errors/errors.rb:11: Method `c` does not exist on `T.class_of(<root>)`
525+
7004 - errors/errors.rb:11: Too many arguments provided for method `Foo#foo`. Expected: `1`, got: `2`
526+
Errors: 2 ignored, 7 total
527+
MSG
528+
refute(result.status)
529+
end
530+
531+
def test_ignore_errors_stale_entry
532+
@project.write!("sorbet/ignored_errors.cfg", <<~CFG)
533+
5002:errors/errors.rb:5
534+
9999:nonexistent.rb:1
535+
CFG
536+
result = @project.spoom("srb tc --no-color")
537+
assert_includes(result.err, "Stale entry in ignore file: 9999:nonexistent.rb:1")
538+
refute(result.status)
539+
end
540+
464541
def test_deprecated_command_spoom_tc
465542
@project.remove!("errors")
466543
result = @project.spoom("tc --no-color")

0 commit comments

Comments
 (0)