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