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.
@@ -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) => strictnessColor(d.key))
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::Config::SPOOM_PATH}/templates/page.erb", String)
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::Config::SPOOM_PATH}/templates/card.erb", String)
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::Config::SPOOM_PATH}/templates/card_snapshot.erb", String)
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}")
@@ -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[String]) }
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
- _, o, e, s = Open3.popen3(*T.unsafe([command, *T.unsafe(arg), opts]))
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
@@ -1,7 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "colorize"
5
4
  require "stringio"
6
5
 
7
6
  module Spoom
data/lib/spoom/sorbet.rb CHANGED
@@ -11,73 +11,112 @@ require "open3"
11
11
 
12
12
  module Spoom
13
13
  module Sorbet
14
- extend T::Sig
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
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns([String, T::Boolean]) }
17
- def self.srb(*arg, path: '.', capture_err: false)
18
- opts = {}
19
- opts[:chdir] = path
20
- out = T.let("", T.nilable(String))
21
- res = T.let(false, T::Boolean)
22
- if capture_err
23
- Open3.popen2e(["bundle", "exec", "srb", *arg].join(" "), opts) do |_, o, t|
24
- out = o.read
25
- res = T.cast(t.value, Process::Status).success?
26
- end
27
- else
28
- Open3.popen2(["bundle", "exec", "srb", *arg].join(" "), opts) do |_, o, t|
29
- out = o.read
30
- res = T.cast(t.value, Process::Status).success?
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
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns([String, T::Boolean]) }
37
- def self.srb_tc(*arg, path: '.', capture_err: false)
38
- srb(*T.unsafe(["tc", *arg]), path: path, capture_err: capture_err)
39
- end
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
- # List all files typechecked by Sorbet from its `config`
42
- sig { params(config: Config, path: String).returns(T::Array[String]) }
43
- def self.srb_files(config, path: '.')
44
- regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
45
- exts = config.allowed_extensions.empty? ? ['.rb', '.rbi'] : config.allowed_extensions
46
- Dir.glob((Pathname.new(path) / "**/*{#{exts.join(',')}}").to_s).reject do |f|
47
- regs.any? { |re| re.match?(f) }
48
- end.sort
49
- end
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
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns(T.nilable(String)) }
52
- def self.srb_version(*arg, path: '.', capture_err: false)
53
- out, res = srb(*T.unsafe(["--version", *arg]), path: path, capture_err: capture_err)
54
- return nil unless res
55
- out.split(" ")[2]
56
- end
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
- sig { params(arg: String, path: String, capture_err: T::Boolean).returns(T.nilable(T::Hash[String, Integer])) }
59
- def self.srb_metrics(*arg, path: '.', capture_err: false)
60
- metrics_file = "metrics.tmp"
61
- metrics_path = "#{path}/#{metrics_file}"
62
- srb_tc(*T.unsafe(["--metrics-file=#{metrics_file}", *arg]), path: path, capture_err: capture_err)
63
- if File.exist?(metrics_path)
64
- metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
65
- File.delete(metrics_path)
66
- return metrics
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
- # Get `gem` version from the `Gemfile.lock` content
72
- #
73
- # Returns `nil` if `gem` cannot be found in the Gemfile.
74
- sig { params(gem: String, path: String).returns(T.nilable(String)) }
75
- def self.version_from_gemfile_lock(gem: 'sorbet', path: '.')
76
- gemfile_path = "#{path}/Gemfile.lock"
77
- return nil unless File.exist?(gemfile_path)
78
- content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
79
- return nil unless content
80
- content[1]
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