spoom 1.2.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 323ebe199d0efed57d5e92af12eb377c3c5af35e468c83db851b27f86b44117b
4
- data.tar.gz: c0fb24e2af1af3c0e460bb7c06ba9f3cff9cda471ec829ba00742b40dd67ab84
3
+ metadata.gz: b116c81229f1eb09a9e8676643fd54bb3436f7fa2534b06275d657bcd995f0c4
4
+ data.tar.gz: 93431ed795ccdcb7aa3afcec9cd3bc468c5b1893bfac4f269c33df9f52e32055
5
5
  SHA512:
6
- metadata.gz: faa5f86d378dd4cf56aa6e680cdee5717c59069b464b823bebfc63e7c8a7a40aeb7a75d86ec4dd0c69e025c4ad397eff740fdfa2ac134f49868736197836790a
7
- data.tar.gz: 67f7d4bc72282ff4bea575e85c2eed80f7195f94d3b518e3a84af57085bed90c757aedab5cfe33fb6d14dc96b62f21c6e8aa8cb56b79d4e9b148318a7c713e98
6
+ metadata.gz: 826328b46d178109253dc01197f1cf80102c2174d7dc5b3ac1d52c142fee9785e7100e4b444c4bba71e5303fa711d15f8368cd173177b3614b54ce14c60c5b09
7
+ data.tar.gz: 60a28d2220e2507d9deaf12d7e3f51e5ea08418f79685c40aa469da5bc317dbddfded2c8d49cd0102035e5a990d9fa60693ae65a7d1e7538118768bea1140f7f
@@ -54,6 +54,7 @@ module Spoom
54
54
  dry = options[:dry]
55
55
  only = options[:only]
56
56
  cmd = options[:suggest_bump_command]
57
+ directory = File.expand_path(directory)
57
58
  exec_path = File.expand_path(self.exec_path)
58
59
 
59
60
  unless Sorbet::Sigils.valid_strictness?(from)
@@ -73,11 +74,9 @@ module Spoom
73
74
 
74
75
  say("Checking files...")
75
76
 
76
- directory = File.expand_path(directory)
77
- files_to_bump = Sorbet::Sigils.files_with_sigil_strictness(directory, from)
78
-
79
- files_from_config = context.srb_files.map { |file| File.expand_path(file) }
80
- files_to_bump.select! { |file| files_from_config.include?(file) }
77
+ files_to_bump = context.srb_files_with_strictness(from, include_rbis: false)
78
+ .map { |file| File.expand_path(file, context.absolute_path) }
79
+ .select { |file| file.start_with?(directory) }
81
80
 
82
81
  if only
83
82
  list = File.read(only).lines.map { |file| File.expand_path(file.strip) }
data/lib/spoom/cli.rb CHANGED
@@ -39,23 +39,19 @@ module Spoom
39
39
 
40
40
  desc "files", "List all the files typechecked by Sorbet"
41
41
  option :tree, type: :boolean, default: true, desc: "Display list as an indented tree"
42
- option :rbi, type: :boolean, default: true, desc: "Show RBI files"
42
+ option :rbi, type: :boolean, default: false, desc: "Show RBI files"
43
43
  def files
44
44
  context = context_requiring_sorbet!
45
- files = context.srb_files
46
-
47
- unless options[:rbi]
48
- files = files.reject { |file| file.end_with?(".rbi") }
49
- end
50
45
 
46
+ files = context.srb_files(include_rbis: options[:rbi])
51
47
  if files.empty?
52
48
  say_error("No file matching `#{Sorbet::CONFIG_PATH}`")
53
49
  exit(1)
54
50
  end
55
51
 
56
52
  if options[:tree]
57
- tree = FileTree.new(files, strip_prefix: exec_path)
58
- tree.print(colors: options[:color], indent_level: 0)
53
+ tree = FileTree.new(files)
54
+ tree.print_with_strictnesses(context, colors: options[:color])
59
55
  else
60
56
  puts files
61
57
  end
@@ -61,14 +61,32 @@ module Spoom
61
61
  end
