spoom 1.3.1 → 1.3.3

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/spoom/cli/deadcode.rb +21 -17
  3. data/lib/spoom/deadcode/index.rb +178 -10
  4. data/lib/spoom/deadcode/indexer.rb +14 -435
  5. data/lib/spoom/deadcode/plugins/action_mailer.rb +3 -3
  6. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +9 -3
  7. data/lib/spoom/deadcode/plugins/actionpack.rb +12 -9
  8. data/lib/spoom/deadcode/plugins/active_model.rb +8 -8
  9. data/lib/spoom/deadcode/plugins/active_record.rb +5 -5
  10. data/lib/spoom/deadcode/plugins/active_support.rb +4 -4
  11. data/lib/spoom/deadcode/plugins/base.rb +70 -57
  12. data/lib/spoom/deadcode/plugins/graphql.rb +8 -8
  13. data/lib/spoom/deadcode/plugins/minitest.rb +4 -3
  14. data/lib/spoom/deadcode/plugins/namespaces.rb +9 -12
  15. data/lib/spoom/deadcode/plugins/rails.rb +9 -9
  16. data/lib/spoom/deadcode/plugins/rubocop.rb +13 -17
  17. data/lib/spoom/deadcode/plugins/ruby.rb +9 -9
  18. data/lib/spoom/deadcode/plugins/sorbet.rb +15 -18
  19. data/lib/spoom/deadcode/plugins/thor.rb +5 -4
  20. data/lib/spoom/deadcode/plugins.rb +4 -5
  21. data/lib/spoom/deadcode/remover.rb +14 -10
  22. data/lib/spoom/deadcode/send.rb +1 -0
  23. data/lib/spoom/deadcode.rb +4 -73
  24. data/lib/spoom/location.rb +84 -0
  25. data/lib/spoom/model/builder.rb +246 -0
  26. data/lib/spoom/model/model.rb +328 -0
  27. data/lib/spoom/model/namespace_visitor.rb +50 -0
  28. data/lib/spoom/model/reference.rb +49 -0
  29. data/lib/spoom/model/references_visitor.rb +200 -0
  30. data/lib/spoom/model.rb +10 -0
  31. data/lib/spoom/parse.rb +28 -0
  32. data/lib/spoom/poset.rb +197 -0
  33. data/lib/spoom/sorbet/errors.rb +5 -3
  34. data/lib/spoom/sorbet/lsp/errors.rb +1 -1
  35. data/lib/spoom/sorbet.rb +1 -1
  36. data/lib/spoom/version.rb +1 -1
  37. data/lib/spoom/visitor.rb +755 -0
  38. data/lib/spoom.rb +2 -0
  39. metadata +20 -13
  40. data/lib/spoom/deadcode/location.rb +0 -86
  41. data/lib/spoom/deadcode/reference.rb +0 -34
  42. data/lib/spoom/deadcode/visitor.rb +0 -755
