spoom 1.1.16 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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