62
62
 
63
63
  # List all files typechecked by Sorbet from its `config`
64
- sig { params(with_config: T.nilable(Spoom::Sorbet::Config)).returns(T::Array[String]) }
65
- def srb_files(with_config: nil)
64
+ sig { params(with_config: T.nilable(Spoom::Sorbet::Config), include_rbis: T::Boolean).returns(T::Array[String]) }
65
+ def srb_files(with_config: nil, include_rbis: true)
66
66
  config = with_config || sorbet_config
67
- regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
68
- exts = config.allowed_extensions.empty? ? [".rb", ".rbi"] : config.allowed_extensions
69
- glob("**/*{#{exts.join(",")}}").reject do |f|
70
- regs.any? { |re| re.match?(f) }
71
- end.sort
67
+
68
+ allowed_extensions = config.allowed_extensions
69
+ allowed_extensions = Spoom::Sorbet::Config::DEFAULT_ALLOWED_EXTENSIONS if allowed_extensions.empty?
70
+ allowed_extensions -= [".rbi"] unless include_rbis
71
+
72
+ excluded_patterns = config.ignore.map { |string| File.join("**", string, "**") }
73
+
74
+ collector = FileCollector.new(allow_extensions: allowed_extensions, exclude_patterns: excluded_patterns)
75
+ collector.visit_paths(config.paths.map { |path| absolute_path_to(path) })
76
+ collector.files.map { |file| file.delete_prefix("#{absolute_path}/") }.sort
77
+ end
78
+
79
+ # List all files typechecked by Sorbet from its `config` that matches `strictness`
80
+ sig do
81
+ params(
82
+ strictness: String,
83
+ with_config: T.nilable(Spoom::Sorbet::Config),
84
+ include_rbis: T::Boolean,
85
+ ).returns(T::Array[String])
86
+ end
87
+ def srb_files_with_strictness(strictness, with_config: nil, include_rbis: true)
88
+ srb_files(with_config: with_config, include_rbis: include_rbis)
89
+ .select { |file| read_file_strictness(file) == strictness }
72
90
  end
73
91
 
74
92
  sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(T.nilable(String)) }
@@ -148,48 +148,34 @@ module Spoom
148
148
  class Sigils < CircleMap
149
149
  extend T::Sig
150
150
 
151
- sig { params(id: String, sigils_tree: FileTree).void }
152
- def initialize(id, sigils_tree)
153
- @scores = T.let({}, T::Hash[FileTree::Node, Float])
154
- @strictnesses = T.let({}, T::Hash[FileTree::Node, T.nilable(String)])
155
- @sigils_tree = sigils_tree
156
- super(id, sigils_tree.roots.map { |r| tree_node_to_json(r) })
151
+ sig do
152
+ params(
153
+ id: String,
154
+ file_tree: FileTree,
155
+ nodes_strictnesses: T::Hash[FileTree::Node, T.nilable(String)],
156
+ nodes_scores: T::Hash[FileTree::Node, Float],
157
+ ).void
158
+ end
159
+ def initialize(id, file_tree, nodes_strictnesses, nodes_scores)
160
+ @nodes_strictnesses = nodes_strictnesses
161
+ @nodes_scores = nodes_scores
162
+ super(id, file_tree.roots.map { |r| tree_node_to_json(r) })
157
163
  end
158
164
 
159
165
  sig { params(node: FileTree::Node).returns(T::Hash[Symbol, T.untyped]) }
160
166
  def tree_node_to_json(node)
161
167
  if node.children.empty?
