spoom 1.0.4 → 1.0.9

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -1
  3. data/README.md +296 -1
  4. data/Rakefile +1 -0
  5. data/lib/spoom.rb +21 -2
  6. data/lib/spoom/cli.rb +56 -10
  7. data/lib/spoom/cli/bump.rb +138 -0
  8. data/lib/spoom/cli/config.rb +51 -0
  9. data/lib/spoom/cli/coverage.rb +206 -0
  10. data/lib/spoom/cli/helper.rb +149 -0
  11. data/lib/spoom/cli/lsp.rb +165 -0
  12. data/lib/spoom/cli/run.rb +109 -0
  13. data/lib/spoom/coverage.rb +89 -0
  14. data/lib/spoom/coverage/d3.rb +110 -0
  15. data/lib/spoom/coverage/d3/base.rb +50 -0
  16. data/lib/spoom/coverage/d3/circle_map.rb +195 -0
  17. data/lib/spoom/coverage/d3/pie.rb +175 -0
  18. data/lib/spoom/coverage/d3/timeline.rb +486 -0
  19. data/lib/spoom/coverage/report.rb +308 -0
  20. data/lib/spoom/coverage/snapshot.rb +132 -0
  21. data/lib/spoom/file_tree.rb +196 -0
  22. data/lib/spoom/git.rb +98 -0
  23. data/lib/spoom/printer.rb +80 -0
  24. data/lib/spoom/sorbet.rb +99 -47
  25. data/lib/spoom/sorbet/config.rb +30 -0
  26. data/lib/spoom/sorbet/errors.rb +33 -15
  27. data/lib/spoom/sorbet/lsp.rb +2 -4
  28. data/lib/spoom/sorbet/lsp/structures.rb +108 -14
  29. data/lib/spoom/sorbet/metrics.rb +10 -79
  30. data/lib/spoom/sorbet/sigils.rb +98 -0
  31. data/lib/spoom/test_helpers/project.rb +112 -0
  32. data/lib/spoom/timeline.rb +53 -0
  33. data/lib/spoom/version.rb +2 -2
  34. data/templates/card.erb +8 -0
  35. data/templates/card_snapshot.erb +22 -0
  36. data/templates/page.erb +50 -0
  37. metadata +28 -11
  38. data/lib/spoom/cli/commands/base.rb +0 -36
  39. data/lib/spoom/cli/commands/config.rb +0 -67
  40. data/lib/spoom/cli/commands/lsp.rb +0 -156
  41. data/lib/spoom/cli/commands/run.rb +0 -92
  42. data/lib/spoom/cli/symbol_printer.rb +0 -71
  43. data/lib/spoom/config.rb +0 -11
@@ -11,11 +11,9 @@ require_relative 'lsp/errors'
11
11
  module Spoom
12
12
  module LSP
13
13
  class Client
14
- def initialize(sorbet_cmd, *sorbet_args)
14
+ def initialize(sorbet_bin, *sorbet_args, path: ".")
15
15
  @id = 0
16
- Bundler.with_clean_env do
17
- @in, @out, @err, @status = Open3.popen3([sorbet_cmd, *sorbet_args].join(" "))
18
- end
16
+ @in, @out, @err, @status = T.unsafe(Open3).popen3(sorbet_bin, *sorbet_args, chdir: path)
19
17
  end
20
18
 
21
19
  def next_id
@@ -1,9 +1,24 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "../../printer"
5
+
4
6
  module Spoom
5
7
  module LSP
8
+ module PrintableSymbol
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ interface!
13
+
14
+ sig { abstract.params(printer: SymbolPrinter).void }
15
+ def accept_printer(printer); end
16
+ end
17
+
6
18
  class Hover < T::Struct
19
+ extend T::Sig
20
+ include PrintableSymbol
21
+
7
22
  const :contents, String
8
23
  const :range, T.nilable(Range)
9
24
 
@@ -14,9 +29,10 @@ module Spoom
14
29
  )
