spoom 1.0.6 → 1.1.1
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 +51 -2
- data/lib/spoom.rb +20 -2
- data/lib/spoom/cli.rb +25 -14
- data/lib/spoom/cli/bump.rb +106 -13
- data/lib/spoom/cli/config.rb +3 -3
- data/lib/spoom/cli/coverage.rb +57 -42
- data/lib/spoom/cli/helper.rb +88 -9
- data/lib/spoom/cli/lsp.rb +20 -20
- data/lib/spoom/cli/run.rb +55 -25
- data/lib/spoom/coverage.rb +33 -7
- data/lib/spoom/coverage/d3/timeline.rb +146 -9
- data/lib/spoom/coverage/report.rb +13 -3
- data/lib/spoom/coverage/snapshot.rb +3 -1
- data/lib/spoom/file_tree.rb +1 -1
- data/lib/spoom/git.rb +2 -1
- data/lib/spoom/printer.rb +0 -1
- data/lib/spoom/sorbet.rb +97 -58
- data/lib/spoom/sorbet/config.rb +30 -0
- data/lib/spoom/sorbet/errors.rb +8 -0
- data/lib/spoom/sorbet/lsp.rb +2 -6
- data/lib/spoom/sorbet/sigils.rb +3 -3
- data/lib/spoom/test_helpers/project.rb +9 -0
- data/lib/spoom/version.rb +2 -2
- metadata +6 -7
- data/lib/spoom/config.rb +0 -11
@@ -36,10 +36,28 @@ module Spoom
|
|
36
36
|
pointer-events: none;
|
37
37
|
}
|
38
38
|
|
39
|
+
.area {
|
40
|
+
fill-opacity: 0.5;
|
41
|
+
}
|
42
|
+
|
43
|
+
.line {
|
44
|
+
stroke-width: 2;
|
45
|
+
fill: transparent;
|
46
|
+
}
|
47
|
+
|
48
|
+
.dot {
|
49
|
+
r: 2;
|
50
|
+
fill: #888;
|
51
|
+
}
|
52
|
+
|
39
53
|
.inverted .grid line {
|
40
54
|
stroke: #777;
|
41
55
|
}
|
42
56
|
|
57
|
+
.inverted .area {
|
58
|
+
fill-opacity: 0.9;
|
59
|
+
}
|
60
|
+
|
43
61
|
.inverted .axis text {
|
44
62
|
fill: #fff;
|
45
63
|
}
|
@@ -47,6 +65,10 @@ module Spoom
|
|
47
65
|
.inverted .axis line {
|
48
66
|
stroke: #fff;
|
49
67
|
}
|
68
|
+
|
69
|
+
.inverted .dot {
|
70
|
+
fill: #fff;
|
71
|
+
}
|
50
72
|
CSS
|
51
73
|
end
|
52
74
|
|
@@ -170,7 +192,6 @@ module Spoom
|
|
170
192
|
.y1((d) => yScale_#{id}(#{y}))
|
171
193
|
.curve(d3.#{curve}))
|
172
194
|
.attr("fill", "#{color}")
|
173
|
-
.attr("fill-opacity", 0.5)
|
174
195
|
HTML
|
175
196
|
end
|
176
197
|
|
@@ -185,8 +206,6 @@ module Spoom
|
|
185
206
|
.y((d) => yScale_#{id}(#{y}))
|
186
207
|
.curve(d3.#{curve}))
|
187
208
|
.attr("stroke", "#{color}")
|
188
|
-
.attr("stroke-width", 3)
|
189
|
-
.attr("fill", "transparent")
|
190
209
|
HTML
|
191
210
|
end
|
192
211
|
|
@@ -198,10 +217,8 @@ module Spoom
|
|
198
217
|
.enter()
|
199
218
|
.append("circle")
|
200
219
|
.attr("class", "dot")
|
201
|
-
.attr("r", 3)
|
202
220
|
.attr("cx", (d) => xScale_#{id}(parseDate(d.timestamp)))
|
203
221
|
.attr("cy", (d, i) => yScale_#{id}(#{y}))
|
204
|
-
.attr("fill", "#aaa")
|
205
222
|
.on("mouseover", (d) => tooltip.style("opacity", 1))
|
206
223
|
.on("mousemove", tooltip_#{id})
|
207
224
|
.on("mouseleave", (d) => tooltip.style("opacity", 0));
|
@@ -381,18 +398,15 @@ module Spoom
|
|
381
398
|
layer.append("path")
|
382
399
|
.attr("class", "area")
|
383
400
|
.attr("d", area_#{id})
|
384
|
-
.attr("fill", (d) =>
|
385
|
-
.attr("fill-opacity", 0.9)
|
401
|
+
.attr("fill", (d) => #{color})
|
386
402
|
|
387
403
|
svg_#{id}.selectAll("circle")
|
388
404
|
.data(points_#{id})
|
389
405
|
.enter()
|
390
406
|
.append("circle")
|
391
407
|
.attr("class", "dot")
|
392
|
-
.attr("r", 2)
|
393
408
|
.attr("cx", (d) => xScale_#{id}(parseDate(#{y})))
|
394
409
|
.attr("cy", (d, i) => yScale_#{id}(d[1]))
|
395
|
-
.attr("fill", "#fff")
|
396
410
|
.on("mouseover", (d) => tooltip.style("opacity", 1))
|
397
411
|
.on("mousemove", tooltip_#{id})
|
398
412
|
.on("mouseleave", (d) => tooltip.style("opacity", 0));
|
@@ -480,6 +494,129 @@ module Spoom
|
|
480
494
|
JS
|
481
495
|
end
|
482
496
|
end
|
497
|
+
|
498
|
+
class RBIs < Stacked
|
499
|
+
extend T::Sig
|
500
|
+
|
501
|
+
sig { params(id: String, snapshots: T::Array[Snapshot]).void }
|
502
|
+
def initialize(id, snapshots)
|
503
|
+
keys = ['rbis', 'files']
|
504
|
+
data = snapshots.map do |snapshot|
|
505
|
+
{
|
506
|
+
timestamp: snapshot.commit_timestamp,
|
507
|
+
commit: snapshot.commit_sha,
|
508
|
+
total: snapshot.files,
|
509
|
+
values: { files: snapshot.files - snapshot.rbi_files, rbis: snapshot.rbi_files },
|
510
|
+
}
|
511
|
+
end
|
512
|
+
super(id, data, keys)
|
513
|
+
end
|
514
|
+
|
515
|
+
sig { override.returns(String) }
|
516
|
+
def tooltip
|
517
|
+
<<~JS
|
518
|
+
function tooltip_#{id}(d) {
|
519
|
+
moveTooltip(d)
|
520
|
+
.html("commit <b>" + d.data.commit + "</b><br>"
|
521
|
+
+ d3.timeFormat("%y/%m/%d")(parseDate(d.data.timestamp)) + "<br><br>"
|
522
|
+
+ "Files: <b>" + d.data.values.files + "</b><br>"
|
523
|
+
+ "RBIs: <b>" + d.data.values.rbis + "</b><br><br>"
|
524
|
+
+ "Total: <b>" + d.data.total + "</b>")
|
525
|
+
}
|
526
|
+
JS
|
527
|
+
end
|
528
|
+
|
529
|
+
sig { override.returns(String) }
|
530
|
+
def script
|
531
|
+
<<~JS
|
532
|
+
#{tooltip}
|
533
|
+
|
534
|
+
var data_#{id} = #{@data.to_json};
|
535
|
+
var keys_#{id} = #{T.unsafe(@keys).to_json};
|
536
|
+
|
537
|
+
var stack_#{id} = d3.stack()
|
538
|
+
.keys(keys_#{id})
|
539
|
+
.value((d, key) => d.values[key]);
|
540
|
+
|
541
|
+
var layers_#{id} = stack_#{id}(data_#{id});
|
542
|
+
|
543
|
+
var points_#{id} = []
|
544
|
+
layers_#{id}.forEach(function(d) {
|
545
|
+
d.forEach(function(p) {
|
546
|
+
p.key = d.key
|
547
|
+
points_#{id}.push(p);
|
548
|
+
});
|
549
|
+
})
|
550
|
+
|
551
|
+
function draw_#{id}() {
|
552
|
+
var width_#{id} = document.getElementById("#{id}").clientWidth;
|
553
|
+
var height_#{id} = 200;
|
554
|
+
|
555
|
+
d3.select("##{id}").selectAll("*").remove()
|
556
|
+
|
557
|
+
var svg_#{id} = d3.select("##{id}")
|
558
|
+
.attr("width", width_#{id})
|
559
|
+
.attr("height", height_#{id});
|
560
|
+
|
561
|
+
#{plot}
|
562
|
+
}
|
563
|
+
|
564
|
+
draw_#{id}();
|
565
|
+
window.addEventListener("resize", draw_#{id});
|
566
|
+
JS
|
567
|
+
end
|
568
|
+
|
569
|
+
sig { override.params(y: String, color: String, curve: String).returns(String) }
|
570
|
+
def line(y:, color: 'strictnessColor(d.key)', curve: 'curveCatmullRom.alpha(1)')
|
571
|
+
<<~JS
|
572
|
+
var area_#{id} = d3.area()
|
573
|
+
.x((d) => xScale_#{id}(parseDate(#{y})))
|
574
|
+
.y0((d) => yScale_#{id}(d[0]))
|
575
|
+
.y1((d) => yScale_#{id}(d[1]))
|
576
|
+
.curve(d3.#{curve});
|
577
|
+
|
578
|
+
var layer = svg_#{id}.selectAll(".layer")
|
579
|
+
.data(layers_#{id})
|
580
|
+
.enter().append("g")
|
581
|
+
.attr("class", "layer")
|
582
|
+
|
583
|
+
layer.append("path")
|
584
|
+
.attr("class", "area")
|
585
|
+
.attr("d", area_#{id})
|
586
|
+
.attr("fill", (d) => #{color})
|
587
|
+
|
588
|
+
layer.append("path")
|
589
|
+
.attr("class", "line")
|
590
|
+
.attr("d", d3.line()
|
591
|
+
.x((d) => xScale_#{id}(parseDate(#{y})))
|
592
|
+
.y((d, i) => yScale_#{id}(d[1]))
|
593
|
+
.curve(d3.#{curve}))
|
594
|
+
.attr("stroke", (d) => #{color})
|
595
|
+
|
596
|
+
svg_#{id}.selectAll("circle")
|
597
|
+
.data(points_#{id})
|
598
|
+
.enter()
|
599
|
+
.append("circle")
|
600
|
+
.attr("class", "dot")
|
601
|
+
.attr("cx", (d) => xScale_#{id}(parseDate(#{y})))
|
602
|
+
.attr("cy", (d, i) => yScale_#{id}(d[1]))
|
603
|
+
.on("mouseover", (d) => tooltip.style("opacity", 1))
|
604
|
+
.on("mousemove", tooltip_#{id})
|
605
|
+
.on("mouseleave", (d) => tooltip.style("opacity", 0));
|
606
|
+
JS
|
607
|
+
end
|
608
|
+
|
609
|
+
sig { override.returns(String) }
|
610
|
+
def plot
|
611
|
+
<<~JS
|
612
|
+
#{x_scale}
|
613
|
+
#{y_scale(min: '0', max: "d3.max(data_#{id}, (d) => d.total + 10)", ticks: 'tickValues([0, 25, 50, 75, 100])')}
|
614
|
+
#{line(y: 'd.data.timestamp', color: "d.key == 'rbis' ? '#8673ff' : '#007bff'")}
|
615
|
+
#{x_ticks}
|
616
|
+
#{y_ticks(ticks: 'tickValues([25, 50, 75])', format: 'd', padding: 20)}
|
617
|
+
JS
|
618
|
+
end
|
619
|
+
end
|
483
620
|
end
|
484
621
|
end
|
485
622
|
end
|
@@ -41,7 +41,7 @@ module Spoom
|
|
41
41
|
|
42
42
|
abstract!
|
43
43
|
|
44
|
-
TEMPLATE = T.let("#{Spoom::
|
44
|
+
TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/page.erb", String)
|
45
45
|
|
46
46
|
sig { returns(String) }
|
47
47
|
attr_reader :title
|
@@ -89,7 +89,7 @@ module Spoom
|
|
89
89
|
class Card < Template
|
90
90
|
extend T::Sig
|
91
91
|
|
92
|
-
TEMPLATE = T.let("#{Spoom::
|
92
|
+
TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/card.erb", String)
|
93
93
|
|
94
94
|
sig { returns(T.nilable(String)) }
|
95
95
|
attr_reader :title, :body
|
@@ -123,7 +123,7 @@ module Spoom
|
|
123
123
|
class Snapshot < Card
|
124
124
|
extend T::Sig
|
125
125
|
|
126
|
-
TEMPLATE = T.let("#{Spoom::
|
126
|
+
TEMPLATE = T.let("#{Spoom::SPOOM_PATH}/templates/card_snapshot.erb", String)
|
127
127
|
|
128
128
|
sig { returns(Coverage::Snapshot) }
|
129
129
|
attr_reader :snapshot
|
@@ -194,6 +194,15 @@ module Spoom
|
|
194
194
|
end
|
195
195
|
end
|
196
196
|
|
197
|
+
class RBIs < Timeline
|
198
|
+
extend T::Sig
|
199
|
+
|
200
|
+
sig { params(snapshots: T::Array[Coverage::Snapshot], title: String).void }
|
201
|
+
def initialize(snapshots:, title: "RBIs Timeline")
|
202
|
+
super(title: title, timeline: D3::Timeline::RBIs.new("timeline_rbis", snapshots))
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
197
206
|
class Versions < Timeline
|
198
207
|
extend T::Sig
|
199
208
|
|
@@ -298,6 +307,7 @@ module Spoom
|
|
298
307
|
cards << Cards::Timeline::Sigils.new(snapshots: snapshots)
|
299
308
|
cards << Cards::Timeline::Calls.new(snapshots: snapshots)
|
300
309
|
cards << Cards::Timeline::Sigs.new(snapshots: snapshots)
|
310
|
+
cards << Cards::Timeline::RBIs.new(snapshots: snapshots)
|
301
311
|
cards << Cards::Timeline::Versions.new(snapshots: snapshots)
|
302
312
|
cards << Cards::Timeline::Runtimes.new(snapshots: snapshots)
|
303
313
|
cards << Cards::SorbetIntro.new(sorbet_intro_commit: sorbet_intro_commit, sorbet_intro_date: sorbet_intro_date)
|
@@ -13,6 +13,7 @@ module Spoom
|
|
13
13
|
prop :commit_sha, T.nilable(String), default: nil
|
14
14
|
prop :commit_timestamp, T.nilable(Integer), default: nil
|
15
15
|
prop :files, Integer, default: 0
|
16
|
+
prop :rbi_files, Integer, default: 0
|
16
17
|
prop :modules, Integer, default: 0
|
17
18
|
prop :classes, Integer, default: 0
|
18
19
|
prop :singleton_classes, Integer, default: 0
|
@@ -46,6 +47,7 @@ module Spoom
|
|
46
47
|
snapshot.commit_sha = obj.fetch("commit_sha", nil)
|
47
48
|
snapshot.commit_timestamp = obj.fetch("commit_timestamp", nil)
|
48
49
|
snapshot.files = obj.fetch("files", 0)
|
50
|
+
snapshot.rbi_files = obj.fetch("rbi_files", 0)
|
49
51
|
snapshot.modules = obj.fetch("modules", 0)
|
50
52
|
snapshot.classes = obj.fetch("classes", 0)
|
51
53
|
snapshot.singleton_classes = obj.fetch("singleton_classes", 0)
|
@@ -86,7 +88,7 @@ module Spoom
|
|
86
88
|
end
|
87
89
|
printl("Content:")
|
88
90
|
indent
|
89
|
-
printl("files: #{snapshot.files}")
|
91
|
+
printl("files: #{snapshot.files} (including #{snapshot.rbi_files} RBIs)")
|
90
92
|
printl("modules: #{snapshot.modules}")
|
91
93
|
printl("classes: #{snapshot.classes - snapshot.singleton_classes}")
|
92
94
|
printl("methods: #{methods}")
|
data/lib/spoom/file_tree.rb
CHANGED
@@ -80,7 +80,7 @@ module Spoom
|
|
80
80
|
|
81
81
|
private
|
82
82
|
|
83
|
-
sig { params(node: FileTree::Node, collected_nodes: T::Array[Node]).returns(T::Array[
|
83
|
+
sig { params(node: FileTree::Node, collected_nodes: T::Array[Node]).returns(T::Array[Node]) }
|
84
84
|
def collect_nodes(node, collected_nodes = [])
|
85
85
|
collected_nodes << node
|
86
86
|
node.children.values.each { |child| collect_nodes(child, collected_nodes) }
|
data/lib/spoom/git.rb
CHANGED
@@ -14,11 +14,12 @@ module Spoom
|
|
14
14
|
return "", "Error: `#{path}` is not a directory.", false unless File.directory?(path)
|
15
15
|
opts = {}
|
16
16
|
opts[:chdir] = path
|
17
|
-
|
17
|
+
i, o, e, s = Open3.popen3(*T.unsafe([command, *T.unsafe(arg), opts]))
|
18
18
|
out = o.read.to_s
|
19
19
|
o.close
|
20
20
|
err = e.read.to_s
|
21
21
|
e.close
|
22
|
+
i.close
|
22
23
|
[out, err, T.cast(s.value, Process::Status).success?]
|
23
24
|
end
|
24
25
|
|
data/lib/spoom/printer.rb
CHANGED
data/lib/spoom/sorbet.rb
CHANGED
@@ -11,73 +11,112 @@ require "open3"
|
|
11
11
|
|
12
12
|
module Spoom
|
13
13
|
module Sorbet
|
14
|
-
|
14
|
+
CONFIG_PATH = "sorbet/config"
|
15
|
+
GEM_PATH = Gem::Specification.find_by_name("sorbet-static").full_gem_path
|
16
|
+
BIN_PATH = (Pathname.new(GEM_PATH) / "libexec" / "sorbet").to_s
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
18
|
+
class << self
|
19
|
+
extend T::Sig
|
20
|
+
|
21
|
+
sig do
|
22
|
+
params(
|
23
|
+
arg: String,
|
24
|
+
path: String,
|
25
|
+
capture_err: T::Boolean,
|
26
|
+
sorbet_bin: T.nilable(String)
|
27
|
+
).returns([String, T::Boolean])
|
28
|
+
end
|
29
|
+
def srb(*arg, path: '.', capture_err: false, sorbet_bin: nil)
|
30
|
+
if sorbet_bin
|
31
|
+
arg.prepend(sorbet_bin)
|
32
|
+
else
|
33
|
+
arg.prepend("bundle", "exec", "srb")
|
31
34
|
end
|
35
|
+
T.unsafe(Spoom).exec(*arg, path: path, capture_err: capture_err)
|
32
36
|
end
|
33
|
-
[out || "", res]
|
34
|
-
end
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
sig do
|
39
|
+
params(
|
40
|
+
arg: String,
|
41
|
+
path: String,
|
42
|
+
capture_err: T::Boolean,
|
43
|
+
sorbet_bin: T.nilable(String)
|
44
|
+
).returns([String, T::Boolean])
|
45
|
+
end
|
46
|
+
def srb_tc(*arg, path: '.', capture_err: false, sorbet_bin: nil)
|
47
|
+
arg.prepend("tc") unless sorbet_bin
|
48
|
+
T.unsafe(self).srb(*arg, path: path, capture_err: capture_err, sorbet_bin: sorbet_bin)
|
49
|
+
end
|
40
50
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
# List all files typechecked by Sorbet from its `config`
|
52
|
+
sig { params(config: Config, path: String).returns(T::Array[String]) }
|
53
|
+
def srb_files(config, path: '.')
|
54
|
+
regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
|
55
|
+
exts = config.allowed_extensions.empty? ? ['.rb', '.rbi'] : config.allowed_extensions
|
56
|
+
Dir.glob((Pathname.new(path) / "**/*{#{exts.join(',')}}").to_s).reject do |f|
|
57
|
+
regs.any? { |re| re.match?(f) }
|
58
|
+
end.sort
|
59
|
+
end
|
50
60
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
61
|
+
sig do
|
62
|
+
params(
|
63
|
+
arg: String,
|
64
|
+
path: String,
|
65
|
+
capture_err: T::Boolean,
|
66
|
+
sorbet_bin: T.nilable(String)
|
67
|
+
).returns(T.nilable(String))
|
68
|
+
end
|
69
|
+
def srb_version(*arg, path: '.', capture_err: false, sorbet_bin: nil)
|
70
|
+
out, res = T.unsafe(self).srb_tc(
|
71
|
+
"--no-config",
|
72
|
+
"--version",
|
73
|
+
*arg,
|
74
|
+
path: path,
|
75
|
+
capture_err: capture_err,
|
76
|
+
sorbet_bin: sorbet_bin
|
77
|
+
)
|
78
|
+
return nil unless res
|
79
|
+
out.split(" ")[2]
|
80
|
+
end
|
57
81
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
82
|
+
sig do
|
83
|
+
params(
|
84
|
+
arg: String,
|
85
|
+
path: String,
|
86
|
+
capture_err: T::Boolean,
|
87
|
+
sorbet_bin: T.nilable(String)
|
88
|
+
).returns(T.nilable(T::Hash[String, Integer]))
|
89
|
+
end
|
90
|
+
def srb_metrics(*arg, path: '.', capture_err: false, sorbet_bin: nil)
|
91
|
+
metrics_file = "metrics.tmp"
|
92
|
+
metrics_path = "#{path}/#{metrics_file}"
|
93
|
+
T.unsafe(self).srb_tc(
|
94
|
+
"--metrics-file",
|
95
|
+
metrics_file,
|
96
|
+
*arg,
|
97
|
+
path: path,
|
98
|
+
capture_err: capture_err,
|
99
|
+
sorbet_bin: sorbet_bin
|
100
|
+
)
|
101
|
+
if File.exist?(metrics_path)
|
102
|
+
metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
|
103
|
+
File.delete(metrics_path)
|
104
|
+
return metrics
|
105
|
+
end
|
106
|
+
nil
|
67
107
|
end
|
68
|
-
nil
|
69
|
-
end
|
70
108
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
109
|
+
# Get `gem` version from the `Gemfile.lock` content
|
110
|
+
#
|
111
|
+
# Returns `nil` if `gem` cannot be found in the Gemfile.
|
112
|
+
sig { params(gem: String, path: String).returns(T.nilable(String)) }
|
113
|
+
def version_from_gemfile_lock(gem: 'sorbet', path: '.')
|
114
|
+
gemfile_path = "#{path}/Gemfile.lock"
|
115
|
+
return nil unless File.exist?(gemfile_path)
|
116
|
+
content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
|
117
|
+
return nil unless content
|
118
|
+
content[1]
|
119
|
+
end
|
81
120
|
end
|
82
121
|
end
|
83
122
|
end
|