162
- return { name: node.name, strictness: tree_node_strictness(node) }
163
- end
164
-
165
- {
166
- name: node.name,
167
- children: node.children.values.map { |n| tree_node_to_json(n) },
168
- score: tree_node_score(node),
169
- }
170
- end
171
-
172
- sig { params(node: FileTree::Node).returns(T.nilable(String)) }
173
- def tree_node_strictness(node)
174
- prefix = @sigils_tree.strip_prefix
175
- path = node.path
176
- path = "#{prefix}/#{path}" if prefix
177
- @strictnesses[node] ||= Spoom::Sorbet::Sigils.file_strictness(path)
178
- end
179
-
180
- sig { params(node: FileTree::Node).returns(Float) }
181
- def tree_node_score(node)
182
- unless @scores.key?(node)
183
- if node.name =~ /\.rbi?$/
184
- case tree_node_strictness(node)
185
- when "true", "strict", "strong"
186
- @scores[node] = 1.0
187
- end
188
- elsif !node.children.empty?
189
- @scores[node] = node.children.values.sum { |n| tree_node_score(n) } / node.children.size.to_f
190
- end
168
+ {
169
+ name: node.name,
170
+ strictness: @nodes_strictnesses.fetch(node, "false"),
171
+ }
172
+ else
173
+ {
174
+ name: node.name,
175
+ children: node.children.values.map { |n| tree_node_to_json(n) },
176
+ score: @nodes_scores.fetch(node, 0.0),
177
+ }
191
178
  end
192
- @scores[node] || 0.0
193
179
  end
194
180
  end
195
181
  end
@@ -153,9 +153,24 @@ module Spoom
153
153
  class Map < Card
154
154
  extend T::Sig
155
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)
156
+ sig do
157
+ params(
158
+ file_tree: FileTree,
159
+ nodes_strictnesses: T::Hash[FileTree::Node, T.nilable(String)],
160
+ nodes_strictness_scores: T::Hash[FileTree::Node, Float],
161
+ title: String,
162
+ ).void
163
+ end
164
+ def initialize(file_tree:, nodes_strictnesses:, nodes_strictness_scores:, title: "Strictness Map")
165
+ super(
166
+ title: title,
167
+ body: D3::CircleMap::Sigils.new(
168
+ "map_sigils",
169
+ file_tree,
170
+ nodes_strictnesses,
171
+ nodes_strictness_scores,
172
+ ).html
173
+ )
159
174
  end
160
175
  end
161
176
 
@@ -246,27 +261,14 @@ module Spoom
246
261
  class Report < Page
247
262
  extend T::Sig
248
263
 
249
- sig { returns(String) }
250
- attr_reader :project_name
251
-
252
- sig { returns(T.nilable(String)) }
253
- attr_reader :sorbet_intro_commit
254
-
255
- sig { returns(T.nilable(Time)) }
256
- attr_reader :sorbet_intro_date
257
-
258
- sig { returns(T::Array[Snapshot]) }
259
- attr_reader :snapshots
260
-
261
- sig { returns(FileTree) }
262
- attr_reader :sigils_tree
263
-
264
264
  sig do
265
265
  params(
266
266
  project_name: String,
267
267
  palette: D3::ColorPalette,
268
268
  snapshots: T::Array[Snapshot],
269
- sigils_tree: FileTree,
269
+ file_tree: FileTree,
270
+ nodes_strictnesses: T::Hash[FileTree::Node, T.nilable(String)],
271
+ nodes_strictness_scores: T::Hash[FileTree::Node, Float],
270
272
  sorbet_intro_commit: T.nilable(String),
271
273
  sorbet_intro_date: T.nilable(Time),
272
274
  ).void
@@ -275,24 +277,28 @@ module Spoom
275
277
  project_name:,
276
278
  palette:,
277
279
  snapshots:,
278
- sigils_tree:,
280
+ file_tree:,
281
+ nodes_strictnesses:,
282
+ nodes_strictness_scores:,
279
283
  sorbet_intro_commit: nil,
280
284
  sorbet_intro_date: nil
281
285
  )
282
286
  super(title: project_name, palette: palette)
283
287
  @project_name = project_name
284
288
  @snapshots = snapshots
285
- @sigils_tree = sigils_tree
289
+ @file_tree = file_tree
290
+ @nodes_strictnesses = nodes_strictnesses
291
+ @nodes_strictness_scores = nodes_strictness_scores
286
292
  @sorbet_intro_commit = sorbet_intro_commit
