spoom 1.0.1 → 1.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +4 -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
- data/templates/card.erb +8 -0
- data/templates/card_snapshot.erb +22 -0
- data/templates/page.erb +50 -0
- metadata +80 -20
@@ -0,0 +1,308 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "d3"
|
5
|
+
|
6
|
+
require "erb"
|
7
|
+
|
8
|
+
module Spoom
|
9
|
+
module Coverage
|
10
|
+
class Template
|
11
|
+
extend T::Sig
|
12
|
+
extend T::Helpers
|
13
|
+
|
14
|
+
abstract!
|
15
|
+
|
16
|
+
# Create a new template from an Erb file path
|
17
|
+
sig { params(template: String).void }
|
18
|
+
def initialize(template:)
|
19
|
+
@template = template
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { returns(String) }
|
23
|
+
def erb
|
24
|
+
File.read(@template)
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { returns(String) }
|
28
|
+
def html
|
29
|
+
ERB.new(erb).result(get_binding)
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { returns(Binding) }
|
33
|
+
def get_binding # rubocop:disable Naming/AccessorMethodName
|
34
|
+
binding
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Page < Template
|
39
|
+
extend T::Sig
|
40
|
+
extend T::Helpers
|
41
|
+
|
42
|
+
abstract!
|
43
|
+
|
44
|
+
TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/page.erb", String)
|
45
|
+
|
46
|
+
sig { returns(String) }
|
47
|
+
attr_reader :title
|
48
|
+
|
49
|
+
sig { returns(D3::ColorPalette) }
|
50
|
+
attr_reader :palette
|
51
|
+
|
52
|
+
sig { params(title: String, palette: D3::ColorPalette, template: String).void }
|
53
|
+
def initialize(title:, palette:, template: TEMPLATE)
|
54
|
+
super(template: template)
|
55
|
+
@title = title
|
56
|
+
@palette = palette
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { returns(String) }
|
60
|
+
def header_style
|
61
|
+
D3.header_style
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { returns(String) }
|
65
|
+
def header_script
|
66
|
+
D3.header_script(palette)
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { returns(String) }
|
70
|
+
def header_html
|
71
|
+
"<h1 class='display-3'>#{title}</h1>"
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { returns(String) }
|
75
|
+
def body_html
|
76
|
+
cards.map(&:html).join("\n")
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { abstract.returns(T::Array[Cards::Card]) }
|
80
|
+
def cards; end
|
81
|
+
|
82
|
+
sig { returns(String) }
|
83
|
+
def footer_html
|
84
|
+
"Generated by <a href='https://github.com/Shopify/spoom'>spoom</a> on #{Time.now.utc}."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
module Cards
|
89
|
+
class Card < Template
|
90
|
+
extend T::Sig
|
91
|
+
|
92
|
+
TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/card.erb", String)
|
93
|
+
|
94
|
+
sig { returns(T.nilable(String)) }
|
95
|
+
attr_reader :title, :body
|
96
|
+
|
97
|
+
sig { params(template: String, title: T.nilable(String), body: T.nilable(String)).void }
|
98
|
+
def initialize(template: TEMPLATE, title: nil, body: nil)
|
99
|
+
super(template: template)
|
100
|
+
@title = title
|
101
|
+
@body = body
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Erb < Card
|
106
|
+
extend T::Sig
|
107
|
+
extend T::Helpers
|
108
|
+
|
109
|
+
abstract!
|
110
|
+
|
111
|
+
sig { void }
|
112
|
+
def initialize; end
|
113
|
+
|
114
|
+
sig { override.returns(String) }
|
115
|
+
def html
|
116
|
+
ERB.new(erb).result(get_binding)
|
117
|
+
end
|
118
|
+
|
119
|
+
sig { abstract.returns(String) }
|
120
|
+
def erb; end
|
121
|
+
end
|
122
|
+
|
123
|
+
class Snapshot < Card
|
124
|
+
extend T::Sig
|
125
|
+
|
126
|
+
TEMPLATE = T.let("#{Spoom::Config::SPOOM_PATH}/templates/card_snapshot.erb", String)
|
127
|
+
|
128
|
+
sig { returns(Coverage::Snapshot) }
|
129
|
+
attr_reader :snapshot
|
130
|
+
|
131
|
+
sig { params(snapshot: Coverage::Snapshot, title: String).void }
|
132
|
+
def initialize(snapshot:, title: "Snapshot")
|
133
|
+
super(template: TEMPLATE, title: title)
|
134
|
+
@snapshot = snapshot
|
135
|
+
end
|
136
|
+
|
137
|
+
sig { returns(D3::Pie::Sigils) }
|
138
|
+
def pie_sigils
|
139
|
+
D3::Pie::Sigils.new('pie_sigils', 'Sigils', snapshot)
|
140
|
+
end
|
141
|
+
|
142
|
+
sig { returns(D3::Pie::Calls) }
|
143
|
+
def pie_calls
|
144
|
+
D3::Pie::Calls.new('pie_calls', 'Calls', snapshot)
|
145
|
+
end
|
146
|
+
|
147
|
+
sig { returns(D3::Pie::Sigs) }
|
148
|
+
def pie_sigs
|
149
|
+
D3::Pie::Sigs.new('pie_sigs', 'Sigs', snapshot)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class Map < Card
|
154
|
+
extend T::Sig
|
155
|
+
|
156
|
+
sig { params(sigils_tree: FileTree, title: String).void }
|
157
|
+
def initialize(sigils_tree:, title: "Strictness Map")
|
158
|
+
super(title: title, body: D3::CircleMap::Sigils.new("map_sigils", sigils_tree).html)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Timeline < Card
|
163
|
+
extend T::Sig
|
164
|
+
|
165
|
+
sig { params(title: String, timeline: D3::Timeline).void }
|
166
|
+
def initialize(title:, timeline:)
|
167
|
+
super(title: title, body: timeline.html)
|
168
|
+
end
|
169
|
+
|
170
|
+
class Sigils < Timeline
|
171
|
+
extend T::Sig
|
172
|
+
|
173
|
+
sig { params(snapshots: T::Array[Coverage::Snapshot], title: String).void }
|
174
|
+
def initialize(snapshots:, title: "Sigils Timeline")
|
175
|
+
super(title: title, timeline: D3::Timeline::Sigils.new("timeline_sigils", snapshots))
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class Calls < Timeline
|
180
|
+
extend T::Sig
|
181
|
+
|
182
|
+
sig { params(snapshots: T::Array[Coverage::Snapshot], title: String).void }
|
183
|
+
def initialize(snapshots:, title: "Calls Timeline")
|
184
|
+
super(title: title, timeline: D3::Timeline::Calls.new("timeline_calls", snapshots))
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class Sigs < Timeline
|
189
|
+
extend T::Sig
|
190
|
+
|
191
|
+
sig { params(snapshots: T::Array[Coverage::Snapshot], title: String).void }
|
192
|
+
def initialize(snapshots:, title: "Signatures Timeline")
|
193
|
+
super(title: title, timeline: D3::Timeline::Sigs.new("timeline_sigs", snapshots))
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
class Versions < Timeline
|
198
|
+
extend T::Sig
|
199
|
+
|
200
|
+
sig { params(snapshots: T::Array[Coverage::Snapshot], title: String).void }
|
201
|
+
def initialize(snapshots:, title: "Sorbet Versions Timeline")
|
202
|
+
super(title: title, timeline: D3::Timeline::Versions.new("timeline_versions", snapshots))
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class Runtimes < Timeline
|
207
|
+
extend T::Sig
|
208
|
+
|
209
|
+
sig { params(snapshots: T::Array[Coverage::Snapshot], title: String).void }
|
210
|
+
def initialize(snapshots:, title: "Sorbet Typechecking Time")
|
211
|
+
super(title: title, timeline: D3::Timeline::Runtimes.new("timeline_runtimes", snapshots))
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
class SorbetIntro < Erb
|
217
|
+
extend T::Sig
|
218
|
+
|
219
|
+
sig { params(sorbet_intro_commit: T.nilable(String), sorbet_intro_date: T.nilable(Time)).void }
|
220
|
+
def initialize(sorbet_intro_commit: nil, sorbet_intro_date: nil)
|
221
|
+
@sorbet_intro_commit = sorbet_intro_commit
|
222
|
+
@sorbet_intro_date = sorbet_intro_date
|
223
|
+
end
|
224
|
+
|
225
|
+
sig { override.returns(String) }
|
226
|
+
def erb
|
227
|
+
<<~ERB
|
228
|
+
<div class="text-center" style="margin-top: 30px">
|
229
|
+
Typchecked by Sorbet since <b>#{@sorbet_intro_date&.strftime('%F')}</b>
|
230
|
+
(commit <b>#{@sorbet_intro_commit}</b>).
|
231
|
+
</div>
|
232
|
+
ERB
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
class Report < Page
|
238
|
+
extend T::Sig
|
239
|
+
|
240
|
+
sig { returns(String) }
|
241
|
+
attr_reader :project_name
|
242
|
+
|
243
|
+
sig { returns(T.nilable(String)) }
|
244
|
+
attr_reader :sorbet_intro_commit
|
245
|
+
|
246
|
+
sig { returns(T.nilable(Time)) }
|
247
|
+
attr_reader :sorbet_intro_date
|
248
|
+
|
249
|
+
sig { returns(T::Array[Snapshot]) }
|
250
|
+
attr_reader :snapshots
|
251
|
+
|
252
|
+
sig { returns(FileTree) }
|
253
|
+
attr_reader :sigils_tree
|
254
|
+
|
255
|
+
sig do
|
256
|
+
params(
|
257
|
+
project_name: String,
|
258
|
+
palette: D3::ColorPalette,
|
259
|
+
snapshots: T::Array[Snapshot],
|
260
|
+
sigils_tree: FileTree,
|
261
|
+
sorbet_intro_commit: T.nilable(String),
|
262
|
+
sorbet_intro_date: T.nilable(Time),
|
263
|
+
).void
|
264
|
+
end
|
265
|
+
def initialize(
|
266
|
+
project_name:,
|
267
|
+
palette:,
|
268
|
+
snapshots:,
|
269
|
+
sigils_tree:,
|
270
|
+
sorbet_intro_commit: nil,
|
271
|
+
sorbet_intro_date: nil
|
272
|
+
)
|
273
|
+
super(title: project_name, palette: palette)
|
274
|
+
@project_name = project_name
|
275
|
+
@snapshots = snapshots
|
276
|
+
@sigils_tree = sigils_tree
|
277
|
+
@sorbet_intro_commit = sorbet_intro_commit
|
278
|
+
@sorbet_intro_date = sorbet_intro_date
|
279
|
+
end
|
280
|
+
|
281
|
+
sig { override.returns(String) }
|
282
|
+
def header_html
|
283
|
+
last = T.must(snapshots.last)
|
284
|
+
<<~ERB
|
285
|
+
<h1 class="display-3">
|
286
|
+
#{project_name}
|
287
|
+
<span class="badge badge-pill badge-dark" style="font-size: 20%;">#{last.commit_sha}</span>
|
288
|
+
</h1>
|
289
|
+
ERB
|
290
|
+
end
|
291
|
+
|
292
|
+
sig { override.returns(T::Array[Cards::Card]) }
|
293
|
+
def cards
|
294
|
+
last = T.must(snapshots.last)
|
295
|
+
cards = []
|
296
|
+
cards << Cards::Snapshot.new(snapshot: last)
|
297
|
+
cards << Cards::Map.new(sigils_tree: sigils_tree)
|
298
|
+
cards << Cards::Timeline::Sigils.new(snapshots: snapshots)
|
299
|
+
cards << Cards::Timeline::Calls.new(snapshots: snapshots)
|
300
|
+
cards << Cards::Timeline::Sigs.new(snapshots: snapshots)
|
301
|
+
cards << Cards::Timeline::Versions.new(snapshots: snapshots)
|
302
|
+
cards << Cards::Timeline::Runtimes.new(snapshots: snapshots)
|
303
|
+
cards << Cards::SorbetIntro.new(sorbet_intro_commit: sorbet_intro_commit, sorbet_intro_date: sorbet_intro_date)
|
304
|
+
cards
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Coverage
|
6
|
+
class Snapshot < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
prop :timestamp, Integer, default: Time.new.getutc.to_i
|
10
|
+
prop :version_static, T.nilable(String), default: nil
|
11
|
+
prop :version_runtime, T.nilable(String), default: nil
|
12
|
+
prop :duration, Integer, default: 0
|
13
|
+
prop :commit_sha, T.nilable(String), default: nil
|
14
|
+
prop :commit_timestamp, T.nilable(Integer), default: nil
|
15
|
+
prop :files, Integer, default: 0
|
16
|
+
prop :modules, Integer, default: 0
|
17
|
+
prop :classes, Integer, default: 0
|
18
|
+
prop :singleton_classes, Integer, default: 0
|
19
|
+
prop :methods_without_sig, Integer, default: 0
|
20
|
+
prop :methods_with_sig, Integer, default: 0
|
21
|
+
prop :calls_untyped, Integer, default: 0
|
22
|
+
prop :calls_typed, Integer, default: 0
|
23
|
+
prop :sigils, T::Hash[String, Integer], default: Hash.new(0)
|
24
|
+
|
25
|
+
# The strictness name as found in the Sorbet metrics file
|
26
|
+
STRICTNESSES = T.let(["ignore", "false", "true", "strict", "strong", "stdlib"].freeze, T::Array[String])
|
27
|
+
|
28
|
+
sig { params(out: T.any(IO, StringIO), colors: T::Boolean, indent_level: Integer).void }
|
29
|
+
def print(out: $stdout, colors: true, indent_level: 0)
|
30
|
+
printer = SnapshotPrinter.new(out: out, colors: colors, indent_level: indent_level)
|
31
|
+
printer.print_snapshot(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { params(json: String).returns(Snapshot) }
|
35
|
+
def self.from_json(json)
|
36
|
+
from_obj(JSON.parse(json))
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { params(obj: T::Hash[String, T.untyped]).returns(Snapshot) }
|
40
|
+
def self.from_obj(obj)
|
41
|
+
snapshot = Snapshot.new
|
42
|
+
snapshot.timestamp = obj.fetch("timestamp", 0)
|
43
|
+
snapshot.version_static = obj.fetch("version_static", nil)
|
44
|
+
snapshot.version_runtime = obj.fetch("version_runtime", nil)
|
45
|
+
snapshot.duration = obj.fetch("duration", 0)
|
46
|
+
snapshot.commit_sha = obj.fetch("commit_sha", nil)
|
47
|
+
snapshot.commit_timestamp = obj.fetch("commit_timestamp", nil)
|
48
|
+
snapshot.files = obj.fetch("files", 0)
|
49
|
+
snapshot.modules = obj.fetch("modules", 0)
|
50
|
+
snapshot.classes = obj.fetch("classes", 0)
|
51
|
+
snapshot.singleton_classes = obj.fetch("singleton_classes", 0)
|
52
|
+
snapshot.methods_with_sig = obj.fetch("methods_with_sig", 0)
|
53
|
+
snapshot.methods_without_sig = obj.fetch("methods_without_sig", 0)
|
54
|
+
snapshot.calls_typed = obj.fetch("calls_typed", 0)
|
55
|
+
snapshot.calls_untyped = obj.fetch("calls_untyped", 0)
|
56
|
+
|
57
|
+
sigils = obj.fetch("sigils", {})
|
58
|
+
if sigils
|
59
|
+
Snapshot::STRICTNESSES.each do |strictness|
|
60
|
+
next unless sigils.key?(strictness)
|
61
|
+
snapshot.sigils[strictness] = sigils[strictness]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
snapshot
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(arg: T.untyped).returns(String) }
|
69
|
+
def to_json(*arg)
|
70
|
+
serialize.to_json(*arg)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class SnapshotPrinter < Spoom::Printer
|
75
|
+
extend T::Sig
|
76
|
+
|
77
|
+
sig { params(snapshot: Snapshot).void }
|
78
|
+
def print_snapshot(snapshot)
|
79
|
+
methods = snapshot.methods_with_sig + snapshot.methods_without_sig
|
80
|
+
calls = snapshot.calls_typed + snapshot.calls_untyped
|
81
|
+
|
82
|
+
if snapshot.version_static || snapshot.version_runtime
|
83
|
+
printl("Sorbet static: #{snapshot.version_static}") if snapshot.version_static
|
84
|
+
printl("Sorbet runtime: #{snapshot.version_runtime}") if snapshot.version_runtime
|
85
|
+
printn
|
86
|
+
end
|
87
|
+
printl("Content:")
|
88
|
+
indent
|
89
|
+
printl("files: #{snapshot.files}")
|
90
|
+
printl("modules: #{snapshot.modules}")
|
91
|
+
printl("classes: #{snapshot.classes - snapshot.singleton_classes}")
|
92
|
+
printl("methods: #{methods}")
|
93
|
+
dedent
|
94
|
+
printn
|
95
|
+
printl("Sigils:")
|
96
|
+
print_map(snapshot.sigils, snapshot.files)
|
97
|
+
printn
|
98
|
+
printl("Methods:")
|
99
|
+
methods_map = {
|
100
|
+
"with signature" => snapshot.methods_with_sig,
|
101
|
+
"without signature" => snapshot.methods_without_sig,
|
102
|
+
}
|
103
|
+
print_map(methods_map, methods)
|
104
|
+
printn
|
105
|
+
printl("Calls:")
|
106
|
+
calls_map = {
|
107
|
+
"typed" => snapshot.calls_typed,
|
108
|
+
"untyped" => snapshot.calls_untyped,
|
109
|
+
}
|
110
|
+
print_map(calls_map, calls)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
sig { params(hash: T::Hash[String, Integer], total: Integer).void }
|
116
|
+
def print_map(hash, total)
|
117
|
+
indent
|
118
|
+
hash.each do |key, value|
|
119
|
+
next unless value > 0
|
120
|
+
printl("#{key}: #{value}#{percent(value, total)}")
|
121
|
+
end
|
122
|
+
dedent
|
123
|
+
end
|
124
|
+
|
125
|
+
sig { params(value: T.nilable(Integer), total: T.nilable(Integer)).returns(String) }
|
126
|
+
def percent(value, total)
|
127
|
+
return "" if value.nil? || total.nil? || total == 0
|
128
|
+
" (#{(value.to_f * 100.0 / total.to_f).round}%)"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
# Build a file hierarchy from a set of file paths.
|
6
|
+
class FileTree
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { returns(T.nilable(String)) }
|
10
|
+
attr_reader :strip_prefix
|
11
|
+
|
12
|
+
sig { params(paths: T::Enumerable[String], strip_prefix: T.nilable(String)).void }
|
13
|
+
def initialize(paths = [], strip_prefix: nil)
|
14
|
+
@roots = T.let({}, T::Hash[String, Node])
|
15
|
+
@strip_prefix = strip_prefix
|
16
|
+
add_paths(paths)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Add all `paths` to the tree
|
20
|
+
sig { params(paths: T::Enumerable[String]).void }
|
21
|
+
def add_paths(paths)
|
22
|
+
paths.each { |path| add_path(path) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add a `path` to the tree
|
26
|
+
#
|
27
|
+
# This will create all nodes until the root of `path`.
|
28
|
+
sig { params(path: String).returns(Node) }
|
29
|
+
def add_path(path)
|
30
|
+
prefix = @strip_prefix
|
31
|
+
path = path.delete_prefix("#{prefix}/") if prefix
|
32
|
+
parts = path.split("/")
|
33
|
+
if path.empty? || parts.size == 1
|
34
|
+
return @roots[path] ||= Node.new(parent: nil, name: path)
|
35
|
+
end
|
36
|
+
parent_path = T.must(parts[0...-1]).join("/")
|
37
|
+
parent = add_path(parent_path)
|
38
|
+
name = T.must(parts.last)
|
39
|
+
parent.children[name] ||= Node.new(parent: parent, name: name)
|
40
|
+
end
|
41
|
+
|
42
|
+
# All root nodes
|
43
|
+
sig { returns(T::Array[Node]) }
|
44
|
+
def roots
|
45
|
+
@roots.values
|
46
|
+
end
|
47
|
+
|
48
|
+
# All the nodes in this tree
|
49
|
+
sig { returns(T::Array[Node]) }
|
50
|
+
def nodes
|
51
|
+
all_nodes = []
|
52
|
+
@roots.values.each { |root| collect_nodes(root, all_nodes) }
|
53
|
+
all_nodes
|
54
|
+
end
|
55
|
+
|
56
|
+
# All the paths in this tree
|
57
|
+
sig { returns(T::Array[String]) }
|
58
|
+
def paths
|
59
|
+
nodes.collect(&:path)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig do
|
63
|
+
params(
|
64
|
+
out: T.any(IO, StringIO),
|
65
|
+
show_strictness: T::Boolean,
|
66
|
+
colors: T::Boolean,
|
67
|
+
indent_level: Integer
|
68
|
+
).void
|
69
|
+
end
|
70
|
+
def print(out: $stdout, show_strictness: true, colors: true, indent_level: 0)
|
71
|
+
printer = TreePrinter.new(
|
72
|
+
tree: self,
|
73
|
+
out: out,
|
74
|
+
show_strictness: show_strictness,
|
75
|
+
colors: colors,
|
76
|
+
indent_level: indent_level
|
77
|
+
)
|
78
|
+
printer.print_tree
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
sig { params(node: FileTree::Node, collected_nodes: T::Array[Node]).returns(T::Array[String]) }
|
84
|
+
def collect_nodes(node, collected_nodes = [])
|
85
|
+
collected_nodes << node
|
86
|
+
node.children.values.each { |child| collect_nodes(child, collected_nodes) }
|
87
|
+
collected_nodes
|
88
|
+
end
|
89
|
+
|
90
|
+
# A node representing either a file or a directory inside a FileTree
|
91
|
+
class Node < T::Struct
|
92
|
+
extend T::Sig
|
93
|
+
|
94
|
+
# Node parent or `nil` if the node is a root one
|
95
|
+
const :parent, T.nilable(Node)
|
96
|
+
|
97
|
+
# File or dir name
|
98
|
+
const :name, String
|
99
|
+
|
100
|
+
# Children of this node (if not empty, it means it's a dir)
|
101
|
+
const :children, T::Hash[String, Node], default: {}
|
102
|
+
|
103
|
+
# Full path to this node from root
|
104
|
+
sig { returns(String) }
|
105
|
+
def path
|
106
|
+
parent = self.parent
|
107
|
+
return name unless parent
|
108
|
+
"#{parent.path}/#{name}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# An internal class used to print a FileTree
|
113
|
+
#
|
114
|
+
# See `FileTree#print`
|
115
|
+
class TreePrinter < Spoom::Printer
|
116
|
+
extend T::Sig
|
117
|
+
|
118
|
+
sig { returns(FileTree) }
|
119
|
+
attr_reader :tree
|
120
|
+
|
121
|
+
sig do
|
122
|
+
params(
|
123
|
+
tree: FileTree,
|
124
|
+
out: T.any(IO, StringIO),
|
125
|
+
show_strictness: T::Boolean,
|
126
|
+
colors: T::Boolean,
|
127
|
+
indent_level: Integer
|
128
|
+
).void
|
129
|
+
end
|
130
|
+
def initialize(tree:, out: $stdout, show_strictness: true, colors: true, indent_level: 0)
|
131
|
+
super(out: out, colors: colors, indent_level: indent_level)
|
132
|
+
@tree = tree
|
133
|
+
@show_strictness = show_strictness
|
134
|
+
end
|
135
|
+
|
136
|
+
sig { void }
|
137
|
+
def print_tree
|
138
|
+
print_nodes(tree.roots)
|
139
|
+
end
|
140
|
+
|
141
|
+
sig { params(node: FileTree::Node).void }
|
142
|
+
def print_node(node)
|
143
|
+
printt
|
144
|
+
if node.children.empty?
|
145
|
+
if @show_strictness
|
146
|
+
strictness = node_strictness(node)
|
147
|
+
if @colors
|
148
|
+
print_colored(node.name, strictness_color(strictness))
|
149
|
+
elsif strictness
|
150
|
+
print("#{node.name} (#{strictness})")
|
151
|
+
else
|
152
|
+
print(node.name.to_s)
|
153
|
+
end
|
154
|
+
else
|
155
|
+
print(node.name.to_s)
|
156
|
+
end
|
157
|
+
print("\n")
|
158
|
+
else
|
159
|
+
print_colored(node.name, :blue)
|
160
|
+
print("/")
|
161
|
+
printn
|
162
|
+
indent
|
163
|
+
print_nodes(node.children.values)
|
164
|
+
dedent
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
sig { params(nodes: T::Array[FileTree::Node]).void }
|
169
|
+
def print_nodes(nodes)
|
170
|
+
nodes.each { |node| print_node(node) }
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
sig { params(node: FileTree::Node).returns(T.nilable(String)) }
|
176
|
+
def node_strictness(node)
|
177
|
+
path = node.path
|
178
|
+
prefix = tree.strip_prefix
|
179
|
+
path = "#{prefix}/#{path}" if prefix
|
180
|
+
Spoom::Sorbet::Sigils.file_strictness(path)
|
181
|
+
end
|
182
|
+
|
183
|
+
sig { params(strictness: T.nilable(String)).returns(Symbol) }
|
184
|
+
def strictness_color(strictness)
|
185
|
+
case strictness
|
186
|
+
when "false"
|
187
|
+
:red
|
188
|
+
when "true", "strict", "strong"
|
189
|
+
:green
|
190
|
+
else
|
191
|
+
:uncolored
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|