spoom 1.1.16 → 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.
@@ -12,9 +12,9 @@ module Spoom
12
12
  class << self
13
13
  extend T::Sig
14
14
 
15
- sig { params(path: String, rbi: T::Boolean, sorbet_bin: T.nilable(String)).returns(Snapshot) }
16
- def snapshot(path: ".", rbi: true, sorbet_bin: nil)
17
- config = sorbet_config(path: path)
15
+ sig { params(context: Context, rbi: T::Boolean, sorbet_bin: T.nilable(String)).returns(Snapshot) }
16
+ def snapshot(context, rbi: true, sorbet_bin: nil)
17
+ config = context.sorbet_config
18
18
  config.allowed_extensions.push(".rb", ".rbi") if config.allowed_extensions.empty?
19
19
 
20
20
  new_config = config.copy
@@ -27,25 +27,16 @@ module Spoom
27
27
  new_config.options_string,
28
28
  ]
29
29
 
30
- metrics = Spoom::Sorbet.srb_metrics(
31
- *flags,
32
- path: path,
33
- capture_err: true,
34
- sorbet_bin: sorbet_bin,
35
- )
30
+ metrics = context.srb_metrics(*flags, sorbet_bin: sorbet_bin)
31
+
36
32
  # Collect extra information using a different configuration
37
33
  flags << "--ignore sorbet/rbi/"
38
- metrics_without_rbis = Spoom::Sorbet.srb_metrics(
39
- *flags,
40
- path: path,
41
- capture_err: true,
42
- sorbet_bin: sorbet_bin,
43
- )
34
+ metrics_without_rbis = context.srb_metrics(*flags, sorbet_bin: sorbet_bin)
44
35
 
45
36
  snapshot = Snapshot.new
46
37
  return snapshot unless metrics
47
38
 
48
- last_commit = Spoom::Git.last_commit(path: path)
39
+ last_commit = context.git_last_commit
49
40
  snapshot.commit_sha = last_commit&.sha
50
41
  snapshot.commit_timestamp = last_commit&.timestamp
51
42
 
@@ -79,48 +70,42 @@ module Spoom
79
70
  end
80
71
  end
81
72
 
82
- snapshot.version_static = Spoom::Sorbet.version_from_gemfile_lock(gem: "sorbet-static", path: path)
83
- snapshot.version_runtime = Spoom::Sorbet.version_from_gemfile_lock(gem: "sorbet-runtime", path: path)
73
+ snapshot.version_static = context.gem_version_from_gemfile_lock("sorbet-static")
74
+ snapshot.version_runtime = context.gem_version_from_gemfile_lock("sorbet-runtime")
84
75
 
85
- files = Spoom::Sorbet.srb_files(new_config, path: path)
76
+ files = context.srb_files(with_config: new_config)
86
77
  snapshot.rbi_files = files.count { |file| file.end_with?(".rbi") }
87
78
 
88
79
  snapshot
89
80
  end
90
81
 
91
- sig { params(snapshots: T::Array[Snapshot], palette: D3::ColorPalette, path: String).returns(Report) }
92
- def report(snapshots, palette:, path: ".")
93
- intro_commit = Git.sorbet_intro_commit(path: path)
82
+ sig { params(context: Context, snapshots: T::Array[Snapshot], palette: D3::ColorPalette).returns(Report) }
83
+ def report(context, snapshots, palette:)
84
+ intro_commit = context.sorbet_intro_commit
85
+
86
+ file_tree = file_tree(context)
87
+ v = FileTree::CollectScores.new(context)
88
+ v.visit_tree(file_tree)
94
89
 
95
90
  Report.new(
96
- project_name: File.basename(File.expand_path(path)),
91
+ project_name: File.basename(context.absolute_path),
97
92
  palette: palette,
98
93
  snapshots: snapshots,
99
- sigils_tree: sigils_tree(path: path),
94
+ file_tree: file_tree,
95
+ nodes_strictnesses: v.strictnesses,
96
+ nodes_strictness_scores: v.scores,
100
97
  sorbet_intro_commit: intro_commit&.sha,
101
98
  sorbet_intro_date: intro_commit&.time,
102
99
  )
103
100
  end
104
101
 