15
30
  end
16
31
 
32
+ sig { override.params(printer: SymbolPrinter).void }
17
33
  def accept_printer(printer)
18
34
  printer.print("#{contents}\n")
19
- printer.visit(range) if range
35
+ printer.print_object(range) if range
20
36
  end
21
37
 
22
38
  def to_s
@@ -25,6 +41,9 @@ module Spoom
25
41
  end
26
42
 
27
43
  class Position < T::Struct
44
+ extend T::Sig
45
+ include PrintableSymbol
46
+
28
47
  const :line, Integer
29
48
  const :char, Integer
30
49
 
@@ -35,8 +54,9 @@ module Spoom
35
54
  )
36
55
  end
37
56
 
57
+ sig { override.params(printer: SymbolPrinter).void }
38
58
  def accept_printer(printer)
39
- printer.print("#{line}:#{char}".light_black)
59
+ printer.print_colored("#{line}:#{char}", :light_black)
40
60
  end
41
61
 
42
62
  def to_s
@@ -45,6 +65,9 @@ module Spoom
45
65
  end
46
66
 
47
67
  class Range < T::Struct
68
+ extend T::Sig
69
+ include PrintableSymbol
70
+
48
71
  const :start, Position
49
72
  const :end, Position
50
73
 
@@ -55,10 +78,11 @@ module Spoom
55
78
  )
56
79
  end
57
80
 
81
+ sig { override.params(printer: SymbolPrinter).void }
58
82
  def accept_printer(printer)
59
- printer.visit(start)
60
- printer.print("-".light_black)
61
- printer.visit(self.end)
83
+ printer.print_object(start)
84
+ printer.print_colored("-", :light_black)
85
+ printer.print_object(self.end)
62
86
  end
63
87
 
64
88
  def to_s
@@ -67,6 +91,9 @@ module Spoom
67
91
  end
68
92
 
69
93
  class Location < T::Struct
94
+ extend T::Sig
95
+ include PrintableSymbol
96
+
70
97
  const :uri, String
71
98
  const :range, LSP::Range
72
99
 
@@ -77,17 +104,21 @@ module Spoom
77
104
  )
78
105
  end
79
106
 
107
+ sig { override.params(printer: SymbolPrinter).void }
80
108
  def accept_printer(printer)
81
- printer.print("#{uri.from_uri}:".light_black)
82
- printer.visit(range)
109
+ printer.print_colored("#{printer.clean_uri(uri)}:", :light_black)
110
+ printer.print_object(range)
83
111
  end
84
112
 
85
113
  def to_s
86
- "#{uri}:#{range})."
114
+ "#{uri}:#{range}"
87
115
  end
88
116
  end
89
117
 
90
118
  class SignatureHelp < T::Struct
119
+ extend T::Sig
120
+ include PrintableSymbol
121
+
91
122
  const :label, T.nilable(String)
92
123
  const :doc, Object # TODO
93
124
  const :params, T::Array[T.untyped] # TODO
@@ -100,6 +131,7 @@ module Spoom
100
131
  )
101
132
  end
102
133
 
134
+ sig { override.params(printer: SymbolPrinter).void }
103
135
  def accept_printer(printer)
104
136
  printer.print(label)
105
137
  printer.print("(")
@@ -113,6 +145,9 @@ module Spoom
113
145
  end
114
146
 
115
147
  class Diagnostic < T::Struct
148
+ extend T::Sig
149
+ include PrintableSymbol
150
+
116
151
  const :range, LSP::Range
117
152
  const :code, Integer
118
153
  const :message, String
@@ -127,12 +162,20 @@ module Spoom
127
162
  )
128
163
  end
129
164
 
165
+ sig { override.params(printer: SymbolPrinter).void }
166
+ def accept_printer(printer)
167
+ printer.print(to_s)
168
+ end
169
+
130
170
  def to_s
131
171
  "Error: #{message} (#{code})."
132
172
  end