287
293
  @sorbet_intro_date = sorbet_intro_date
288
294
  end
289
295
 
290
296
  sig { override.returns(String) }
291
297
  def header_html
292
- last = T.must(snapshots.last)
298
+ last = T.must(@snapshots.last)
293
299
  <<~ERB
294
300
  <h1 class="display-3">
295
- #{project_name}
301
+ #{@project_name}
296
302
  <span class="badge badge-pill badge-dark" style="font-size: 20%;">#{last.commit_sha}</span>
297
303
  </h1>
298
304
  ERB
@@ -300,17 +306,24 @@ module Spoom
300
306
 
301
307
  sig { override.returns(T::Array[Cards::Card]) }
302
308
  def cards
303
- last = T.must(snapshots.last)
309
+ last = T.must(@snapshots.last)
304
310
  cards = []
305
311
  cards << Cards::Snapshot.new(snapshot: last)
306
- cards << Cards::Map.new(sigils_tree: sigils_tree)
307
- cards << Cards::Timeline::Sigils.new(snapshots: snapshots)
308
- cards << Cards::Timeline::Calls.new(snapshots: snapshots)
309
- cards << Cards::Timeline::Sigs.new(snapshots: snapshots)
310
- cards << Cards::Timeline::RBIs.new(snapshots: snapshots)
311
- cards << Cards::Timeline::Versions.new(snapshots: snapshots)
312
- cards << Cards::Timeline::Runtimes.new(snapshots: snapshots)
313
- cards << Cards::SorbetIntro.new(sorbet_intro_commit: sorbet_intro_commit, sorbet_intro_date: sorbet_intro_date)
312
+ cards << Cards::Map.new(
313
+ file_tree: @file_tree,
314
+ nodes_strictnesses: @nodes_strictnesses,
315
+ nodes_strictness_scores: @nodes_strictness_scores,
316
+ )
317
+ cards << Cards::Timeline::Sigils.new(snapshots: @snapshots)
318
+ cards << Cards::Timeline::Calls.new(snapshots: @snapshots)
319
+ cards << Cards::Timeline::Sigs.new(snapshots: @snapshots)
320
+ cards << Cards::Timeline::RBIs.new(snapshots: @snapshots)
321
+ cards << Cards::Timeline::Versions.new(snapshots: @snapshots)
322
+ cards << Cards::Timeline::Runtimes.new(snapshots: @snapshots)
323
+ cards << Cards::SorbetIntro.new(
324
+ sorbet_intro_commit: @sorbet_intro_commit,
325
+ sorbet_intro_date: @sorbet_intro_date,
326
+ )
314
327
  cards
315
328
  end
316
329
  end
@@ -83,29 +83,29 @@ module Spoom
83
83
  def report(context, snapshots, palette:)
84
84
  intro_commit = context.sorbet_intro_commit
85
85
 
86
+ file_tree = file_tree(context)
87
+ v = FileTree::CollectScores.new(context)
88
+ v.visit_tree(file_tree)
89
+
86
90
  Report.new(
87
91
  project_name: File.basename(context.absolute_path),
88
92
  palette: palette,
89
93
  snapshots: snapshots,
90
- sigils_tree: sigils_tree(context),
94
+ file_tree: file_tree,
95
+ nodes_strictnesses: v.strictnesses,
96
+ nodes_strictness_scores: v.scores,
91
97
  sorbet_intro_commit: intro_commit&.sha,
92
98
  sorbet_intro_date: intro_commit&.time,
93
99
  )
94
100
  end
95
101
 
96
102
  sig { params(context: Context).returns(FileTree) }
