spoom 1.5.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -0
  3. data/lib/spoom/backtrace_filter/minitest.rb +3 -4
  4. data/lib/spoom/cli/deadcode.rb +1 -2
  5. data/lib/spoom/cli/helper.rb +41 -31
  6. data/lib/spoom/cli/srb/assertions.rb +48 -0
  7. data/lib/spoom/cli/srb/bump.rb +1 -2
  8. data/lib/spoom/cli/srb/coverage.rb +1 -1
  9. data/lib/spoom/cli/srb/metrics.rb +68 -0
  10. data/lib/spoom/cli/srb/sigs.rb +209 -0
  11. data/lib/spoom/cli/srb/tc.rb +16 -1
  12. data/lib/spoom/cli/srb.rb +16 -4
  13. data/lib/spoom/cli.rb +1 -2
  14. data/lib/spoom/colors.rb +2 -6
  15. data/lib/spoom/context/bundle.rb +8 -9
  16. data/lib/spoom/context/exec.rb +3 -6
  17. data/lib/spoom/context/file_system.rb +12 -19
  18. data/lib/spoom/context/git.rb +14 -19
  19. data/lib/spoom/context/sorbet.rb +14 -27
  20. data/lib/spoom/context.rb +4 -8
  21. data/lib/spoom/counters.rb +22 -0
  22. data/lib/spoom/coverage/d3/base.rb +6 -8
  23. data/lib/spoom/coverage/d3/circle_map.rb +6 -16
  24. data/lib/spoom/coverage/d3/pie.rb +14 -19
  25. data/lib/spoom/coverage/d3/timeline.rb +46 -47
  26. data/lib/spoom/coverage/d3.rb +2 -4
  27. data/lib/spoom/coverage/report.rb +41 -79
  28. data/lib/spoom/coverage/snapshot.rb +8 -14
  29. data/lib/spoom/coverage.rb +3 -5
  30. data/lib/spoom/deadcode/definition.rb +12 -14
  31. data/lib/spoom/deadcode/erb.rb +10 -8
  32. data/lib/spoom/deadcode/index.rb +21 -25
  33. data/lib/spoom/deadcode/indexer.rb +5 -6
  34. data/lib/spoom/deadcode/plugins/action_mailer.rb +2 -3
  35. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +2 -3
  36. data/lib/spoom/deadcode/plugins/actionpack.rb +19 -22
  37. data/lib/spoom/deadcode/plugins/active_model.rb +2 -3
  38. data/lib/spoom/deadcode/plugins/active_record.rb +62 -53
  39. data/lib/spoom/deadcode/plugins/active_support.rb +3 -2
  40. data/lib/spoom/deadcode/plugins/base.rb +29 -32
  41. data/lib/spoom/deadcode/plugins/graphql.rb +2 -3
  42. data/lib/spoom/deadcode/plugins/minitest.rb +4 -4
  43. data/lib/spoom/deadcode/plugins/namespaces.rb +5 -5
  44. data/lib/spoom/deadcode/plugins/rails.rb +5 -5
  45. data/lib/spoom/deadcode/plugins/rubocop.rb +5 -5
  46. data/lib/spoom/deadcode/plugins/ruby.rb +3 -4
  47. data/lib/spoom/deadcode/plugins/sorbet.rb +12 -6
  48. data/lib/spoom/deadcode/plugins/thor.rb +2 -3
  49. data/lib/spoom/deadcode/plugins.rb +23 -31
  50. data/lib/spoom/deadcode/remover.rb +58 -79
  51. data/lib/spoom/deadcode/send.rb +2 -8
  52. data/lib/spoom/file_collector.rb +11 -19
  53. data/lib/spoom/file_tree.rb +36 -51
  54. data/lib/spoom/location.rb +9 -20
  55. data/lib/spoom/model/builder.rb +54 -17
  56. data/lib/spoom/model/model.rb +71 -74
  57. data/lib/spoom/model/namespace_visitor.rb +4 -3
  58. data/lib/spoom/model/reference.rb +4 -8
  59. data/lib/spoom/model/references_visitor.rb +50 -30
  60. data/lib/spoom/parse.rb +4 -4
  61. data/lib/spoom/poset.rb +22 -24
  62. data/lib/spoom/printer.rb +10 -13
  63. data/lib/spoom/rbs.rb +77 -0
  64. data/lib/spoom/sorbet/config.rb +17 -24
  65. data/lib/spoom/sorbet/errors.rb +87 -45
  66. data/lib/spoom/sorbet/lsp/base.rb +10 -16
  67. data/lib/spoom/sorbet/lsp/errors.rb +8 -16
  68. data/lib/spoom/sorbet/lsp/structures.rb +65 -91
  69. data/lib/spoom/sorbet/lsp.rb +20 -22
  70. data/lib/spoom/sorbet/metrics/code_metrics_visitor.rb +236 -0
  71. data/lib/spoom/sorbet/metrics/metrics_file_parser.rb +34 -0
  72. data/lib/spoom/sorbet/metrics.rb +2 -32
  73. data/lib/spoom/sorbet/sigils.rb +16 -23
  74. data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +242 -0
  75. data/lib/spoom/sorbet/translate/sorbet_assertions_to_rbs_comments.rb +123 -0
  76. data/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +293 -0
  77. data/lib/spoom/sorbet/translate/strip_sorbet_sigs.rb +23 -0
  78. data/lib/spoom/sorbet/translate/translator.rb +71 -0
  79. data/lib/spoom/sorbet/translate.rb +49 -0
  80. data/lib/spoom/sorbet.rb +6 -12
  81. data/lib/spoom/source/rewriter.rb +167 -0
  82. data/lib/spoom/source.rb +4 -0
  83. data/lib/spoom/timeline.rb +4 -6
  84. data/lib/spoom/version.rb +1 -1
  85. data/lib/spoom/visitor.rb +298 -151
  86. data/lib/spoom.rb +4 -3
  87. data/rbi/spoom.rbi +3567 -0
  88. metadata +62 -8