133
173
  end
134
174
 
135
175
  class DocumentSymbol < T::Struct
176
+ extend T::Sig
177
+ include PrintableSymbol
178
+
136
179
  const :name, String
137
180
  const :detail, T.nilable(String)
138
181
  const :kind, Integer
@@ -151,6 +194,7 @@ module Spoom
151
194
  )
152
195
  end
153
196
 
197
+ sig { override.params(printer: SymbolPrinter).void }
154
198
  def accept_printer(printer)
155
199
  h = serialize.hash
156
200
  return if printer.seen.include?(h)
@@ -159,18 +203,18 @@ module Spoom
159
203
  printer.printt
160
204
  printer.print(kind_string)
161
205
  printer.print(' ')
162
- printer.print(name.blue.bold)
163
- printer.print(' ('.light_black)
206
+ printer.print_colored(name, :blue, :bold)
207
+ printer.print_colored(' (', :light_black)
164
208
  if range
165
- printer.visit(range)
209
+ printer.print_object(range)
166
210
  elsif location
167
- printer.visit(location)
211
+ printer.print_object(location)
168
212
  end
169
- printer.print(')'.light_black)
213
+ printer.print_colored(')', :light_black)
170
214
  printer.printn
171
215
  unless children.empty?
172
216
  printer.indent
173
- printer.visit(children)
217
+ printer.print_objects(children)
174
218
  printer.dedent
175
219
  end
176
220
  # TODO: also display details?
@@ -214,5 +258,55 @@ module Spoom
214
258
  26 => "type_parameter",
215
259
  }
216
260
  end
261
+
262
+ class SymbolPrinter < Printer
263
+ extend T::Sig
264
+
265
+ attr_accessor :seen, :prefix
266
+
267
+ sig do
268
+ params(
269
+ out: T.any(IO, StringIO),
270
+ colors: T::Boolean,
271
+ indent_level: Integer,
272
+ prefix: T.nilable(String)
273
+ ).void
274
+ end
275
+ def initialize(out: $stdout, colors: true, indent_level: 0, prefix: nil)
276
+ super(out: out, colors: colors, indent_level: indent_level)
277
+ @seen = Set.new
278
+ @out = out
279
+ @colors = colors
280
+ @indent_level = indent_level
281
+ @prefix = prefix
282
+ end
283
+
284
+ sig { params(object: T.nilable(PrintableSymbol)).void }
285
+ def print_object(object)
286
+ return unless object
287
+ object.accept_printer(self)
288
+ end
289
+
290
+ sig { params(objects: T::Array[PrintableSymbol]).void }
291
+ def print_objects(objects)
292
+ objects.each { |object| print_object(object) }
293
+ end
294
+
295
+ sig { params(uri: String).returns(String) }
296
+ def clean_uri(uri)
297
+ return uri unless prefix
298
+ uri.delete_prefix(prefix)
299
+ end
300
+
301
+ sig { params(objects: T::Array[PrintableSymbol]).void }
302
+ def print_list(objects)
303
+ objects.each do |object|
304
+ printt
305
+ print "* "
306
+ print_object(object)
307
+ printn
308
+ end
309
+ end
310
+ end
217
311
  end
218
312
  end
@@ -1,102 +1,33 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "sigils"
5
+
4
6
  module Spoom
5
7
  module Sorbet
6
- class Metrics < T::Struct
8
+ module MetricsParser
7
9
  extend T::Sig
8
10
 
9
11
  DEFAULT_PREFIX = "ruby_typer.unknown.."
10
- SIGILS = T.let(["ignore", "false", "true", "strict", "strong", "__STDLIB_INTERNAL"], T::Array[String])
11
-
12
- const :repo, String
13
- const :sha, String
14
- const :status, String
15
- const :branch, String
16
- const :timestamp, Integer
17
- const :uuid, String
18
- const :metrics, T::Hash[String, T.nilable(Integer)]
19
12
 
