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,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