data/lib/spoom/poset.rb CHANGED
@@ -7,22 +7,21 @@ module Spoom
7
7
  # The partial order relation is a binary relation that is reflexive, antisymmetric, and transitive.
8
8
  # It can be used to represent a hierarchy of classes or modules, the dependencies between gems, etc.
9
9
  class Poset
10
- extend T::Sig
11
10
  extend T::Generic
12
11
 
13
12
  class Error < Spoom::Error; end
14
13
 
15
14
  E = type_member { { upper: Object } }
16
15
 
17
- sig { void }
16
+ #: -> void
18
17
  def initialize
19
- @elements = T.let({}, T::Hash[E, Element[E]])
18
+ @elements = {} #: Hash[E, Element[E]]
20
19
  end
21
20
 
22
21
  # Get the POSet element for a given value
23
22
  #
24
23
  # Raises if the element is not found
25
- sig { params(value: E).returns(Element[E]) }
24
+ #: (E value) -> Element[E]
26
25
  def [](value)
27
26
  element = @elements[value]
28
27
  raise Error, "POSet::Element not found for #{value}" unless element
@@ -31,7 +30,7 @@ module Spoom
31
30
  end
32
31
 
33
32
  # Add an element to the POSet
34
- sig { params(value: E).returns(Element[E]) }
33
+ #: (E value) -> Element[E]
35
34
  def add_element(value)
36
35
  element = @elements[value]
37
36
  return element if element
@@ -40,7 +39,7 @@ module Spoom
40
39
  end
41
40
 
42
41
  # Is the given value a element in the POSet?
43
- sig { params(value: E).returns(T::Boolean) }
42
+ #: (E value) -> bool
44
43
  def element?(value)
45
44
  @elements.key?(value)
46
45
  end
@@ -50,7 +49,7 @@ module Spoom
50
49
  # Transitive edges (transitive closure) are automatically computed.
51
50
  # Adds the elements if they don't exist.
52
51
  # If the direct edge already exists, nothing is done.
53
- sig { params(from: E, to: E).void }
52
+ #: (E from, E to) -> void
54
53
  def add_direct_edge(from, to)
55
54
  from_element = add_element(from)
56
55
  to_element = add_element(to)
@@ -88,7 +87,7 @@ module Spoom
88
87
  end
89
88
 
90
89
  # Is there an edge (direct or indirect) from `from` to `to`?
91
- sig { params(from: E, to: E).returns(T::Boolean) }
90
+ #: (E from, E to) -> bool
92
91
  def edge?(from, to)
93
92
  from_element = @elements[from]
94
93
  return false unless from_element
@@ -97,13 +96,13 @@ module Spoom
97
96
  end
