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,61 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ class Index
7
+ extend T::Sig
8
+
9
+ sig { returns(T::Hash[String, T::Array[Definition]]) }
10
+ attr_reader :definitions
11
+
12
+ sig { returns(T::Hash[String, T::Array[Reference]]) }
13
+ attr_reader :references
14
+
15
+ sig { void }
16
+ def initialize
17
+ @definitions = T.let({}, T::Hash[String, T::Array[Definition]])
18
+ @references = T.let({}, T::Hash[String, T::Array[Reference]])
19
+ end
20
+
21
+ # Indexing
22
+
23
+ sig { params(definition: Definition).void }
24
+ def define(definition)
25
+ (@definitions[definition.name] ||= []) << definition
26
+ end
27
+
28
+ sig { params(reference: Reference).void }
29
+ def reference(reference)
30
+ (@references[reference.name] ||= []) << reference
31
+ end
32
+
33
+ # Mark all definitions having a reference of the same name as `alive`
34
+ #
35
+ # To be called once all the files have been indexed and all the definitions and references discovered.
36
+ sig { void }
37
+ def finalize!
38
+ @references.keys.each do |name|
39
+ definitions_for_name(name).each(&:alive!)
40
+ end
41
+ end
42
+
43
+ # Utils
44
+
45
+ sig { params(name: String).returns(T::Array[Definition]) }
46
+ def definitions_for_name(name)
47
+ @definitions[name] || []
48
+ end
49
+
50
+ sig { returns(T::Array[Definition]) }
51
+ def all_definitions
52
+ @definitions.values.flatten
53
+ end
54
+
55
+ sig { returns(T::Array[Reference]) }
56
+ def all_references
57
+ @references.values.flatten
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,394 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ class Indexer < SyntaxTree::Visitor
7
+ extend T::Sig
8
+
9
+ sig { returns(String) }
10
+ attr_reader :path, :file_name
11
+
12
+ sig { returns(Index) }
13
+ attr_reader :index
14
+
15
+ sig { params(path: String, source: String, index: Index).void }
16
+ def initialize(path, source, index)
17
+ super()
18
+
19
+ @path = path
20
+ @file_name = T.let(File.basename(path), String)
21
+ @source = source
22
+ @index = index
23
+ @previous_node = T.let(nil, T.nilable(SyntaxTree::Node))
24
+ @names_nesting = T.let([], T::Array[String])
25
+ @nodes_nesting = T.let([], T::Array[SyntaxTree::Node])
26
+ @in_const_field = T.let(false, T::Boolean)
27
+ @in_opassign = T.let(false, T::Boolean)
28
+ @in_symbol_literal = T.let(false, T::Boolean)
29
+ end
30
+
31
+ # Visit
32
+
33
+ sig { override.params(node: T.nilable(SyntaxTree::Node)).void }
34
+ def visit(node)
35
+ return unless node
36
+
37
+ @nodes_nesting << node
38
+ super
39
+ @nodes_nesting.pop
40
+ @previous_node = node
41
+ end
42
+
43
+ sig { override.params(node: SyntaxTree::AliasNode).void }
44
+ def visit_alias(node)
45
+ reference_method(node_string(node.right), node)
46
+ end
47
+
48
+ sig { override.params(node: SyntaxTree::ARef).void }
49
+ def visit_aref(node)
50
+ super
51
+
52
+ reference_method("[]", node)
53
+ end
54
+
55
+ sig { override.params(node: SyntaxTree::ARefField).void }
56
+ def visit_aref_field(node)
57
+ super
58
+
59
+ reference_method("[]=", node)
60
+ end
61
+
62
+ sig { override.params(node: SyntaxTree::ArgBlock).void }
63
+ def visit_arg_block(node)
64
+ value = node.value
65
+
66
+ case value
67
+ when SyntaxTree::SymbolLiteral
68
+ # If the block call is something like `x.select(&:foo)`, we need to reference the `foo` method
69
+ reference_method(symbol_string(value), node)
70
+ when SyntaxTree::VCall
71
+ # If the block call is something like `x.select { ... }`, we need to visit the block
72
+ super
73
+ end
74
+ end
75
+
76
+ sig { override.params(node: SyntaxTree::Binary).void }
77
+ def visit_binary(node)
78
+ super
79
+
80
+ op = node.operator
81
+
82
+ # Reference the operator itself
83
+ reference_method(op.to_s, node)
84
+
85
+ case op
86
+ when :<, :>, :<=, :>=
87
+ # For comparison operators, we also reference the `<=>` method
88
+ reference_method("<=>", node)
89
+ end
90
+ end
91
+
92
+ sig { override.params(node: SyntaxTree::CallNode).void }
93
+ def visit_call(node)
94
+ visit_send(
95
+ Send.new(
96
+ node: node,
97
+ name: node_string(node.message),
98
+ recv: node.receiver,
99
+ args: call_args(node.arguments),
100
+ ),
101
+ )
102
+ end
103
+
104
+ sig { override.params(node: SyntaxTree::ClassDeclaration).void }
105
+ def visit_class(node)
106
+ const_name = node_string(node.constant)
107
+ @names_nesting << const_name
108
+ define_class(T.must(const_name.split("::").last), @names_nesting.join("::"), node)
109
+
110
+ # We do not call `super` here because we don't want to visit the `constant` again
111
+ visit(node.superclass) if node.superclass
112
+ visit(node.bodystmt)
113
+
114
+ @names_nesting.pop
115
+ end
116
+
117
+ sig { override.params(node: SyntaxTree::Command).void }
118
+ def visit_command(node)
119
+ visit_send(
120
+ Send.new(
121
+ node: node,
122
+ name: node_string(node.message),
123
+ args: call_args(node.arguments),
124
+ block: node.block,
125
+ ),
126
+ )
127
+ end
128
+
129
+ sig { override.params(node: SyntaxTree::CommandCall).void }
130
+ def visit_command_call(node)
131
+ visit_send(
132
+ Send.new(
133
+ node: node,
134
+ name: node_string(node.message),
135
+ recv: node.receiver,
136
+ args: call_args(node.arguments),
137
+ block: node.block,
138
+ ),
139
+ )
140
+ end
141
+
142
+ sig { override.params(node: SyntaxTree::Const).void }
143
+ def visit_const(node)
144
+ reference_constant(node.value, node) unless @in_symbol_literal
145
+ end
146
+
147
+ sig { override.params(node: SyntaxTree::ConstPathField).void }
148
+ def visit_const_path_field(node)
149
+ # We do not call `super` here because we don't want to visit the `constant` again
150
+ visit(node.parent)
151
+
152
+ name = node.constant.value
153
+ full_name = [*@names_nesting, node_string(node.parent), name].join("::")
154
+ define_constant(name, full_name, node)
155
+ end
156
+
157
+ sig { override.params(node: SyntaxTree::DefNode).void }
158
+ def visit_def(node)
159
+ super
160
+
161
+ name = node_string(node.name)
162
+ define_method(name, [*@names_nesting, name].join("::"), node)
163
+ end
164
+
165
+ sig { override.params(node: SyntaxTree::Field).void }
166
+ def visit_field(node)
167
+ visit(node.parent)
168
+
169
+ name = node.name
170
+ case name
171
+ when SyntaxTree::Const
172
+ name = name.value
173
+ full_name = [*@names_nesting, node_string(node.parent), name].join("::")
174
+ define_constant(name, full_name, node)
175
+ when SyntaxTree::Ident
176
+ reference_method(name.value, node) if @in_opassign
177
+ reference_method("#{name.value}=", node)
178
+ end
179
+ end
180
+
181
+ sig { override.params(node: SyntaxTree::ModuleDeclaration).void }
182
+ def visit_module(node)
183
+ const_name = node_string(node.constant)
184
+ @names_nesting << const_name
185
+ define_module(T.must(const_name.split("::").last), @names_nesting.join("::"), node)
186
+
187
+ # We do not call `super` here because we don't want to visit the `constant` again
188
+ visit(node.bodystmt)
189
+
190
+ @names_nesting.pop
191
+ end
192
+
193
+ sig { override.params(node: SyntaxTree::OpAssign).void }
194
+ def visit_opassign(node)
195
+ # Both `FOO = x` and `FOO += x` yield a VarField node, but the former is a constant definition and the latter is
196
+ # a constant reference. We need to distinguish between the two cases.
197
+ @in_opassign = true
198
+ super
199
+ @in_opassign = false
200
+ end
201
+
202
+ sig { params(send: Send).void }
203
+ def visit_send(send)
204
+ visit(send.recv)
205
+
206
+ case send.name
207
+ when "attr_reader"
208
+ send.args.each do |arg|
209
+ next unless arg.is_a?(SyntaxTree::SymbolLiteral)
210
+
211
+ name = symbol_string(arg)
212
+ define_attr_reader(name, [*@names_nesting, name].join("::"), arg)
213
+ end
214
+ when "attr_writer"
215
+ send.args.each do |arg|
216
+ next unless arg.is_a?(SyntaxTree::SymbolLiteral)
217
+
218
+ name = symbol_string(arg)
219
+ define_attr_writer("#{name}=", "#{[*@names_nesting, name].join("::")}=", arg)
220
+ end
221
+ when "attr_accessor"
222
+ send.args.each do |arg|
223
+ next unless arg.is_a?(SyntaxTree::SymbolLiteral)
224
+
225
+ name = symbol_string(arg)
226
+ full_name = [*@names_nesting, name].join("::")
227
+ define_attr_reader(name, full_name, arg)
228
+ define_attr_writer("#{name}=", "#{full_name}=", arg)
229
+ end
230
+ else
231
+ reference_method(send.name, send.node)
232
+ visit_all(send.args)
233
+ visit(send.block)
234
+ end
235
+ end
236
+
237
+ sig { override.params(node: SyntaxTree::SymbolLiteral).void }
238
+ def visit_symbol_literal(node)
239
+ # Something like `:FOO` will yield a Const node but we do not want to treat it as a constant reference.
240
+ # So we need to distinguish between the two cases.
241
+ @in_symbol_literal = true
242
+ super
243
+ @in_symbol_literal = false
244
+ end
245
+
246
+ sig { override.params(node: SyntaxTree::TopConstField).void }
247
+ def visit_top_const_field(node)
248
+ define_constant(node.constant.value, node.constant.value, node)
249
+ end
250
+
251
+ sig { override.params(node: SyntaxTree::VarField).void }
252
+ def visit_var_field(node)
253
+ value = node.value
254
+ case value
255
+ when SyntaxTree::Const
256
+ if @in_opassign
257
+ reference_constant(value.value, node)
258
+ else
259
+ name = value.value
260
+ define_constant(name, [*@names_nesting, name].join("::"), node)
261
+ end
262
+ when SyntaxTree::Ident
263
+ reference_method(value.value, node) if @in_opassign
264
+ reference_method("#{value.value}=", node)
265
+ end
266
+ end
267
+
268
+ sig { override.params(node: SyntaxTree::VCall).void }
269
+ def visit_vcall(node)
270
+ visit_send(Send.new(node: node, name: node_string(node.value)))
271
+ end
272
+
273
+ private
274
+
275
+ # Definition indexing
276
+
277
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
278
+ def define_attr_reader(name, full_name, node)
279
+ definition = Definition.new(
280
+ kind: Definition::Kind::AttrReader,
281
+ name: name,
282
+ full_name: full_name,
283
+ location: node_location(node),
284
+ )
285
+ @index.define(definition)
286
+ end
287
+
288
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
289
+ def define_attr_writer(name, full_name, node)
290
+ definition = Definition.new(
291
+ kind: Definition::Kind::AttrWriter,
292
+ name: name,
293
+ full_name: full_name,
294
+ location: node_location(node),
295
+ )
296
+ @index.define(definition)
297
+ end
298
+
299
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
300
+ def define_class(name, full_name, node)
301
+ definition = Definition.new(
302
+ kind: Definition::Kind::Class,
303
+ name: name,
304
+ full_name: full_name,
305
+ location: node_location(node),
306
+ )
307
+ @index.define(definition)
308
+ end
309
+
310
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
311
+ def define_constant(name, full_name, node)
312
+ definition = Definition.new(
313
+ kind: Definition::Kind::Constant,
314
+ name: name,
315
+ full_name: full_name,
316
+ location: node_location(node),
317
+ )
318
+ @index.define(definition)
319
+ end
320
+
321
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
322
+ def define_method(name, full_name, node)
323
+ definition = Definition.new(
324
+ kind: Definition::Kind::Method,
325
+ name: name,
326
+ full_name: full_name,
327
+ location: node_location(node),
328
+ )
329
+ @index.define(definition)
330
+ end
331
+
332
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
333
+ def define_module(name, full_name, node)
334
+ definition = Definition.new(
335
+ kind: Definition::Kind::Module,
336
+ name: name,
337
+ full_name: full_name,
338
+ location: node_location(node),
339
+ )
340
+ @index.define(definition)
341
+ end
342
+
343
+ # Reference indexing
344
+
345
+ sig { params(name: String, node: SyntaxTree::Node).void }
346
+ def reference_constant(name, node)
347
+ @index.reference(Reference.new(name: name, kind: Reference::Kind::Constant, location: node_location(node)))
348
+ end
349
+
350
+ sig { params(name: String, node: SyntaxTree::Node).void }
351
+ def reference_method(name, node)
352
+ @index.reference(Reference.new(name: name, kind: Reference::Kind::Method, location: node_location(node)))
353
+ end
354
+
355
+ # Node utils
356
+
357
+ sig { params(node: T.any(Symbol, SyntaxTree::Node)).returns(String) }
358
+ def node_string(node)
359
+ case node
360
+ when Symbol
361
+ node.to_s
362
+ else
363
+ T.must(@source[node.location.start_char...node.location.end_char])
364
+ end
365
+ end
366
+
367
+ sig { params(node: SyntaxTree::Node).returns(Location) }
368
+ def node_location(node)
369
+ Location.from_syntax_tree(@path, node.location)
370
+ end
371
+
372
+ sig { params(node: SyntaxTree::Node).returns(String) }
373
+ def symbol_string(node)
374
+ node_string(node).delete_prefix(":")
375
+ end
376
+
377
+ sig do
378
+ params(
379
+ node: T.any(SyntaxTree::Args, SyntaxTree::ArgParen, SyntaxTree::ArgsForward, NilClass),
380
+ ).returns(T::Array[SyntaxTree::Node])
381
+ end
382
+ def call_args(node)
383
+ case node
384
+ when SyntaxTree::ArgParen
385
+ call_args(node.arguments)
386
+ when SyntaxTree::Args
387
+ node.parts
388
+ else
389
+ []
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,58 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ class Location
7
+ extend T::Sig
8
+
9
+ include Comparable
10
+
11
+ class LocationError < Spoom::Error; end
12
+
13
+ class << self
14
+ extend T::Sig
15
+
16
+ sig { params(file: String, location: SyntaxTree::Location).returns(Location) }
17
+ def from_syntax_tree(file, location)
18
+ new(file, location.start_line, location.start_column, location.end_line, location.end_column)
19
+ end
20
+ end
21
+
22
+ sig { returns(String) }
23
+ attr_reader :file
24
+
25
+ sig { returns(Integer) }
26
+ attr_reader :start_line, :start_column, :end_line, :end_column
27
+
28
+ sig do
29
+ params(
30
+ file: String,
31
+ start_line: Integer,
32
+ start_column: Integer,
33
+ end_line: Integer,
34
+ end_column: Integer,
35
+ ).void
36
+ end
37
+ def initialize(file, start_line, start_column, end_line, end_column)
38
+ @file = file
39
+ @start_line = start_line
40
+ @start_column = start_column
41
+ @end_line = end_line
42
+ @end_column = end_column
43
+ end
44
+
45
+ sig { override.params(other: BasicObject).returns(T.nilable(Integer)) }
46
+ def <=>(other)
47
+ return nil unless Location === other
48
+
49
+ to_s <=> other.to_s
50
+ end
51
+
52
+ sig { returns(String) }
53
+ def to_s
54
+ "#{@file}:#{@start_line}:#{@start_column}-#{@end_line}:#{@end_column}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ # A reference is a call to a method or a constant
7
+ class Reference < T::Struct
8
+ extend T::Sig
9
+
10
+ class Kind < T::Enum
11
+ enums do
12
+ Constant = new
13
+ Method = new
14
+ end
15
+ end
16
+
17
+ const :kind, Kind
18
+ const :name, String
19
+ const :location, Location
20
+
21
+ # Kind
22
+
23
+ sig { returns(T::Boolean) }
24
+ def constant?
25
+ kind == Kind::Constant
26
+ end
27
+
28
+ sig { returns(T::Boolean) }
29
+ def method?
30
+ kind == Kind::Method
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ # An abstraction to simplify handling of SyntaxTree::CallNode, SyntaxTree::Command, SyntaxTree::CommandCall and
7
+ # SyntaxTree::VCall nodes.
8
+ class Send < T::Struct
9
+ extend T::Sig
10
+
11
+ const :node, SyntaxTree::Node
12
+ const :name, String
13
+ const :recv, T.nilable(SyntaxTree::Node), default: nil
14
+ const :args, T::Array[SyntaxTree::Node], default: []
15
+ const :block, T.nilable(SyntaxTree::Node), default: nil
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,55 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "erubi"
5
+ require "syntax_tree"
6
+
7
+ require_relative "deadcode/erb"
8
+ require_relative "deadcode/index"
9
+ require_relative "deadcode/indexer"
10
+
11
+ require_relative "deadcode/location"
12
+ require_relative "deadcode/definition"
13
+ require_relative "deadcode/reference"
14
+ require_relative "deadcode/send"
15
+
16
+ module Spoom
17
+ module Deadcode
18
+ class Error < Spoom::Error
19
+ extend T::Sig
20
+ extend T::Helpers
21
+
22
+ abstract!
23
+
24
+ sig { params(message: String, parent: Exception).void }
25
+ def initialize(message, parent:)
26
+ super(message)
27
+ set_backtrace(parent.backtrace)
28
+ end
29
+ end
30
+
31
+ class ParserError < Error; end
32
+ class IndexerError < Error; end
33
+
34
+ class << self
35
+ extend T::Sig
36
+
37
+ sig { params(index: Index, ruby: String, file: String).void }
38
+ def index_ruby(index, ruby, file:)
39
+ node = SyntaxTree.parse(ruby)
40
+ visitor = Spoom::Deadcode::Indexer.new(file, ruby, index)
41
+ visitor.visit(node)
42
+ rescue SyntaxTree::Parser::ParseError => e
43
+ raise ParserError.new("Error while parsing #{file} (#{e.message} at #{e.lineno}:#{e.column})", parent: e)
44
+ rescue => e
45
+ raise IndexerError.new("Error while indexing #{file} (#{e.message})", parent: e)
46
+ end
47
+
48
+ sig { params(index: Index, erb: String, file: String).void }
49
+ def index_erb(index, erb, file:)
50
+ ruby = ERB.new(erb).src
51
+ index_ruby(index, ruby, file: file)
52
+ end
53
+ end
54
+ end
55
+ end