spoom 1.0.3 → 1.0.8
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.
- checksums.yaml +4 -4
- data/Gemfile +4 -0
- data/README.md +296 -1
- data/Rakefile +2 -0
- data/exe/spoom +7 -0
- data/lib/spoom.rb +26 -1
- data/lib/spoom/cli.rb +69 -0
- data/lib/spoom/cli/bump.rb +118 -0
- data/lib/spoom/cli/config.rb +51 -0
- data/lib/spoom/cli/coverage.rb +202 -0
- data/lib/spoom/cli/helper.rb +75 -0
- data/lib/spoom/cli/lsp.rb +165 -0
- data/lib/spoom/cli/run.rb +109 -0
- data/lib/spoom/coverage.rb +89 -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 +121 -0
- data/lib/spoom/sorbet/config.rb +51 -9
- data/lib/spoom/sorbet/errors.rb +147 -0
- data/lib/spoom/sorbet/lsp.rb +192 -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 +78 -19
@@ -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::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::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::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[Node]) }
|
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
|