98
97
 
99
98
  # Is there a direct edge from `from` to `to`?
100
- sig { params(from: E, to: E).returns(T::Boolean) }
99
+ #: (E from, E to) -> bool
101
100
  def direct_edge?(from, to)
102
101
  self[from].parents.include?(to)
103
102
  end
104
103
 
105
104
  # Show the POSet as a DOT graph using xdot (used for debugging)
106
- sig { params(direct: T::Boolean, transitive: T::Boolean).void }
105
+ #: (?direct: bool, ?transitive: bool) -> void
107
106
  def show_dot(direct: true, transitive: true)
108
107
  Open3.popen3("xdot -") do |stdin, _stdout, _stderr, _thread|
109
108
  stdin.write(to_dot(direct: direct, transitive: transitive))
@@ -112,7 +111,7 @@ module Spoom
112
111
  end
113
112
 
114
113
  # Return the POSet as a DOT graph
115
- sig { params(direct: T::Boolean, transitive: T::Boolean).returns(String) }
114
+ #: (?direct: bool, ?transitive: bool) -> String
116
115
  def to_dot(direct: true, transitive: true)
117
116
  dot = +"digraph {\n"
118
117
  dot << " rankdir=BT;\n"
@@ -134,30 +133,29 @@ module Spoom
134
133
 
135
134
  # An element in a POSet
136
135
  class Element
137
- extend T::Sig
138
136
  extend T::Generic
139
137
  include Comparable
140
138
 
141
139
  E = type_member { { upper: Object } }
142
140
 
143
141
  # The value held by this element
144
- sig { returns(E) }
142
+ #: E
145
143
  attr_reader :value
146
144
 
147
145
  # Edges (direct and indirect) from this element to other elements in the same POSet
148
- sig { returns(T::Set[Element[E]]) }
146
+ #: Set[Element[E]]
149
147
  attr_reader :dtos, :tos, :dfroms, :froms
150
148
 
151
- sig { params(value: E).void }
149
+ #: (E value) -> void
152
150
  def initialize(value)
153
151
  @value = value
154
- @dtos = T.let(Set.new, T::Set[Element[E]])
155
- @tos = T.let(Set.new, T::Set[Element[E]])
156
- @dfroms = T.let(Set.new, T::Set[Element[E]])
157
- @froms = T.let(Set.new, T::Set[Element[E]])
152
+ @dtos = Set.new #: Set[Element[E]]
153
+ @tos = Set.new #: Set[Element[E]]
154
+ @dfroms = Set.new #: Set[Element[E]]
155
+ @froms = Set.new #: Set[Element[E]]
158
156
  end
159
157
 
160
- sig { params(other: T.untyped).returns(T.nilable(Integer)) }
158
+ #: (untyped other) -> Integer?
161
159
  def <=>(other)
162
160
  return unless other.is_a?(Element)
163
161
  return 0 if self == other
@@ -170,25 +168,25 @@ module Spoom
170
168
  end
171
169
 
172
170
  # Direct parents of this element
173
- sig { returns(T::Array[E]) }
171
+ #: -> Array[E]
174
172
  def parents
175
173
  @dtos.map(&:value)
176
174
  end
177
175
 
178
176
  # Direct and indirect ancestors of this element
179
- sig { returns(T::Array[E]) }
177
+ #: -> Array[E]
180
178
  def ancestors
181
179
  @tos.map(&:value)
182
180
  end
183
181
 
184
182
  # Direct children of this element
185
- sig { returns(T::Array[E]) }
183
+ #: -> Array[E]
186
184
  def children
187
185
  @dfroms.map(&:value)
188
186
  end
189
187
 
190
188
  # Direct and indirect descendants of this element
191
- sig { returns(T::Array[E]) }
189
+ #: -> Array[E]
192
190
  def descendants
193
191
  @froms.map(&:value)
194
192
  end
data/lib/spoom/printer.rb CHANGED
@@ -5,15 +5,12 @@ require "stringio"
5
5
 
6
6
  module Spoom
7
7
  class Printer
8
- extend T::Sig
9
- extend T::Helpers
10
-
11
8
  include Colorize
12
9
 
13
- sig { returns(T.any(IO, StringIO)) }
10
+ #: (IO | StringIO)
14
11
  attr_accessor :out
15
12
 
