spoom 1.2.0 → 1.2.2

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.
@@ -0,0 +1,102 @@
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
+ #
16
+ # If `allow_mime_types` is empty, all files are collected.
17
+ # If `allow_mime_types` is an array of mimetypes, files without an extension are collected if their mimetype is in
18
+ # the list.
19
+ sig do
20
+ params(
21
+ allow_extensions: T::Array[String],
22
+ allow_mime_types: T::Array[String],
23
+ exclude_patterns: T::Array[String],
24
+ ).void
25
+ end
26
+ def initialize(allow_extensions: [], allow_mime_types: [], exclude_patterns: [])
27
+ @files = T.let([], T::Array[String])
28
+ @allow_extensions = allow_extensions
29
+ @allow_mime_types = allow_mime_types
30
+ @exclude_patterns = exclude_patterns
31
+ end
32
+
33
+ sig { params(paths: T::Array[String]).void }
34
+ def visit_paths(paths)
35
+ paths.each { |path| visit_path(path) }
36
+ end
37
+
38
+ sig { params(path: String).void }
39
+ def visit_path(path)
40
+ path = clean_path(path)
41
+
42
+ return if excluded_path?(path)
43
+
44
+ if File.file?(path)
45
+ visit_file(path)
46
+ elsif File.directory?(path)
47
+ visit_directory(path)
48
+ else # rubocop:disable Style/EmptyElse
49
+ # Ignore aliases, sockets, etc.
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ sig { params(path: String).returns(String) }
56
+ def clean_path(path)
57
+ Pathname.new(path).cleanpath.to_s
58
+ end
59
+
60
+ sig { params(path: String).void }
61
+ def visit_file(path)
62
+ return if excluded_file?(path)
63
+
64
+ @files << path
65
+ end
66
+
67
+ sig { params(path: String).void }
68
+ def visit_directory(path)
69
+ visit_paths(Dir.glob("#{path}/*"))
70
+ end
71
+
72
+ sig { params(path: String).returns(T::Boolean) }
73
+ def excluded_file?(path)
74
+ return false if @allow_extensions.empty?
75
+
76
+ extension = File.extname(path)
77
+ if extension.empty?
78
+ return true if @allow_mime_types.empty?
79
+
80
+ mime = mime_type_for(path)
81
+ @allow_mime_types.none? { |allowed| mime == allowed }
82
+ else
83
+ @allow_extensions.none? { |allowed| extension == allowed }
84
+ end
85
+ end
86
+
87
+ sig { params(path: String).returns(T::Boolean) }
88
+ def excluded_path?(path)
89
+ @exclude_patterns.any? do |pattern|
90
+ # Use `FNM_PATHNAME` so patterns do not match directory separators
91
+ # Use `FNM_EXTGLOB` to allow file globbing through `{a,b}`
92
+ File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
93
+ end
94
+ end
95
+
96
+ sig { params(path: String).returns(T.nilable(String)) }
97
+ def mime_type_for(path)
98
+ # The `file` command appears to be hanging on MacOS for some files so we timeout after 1s.
99
+ %x{timeout 1s file --mime-type -b '#{path}'}.split("; ").first&.strip
100
+ end
101
+ end
102
+ 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
data/lib/spoom/printer.rb CHANGED
@@ -10,8 +10,6 @@ module Spoom
10
10
 
11
11
  include Colorize
12
12
 
13
- abstract!
14
-
15
13
  sig { returns(T.any(IO, StringIO)) }
16
14
  attr_accessor :out
17
15
 
@@ -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
@@ -12,9 +12,6 @@ module Spoom
12
12
  class Message
13
13
  extend T::Sig
14
14
 
15
- sig { returns(String) }
16
- attr_reader :jsonrpc
17
-
18
15
  sig { void }
19
16
  def initialize
20
17
  @jsonrpc = T.let("2.0", String)
@@ -43,9 +40,6 @@ module Spoom
43
40
  sig { returns(Integer) }
44
41
  attr_reader :id
45
42
 
46
- sig { returns(String) }
47
- attr_reader :method
48
-
49
43
  sig { returns(T::Hash[T.untyped, T.untyped]) }
50
44
  attr_reader :params
51
45
 
@@ -310,7 +310,7 @@ module Spoom
310
310
  extend T::Sig
311
311
 
312
312
  sig { returns(T::Set[Integer]) }
313
- attr_accessor :seen
313
+ attr_reader :seen
314
314
 
315
315
  sig { returns(T.nilable(String)) }
316
316
  attr_accessor :prefix
@@ -28,7 +28,7 @@ module Spoom
28
28
  T::Array[String],
29
29
  )
30
30
 
31
- SIGIL_REGEXP = T.let(/^#[\ t]*typed[\ t]*:[ \t]*(\w*)[ \t]*/.freeze, Regexp)
31
+ SIGIL_REGEXP = T.let(/^#[\ t]*typed[\ t]*:[ \t]*(\w*)[ \t]*/, Regexp)
32
32
 
33
33
  class << self
34
34
  extend T::Sig
@@ -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.2"
6
6
  end
data/lib/spoom.rb CHANGED
@@ -12,8 +12,10 @@ 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"
18
+ require "spoom/deadcode"
17
19
  require "spoom/sorbet"
18
20
  require "spoom/cli"
19
21
  require "spoom/version"
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.2
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-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 13.0.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: erubi
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 1.10.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 1.10.0
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: sorbet
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,20 @@ dependencies:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
110
  version: 0.5.9204
111
+ - !ruby/object:Gem::Dependency
112
+ name: syntax_tree
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 6.1.1
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 6.1.1
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: thor
99
127
  requirement: !ruby/object:Gem::Requirement
@@ -143,6 +171,15 @@ files:
143
171
  - lib/spoom/coverage/d3/timeline.rb
144
172
  - lib/spoom/coverage/report.rb
145
173
  - lib/spoom/coverage/snapshot.rb
174
+ - lib/spoom/deadcode.rb
175
+ - lib/spoom/deadcode/definition.rb
176
+ - lib/spoom/deadcode/erb.rb
177
+ - lib/spoom/deadcode/index.rb
178
+ - lib/spoom/deadcode/indexer.rb
179
+ - lib/spoom/deadcode/location.rb
180
+ - lib/spoom/deadcode/reference.rb
181
+ - lib/spoom/deadcode/send.rb
182
+ - lib/spoom/file_collector.rb
146
183
  - lib/spoom/file_tree.rb
147
184
  - lib/spoom/printer.rb
148
185
  - lib/spoom/sorbet.rb
@@ -172,14 +209,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
172
209
  requirements:
173
210
  - - ">="
174
211
  - !ruby/object:Gem::Version
175
- version: 2.7.0
212
+ version: 3.0.0
176
213
  required_rubygems_version: !ruby/object:Gem::Requirement
177
214
  requirements:
178
215
  - - ">="
179
216
  - !ruby/object:Gem::Version
180
217
  version: '0'
181
218
  requirements: []
182
- rubygems_version: 3.4.9
219
+ rubygems_version: 3.4.14
183
220
  signing_key:
184
221
  specification_version: 4
185
222
  summary: Useful tools for Sorbet enthusiasts.