@@ -0,0 +1,328 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ class Model
6
+ extend T::Sig
7
+
8
+ class Error < Spoom::Error; end
9
+
10
+ # A Symbol is a uniquely named entity in the Ruby codebase
11
+ #
12
+ # A symbol can have multiple definitions, e.g. a class can be reopened.
13
+ # Sometimes a symbol can have multiple definitions of different types,
14
+ # e.g. `foo` method can be defined both as a method and as an attribute accessor.
15
+ class Symbol
16
+ extend T::Sig
17
+
18
+ # The full, unique name of this symbol
19
+ sig { returns(String) }
20
+ attr_reader :full_name
21
+
22
+ # The definitions of this symbol (where it exists in the code)
23
+ sig { returns(T::Array[SymbolDef]) }
24
+ attr_reader :definitions
25
+
26
+ sig { params(full_name: String).void }
27
+ def initialize(full_name)
28
+ @full_name = full_name
29
+ @definitions = T.let([], T::Array[SymbolDef])
30
+ end
31
+
32
+ # The short name of this symbol
33
+ sig { returns(String) }
34
+ def name
35
+ T.must(@full_name.split("::").last)
36
+ end
37
+
38
+ sig { returns(String) }
39
+ def to_s
40
+ @full_name
41
+ end
42
+ end
43
+
44
+ class UnresolvedSymbol < Symbol
45
+ sig { override.returns(String) }
46
+ def to_s
47
+ "<#{@full_name}>"
48
+ end
49
+ end
50
+
51
+ # A SymbolDef is a definition of a Symbol
52
+ #
53
+ # It can be a class, module, constant, method, etc.
54
+ # A SymbolDef has a location pointing to the actual code that defines the symbol.
55
+ class SymbolDef
56
+ extend T::Sig
57
+ extend T::Helpers
58
+
59
+ abstract!
60
+
61
+ # The symbol this definition belongs to
62
+ sig { returns(Symbol) }
63
+ attr_reader :symbol
64
+
65
+ # The enclosing namespace this definition belongs to
66
+ sig { returns(T.nilable(Namespace)) }
67
+ attr_reader :owner
68
+
69
+ # The actual code location of this definition
70
+ sig { returns(Location) }
71
+ attr_reader :location
72
+
73
+ sig { params(symbol: Symbol, owner: T.nilable(Namespace), location: Location).void }
74
+ def initialize(symbol, owner:, location:)
75
+ @symbol = symbol
76
+ @owner = owner
77
+ @location = location
78
+
79
+ symbol.definitions << self
80
+ owner.children << self if owner
81
+ end
82
+
83
+ # The full name of the symbol this definition belongs to
84
+ sig { returns(String) }
85
+ def full_name
86
+ @symbol.full_name
87
+ end
88
+
89
+ # The short name of the symbol this definition belongs to
90
+ sig { returns(String) }
91
+ def name
92
+ @symbol.name
93
+ end
94
+ end
95
+
96
+ # A class or module
97
+ class Namespace < SymbolDef
98
+ abstract!
99
+
100
+ sig { returns(T::Array[SymbolDef]) }
101
+ attr_reader :children
102
+
103
+ sig { returns(T::Array[Mixin]) }
104
+ attr_reader :mixins
105
+
106
+ sig { params(symbol: Symbol, owner: T.nilable(Namespace), location: Location).void }
107
+ def initialize(symbol, owner:, location:)
108
+ super(symbol, owner: owner, location: location)
109
+
110
+ @children = T.let([], T::Array[SymbolDef])
111
+ @mixins = T.let([], T::Array[Mixin])
112
+ end
113
+ end
114
+
115
+ class SingletonClass < Namespace; end
116
+
117
+ class Class < Namespace
118
+ sig { returns(T.nilable(String)) }
119
+ attr_accessor :superclass_name
120
+
121
+ sig do
122
+ params(
123
+ symbol: Symbol,
124
+ owner: T.nilable(Namespace),
125
+ location: Location,
126
+ superclass_name: T.nilable(String),
127
+ ).void
128
+ end
129
+ def initialize(symbol, owner:, location:, superclass_name: nil)
130
+ super(symbol, owner: owner, location: location)
131
+
132
+ @superclass_name = superclass_name
133
+ end
134
+ end
135
+
136
+ class Module < Namespace; end
137
+
138
+ class Constant < SymbolDef
139
+ sig { returns(String) }
140
+ attr_reader :value
141
+
142
+ sig { params(symbol: Symbol, owner: T.nilable(Namespace), location: Location, value: String).void }
143
+ def initialize(symbol, owner:, location:, value:)
144
+ super(symbol, owner: owner, location: location)
145
+
146
+ @value = value
147
+ end
148
+ end
149
+
150
+ # A method or an attribute accessor
151
+ class Property < SymbolDef
152
+ abstract!
153
+
154
+ sig { returns(Visibility) }
155
+ attr_reader :visibility
156
+
157
+ sig { returns(T::Array[Sig]) }
158
+ attr_reader :sigs
159
+
160
+ sig do
161
+ params(
162
+ symbol: Symbol,
163
+ owner: T.nilable(Namespace),
164
+ location: Location,
165
+ visibility: Visibility,
166
+ sigs: T::Array[Sig],
167
+ ).void
168
+ end
169
+ def initialize(symbol, owner:, location:, visibility:, sigs: [])
170
+ super(symbol, owner: owner, location: location)
171
+
172
+ @visibility = visibility
173
+ @sigs = sigs
174
+ end
175
+ end
176
+
177
+ class Method < Property; end
178
+
179
+ class Attr < Property
180
+ abstract!
181
+ end
182
+
183
+ class AttrReader < Attr; end
184
+ class AttrWriter < Attr; end
185
+ class AttrAccessor < Attr; end
186
+
187
+ class Visibility < T::Enum
188
+ enums do
189
+ Public = new("public")
190
+ Protected = new("protected")
191
+ Private = new("private")
192
+ end
193
+ end
194
+
195
+ # A mixin (include, prepend, extend) to a namespace
196
+ class Mixin
197
+ extend T::Sig
198
+ extend T::Helpers
199
+
200
+ abstract!
201
+
202
+ sig { returns(String) }
203
+ attr_reader :name
204
+
205
+ sig { params(name: String).void }
206
+ def initialize(name)
207
+ @name = name
208
+ end
209
+ end
210
+
211
+ class Include < Mixin; end
212
+ class Prepend < Mixin; end
213
+ class Extend < Mixin; end
214
+
215
+ # A Sorbet signature (sig block)
216
+ class Sig
217
+ extend T::Sig
218
+
219
+ sig { returns(String) }
220
+ attr_reader :string
221
+
222
+ sig { params(string: String).void }
223
+ def initialize(string)
224
+ @string = string
225
+ end
226
+ end
227
+
228
+ # Model
229
+
230
+ # All the symbols registered in this model
231
+ sig { returns(T::Hash[String, Symbol]) }
232
+ attr_reader :symbols
233
+
234
+ sig { returns(Poset[Symbol]) }
235
+ attr_reader :symbols_hierarchy
236
+
237
+ sig { void }
238
+ def initialize
239
+ @symbols = T.let({}, T::Hash[String, Symbol])
240
+ @symbols_hierarchy = T.let(Poset[Symbol].new, Poset[Symbol])
241
+ end
242
+
243
+ # Get a symbol by it's full name
244
+ #
245
+ # Raises an error if the symbol is not found
246
+ sig { params(full_name: String).returns(Symbol) }
247
+ def [](full_name)
248
+ symbol = @symbols[full_name]
249
+ raise Error, "Symbol not found: #{full_name}" unless symbol
250
+
251
+ symbol
252
+ end
253
+
254
+ # Register a new symbol by it's full name
255
+ #
256
+ # If the symbol already exists, it will be returned.
257
+ sig { params(full_name: String).returns(Symbol) }
258
+ def register_symbol(full_name)
259
+ @symbols[full_name] ||= Symbol.new(full_name)
260
+ end
261
+
262
+ sig { params(full_name: String, context: Symbol).returns(Symbol) }
263
+ def resolve_symbol(full_name, context:)
264
+ if full_name.start_with?("::")
265
+ full_name = full_name.delete_prefix("::")
266
+ return @symbols[full_name] ||= UnresolvedSymbol.new(full_name)
267
+ end
268
+
269
+ target = T.let(@symbols[full_name], T.nilable(Symbol))
270
+ return target if target
271
+
272
+ parts = context.full_name.split("::")
273
+ until parts.empty?
274
+ target = @symbols["#{parts.join("::")}::#{full_name}"]
275
+ return target if target
276
+
277
+ parts.pop
278
+ end
279
+
280
+ @symbols[full_name] = UnresolvedSymbol.new(full_name)
281
+ end
282
+
283
+ sig { params(symbol: Symbol).returns(T::Array[Symbol]) }
284
+ def supertypes(symbol)
285
+ poe = @symbols_hierarchy[symbol]
286
+ poe.ancestors
287
+ end
288
+
289
+ sig { params(symbol: Symbol).returns(T::Array[Symbol]) }
290
+ def subtypes(symbol)
291
+ poe = @symbols_hierarchy[symbol]
292
+ poe.descendants
293
+ end
294
+
295
+ sig { void }
296
+ def finalize!
297
+ compute_symbols_hierarchy!
298
+ end
299
+
300
+ private
301
+
302
+ sig { void }
303
+ def compute_symbols_hierarchy!
304
+ @symbols.dup.each do |_full_name, symbol|
305
+ symbol.definitions.each do |definition|
306
+ next unless definition.is_a?(Namespace)
307
+
308
+ @symbols_hierarchy.add_element(symbol)
309
+
310
+ if definition.is_a?(Class)
311
+ superclass_name = definition.superclass_name
312
+ if superclass_name
313
+ superclass = resolve_symbol(superclass_name, context: symbol)
314
+ @symbols_hierarchy.add_direct_edge(symbol, superclass)
315
+ end
316
+ end
317
+
318
+ definition.mixins.each do |mixin|
319
+ next if mixin.is_a?(Extend)
320
+
321
+ target = resolve_symbol(mixin.name, context: symbol)
322
+ @symbols_hierarchy.add_direct_edge(symbol, target)
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ class Model
6
+ class NamespaceVisitor < Visitor
7
+ extend T::Helpers
8
+
9
+ abstract!
10
+
11
+ sig { void }
12
+ def initialize
13
+ super()
14
+
15
+ @names_nesting = T.let([], T::Array[String])
16
+ end
17
+
18
+ sig { override.params(node: T.nilable(Prism::Node)).void }
19
+ def visit(node)
20
+ case node
21
+ when Prism::ClassNode, Prism::ModuleNode
22
+ constant_path = node.constant_path.slice
23
+
24
+ if constant_path.start_with?("::")
25
+ full_name = constant_path.delete_prefix("::")
26
+
27
+ # We found a top level definition such as `class ::A; end`, we need to reset the name nesting
28
+ old_nesting = @names_nesting.dup
29
+ @names_nesting.clear
30
+ @names_nesting << full_name
31
+
32
+ super
33
+
34
+ # Restore the name nesting once we finished visited the class
35
+ @names_nesting.clear
36
+ @names_nesting = old_nesting
37
+ else
38
+ @names_nesting << constant_path
39
+
40
+ super
41
+
42
+ @names_nesting.pop
43
+ end
44
+ else
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ class Model
6
+ # A reference to something that looks like a constant or a method
7
+ #
8
+ # Constants could be classes, modules, or actual constants.
9
+ # Methods could be accessors, instance or class methods, aliases, etc.
10
+ class Reference < T::Struct
11
+ extend T::Sig
12
+
13
+ class Kind < T::Enum
14
+ enums do
15
+ Constant = new("constant")
16
+ Method = new("method")
17
+ end
18
+ end
19
+
20
+ class << self
21
+ extend T::Sig
22
+
23
+ sig { params(name: String, location: Spoom::Location).returns(Reference) }
24
+ def constant(name, location)
25
+ new(name: name, kind: Kind::Constant, location: location)
26
+ end
27
+
28
+ sig { params(name: String, location: Spoom::Location).returns(Reference) }
29
+ def method(name, location)
30
+ new(name: name, kind: Kind::Method, location: location)
31
+ end
32
+ end
33
+
34
+ const :kind, Kind
35
+ const :name, String
36
+ const :location, Spoom::Location
37
+
38
+ sig { returns(T::Boolean) }
39
+ def constant?
40
+ kind == Kind::Constant
41
+ end
42
+
43
+ sig { returns(T::Boolean) }
44
+ def method?
45
+ kind == Kind::Method
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,200 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ class Model
6
+ # Visit a file to collect all the references to constants and methods
7
+ class ReferencesVisitor < Visitor
8
+ extend T::Sig
9
+
10
+ sig { returns(T::Array[Reference]) }
11
+ attr_reader :references
12
+
13
+ sig { params(file: String).void }
14
+ def initialize(file)
15
+ super()
16
+
17
+ @file = file
18
+ @references = T.let([], T::Array[Reference])
19
+ end
20
+
21
+ sig { override.params(node: Prism::AliasMethodNode).void }
22
+ def visit_alias_method_node(node)
23
+ reference_method(node.old_name.slice, node)
24
+ end
25
+
26
+ sig { override.params(node: Prism::AndNode).void }
27
+ def visit_and_node(node)
28
+ reference_method(node.operator_loc.slice, node)
29
+ super
30
+ end
31
+
32
+ sig { override.params(node: Prism::BlockArgumentNode).void }
33
+ def visit_block_argument_node(node)
34
+ expression = node.expression
35
+ case expression
36
+ when Prism::SymbolNode
37
+ reference_method(expression.unescaped, expression)
38
+ else
39
+ visit(expression)
40
+ end
41
+ end
42
+
43
+ sig { override.params(node: Prism::CallAndWriteNode).void }
44
+ def visit_call_and_write_node(node)
45
+ visit(node.receiver)
46
+ reference_method(node.read_name.to_s, node)
47
+ reference_method(node.write_name.to_s, node)
48
+ visit(node.value)
49
+ end
50
+
51
+ sig { override.params(node: Prism::CallOperatorWriteNode).void }
52
+ def visit_call_operator_write_node(node)
53
+ visit(node.receiver)
54
+ reference_method(node.read_name.to_s, node)
55
+ reference_method(node.write_name.to_s, node)
56
+ visit(node.value)
57
+ end
58
+
59
+ sig { override.params(node: Prism::CallOrWriteNode).void }
60
+ def visit_call_or_write_node(node)
61
+ visit(node.receiver)
62
+ reference_method(node.read_name.to_s, node)
63
+ reference_method(node.write_name.to_s, node)
64
+ visit(node.value)
65
+ end
66
+
67
+ sig { override.params(node: Prism::CallNode).void }
68
+ def visit_call_node(node)
69
+ visit(node.receiver)
70
+
71
+ name = node.name.to_s
72
+ reference_method(name, node)
73
+
74
+ case name
75
+ when "<", ">", "<=", ">="
76
+ # For comparison operators, we also reference the `<=>` method
77
+ reference_method("<=>", node)
78
+ end
79
+
80
+ visit(node.arguments)
81
+ visit(node.block)
82
+ end
83
+
84
+ sig { override.params(node: Prism::ClassNode).void }
85
+ def visit_class_node(node)
86
+ visit(node.superclass) if node.superclass
87
+ visit(node.body)
88
+ end
89
+
90
+ sig { override.params(node: Prism::ConstantAndWriteNode).void }
91
+ def visit_constant_and_write_node(node)
92
+ reference_constant(node.name.to_s, node)
93
+ visit(node.value)
94
+ end
95
+
96
+ sig { override.params(node: Prism::ConstantOperatorWriteNode).void }
97
+ def visit_constant_operator_write_node(node)
98
+ reference_constant(node.name.to_s, node)
99
+ visit(node.value)
100
+ end
101
+
102
+ sig { override.params(node: Prism::ConstantOrWriteNode).void }
103
+ def visit_constant_or_write_node(node)
104
+ reference_constant(node.name.to_s, node)
105
+ visit(node.value)
106
+ end
107
+
108
+ sig { override.params(node: Prism::ConstantPathNode).void }
109
+ def visit_constant_path_node(node)
110
+ visit(node.parent)
111
+ reference_constant(node.name.to_s, node)
112
+ end
113
+
114
+ sig { override.params(node: Prism::ConstantPathWriteNode).void }
115
+ def visit_constant_path_write_node(node)
116
+ visit(node.target.parent)
117
+ visit(node.value)
118
+ end
119
+
120
+ sig { override.params(node: Prism::ConstantReadNode).void }
121
+ def visit_constant_read_node(node)
122
+ reference_constant(node.name.to_s, node)
123
+ end
124
+
125
+ sig { override.params(node: Prism::ConstantWriteNode).void }
126
+ def visit_constant_write_node(node)
127
+ visit(node.value)
128
+ end
129
+
130
+ sig { override.params(node: Prism::LocalVariableAndWriteNode).void }
131
+ def visit_local_variable_and_write_node(node)
132
+ name = node.name.to_s
133
+ reference_method(name, node)
134
+ reference_method("#{name}=", node)
135
+ visit(node.value)
136
+ end
137
+
138
+ sig { override.params(node: Prism::LocalVariableOperatorWriteNode).void }
139
+ def visit_local_variable_operator_write_node(node)
140
+ name = node.name.to_s
141
+ reference_method(name, node)
142
+ reference_method("#{name}=", node)
143
+ visit(node.value)
144
+ end
145
+
146
+ sig { override.params(node: Prism::LocalVariableOrWriteNode).void }
147
+ def visit_local_variable_or_write_node(node)
148
+ name = node.name.to_s
149
+ reference_method(name, node)
150
+ reference_method("#{name}=", node)
151
+ visit(node.value)
152
+ end
153
+
154
+ sig { override.params(node: Prism::LocalVariableWriteNode).void }
155
+ def visit_local_variable_write_node(node)
156
+ reference_method("#{node.name}=", node)
157
+ visit(node.value)
158
+ end
159
+
160
+ sig { override.params(node: Prism::ModuleNode).void }
161
+ def visit_module_node(node)
162
+ visit(node.body)
163
+ end
164
+
165
+ sig { override.params(node: Prism::MultiWriteNode).void }
166
+ def visit_multi_write_node(node)
167
+ node.lefts.each do |const|
168
+ case const
169
+ when Prism::LocalVariableTargetNode
170
+ reference_method("#{const.name}=", node)
171
+ end
172
+ end
173
+ visit(node.value)
174
+ end
175
+
176
+ sig { override.params(node: Prism::OrNode).void }
177
+ def visit_or_node(node)
178
+ reference_method(node.operator_loc.slice, node)
179
+ super
180
+ end
181
+
182
+ private
183
+
184
+ sig { params(name: String, node: Prism::Node).void }
185
+ def reference_constant(name, node)
186
+ @references << Reference.constant(name, node_location(node))
187
+ end
188
+
189
+ sig { params(name: String, node: Prism::Node).void }
190
+ def reference_method(name, node)
191
+ @references << Reference.method(name, node_location(node))
192
+ end
193
+
194
+ sig { params(node: Prism::Node).returns(Location) }
195
+ def node_location(node)
196
+ Location.from_prism(@file, node.location)
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,10 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "location"
5
+ require_relative "parse"
6
+ require_relative "model/model"
7
+ require_relative "model/namespace_visitor"
8
+ require_relative "model/builder"
9
+ require_relative "model/reference"
10
+ require_relative "model/references_visitor"
@@ -0,0 +1,28 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "spoom/visitor"
5
+
6
+ module Spoom
7
+ class ParseError < Error; end
8
+
9
+ class << self
10
+ extend T::Sig
11
+
12
+ sig { params(ruby: String, file: String).returns(Prism::Node) }
13
+ def parse_ruby(ruby, file:)
14
+ result = Prism.parse(ruby)
15
+ unless result.success?
16
+ message = +"Error while parsing #{file}:\n"
17
+
18
+ result.errors.each do |e|
19
+ message << "- #{e.message} (at #{e.location.start_line}:#{e.location.start_column})\n"
20
+ end
21
+
22
+ raise ParseError, message
23
+ end
24
+
25
+ result.value
26
+ end
27
+ end
28
+ end