16
- sig { params(out: T.any(IO, StringIO), colors: T::Boolean, indent_level: Integer).void }
13
+ #: (?out: (IO | StringIO), ?colors: bool, ?indent_level: Integer) -> void
17
14
  def initialize(out: $stdout, colors: true, indent_level: 0)
18
15
  @out = out
19
16
  @colors = colors
@@ -21,19 +18,19 @@ module Spoom
21
18
  end
22
19
 
23
20
  # Increase indent level
24
- sig { void }
21
+ #: -> void
25
22
  def indent
26
23
  @indent_level += 2
27
24
  end
28
25
 
29
26
  # Decrease indent level
30
- sig { void }
27
+ #: -> void
31
28
  def dedent
32
29
  @indent_level -= 2
33
30
  end
34
31
 
35
32
  # Print `string` into `out`
36
- sig { params(string: T.nilable(String)).void }
33
+ #: (String? string) -> void
37
34
  def print(string)
38
35
  return unless string
39
36
 
@@ -43,7 +40,7 @@ module Spoom
43
40
  # Print `string` colored with `color` into `out`
44
41
  #
45
42
  # Does not use colors unless `@colors`.
46
- sig { params(string: T.nilable(String), color: Color).void }
43
+ #: (String? string, *Color color) -> void
47
44
  def print_colored(string, *color)
48
45
  return unless string
49
46
 
@@ -52,13 +49,13 @@ module Spoom
52
49
  end
53
50
 
54
51
  # Print a new line into `out`
55
- sig { void }
52
+ #: -> void
56
53
  def printn
57
54
  print("\n")
58
55
  end
59
56
 
60
57
  # Print `string` with indent and newline
61
- sig { params(string: T.nilable(String)).void }
58
+ #: (String? string) -> void
62
59
  def printl(string)
63
60
  return unless string
64
61
 
@@ -68,13 +65,13 @@ module Spoom
68
65
  end
69
66
 
70
67
  # Print an indent space into `out`
71
- sig { void }
68
+ #: -> void
72
69
  def printt
73
70
  print(" " * @indent_level)
74
71
  end
75
72
 
76
73
  # Colorize `string` with color if `@colors`
77
- sig { params(string: String, color: Spoom::Color).returns(String) }
74
+ #: (String string, *Spoom::Color color) -> String
78
75
  def colorize(string, *color)
79
76
  return string unless @colors
80
77
 
data/lib/spoom/rbs.rb ADDED
@@ -0,0 +1,77 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module RBS
6
+ class Comments
7
+ #: Array[Annotations]
8
+ attr_reader :annotations
9
+
10
+ #: Array[Signature]
11
+ attr_reader :signatures
12
+
13
+ #: -> void
14
+ def initialize
15
+ @annotations = [] #: Array[Annotations]
16
+ @signatures = [] #: Array[Signature]
17
+ end
18
+
19
+ #: -> bool
20
+ def empty?
21
+ @annotations.empty? && @signatures.empty?
22
+ end
23
+ end
24
+
25
+ class Comment
26
+ #: String
27
+ attr_reader :string
28
+
29
+ #: Prism::Location
30
+ attr_reader :location
31
+
32
+ #: (String, Prism::Location) -> void
33
+ def initialize(string, location)
34
+ @string = string
35
+ @location = location
36
+ end
37
+ end
38
+
39
+ class Annotations < Comment; end
40
+ class Signature < Comment; end
41
+
42
+ module ExtractRBSComments
43
+ #: (Prism::Node) -> Comments
44
+ def node_rbs_comments(node)
45
+ res = Comments.new
46
+
47
+ comments = node.location.leading_comments.reverse
48
+ return res if comments.empty?
49
+
50
+ continuation_comments = [] #: Array[Prism::Comment]
51
+
52
+ comments.each do |comment|
53
+ string = comment.slice
54
+
55
+ if string.start_with?("# @")
56
+ string = string.delete_prefix("#").strip
57
+ res.annotations << Annotations.new(string, comment.location)
58
+ elsif string.start_with?("#: ")
59
+ string = string.delete_prefix("#:").strip
60
+ location = comment.location
61
+
62
+ continuation_comments.reverse_each do |continuation_comment|
63
+ string = "#{string}#{continuation_comment.slice.delete_prefix("#|")}"
64
+ location = location.join(continuation_comment.location)
65
+ end
66
+ continuation_comments.clear
67
+ res.signatures << Signature.new(string, location)
68
+ elsif string.start_with?("#|")
69
+ continuation_comments << comment
70
+ end
71
+ end
72
+
73
+ res
74
+ end
75
+ end
76
+ end
77
+ end
@@ -24,25 +24,23 @@ module Spoom
24
24
  # puts config.ignore # "c"