97
- def sigils_tree(context)
98
- files = context.srb_files
99
-
100
- extensions = context.sorbet_config.allowed_extensions
101
- extensions = [".rb"] if extensions.empty?
102
- extensions -= [".rbi"]
103
-
104
- pattern = /\.(#{Regexp.union(extensions.map { |ext| ext[1..-1] })})$/
105
- files.select! { |file| file =~ pattern }
106
- files.reject! { |file| file =~ %r{/test/} }
103
+ def file_tree(context)
104
+ config = context.sorbet_config
105
+ config.ignore += ["test"]
107
106
 
108
- FileTree.new(files, strip_prefix: context.absolute_path)
107
+ files = context.srb_files(with_config: config, include_rbis: false)
108
+ FileTree.new(files)
109
109
  end
110
110
  end
111
111
  end
@@ -0,0 +1,79 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ class FileCollector
6
+ extend T::Sig
7
+
8
+ sig { returns(T::Array[String]) }
9
+ attr_reader :files
10
+
11
+ # Initialize a new file collector
12
+ #
13
+ # If `allow_extensions` is empty, all files are collected.
14
+ # If `allow_extensions` is an array of extensions, only files with one of these extensions are collected.
15
+ sig do
16
+ params(
17
+ allow_extensions: T::Array[String],
18
+ exclude_patterns: T::Array[String],
19
+ ).void
20
+ end
21
+ def initialize(allow_extensions: [], exclude_patterns: [])
22
+ @files = T.let([], T::Array[String])
23
+ @allow_extensions = allow_extensions
24
+ @exclude_patterns = exclude_patterns
25
+ end
26
+
27
+ sig { params(paths: T::Array[String]).void }
28
+ def visit_paths(paths)
29
+ paths.each { |path| visit_path(path) }
30
+ end
31
+
32
+ sig { params(path: String).void }
33
+ def visit_path(path)
34
+ path = clean_path(path)
35
+
36
+ return if excluded_path?(path)
37
+
38
+ if File.file?(path)
39
+ visit_file(path)
40
+ elsif File.directory?(path)
41
+ visit_directory(path)
42
+ else # rubocop:disable Style/EmptyElse
43
+ # Ignore aliases, sockets, etc.
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ sig { params(path: String).returns(String) }
50
+ def clean_path(path)
51
+ Pathname.new(path).cleanpath.to_s
52
+ end
53
+
54
+ sig { params(path: String).void }
55
+ def visit_file(path)
56
+ return if excluded_file?(path)
57
+
58
+ @files << path
59
+ end
60
+
61
+ sig { params(path: String).void }
62
+ def visit_directory(path)
63
+ visit_paths(Dir.glob("#{path}/*"))
64
+ end
65
+
66
+ sig { params(path: String).returns(T::Boolean) }
67
+ def excluded_file?(path)
68
+ return false if @allow_extensions.empty?
69
+
70
+ extension = File.extname(path)
71
+ @allow_extensions.none? { |allowed| extension == allowed }
72
+ end
73
+
74
+ sig { params(path: String).returns(T::Boolean) }
75
+ def excluded_path?(path)
76
+ @exclude_patterns.any? { |pattern| File.fnmatch?(pattern, path) }
77
+ end
78
+ end
79
+ end
@@ -6,13 +6,9 @@ module Spoom
6
6
  class FileTree
7
7
  extend T::Sig
8
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)
9
+ sig { params(paths: T::Enumerable[String]).void }
10
+ def initialize(paths = [])
14
11
  @roots = T.let({}, T::Hash[String, Node])
15
- @strip_prefix = strip_prefix
16
12
  add_paths(paths)
17
13
  end
18
14
 
@@ -27,8 +23,6 @@ module Spoom
27
23
  # This will create all nodes until the root of `path`.
28
24
  sig { params(path: String).returns(Node) }
29
25
  def add_path(path)
30
- prefix = @strip_prefix
31
- path = path.delete_prefix("#{prefix}/") if prefix
32
26
  parts = path.split("/")
33
27
  if path.empty? || parts.size == 1
34
28
  return @roots[path] ||= Node.new(parent: nil, name: path)
@@ -49,43 +43,51 @@ module Spoom
49
43
  # All the nodes in this tree
50
44
  sig { returns(T::Array[Node]) }
51
45
  def nodes
52
- all_nodes = []
53
- @roots.values.each { |root| collect_nodes(root, all_nodes) }
54
- all_nodes
46
+ v = CollectNodes.new
47
+ v.visit_tree(self)
48
+ v.nodes
55
49
  end
