spoom 1.2.0 → 1.2.2

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