25
25
  # ```
26
26
  class Config
27
- extend T::Sig
27
+ DEFAULT_ALLOWED_EXTENSIONS = [".rb", ".rbi"].freeze #: Array[String]
28
28
 
29
- DEFAULT_ALLOWED_EXTENSIONS = T.let([".rb", ".rbi"].freeze, T::Array[String])
30
-
31
- sig { returns(T::Array[String]) }
29
+ #: Array[String]
32
30
  attr_accessor :paths, :ignore, :allowed_extensions
33
31
 
34
- sig { returns(T::Boolean) }
32
+ #: bool
35
33
  attr_accessor :no_stdlib
36
34
 
37
- sig { void }
35
+ #: -> void
38
36
  def initialize
39
- @paths = T.let([], T::Array[String])
40
- @ignore = T.let([], T::Array[String])
41
- @allowed_extensions = T.let([], T::Array[String])
42
- @no_stdlib = T.let(false, T::Boolean)
37
+ @paths = [] #: Array[String]
38
+ @ignore = [] #: Array[String]
39
+ @allowed_extensions = [] #: Array[String]
40
+ @no_stdlib = false #: bool
43
41
  end
44
42
 
45
- sig { returns(Config) }
43
+ #: -> Config
46
44
  def copy
47
45
  new_config = Sorbet::Config.new
48
46
  new_config.paths.concat(@paths)
@@ -64,28 +62,26 @@ module Spoom
64
62
  #
65
63
  # puts config.options_string # "/foo /bar --ignore /baz --allowed-extension .rb"
66
64
  # ~~~
67
- sig { returns(String) }
65
+ #: -> String
68
66
  def options_string
69
67
  opts = []
70
- opts.concat(paths)
71
- opts.concat(ignore.map { |p| "--ignore #{p}" })
72
- opts.concat(allowed_extensions.map { |ext| "--allowed-extension #{ext}" })
68
+ opts.concat(paths.map { |p| "'#{p}'" })
69
+ opts.concat(ignore.map { |p| "--ignore '#{p}'" })
70
+ opts.concat(allowed_extensions.map { |ext| "--allowed-extension '#{ext}'" })
73
71
  opts << "--no-stdlib" if @no_stdlib
74
72
  opts.join(" ")
75
73
  end
76
74
 
77
75
  class << self
78
- extend T::Sig
79
-
80
- sig { params(sorbet_config_path: String).returns(Spoom::Sorbet::Config) }
76
+ #: (String sorbet_config_path) -> Spoom::Sorbet::Config
81
77
  def parse_file(sorbet_config_path)
82
78
  parse_string(File.read(sorbet_config_path))
83
79
  end
84
80
 
85
- sig { params(sorbet_config: String).returns(Spoom::Sorbet::Config) }
81
+ #: (String sorbet_config) -> Spoom::Sorbet::Config
86
82
  def parse_string(sorbet_config)
87
83
  config = Config.new
88
- state = T.let(nil, T.nilable(Symbol))
84
+ state = nil #: Symbol?
89
85
  sorbet_config.each_line do |line|
90
86
  line = line.strip
91
87
  case line
@@ -95,9 +91,6 @@ module Spoom
95
91
  when /^--allowed-extension=/
96
92
  config.allowed_extensions << parse_option(line)
97
93
  next
98
- when /^--ignore=/
99
- config.ignore << parse_option(line)
100
- next
101
94
  when /^--ignore$/
102
95
  state = :ignore
103
96
  next
@@ -146,7 +139,7 @@ module Spoom
146
139
 
147
140
  private
148
141
 
149
- sig { params(line: String).returns(String) }
142
+ #: (String line) -> String
150
143
  def parse_option(line)
151
144
  T.must(line.split("=").last).strip
152
145
  end
@@ -1,54 +1,76 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "rexml/document"
5
+
4
6
  module Spoom
5
7
  module Sorbet
6
8
  module Errors
7
9
  DEFAULT_ERROR_URL_BASE = "https://srb.help/"