56
50
 
57
51
  # All the paths in this tree
58
52
  sig { returns(T::Array[String]) }
59
53
  def paths
60
- nodes.collect(&:path)
54
+ nodes.map(&:path)
61
55
  end
62
56
 
63
- sig do
64
- params(
65
- out: T.any(IO, StringIO),
66
- show_strictness: T::Boolean,
67
- colors: T::Boolean,
68
- indent_level: Integer,
69
- ).void
57
+ # Return a map of strictnesses for each node in the tree
58
+ sig { params(context: Context).returns(T::Hash[Node, T.nilable(String)]) }
59
+ def nodes_strictnesses(context)
60
+ v = CollectStrictnesses.new(context)
61
+ v.visit_tree(self)
62
+ v.strictnesses
70
63
  end
71
- def print(out: $stdout, show_strictness: true, colors: true, indent_level: 0)
72
- printer = TreePrinter.new(
73
- tree: self,
74
- out: out,
75
- show_strictness: show_strictness,
76
- colors: colors,
77
- indent_level: indent_level,
78
- )
79
- printer.print_tree
64
+
65
+ # Return a map of typing scores for each node in the tree
66
+ sig { params(context: Context).returns(T::Hash[Node, Float]) }
67
+ def nodes_strictness_scores(context)
68
+ v = CollectScores.new(context)
69
+ v.visit_tree(self)
70
+ v.scores
80
71
  end
81
72
 
82
- private
73
+ # Return a map of typing scores for each path in the tree
74
+ sig { params(context: Context).returns(T::Hash[String, Float]) }
75
+ def paths_strictness_scores(context)
76
+ nodes_strictness_scores(context).map { |node, score| [node.path, score] }.to_h
77
+ end
83
78
 
84
- sig { params(node: FileTree::Node, collected_nodes: T::Array[Node]).returns(T::Array[Node]) }
85
- def collect_nodes(node, collected_nodes = [])
86
- collected_nodes << node
87
- node.children.values.each { |child| collect_nodes(child, collected_nodes) }
88
- collected_nodes
79
+ sig { params(out: T.any(IO, StringIO), colors: T::Boolean).void }
80
+ def print(out: $stdout, colors: true)
81
+ printer = Printer.new({}, out: out, colors: colors)
82
+ printer.visit_tree(self)
83
+ end
84
+
85
+ sig { params(context: Context, out: T.any(IO, StringIO), colors: T::Boolean).void }
86
+ def print_with_strictnesses(context, out: $stdout, colors: true)
87
+ strictnesses = nodes_strictnesses(context)
88
+
89
+ printer = Printer.new(strictnesses, out: out, colors: colors)
90
+ printer.visit_tree(self)
89
91
  end
90
92
 
91
93
  # A node representing either a file or a directory inside a FileTree
@@ -111,77 +113,160 @@ module Spoom
111
113
  end
112
114
  end
113
115
 
