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.
- checksums.yaml +4 -4
- data/Gemfile +0 -1
- data/README.md +296 -1
- data/Rakefile +1 -0
- data/lib/spoom.rb +21 -2
- data/lib/spoom/cli.rb +56 -10
- data/lib/spoom/cli/bump.rb +138 -0
- data/lib/spoom/cli/config.rb +51 -0
- data/lib/spoom/cli/coverage.rb +206 -0
- data/lib/spoom/cli/helper.rb +149 -0
- data/lib/spoom/cli/lsp.rb +165 -0
- data/lib/spoom/cli/run.rb +109 -0
- data/lib/spoom/coverage.rb +89 -0
- data/lib/spoom/coverage/d3.rb +110 -0
- data/lib/spoom/coverage/d3/base.rb +50 -0
- data/lib/spoom/coverage/d3/circle_map.rb +195 -0
- data/lib/spoom/coverage/d3/pie.rb +175 -0
- data/lib/spoom/coverage/d3/timeline.rb +486 -0
- data/lib/spoom/coverage/report.rb +308 -0
- data/lib/spoom/coverage/snapshot.rb +132 -0
- data/lib/spoom/file_tree.rb +196 -0
- data/lib/spoom/git.rb +98 -0
- data/lib/spoom/printer.rb +80 -0
- data/lib/spoom/sorbet.rb +99 -47
- data/lib/spoom/sorbet/config.rb +30 -0
- data/lib/spoom/sorbet/errors.rb +33 -15
- data/lib/spoom/sorbet/lsp.rb +2 -4
- data/lib/spoom/sorbet/lsp/structures.rb +108 -14
- data/lib/spoom/sorbet/metrics.rb +10 -79
- data/lib/spoom/sorbet/sigils.rb +98 -0
- data/lib/spoom/test_helpers/project.rb +112 -0
- data/lib/spoom/timeline.rb +53 -0
- data/lib/spoom/version.rb +2 -2
- data/templates/card.erb +8 -0
- data/templates/card_snapshot.erb +22 -0
- data/templates/page.erb +50 -0
- metadata +28 -11
- data/lib/spoom/cli/commands/base.rb +0 -36
- data/lib/spoom/cli/commands/config.rb +0 -67
- data/lib/spoom/cli/commands/lsp.rb +0 -156
- data/lib/spoom/cli/commands/run.rb +0 -92
- data/lib/spoom/cli/symbol_printer.rb +0 -71
- data/lib/spoom/config.rb +0 -11
data/lib/spoom/sorbet/lsp.rb
CHANGED
@@ -11,11 +11,9 @@ require_relative 'lsp/errors'
|
|
11
11
|
module Spoom
|
12
12
|
module LSP
|
13
13
|
class Client
|
14
|
-
def initialize(
|
14
|
+
def initialize(sorbet_bin, *sorbet_args, path: ".")
|
15
15
|
@id = 0
|
16
|
-
|
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.
|
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.
|
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.
|
60
|
-
printer.
|
61
|
-
printer.
|
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.
|
82
|
-
printer.
|
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.
|
163
|
-
printer.
|
206
|
+
printer.print_colored(name, :blue, :bold)
|
207
|
+
printer.print_colored(' (', :light_black)
|
164
208
|
if range
|
165
|
-
printer.
|
209
|
+
printer.print_object(range)
|
166
210
|
elsif location
|
167
|
-
printer.
|
211
|
+
printer.print_object(location)
|
168
212
|
end
|
169
|
-
printer.
|
213
|
+
printer.print_colored(')', :light_black)
|
170
214
|
printer.printn
|
171
215
|
unless children.empty?
|
172
216
|
printer.indent
|
173
|
-
printer.
|
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
|
data/lib/spoom/sorbet/metrics.rb
CHANGED
@@ -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
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|