8
10
 
9
11
  class << self
10
- extend T::Sig
11
-
12
- sig { params(errors: T::Array[Error]).returns(T::Array[Error]) }
12
+ #: (Array[Error] errors) -> Array[Error]
13
13
  def sort_errors_by_code(errors)
14
14
  errors.sort_by { |e| [e.code, e.file, e.line, e.message] }
15
15
  end
16
+
17
+ #: (Array[Error]) -> REXML::Document
18
+ def to_junit_xml(errors)
19
+ testsuite_element = REXML::Element.new("testsuite")
20
+ testsuite_element.add_attributes(
21
+ "name" => "Sorbet",
22
+ "failures" => errors.size,
23
+ )
24
+
25
+ if errors.empty?
26
+ # Avoid creating an empty report when there are no errors so that
27
+ # reporting tools know that the type checking ran successfully.
28
+ testcase_element = testsuite_element.add_element("testcase")
29
+ testcase_element.add_attributes(
30
+ "name" => "Typecheck",
31
+ "tests" => 1,
32
+ )
33
+ else
34
+ errors.each do |error|
35
+ testsuite_element.add_element(error.to_junit_xml_element)
36
+ end
37
+ end
38
+
39
+ doc = REXML::Document.new
40
+ doc << REXML::XMLDecl.new
41
+ doc.add_element(testsuite_element)
42
+
43
+ doc
44
+ end
16
45
  end
17
46
  # Parse errors from Sorbet output
18
47
  class Parser
19
- extend T::Sig
20
-
21
48
  class ParseError < Spoom::Error; end
22
49
 
23
- HEADER = T.let(
24
- [
25
- "👋 Hey there! Heads up that this is not a release build of sorbet.",
26
- "Release builds are faster and more well-supported by the Sorbet team.",
27
- "Check out the README to learn how to build Sorbet in release mode.",
28
- "To forcibly silence this error, either pass --silence-dev-message,",
29
- "or set SORBET_SILENCE_DEV_MESSAGE=1 in your shell environment.",
30
- ],
31
- T::Array[String],
32
- )
50
+ HEADER = [
51
+ "👋 Hey there! Heads up that this is not a release build of sorbet.",
52
+ "Release builds are faster and more well-supported by the Sorbet team.",
53
+ "Check out the README to learn how to build Sorbet in release mode.",
54
+ "To forcibly silence this error, either pass --silence-dev-message,",
55
+ "or set SORBET_SILENCE_DEV_MESSAGE=1 in your shell environment.",
56
+ ] #: Array[String]
33
57
 
34
58
  class << self
35
- extend T::Sig
36
-
37
- sig { params(output: String, error_url_base: String).returns(T::Array[Error]) }
59
+ #: (String output, ?error_url_base: String) -> Array[Error]
38
60
  def parse_string(output, error_url_base: DEFAULT_ERROR_URL_BASE)
39
61
  parser = Spoom::Sorbet::Errors::Parser.new(error_url_base: error_url_base)
40
62
  parser.parse(output)
41
63
  end
42
64
  end
43
65
 
44
- sig { params(error_url_base: String).void }
66
+ #: (?error_url_base: String) -> void
45
67
  def initialize(error_url_base: DEFAULT_ERROR_URL_BASE)
46
- @errors = T.let([], T::Array[Error])
47
- @error_line_match_regex = T.let(error_line_match_regexp(error_url_base), Regexp)
48
- @current_error = T.let(nil, T.nilable(Error))
68
+ @errors = [] #: Array[Error]
69
+ @error_line_match_regex = error_line_match_regexp(error_url_base) #: Regexp
70
+ @current_error = nil #: Error?
49
71
  end
50
72
 
51
- sig { params(output: String).returns(T::Array[Error]) }
73
+ #: (String output) -> Array[Error]
52
74
  def parse(output)
53
75
  output.each_line do |line|
54
76
  break if /^No errors! Great job\./.match?(line)
@@ -71,7 +93,7 @@ module Spoom
71
93
 
72
94
  private
73
95
 
74
- sig { params(error_url_base: String).returns(Regexp) }
96
+ #: (String error_url_base) -> Regexp
75
97
  def error_line_match_regexp(error_url_base)
76
98
  url = Regexp.escape(error_url_base)
77
99
  %r{
@@ -88,7 +110,7 @@ module Spoom
88
110
  }x