116
+ # An abstract visitor for FileTree
117
+ class Visitor
118
+ extend T::Sig
119
+ extend T::Helpers
120
+
121
+ abstract!
122
+
123
+ sig { params(tree: FileTree).void }
124
+ def visit_tree(tree)
125
+ visit_nodes(tree.roots)
126
+ end
127
+
128
+ sig { params(node: FileTree::Node).void }
129
+ def visit_node(node)
130
+ visit_nodes(node.children.values)
131
+ end
132
+
133
+ sig { params(nodes: T::Array[FileTree::Node]).void }
134
+ def visit_nodes(nodes)
135
+ nodes.each { |node| visit_node(node) }
136
+ end
137
+ end
138
+
139
+ # A visitor that collects all the nodes in a tree
140
+ class CollectNodes < Visitor
141
+ extend T::Sig
142
+
143
+ sig { returns(T::Array[FileTree::Node]) }
144
+ attr_reader :nodes
145
+
146
+ sig { void }
147
+ def initialize
148
+ super()
149
+ @nodes = T.let([], T::Array[FileTree::Node])
150
+ end
151
+
152
+ sig { override.params(node: FileTree::Node).void }
153
+ def visit_node(node)
154
+ @nodes << node
155
+ super
156
+ end
157
+ end
158
+
159
+ # A visitor that collects the strictness of each node in a tree
160
+ class CollectStrictnesses < Visitor
161
+ extend T::Sig
162
+
163
+ sig { returns(T::Hash[Node, T.nilable(String)]) }
164
+ attr_reader :strictnesses
165
+
166
+ sig { params(context: Context).void }
167
+ def initialize(context)
168
+ super()
169
+ @context = context
170
+ @strictnesses = T.let({}, T::Hash[Node, T.nilable(String)])
171
+ end
172
+
173
+ sig { override.params(node: FileTree::Node).void }
174
+ def visit_node(node)
175
+ path = node.path
176
+ @strictnesses[node] = @context.read_file_strictness(path) if @context.file?(path)
177
+
178
+ super
179
+ end
180
+ end
181
+
182
+ # A visitor that collects the typing score of each node in a tree
183
+ class CollectScores < CollectStrictnesses
184
+ extend T::Sig
185
+
186
+ sig { returns(T::Hash[Node, Float]) }
187
+ attr_reader :scores
188
+
189
+ sig { params(context: Context).void }
190
+ def initialize(context)
191
+ super
192
+ @context = context
193
+ @scores = T.let({}, T::Hash[Node, Float])
194
+ end
195
+
196
+ sig { override.params(node: FileTree::Node).void }
197
+ def visit_node(node)
198
+ super
199
+
200
+ @scores[node] = node_score(node)
201
+ end
202
+
203
+ private
204
+
205
+ sig { params(node: Node).returns(Float) }
206
+ def node_score(node)
207
+ if @context.file?(node.path)
208
+ strictness_score(@strictnesses[node])
209
+ else
210
+ node.children.values.sum { |child| @scores.fetch(child, 0.0) } / node.children.size.to_f
211
+ end
212
+ end
213
+
214
+ sig { params(strictness: T.nilable(String)).returns(Float) }
215
+ def strictness_score(strictness)
216
+ case strictness
217
+ when "true", "strict", "strong"
218
+ 1.0
219
+ else
220
+ 0.0
221
+ end
222
+ end
223
+ end
224
+
114
225
  # An internal class used to print a FileTree
115
226
  #
116
227
  # See `FileTree#print`
117
- class TreePrinter < Spoom::Printer
228
+ class Printer < Visitor
118
229
  extend T::Sig
119
230
 
120
- sig { returns(FileTree) }
121
- attr_reader :tree
122
-
123
231
  sig do
124
232
  params(
125
- tree: FileTree,
233
+ strictnesses: T::Hash[FileTree::Node, T.nilable(String)],
126
234
  out: T.any(IO, StringIO),
127
- show_strictness: T::Boolean,
128
235
  colors: T::Boolean,
129
- indent_level: Integer,
130
236
  ).void
131
237
  end
132
- def initialize(tree:, out: $stdout, show_strictness: true, colors: true, indent_level: 0)
133
- super(out: out, colors: colors, indent_level: indent_level)
134
- @tree = tree
135
- @show_strictness = show_strictness
238
+ def initialize(strictnesses, out: $stdout, colors: true)
239
+ super()
240
+ @strictnesses = strictnesses
241
+ @colors = colors
242
+ @printer = T.let(Spoom::Printer.new(out: out, colors: colors), Spoom::Printer)
136
243
  end
137
244
 
138
- sig { void }
139
- def print_tree
140
- print_nodes(tree.roots)
141
- end
142
-
143
- sig { params(node: FileTree::Node).void }
144
- def print_node(node)
145
- printt
245
+ sig { override.params(node: FileTree::Node).void }
246
+ def visit_node(node)
247
+ @printer.printt
146
248
  if node.children.empty?
