spoom 1.0.0 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +7 -1
- data/README.md +253 -1
- data/Rakefile +2 -0
- data/exe/spoom +7 -0
- data/lib/spoom.rb +9 -1
- data/lib/spoom/cli.rb +68 -0
- data/lib/spoom/cli/bump.rb +59 -0
- data/lib/spoom/cli/config.rb +51 -0
- data/lib/spoom/cli/coverage.rb +191 -0
- data/lib/spoom/cli/helper.rb +70 -0
- data/lib/spoom/cli/lsp.rb +165 -0
- data/lib/spoom/cli/run.rb +79 -0
- data/lib/spoom/config.rb +11 -0
- data/lib/spoom/coverage.rb +73 -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 +81 -0
- data/lib/spoom/sorbet.rb +83 -0
- data/lib/spoom/sorbet/config.rb +21 -9
- data/lib/spoom/sorbet/errors.rb +139 -0
- data/lib/spoom/sorbet/lsp.rb +196 -0
- data/lib/spoom/sorbet/lsp/base.rb +58 -0
- data/lib/spoom/sorbet/lsp/errors.rb +45 -0
- data/lib/spoom/sorbet/lsp/structures.rb +312 -0
- data/lib/spoom/sorbet/metrics.rb +33 -0
- data/lib/spoom/sorbet/sigils.rb +98 -0
- data/lib/spoom/test_helpers/project.rb +103 -0
- data/lib/spoom/timeline.rb +53 -0
- data/lib/spoom/version.rb +2 -1
- metadata +68 -25
@@ -0,0 +1,79 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Cli
|
6
|
+
class Run < Thor
|
7
|
+
include Helper
|
8
|
+
|
9
|
+
default_task :tc
|
10
|
+
|
11
|
+
desc "tc", "run srb tc"
|
12
|
+
option :limit, type: :numeric, aliases: :l
|
13
|
+
option :code, type: :numeric, aliases: :c
|
14
|
+
option :sort, type: :string, aliases: :s
|
15
|
+
def tc
|
16
|
+
in_sorbet_project!
|
17
|
+
|
18
|
+
path = exec_path
|
19
|
+
limit = options[:limit]
|
20
|
+
sort = options[:sort]
|
21
|
+
code = options[:code]
|
22
|
+
colors = options[:color]
|
23
|
+
|
24
|
+
unless limit || code || sort
|
25
|
+
return Spoom::Sorbet.srb_tc(path: path, capture_err: false).last
|
26
|
+
end
|
27
|
+
|
28
|
+
output, status = Spoom::Sorbet.srb_tc(path: path, capture_err: true)
|
29
|
+
if status
|
30
|
+
$stderr.print(output)
|
31
|
+
return 0
|
32
|
+
end
|
33
|
+
|
34
|
+
errors = Spoom::Sorbet::Errors::Parser.parse_string(output)
|
35
|
+
errors_count = errors.size
|
36
|
+
|
37
|
+
errors = sort == "code" ? errors.sort_by { |e| [e.code, e.file, e.line, e.message] } : errors.sort
|
38
|
+
errors = errors.select { |e| e.code == code } if code
|
39
|
+
errors = T.must(errors.slice(0, limit)) if limit
|
40
|
+
|
41
|
+
errors.each do |e|
|
42
|
+
code = colorize_code(e.code, colors)
|
43
|
+
message = colorize_message(e.message, colors)
|
44
|
+
$stderr.puts "#{code} - #{e.file}:#{e.line}: #{message}"
|
45
|
+
end
|
46
|
+
|
47
|
+
if errors_count == errors.size
|
48
|
+
$stderr.puts "Errors: #{errors_count}"
|
49
|
+
else
|
50
|
+
$stderr.puts "Errors: #{errors.size} shown, #{errors_count} total"
|
51
|
+
end
|
52
|
+
|
53
|
+
1
|
54
|
+
end
|
55
|
+
|
56
|
+
no_commands do
|
57
|
+
def colorize_code(code, colors = true)
|
58
|
+
return code.to_s unless colors
|
59
|
+
code.to_s.light_black
|
60
|
+
end
|
61
|
+
|
62
|
+
def colorize_message(message, colors = true)
|
63
|
+
return message unless colors
|
64
|
+
|
65
|
+
cyan = T.let(false, T::Boolean)
|
66
|
+
word = StringIO.new
|
67
|
+
message.chars.each do |c|
|
68
|
+
if c == '`'
|
69
|
+
cyan = !cyan
|
70
|
+
next
|
71
|
+
end
|
72
|
+
word << (cyan ? c.cyan : c.red)
|
73
|
+
end
|
74
|
+
word.string
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/spoom/config.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Config
|
6
|
+
SORBET_CONFIG = "sorbet/config"
|
7
|
+
SORBET_GEM_PATH = Gem::Specification.find_by_name("sorbet-static").full_gem_path
|
8
|
+
SORBET_PATH = (Pathname.new(SORBET_GEM_PATH) / "libexec" / "sorbet").to_s
|
9
|
+
SPOOM_PATH = (Pathname.new(__FILE__) / ".." / ".." / "..").to_s
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "coverage/snapshot"
|
5
|
+
require_relative "coverage/report"
|
6
|
+
require_relative "file_tree"
|
7
|
+
|
8
|
+
require "date"
|
9
|
+
|
10
|
+
module Spoom
|
11
|
+
module Coverage
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
sig { params(path: String).returns(Snapshot) }
|
15
|
+
def self.snapshot(path: '.')
|
16
|
+
snapshot = Snapshot.new
|
17
|
+
metrics = Spoom::Sorbet.srb_metrics(path: path, capture_err: true)
|
18
|
+
return snapshot unless metrics
|
19
|
+
|
20
|
+
sha = Spoom::Git.last_commit(path: path)
|
21
|
+
snapshot.commit_sha = sha
|
22
|
+
snapshot.commit_timestamp = Spoom::Git.commit_timestamp(sha, path: path).to_i if sha
|
23
|
+
|
24
|
+
snapshot.files = metrics.fetch("types.input.files", 0)
|
25
|
+
snapshot.modules = metrics.fetch("types.input.modules.total", 0)
|
26
|
+
snapshot.classes = metrics.fetch("types.input.classes.total", 0)
|
27
|
+
snapshot.singleton_classes = metrics.fetch("types.input.singleton_classes.total", 0)
|
28
|
+
snapshot.methods_with_sig = metrics.fetch("types.sig.count", 0)
|
29
|
+
snapshot.methods_without_sig = metrics.fetch("types.input.methods.total", 0) - snapshot.methods_with_sig
|
30
|
+
snapshot.calls_typed = metrics.fetch("types.input.sends.typed", 0)
|
31
|
+
snapshot.calls_untyped = metrics.fetch("types.input.sends.total", 0) - snapshot.calls_typed
|
32
|
+
|
33
|
+
snapshot.duration += metrics.fetch("run.utilization.system_time.us", 0)
|
34
|
+
snapshot.duration += metrics.fetch("run.utilization.user_time.us", 0)
|
35
|
+
|
36
|
+
Snapshot::STRICTNESSES.each do |strictness|
|
37
|
+
next unless metrics.key?("types.input.files.sigil.#{strictness}")
|
38
|
+
snapshot.sigils[strictness] = T.must(metrics["types.input.files.sigil.#{strictness}"])
|
39
|
+
end
|
40
|
+
|
41
|
+
snapshot.version_static = Spoom::Sorbet.version_from_gemfile_lock(gem: "sorbet-static", path: path)
|
42
|
+
snapshot.version_runtime = Spoom::Sorbet.version_from_gemfile_lock(gem: "sorbet-runtime", path: path)
|
43
|
+
|
44
|
+
snapshot
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { params(snapshots: T::Array[Snapshot], palette: D3::ColorPalette, path: String).returns(Report) }
|
48
|
+
def self.report(snapshots, palette:, path: ".")
|
49
|
+
intro_commit = Git.sorbet_intro_commit(path: path)
|
50
|
+
intro_date = intro_commit ? Git.commit_time(intro_commit, path: path) : nil
|
51
|
+
|
52
|
+
Report.new(
|
53
|
+
project_name: File.basename(File.expand_path(path)),
|
54
|
+
palette: palette,
|
55
|
+
snapshots: snapshots,
|
56
|
+
sigils_tree: sigils_tree(path: path),
|
57
|
+
sorbet_intro_commit: intro_commit,
|
58
|
+
sorbet_intro_date: intro_date,
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { params(path: String).returns(FileTree) }
|
63
|
+
def self.sigils_tree(path: ".")
|
64
|
+
config_file = "#{path}/#{Spoom::Config::SORBET_CONFIG}"
|
65
|
+
return FileTree.new unless File.exist?(config_file)
|
66
|
+
config = Sorbet::Config.parse_file(config_file)
|
67
|
+
files = Sorbet.srb_files(config, path: path)
|
68
|
+
files.select! { |file| file =~ /\.rb$/ }
|
69
|
+
files.reject! { |file| file =~ %r{/test/} }
|
70
|
+
FileTree.new(files, strip_prefix: path)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "d3/circle_map"
|
5
|
+
require_relative "d3/pie"
|
6
|
+
require_relative "d3/timeline"
|
7
|
+
|
8
|
+
module Spoom
|
9
|
+
module Coverage
|
10
|
+
module D3
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
COLOR_IGNORE = "#999"
|
14
|
+
COLOR_FALSE = "#db4437"
|
15
|
+
COLOR_TRUE = "#0f9d58"
|
16
|
+
COLOR_STRICT = "#0a7340"
|
17
|
+
COLOR_STRONG = "#064828"
|
18
|
+
|
19
|
+
sig { returns(String) }
|
20
|
+
def self.header_style
|
21
|
+
<<~CSS
|
22
|
+
svg {
|
23
|
+
width: 100%;
|
24
|
+
height: 100%;
|
25
|
+
}
|
26
|
+
|
27
|
+
.tooltip {
|
28
|
+
font: 12px Arial, sans-serif;
|
29
|
+
color: #fff;
|
30
|
+
text-align: center;
|
31
|
+
background: rgba(0, 0, 0, 0.6);
|
32
|
+
padding: 5px;
|
33
|
+
border: 0px;
|
34
|
+
border-radius: 4px;
|
35
|
+
position: absolute;
|
36
|
+
top: 0;
|
37
|
+
left: 0;
|
38
|
+
opacity: 0;
|
39
|
+
}
|
40
|
+
|
41
|
+
.label {
|
42
|
+
font: 14px Arial, sans-serif;
|
43
|
+
font-weight: bold;
|
44
|
+
fill: #fff;
|
45
|
+
text-anchor: middle;
|
46
|
+
pointer-events: none;
|
47
|
+
}
|
48
|
+
|
49
|
+
.label .small {
|
50
|
+
font-size: 10px;
|
51
|
+
}
|
52
|
+
|
53
|
+
#{Pie.header_style}
|
54
|
+
#{CircleMap.header_style}
|
55
|
+
#{Timeline.header_style}
|
56
|
+
CSS
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(palette: ColorPalette).returns(String) }
|
60
|
+
def self.header_script(palette)
|
61
|
+
<<~JS
|
62
|
+
var parseDate = d3.timeParse("%s");
|
63
|
+
|
64
|
+
function strictnessColor(strictness) {
|
65
|
+
switch(strictness) {
|
66
|
+
case "ignore":
|
67
|
+
return "#{palette.ignore}";
|
68
|
+
case "false":
|
69
|
+
return "#{palette.false}";
|
70
|
+
case "true":
|
71
|
+
return "#{palette.true}";
|
72
|
+
case "strict":
|
73
|
+
return "#{palette.strict}";
|
74
|
+
case "strong":
|
75
|
+
return "#{palette.strong}";
|
76
|
+
}
|
77
|
+
return "#{palette.false}";
|
78
|
+
}
|
79
|
+
|
80
|
+
function toPercent(value, sum) {
|
81
|
+
return value ? Math.round(value * 100 / sum) : 0;
|
82
|
+
}
|
83
|
+
|
84
|
+
var tooltip = d3.select("body")
|
85
|
+
.append("div")
|
86
|
+
.append("div")
|
87
|
+
.attr("class", "tooltip");
|
88
|
+
|
89
|
+
function moveTooltip(d) {
|
90
|
+
return tooltip
|
91
|
+
.style("left", (d3.event.pageX + 20) + "px")
|
92
|
+
.style("top", (d3.event.pageY) + "px")
|
93
|
+
}
|
94
|
+
|
95
|
+
#{Pie.header_script}
|
96
|
+
#{CircleMap.header_script}
|
97
|
+
#{Timeline.header_script}
|
98
|
+
JS
|
99
|
+
end
|
100
|
+
|
101
|
+
class ColorPalette < T::Struct
|
102
|
+
prop :ignore, String
|
103
|
+
prop :false, String
|
104
|
+
prop :true, String
|
105
|
+
prop :strict, String
|
106
|
+
prop :strong, String
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Coverage
|
6
|
+
module D3
|
7
|
+
class Base
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
|
11
|
+
abstract!
|
12
|
+
|
13
|
+
sig { returns(String) }
|
14
|
+
attr_reader :id
|
15
|
+
|
16
|
+
sig { params(id: String, data: T.untyped).void }
|
17
|
+
def initialize(id, data)
|
18
|
+
@id = id
|
19
|
+
@data = data
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { returns(String) }
|
23
|
+
def self.header_style
|
24
|
+
""
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { returns(String) }
|
28
|
+
def self.header_script
|
29
|
+
""
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { returns(String) }
|
33
|
+
def html
|
34
|
+
<<~HTML
|
35
|
+
<svg id="#{id}"></svg>
|
36
|
+
<script>#{script}</script>
|
37
|
+
HTML
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { returns(String) }
|
41
|
+
def tooltip
|
42
|
+
""
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { abstract.returns(String) }
|
46
|
+
def script; end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "base"
|
5
|
+
|
6
|
+
module Spoom
|
7
|
+
module Coverage
|
8
|
+
module D3
|
9
|
+
class CircleMap < Base
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(String) }
|
13
|
+
def self.header_style
|
14
|
+
<<~CSS
|
15
|
+
.node {
|
16
|
+
cursor: pointer;
|
17
|
+
}
|
18
|
+
|
19
|
+
.node:hover {
|
20
|
+
stroke: #333;
|
21
|
+
stroke-width: 1px;
|
22
|
+
}
|
23
|
+
|
24
|
+
.label.dir {
|
25
|
+
fill: #333;
|
26
|
+
}
|
27
|
+
|
28
|
+
.label.file {
|
29
|
+
font: 12px Arial, sans-serif;
|
30
|
+
}
|
31
|
+
|
32
|
+
.node.root, .node.file {
|
33
|
+
pointer-events: none;
|
34
|
+
}
|
35
|
+
CSS
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { returns(String) }
|
39
|
+
def self.header_script
|
40
|
+
<<~JS
|
41
|
+
function treeHeight(root, height = 0) {
|
42
|
+
height += 1;
|
43
|
+
if (root.children && root.children.length > 0)
|
44
|
+
return Math.max(...root.children.map(child => treeHeight(child, height)));
|
45
|
+
else
|
46
|
+
return height;
|
47
|
+
}
|
48
|
+
|
49
|
+
function tooltipMap(d) {
|
50
|
+
moveTooltip(d)
|
51
|
+
.html("<b>" + d.data.name + "</b>")
|
52
|
+
}
|
53
|
+
JS
|
54
|
+
end
|
55
|
+
|
56
|
+
sig { override.returns(String) }
|
57
|
+
def script
|
58
|
+
<<~JS
|
59
|
+
var root = {children: #{@data.to_json}}
|
60
|
+
var dataHeight = treeHeight(root)
|
61
|
+
|
62
|
+
var opacity = d3.scaleLinear()
|
63
|
+
.domain([0, dataHeight])
|
64
|
+
.range([0, 0.2])
|
65
|
+
|
66
|
+
root = d3.hierarchy(root)
|
67
|
+
.sum((d) => d.children ? d.children.length : 1)
|
68
|
+
.sort((a, b) => b.value - a.value);
|
69
|
+
|
70
|
+
var dirColor = d3.scaleLinear()
|
71
|
+
.domain([1, 0])
|
72
|
+
.range([strictnessColor("true"), strictnessColor("false")])
|
73
|
+
.interpolate(d3.interpolateRgb);
|
74
|
+
|
75
|
+
function redraw() {
|
76
|
+
var diameter = document.getElementById("#{id}").clientWidth - 20;
|
77
|
+
d3.select("##{id}").selectAll("*").remove()
|
78
|
+
|
79
|
+
var svg_#{id} = d3.select("##{id}")
|
80
|
+
.attr("width", diameter)
|
81
|
+
.attr("height", diameter)
|
82
|
+
.append("g")
|
83
|
+
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
|
84
|
+
|
85
|
+
var pack = d3.pack()
|
86
|
+
.size([diameter, diameter])
|
87
|
+
.padding(2);
|
88
|
+
|
89
|
+
var focus = root,
|
90
|
+
nodes = pack(root).descendants(),
|
91
|
+
view;
|
92
|
+
|
93
|
+
var circle = svg_#{id}.selectAll("circle")
|
94
|
+
.data(nodes)
|
95
|
+
.enter().append("circle")
|
96
|
+
.attr("class", (d) => d.parent ? d.children ? "node" : "node file" : "node root")
|
97
|
+
.attr("fill", (d) => d.children ? dirColor(d.data.score) : strictnessColor(d.data.strictness))
|
98
|
+
.attr("fill-opacity", (d) => d.children ? opacity(d.depth) : 1)
|
99
|
+
.on("click", function(d) { if (focus !== d) zoom(d), d3.event.stopPropagation(); })
|
100
|
+
.on("mouseover", (d) => tooltip.style("opacity", 1))
|
101
|
+
.on("mousemove", tooltipMap)
|
102
|
+
.on("mouseleave", (d) => tooltip.style("opacity", 0));
|
103
|
+
|
104
|
+
var text = svg_#{id}.selectAll("text")
|
105
|
+
.data(nodes)
|
106
|
+
.enter().append("text")
|
107
|
+
.attr("class", (d) => d.children ? "label dir" : "label file")
|
108
|
+
.attr("fill-opacity", (d) => d.depth <= 1 ? 1 : 0)
|
109
|
+
.attr("display", (d) => d.depth <= 1 ? "inline" : "none")
|
110
|
+
.text((d) => d.data.name);
|
111
|
+
|
112
|
+
var node = svg_#{id}.selectAll("circle,text");
|
113
|
+
|
114
|
+
function zoom(d) {
|
115
|
+
var focus0 = focus; focus = d;
|
116
|
+
|
117
|
+
var transition = d3.transition()
|
118
|
+
.duration(d3.event.altKey ? 7500 : 750)
|
119
|
+
.tween("zoom", function(d) {
|
120
|
+
var i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2]);
|
121
|
+
return (t) => zoomTo(i(t));
|
122
|
+
});
|
123
|
+
|
124
|
+
transition.selectAll("text")
|
125
|
+
.filter(function(d) { return d && d.parent === focus || this.style.display === "inline"; })
|
126
|
+
.attr("fill-opacity", function(d) { return d.parent === focus ? 1 : 0; })
|
127
|
+
.on("start", function(d) { if (d.parent === focus) this.style.display = "inline"; })
|
128
|
+
.on("end", function(d) { if (d.parent !== focus) this.style.display = "none"; });
|
129
|
+
}
|
130
|
+
|
131
|
+
function zoomTo(v) {
|
132
|
+
var k = diameter / v[2]; view = v;
|
133
|
+
node.attr("transform", (d) => "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")");
|
134
|
+
circle.attr("r", (d) => d.r * k);
|
135
|
+
}
|
136
|
+
|
137
|
+
zoomTo([root.x, root.y, root.r * 2]);
|
138
|
+
d3.select("##{id}").on("click", () => zoom(root));
|
139
|
+
}
|
140
|
+
|
141
|
+
redraw();
|
142
|
+
window.addEventListener("resize", redraw);
|
143
|
+
JS
|
144
|
+
end
|
145
|
+
|
146
|
+
class Sigils < CircleMap
|
147
|
+
extend T::Sig
|
148
|
+
|
149
|
+
sig { params(id: String, sigils_tree: FileTree).void }
|
150
|
+
def initialize(id, sigils_tree)
|
151
|
+
@scores = T.let({}, T::Hash[FileTree::Node, Float])
|
152
|
+
@strictnesses = T.let({}, T::Hash[FileTree::Node, T.nilable(String)])
|
153
|
+
@sigils_tree = sigils_tree
|
154
|
+
super(id, sigils_tree.roots.map { |r| tree_node_to_json(r) })
|
155
|
+
end
|
156
|
+
|
157
|
+
sig { params(node: FileTree::Node).returns(T::Hash[Symbol, T.untyped]) }
|
158
|
+
def tree_node_to_json(node)
|
159
|
+
if node.children.empty?
|
160
|
+
return { name: node.name, strictness: tree_node_strictness(node) }
|
161
|
+
end
|
162
|
+
{
|
163
|
+
name: node.name,
|
164
|
+
children: node.children.values.map { |n| tree_node_to_json(n) },
|
165
|
+
score: tree_node_score(node),
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
sig { params(node: FileTree::Node).returns(T.nilable(String)) }
|
170
|
+
def tree_node_strictness(node)
|
171
|
+
prefix = @sigils_tree.strip_prefix
|
172
|
+
path = node.path
|
173
|
+
path = "#{prefix}/#{path}" if prefix
|
174
|
+
@strictnesses[node] ||= Spoom::Sorbet::Sigils.file_strictness(path)
|
175
|
+
end
|
176
|
+
|
177
|
+
sig { params(node: FileTree::Node).returns(Float) }
|
178
|
+
def tree_node_score(node)
|
179
|
+
unless @scores.key?(node)
|
180
|
+
if node.name =~ /\.rbi?$/
|
181
|
+
case tree_node_strictness(node)
|
182
|
+
when "true", "strict", "strong"
|
183
|
+
@scores[node] = 1.0
|
184
|
+
end
|
185
|
+
elsif !node.children.empty?
|
186
|
+
@scores[node] = node.children.values.sum { |n| tree_node_score(n) } / node.children.size.to_f
|
187
|
+
end
|
188
|
+
end
|
189
|
+
@scores[node] || 0.0
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|