spoom 1.0.6 → 1.1.1
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 +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
|