20
- sig { params(path: String, prefix: String).returns(Metrics) }
13
+ sig { params(path: String, prefix: String).returns(T::Hash[String, Integer]) }
21
14
  def self.parse_file(path, prefix = DEFAULT_PREFIX)
22
15
  parse_string(File.read(path), prefix)
23
16
  end
24
17
 
25
- sig { params(string: String, prefix: String).returns(Metrics) }
18
+ sig { params(string: String, prefix: String).returns(T::Hash[String, Integer]) }
26
19
  def self.parse_string(string, prefix = DEFAULT_PREFIX)
27
20
  parse_hash(JSON.parse(string), prefix)
28
21
  end
29
22
 
30
- sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(Metrics) }
23
+ sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(T::Hash[String, Integer]) }
31
24
  def self.parse_hash(obj, prefix = DEFAULT_PREFIX)
32
- Metrics.new(
33
- repo: obj.fetch("repo"),
34
- sha: obj.fetch("sha"),
35
- status: obj.fetch("status"),
36
- branch: obj.fetch("branch"),
37
- timestamp: obj.fetch("timestamp").to_i,
38
- uuid: obj.fetch("uuid"),
39
- metrics: obj["metrics"].each_with_object({}) do |metric, all|
40
- name = metric["name"]
41
- name = name.sub(prefix, '')
42
- all[name] = metric["value"].to_i
43
- end,
44
- )
45
- end
46
-
47
- sig { returns(T::Hash[String, T.nilable(Integer)]) }
48
- def files_by_strictness
49
- SIGILS.each_with_object({}) do |sigil, map|
50
- map[sigil] = metrics["types.input.files.sigil.#{sigil}"]
25
+ obj["metrics"].each_with_object(Hash.new(0)) do |metric, metrics|
26
+ name = metric["name"]
27
+ name = name.sub(prefix, '')
28
+ metrics[name] = metric["value"] || 0
51
29
  end
52
30
  end
53
-
54
- sig { returns(Integer) }
55
- def files_count
56
- files_by_strictness.values.compact.sum
57
- end
58
-
59
- sig { params(key: String).returns(T.nilable(Integer)) }
60
- def [](key)
61
- metrics[key]
62
- end
63
-
64
- sig { returns(String) }
65
- def to_s
66
- "Metrics<#{repo}-#{timestamp}-#{status}>"
67
- end
68
-
69
- sig { params(out: T.any(IO, StringIO)).void }
70
- def show(out = $stdout)
71
- files = files_count
72
-
73
- out.puts "Sigils:"
74
- out.puts " files: #{files}"
75
- files_by_strictness.each do |sigil, value|
76
- next unless value
77
- out.puts " #{sigil}: #{value}#{percent(value, files)}"
78
- end
79
-
80
- out.puts "\nMethods:"
81
- m = metrics['types.input.methods.total']
82
- s = metrics['types.sig.count']
83
- out.puts " methods: #{m}"
84
- out.puts " signatures: #{s}#{percent(s, m)}"
85
-
86
- out.puts "\nSends:"
87
- t = metrics['types.input.sends.typed']
88
- s = metrics['types.input.sends.total']
89
- out.puts " sends: #{s}"
90
- out.puts " typed: #{t}#{percent(t, s)}"
91
- end
92
-
93
- private
94
-
95
- sig { params(value: T.nilable(Integer), total: T.nilable(Integer)).returns(String) }
96
- def percent(value, total)
97
- return "" if value.nil? || total.nil? || total == 0
98
- " (#{value * 100 / total}%)"
99
- end
100
31
  end
101
32
  end
102
33
  end
