spoom 1.0.6 → 1.1.1

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