147
- if @show_strictness
148
- strictness = node_strictness(node)
149
- if @colors
150
- print_colored(node.name, strictness_color(strictness))
151
- elsif strictness
152
- print("#{node.name} (#{strictness})")
153
- else
154
- print(node.name.to_s)
155
- end
249
+ strictness = @strictnesses[node]
250
+ if @colors
251
+ @printer.print_colored(node.name, strictness_color(strictness))
252
+ elsif strictness
253
+ @printer.print("#{node.name} (#{strictness})")
156
254
  else
157
- print(node.name.to_s)
255
+ @printer.print(node.name.to_s)
158
256
  end
159
- print("\n")
257
+ @printer.print("\n")
160
258
  else
161
- print_colored(node.name, Color::BLUE)
162
- print("/")
163
- printn
164
- indent
165
- print_nodes(node.children.values)
166
- dedent
259
+ @printer.print_colored(node.name, Color::BLUE)
260
+ @printer.print("/")
261
+ @printer.printn
262
+ @printer.indent
263
+ super
264
+ @printer.dedent
167
265
  end
168
266
  end
169
267
 
170
- sig { params(nodes: T::Array[FileTree::Node]).void }
171
- def print_nodes(nodes)
172
- nodes.each { |node| print_node(node) }
173
- end
174
-
175
268
  private
176
269
 
177
- sig { params(node: FileTree::Node).returns(T.nilable(String)) }
178
- def node_strictness(node)
179
- path = node.path
180
- prefix = tree.strip_prefix
181
- path = "#{prefix}/#{path}" if prefix
182
- Spoom::Sorbet::Sigils.file_strictness(path)
183
- end
184
-
185
270
  sig { params(strictness: T.nilable(String)).returns(Color) }
186
271
  def strictness_color(strictness)
187
272
  case strictness
@@ -26,8 +26,10 @@ module Spoom
26
26
  class Config
27
27
  extend T::Sig
28
28
 
29
+ DEFAULT_ALLOWED_EXTENSIONS = T.let([".rb", ".rbi"].freeze, T::Array[String])
30
+
29
31
  sig { returns(T::Array[String]) }
30
- attr_reader :paths, :ignore, :allowed_extensions
32
+ attr_accessor :paths, :ignore, :allowed_extensions
31
33
 
32
34
  sig { returns(T::Boolean) }
33
35
  attr_accessor :no_stdlib
@@ -85,21 +85,6 @@ module Spoom
85
85
  change_sigil_in_file(path, new_strictness)
86
86
  end
87
87
  end
88
-
89
- # finds all files in the specified directory with the passed strictness
90
- sig do
91
- params(
92
- directory: T.any(String, Pathname),
93
- strictness: String,
94
- extension: String,
95
- ).returns(T::Array[String])
96
- end
97
- def files_with_sigil_strictness(directory, strictness, extension: ".rb")
98
- paths = Dir.glob("#{File.expand_path(directory)}/**/*#{extension}").sort.uniq
99
- paths.filter do |path|
100
- file_strictness(path) == strictness
101
- end
102
- end
103
88
  end
104
89
  end
105
90
  end
data/lib/spoom/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.2.0"
5
+ VERSION = "1.2.1"
6
6
  end
data/lib/spoom.rb CHANGED
@@ -12,6 +12,7 @@ module Spoom
12
12
  class Error < StandardError; end
13
13
  end
14
14
 
15
+ require "spoom/file_collector"
15
16
  require "spoom/context"
16
17
  require "spoom/colors"
17
18
  require "spoom/sorbet"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spoom
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Terrasa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-30 00:00:00.000000000 Z
11
+ date: 2023-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -143,6 +143,7 @@ files:
143
143
  - lib/spoom/coverage/d3/timeline.rb
144
144
  - lib/spoom/coverage/report.rb
145
145
  - lib/spoom/coverage/snapshot.rb
146
+ - lib/spoom/file_collector.rb
146
147
  - lib/spoom/file_tree.rb
147
148
  - lib/spoom/printer.rb
148
149
  - lib/spoom/sorbet.rb