spoom 1.3.1 → 1.3.3

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