@@ -0,0 +1,98 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # The term "sigil" refers to the magic comment at the top of the file that has the form `# typed: <strictness>`,
5
+ # where "strictness" represents the level at which Sorbet will report errors
6
+ # See https://sorbet.org/docs/static for a more complete explanation
7
+ module Spoom
8
+ module Sorbet
9
+ module Sigils
10
+ extend T::Sig
11
+
12
+ STRICTNESS_IGNORE = "ignore"
13
+ STRICTNESS_FALSE = "false"
14
+ STRICTNESS_TRUE = "true"
15
+ STRICTNESS_STRICT = "strict"
16
+ STRICTNESS_STRONG = "strong"
17
+ STRICTNESS_INTERNAL = "__STDLIB_INTERNAL"
18
+
19
+ VALID_STRICTNESS = T.let([
20
+ STRICTNESS_IGNORE,
21
+ STRICTNESS_FALSE,
22
+ STRICTNESS_TRUE,
23
+ STRICTNESS_STRICT,
24
+ STRICTNESS_STRONG,
25
+ STRICTNESS_INTERNAL,
26
+ ].freeze, T::Array[String])
27
+
28
+ SIGIL_REGEXP = T.let(/^#\s*typed\s*:\s*(\w*)\s*$/.freeze, Regexp)
29
+
30
+ # returns the full sigil comment string for the passed strictness
31
+ sig { params(strictness: String).returns(String) }
32
+ def self.sigil_string(strictness)
33
+ "# typed: #{strictness}"
34
+ end
35
+
36
+ # returns true if the passed string is a valid strictness (else false)
37
+ sig { params(strictness: String).returns(T::Boolean) }
38
+ def self.valid_strictness?(strictness)
39
+ VALID_STRICTNESS.include?(strictness.strip)
40
+ end
41
+
42
+ # returns the strictness of a sigil in the passed file content string (nil if no sigil)
43
+ sig { params(content: String).returns(T.nilable(String)) }
44
+ def self.strictness_in_content(content)
45
+ SIGIL_REGEXP.match(content)&.[](1)
46
+ end
47
+
48
+ # returns a string which is the passed content but with the sigil updated to a new strictness
49
+ sig { params(content: String, new_strictness: String).returns(String) }
50
+ def self.update_sigil(content, new_strictness)
51
+ content.sub(SIGIL_REGEXP, sigil_string(new_strictness))
52
+ end
53
+
54
+ # returns a string containing the strictness of a sigil in a file at the passed path
55
+ # * returns nil if no sigil
56
+ sig { params(path: T.any(String, Pathname)).returns(T.nilable(String)) }
57
+ def self.file_strictness(path)
58
+ return nil unless File.exist?(path)
59
+ content = File.read(path, encoding: Encoding::ASCII_8BIT)
60
+ strictness_in_content(content)
61
+ end
62
+
63
+ # changes the sigil in the file at the passed path to the specified new strictness
64
+ sig { params(path: T.any(String, Pathname), new_strictness: String).returns(T::Boolean) }
65
+ def self.change_sigil_in_file(path, new_strictness)
66
+ content = File.read(path, encoding: Encoding::ASCII_8BIT)
67
+ new_content = update_sigil(content, new_strictness)
68
+
69
+ File.write(path, new_content)
70
+
71
+ strictness_in_content(new_content) == new_strictness
72
+ end
73
+
74
+ # changes the sigil to have a new strictness in a list of files
75
+ sig { params(path_list: T::Array[String], new_strictness: String).returns(T::Array[String]) }
76
+ def self.change_sigil_in_files(path_list, new_strictness)
77
+ path_list.filter do |path|
78
+ change_sigil_in_file(path, new_strictness)
79
+ end
80
+ end
81
+
82
+ # finds all files in the specified directory with the passed strictness
83
+ sig do
84
+ params(
85
+ directory: T.any(String, Pathname),
86
+ strictness: String,
87
+ extension: String
88
+ ).returns(T::Array[String])
89
+ end
90
+ def self.files_with_sigil_strictness(directory, strictness, extension: ".rb")
91
+ paths = Dir.glob("#{File.expand_path(directory)}/**/*#{extension}").sort.uniq
92
+ paths.filter do |path|
93
+ file_strictness(path) == strictness
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end