spoom 1.0.4 → 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
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