89
111
  end
90
112
 
91
- sig { params(line: String).returns(T.nilable(Error)) }
113
+ #: (String line) -> Error?
92
114
  def match_error_line(line)
93
115
  match = line.match(@error_line_match_regex)
94
116
  return unless match
@@ -97,14 +119,14 @@ module Spoom
97
119
  Error.new(file, line&.to_i, message, code&.to_i)
98
120
  end
99
121
 
100
- sig { params(error: Error).void }
122
+ #: (Error error) -> void
101
123
  def open_error(error)
102
124
  raise ParseError, "Error: Already parsing an error!" if @current_error
103
125
 
104
126
  @current_error = error
105
127
  end
106
128
 
107
- sig { void }
129
+ #: -> void
108
130
  def close_error
109
131
  raise ParseError, "Error: Not already parsing an error!" unless @current_error
110
132
 
@@ -112,7 +134,7 @@ module Spoom
112
134
  @current_error = nil
113
135
  end
114
136
 
115
- sig { params(line: String).void }
137
+ #: (String line) -> void
116
138
  def append_error(line)
117
139
  raise ParseError, "Error: Not already parsing an error!" unless @current_error
118
140
 
@@ -126,51 +148,71 @@ module Spoom
126
148
 
127
149
  class Error
128
150
  include Comparable
129
- extend T::Sig
130
151
 
131
- sig { returns(T.nilable(String)) }
152
+ #: String?
132
153
  attr_reader :file, :message
133
154
 
134
- sig { returns(T.nilable(Integer)) }
155
+ #: Integer?
135
156
  attr_reader :line, :code
136
157
 
137
- sig { returns(T::Array[String]) }
158
+ #: Array[String]
138
159
  attr_reader :more
139
160
 
140
161
  # Other files associated with the error
141
- sig { returns(T::Set[String]) }
162
+ #: Set[String]
142
163
  attr_reader :files_from_error_sections
143
164
 
144
- sig do
145
- params(
146
- file: T.nilable(String),
147
- line: T.nilable(Integer),
148
- message: T.nilable(String),
149
- code: T.nilable(Integer),
150
- more: T::Array[String],
151
- ).void
152
- end
165
+ #: (String? file, Integer? line, String? message, Integer? code, ?Array[String] more) -> void
153
166
  def initialize(file, line, message, code, more = [])
154
167
  @file = file
155
168
  @line = line
156
169
  @message = message
157
170
  @code = code
158
171
  @more = more
159
- @files_from_error_sections = T.let(Set.new, T::Set[String])
172
+ @files_from_error_sections = Set.new #: Set[String]
160
173
  end
161
174
 
162
175
  # By default errors are sorted by location
163
- sig { params(other: T.untyped).returns(Integer) }
176
+ #: (untyped other) -> Integer
164
177
  def <=>(other)
165
178
  return 0 unless other.is_a?(Error)
166
179
 
167
180
  [file, line, code, message] <=> [other.file, other.line, other.code, other.message]
168
181
  end
169
182
 
170
- sig { returns(String) }
183
+ #: -> String
171
184
  def to_s
172
185
  "#{file}:#{line}: #{message} (#{code})"
173
186
  end
187
+
188
+ #: -> REXML::Element
189
+ def to_junit_xml_element
190
+ testcase_element = REXML::Element.new("testcase")
191
+ # Unlike traditional test suites, we can't report all tests
192
+ # regardless of outcome; we only have errors to report. As a
193
+ # result we reinterpret the definitions of the test properties
194
+ # bit: the error message becomes the test name and the full error
195
+ # info gets plugged into the failure body along with file/line
196
+ # information (displayed in Jenkins as the "Stacktrace" for the
197
+ # error).
198
+ testcase_element.add_attributes(
199
+ "name" => message,
200
+ "file" => file,
201
+ "line" => line,
202
+ )
203
+ failure_element = testcase_element.add_element("failure")
204
+ failure_element.add_attributes(
205
+ "type" => code,
206
+ )
207
+ explanation_text = [
208
+ "In file #{file}:\n",
209
+ *more,
210
+ ].join.chomp
211
+ # Use CDATA so that parsers know the whitespace is significant.
212
+ failure_element.add(REXML::CData.new(explanation_text))
213
+
214
+ testcase_element
215
+ end
174
216
  end
175
217
  end
176
218
  end