105
- sig { params(path: String).returns(Sorbet::Config) }
106
- def sorbet_config(path: ".")
107
- Sorbet::Config.parse_file("#{path}/#{Spoom::Sorbet::CONFIG_PATH}")
108
- end
109
-
110
- sig { params(path: String).returns(FileTree) }
111
- def sigils_tree(path: ".")
112
- config = sorbet_config(path: path)
113
- files = Sorbet.srb_files(config, path: path)
114
-
115
- extensions = config.allowed_extensions
116
- extensions = [".rb"] if extensions.empty?
117
- extensions -= [".rbi"]
118
-
119
- pattern = /\.(#{Regexp.union(extensions.map { |ext| ext[1..-1] })})$/
120
- files.select! { |file| file =~ pattern }
121
- files.reject! { |file| file =~ %r{/test/} }
102
+ sig { params(context: Context).returns(FileTree) }
103
+ def file_tree(context)
104
+ config = context.sorbet_config
105
+ config.ignore += ["test"]
122
106
 
123
- FileTree.new(files, strip_prefix: path)
107
+ files = context.srb_files(with_config: config, include_rbis: false)
108
+ FileTree.new(files)
124
109
  end
125
110
  end
126
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/sorbet.rb CHANGED
@@ -39,124 +39,5 @@ module Spoom
39
39
 
40
40
  KILLED_CODE = 137
41
41
  SEGFAULT_CODE = 139
42
-
43
- class << self
44
- extend T::Sig
45
-
46
- sig do
47
- params(
48
- arg: String,
49
- path: String,
50
- capture_err: T::Boolean,
51
- sorbet_bin: T.nilable(String),
52
- ).returns(ExecResult)
53
- end
54
- def srb(*arg, path: ".", capture_err: false, sorbet_bin: nil)
55
- if sorbet_bin
56
- arg.prepend(sorbet_bin)
57
- else
58
- arg.prepend("bundle", "exec", "srb")
59
- end
60
- result = Spoom.exec(*T.unsafe(arg), path: path, capture_err: capture_err)
61
-
62
- case result.exit_code
63
- when KILLED_CODE
64
- raise Error::Killed.new("Sorbet was killed.", result)
65
- when SEGFAULT_CODE
66
- raise Error::Segfault.new("Sorbet segfaulted.", result)
67
- end
68
-
69
- result
70
- end
71
-
72
- sig do
73
- params(
74
- arg: String,
75
- path: String,
76
- capture_err: T::Boolean,
77
- sorbet_bin: T.nilable(String),
78
- ).returns(ExecResult)
79
- end
80
- def srb_tc(*arg, path: ".", capture_err: false, sorbet_bin: nil)
81
- arg.prepend("tc") unless sorbet_bin
82
- srb(*T.unsafe(arg), path: path, capture_err: capture_err, sorbet_bin: sorbet_bin)
83
- end
84
-
85
- # List all files typechecked by Sorbet from its `config`
86
- sig { params(config: Config, path: String).returns(T::Array[String]) }
87
- def srb_files(config, path: ".")
88
- regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
89
- exts = config.allowed_extensions.empty? ? [".rb", ".rbi"] : config.allowed_extensions
90
- Dir.glob((Pathname.new(path) / "**/*{#{exts.join(",")}}").to_s).reject do |f|
91
- regs.any? { |re| re.match?(f) }
92
- end.sort
93
- end
94
-
95
- sig do
96
- params(
97
- arg: String,
98
- path: String,
99
- capture_err: T::Boolean,
100
- sorbet_bin: T.nilable(String),
101
- ).returns(T.nilable(String))
102
- end
103
- def srb_version(*arg, path: ".", capture_err: false, sorbet_bin: nil)
104
- result = T.let(
105
- T.unsafe(self).srb_tc(
106
- "--no-config",
107
- "--version",
108
- *arg,
109
- path: path,
110
- capture_err: capture_err,
111
- sorbet_bin: sorbet_bin,
112
- ),
113
- ExecResult,
114
- )
115
- return nil unless result.status
116
-
117
- result.out.split(" ")[2]
118
- end
119
-
120
- sig do
121
- params(
122
- arg: String,
123
- path: String,
124
- capture_err: T::Boolean,
125
- sorbet_bin: T.nilable(String),
126
- ).returns(T.nilable(T::Hash[String, Integer]))
127
- end
128
- def srb_metrics(*arg, path: ".", capture_err: false, sorbet_bin: nil)
129
- metrics_file = "metrics.tmp"
130
- metrics_path = "#{path}/#{metrics_file}"
131
- T.unsafe(self).srb_tc(
132
- "--metrics-file",
133
- metrics_file,
134
- *arg,
135
- path: path,
136
- capture_err: capture_err,
137
- sorbet_bin: sorbet_bin,
138
- )
139
- if File.exist?(metrics_path)
140
- metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
141
- File.delete(metrics_path)
142
- return metrics
143
- end
144
- nil
145
- end
146
-
147
- # Get `gem` version from the `Gemfile.lock` content
148
- #
149
- # Returns `nil` if `gem` cannot be found in the Gemfile.
150
- sig { params(gem: String, path: String).returns(T.nilable(String)) }
151
- def version_from_gemfile_lock(gem: "sorbet", path: ".")
152
- gemfile_path = "#{path}/Gemfile.lock"
153
- return nil unless File.exist?(gemfile_path)
154
-
155
- content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
156
- return nil unless content
157
-
158
- content[1]
159
- end
160
- end
161
42
  end
162
43
  end