spoom 1.2.2 → 1.2.4

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/lib/spoom/backtrace_filter/minitest.rb +21 -0
  4. data/lib/spoom/cli/coverage.rb +1 -1
  5. data/lib/spoom/context/git.rb +4 -4
  6. data/lib/spoom/context/sorbet.rb +6 -6
  7. data/lib/spoom/deadcode/erb.rb +4 -4
  8. data/lib/spoom/deadcode/indexer.rb +83 -6
  9. data/lib/spoom/deadcode/location.rb +29 -1
  10. data/lib/spoom/deadcode/plugins/action_mailer.rb +21 -0
  11. data/lib/spoom/deadcode/plugins/actionpack.rb +61 -0
  12. data/lib/spoom/deadcode/plugins/active_job.rb +13 -0
  13. data/lib/spoom/deadcode/plugins/active_model.rb +46 -0
  14. data/lib/spoom/deadcode/plugins/active_record.rb +111 -0
  15. data/lib/spoom/deadcode/plugins/active_support.rb +21 -0
  16. data/lib/spoom/deadcode/plugins/base.rb +354 -0
  17. data/lib/spoom/deadcode/plugins/graphql.rb +47 -0
  18. data/lib/spoom/deadcode/plugins/minitest.rb +28 -0
  19. data/lib/spoom/deadcode/plugins/namespaces.rb +34 -0
  20. data/lib/spoom/deadcode/plugins/rails.rb +31 -0
  21. data/lib/spoom/deadcode/plugins/rake.rb +12 -0
  22. data/lib/spoom/deadcode/plugins/rspec.rb +19 -0
  23. data/lib/spoom/deadcode/plugins/rubocop.rb +41 -0
  24. data/lib/spoom/deadcode/plugins/ruby.rb +65 -0
  25. data/lib/spoom/deadcode/plugins/sorbet.rb +46 -0
  26. data/lib/spoom/deadcode/plugins/thor.rb +21 -0
  27. data/lib/spoom/deadcode/plugins.rb +95 -0
  28. data/lib/spoom/deadcode/remover.rb +616 -0
  29. data/lib/spoom/deadcode/send.rb +22 -0
  30. data/lib/spoom/deadcode.rb +8 -6
  31. data/lib/spoom/sorbet/lsp.rb +2 -2
  32. data/lib/spoom/sorbet/sigils.rb +2 -2
  33. data/lib/spoom/sorbet.rb +1 -0
  34. data/lib/spoom/version.rb +1 -1
  35. metadata +24 -18
@@ -0,0 +1,616 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ class Remover
7
+ extend T::Sig
8
+
9
+ class Error < Spoom::Error; end
10
+
11
+ sig { params(context: Context).void }
12
+ def initialize(context)
13
+ @context = context
14
+ end
15
+
16
+ sig { params(kind: Definition::Kind, location: Location).void }
17
+ def remove_location(kind, location)
18
+ file = location.file
19
+
20
+ unless @context.file?(file)
21
+ raise Error, "Can't find file at #{file}"
22
+ end
23
+
24
+ node_remover = NodeRemover.new(@context.read(file), kind, location)
25
+ node_remover.apply_edit
26
+ @context.write!(file, node_remover.new_source)
27
+ end
28
+
29
+ class NodeRemover
30
+ extend T::Sig
31
+
32
+ sig { returns(String) }
33
+ attr_reader :new_source
34
+
35
+ sig { params(source: String, kind: Definition::Kind, location: Location).void }
36
+ def initialize(source, kind, location)
37
+ @old_source = source
38
+ @new_source = T.let(source.dup, String)
39
+ @kind = kind
40
+ @location = location
41
+
42
+ @node_context = T.let(NodeFinder.find(source, location, kind), NodeContext)
43
+ end
44
+
45
+ sig { void }
46
+ def apply_edit
47
+ sclass_context = @node_context.sclass_context
48
+ if sclass_context
49
+ delete_node_and_comments_and_sigs(sclass_context)
50
+ return
51
+ end
52
+
53
+ node = @node_context.node
54
+ case node
55
+ when SyntaxTree::ClassDeclaration, SyntaxTree::ModuleDeclaration, SyntaxTree::DefNode
56
+ delete_node_and_comments_and_sigs(@node_context)
57
+ when SyntaxTree::Const, SyntaxTree::ConstPathField
58
+ delete_constant_assignment(@node_context)
59
+ when SyntaxTree::SymbolLiteral # for attr accessors
60
+ delete_attr_accessor(@node_context)
61
+ else
62
+ raise Error, "Unsupported node type: #{node.class}"
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ sig { params(context: NodeContext).void }
69
+ def delete_constant_assignment(context)
70
+ # Pop the Varfield node from the nesting nodes
71
+ if context.node.is_a?(SyntaxTree::Const)
72
+ context = context.parent_context
73
+ end
74
+
75
+ parent_context = context.parent_context
76
+ parent_node = parent_context.node
77
+ if parent_node.is_a?(SyntaxTree::Assign)
78
+ # Nesting node is an assign, it means only one constant is assigned on the line
79
+ # so we can remove the whole assign
80
+ delete_node_and_comments_and_sigs(parent_context)
81
+ return
82
+ elsif parent_node.is_a?(SyntaxTree::MLHS) && parent_node.parts.size == 1
83
+ # Nesting node is a single left hand side, it means only one constant is assigned
84
+ # so we can remove the whole line
85
+ delete_node_and_comments_and_sigs(parent_context.parent_context)
86
+ return
87
+ end
88
+
89
+ # Nesting node is a multiple left hand side, it means multiple constants are assigned
90
+ # so we need to remove only the right node from the left hand side
91
+ node = context.node
92
+ prev_node = context.previous_node
93
+ next_node = context.next_node
94
+
95
+ if (prev_node && prev_node.location.end_line != node.location.start_line) &&
96
+ (next_node && next_node.location.start_line != node.location.end_line)
97
+ # We have a node before and after, but on different lines, we need to remove the whole line
98
+ #
99
+ # ~~~
100
+ # FOO,
101
+ # BAR, # we need to remove BAR
102
+ # BAZ = 42
103
+ # ~~~
104
+ delete_lines(node.location.start_line, node.location.end_line)
105
+ elsif prev_node && next_node
106
+ # We have a node before and after one the same line, just remove the part of the line
107
+ #
108
+ # ~~~
109
+ # FOO, BAR, BAZ = 42 # we need to remove BAR
110
+ # ~~~
111
+ replace_chars(prev_node.location.end_char, next_node.location.start_char, ", ")
112
+ elsif prev_node
113
+ # We have a node before, on the same line, but no node after, just remove the part of the line
114
+ #
115
+ # ~~~
116
+ # FOO, BAR = 42 # we need to remove BAR
117
+ # ~~~
118
+ nesting_context = parent_context.parent_context
119
+ nesting_assign = T.cast(nesting_context.node, T.any(SyntaxTree::MAssign, SyntaxTree::MLHSParen))
120
+ case nesting_assign
121
+ when SyntaxTree::MAssign
122
+ replace_chars(prev_node.location.end_char, nesting_assign.value.location.start_char, " = ")
123
+ when SyntaxTree::MLHSParen
124
+ nesting_context = nesting_context.parent_context
125
+ nesting_assign = T.cast(nesting_context.node, SyntaxTree::MAssign)
126
+ replace_chars(prev_node.location.end_char, nesting_assign.value.location.start_char, ") = ")
127
+ end
128
+ elsif next_node
129
+ # We don't have a node before but a node after on the same line, just remove the part of the line
130
+ #
131
+ # ~~~
132
+ # FOO, BAR = 42 # we need to remove FOO
133
+ # ~~~
134
+ delete_chars(node.location.start_char, next_node.location.start_char)
135
+ else
136
+ # Should have been removed as a single MLHS node
137
+ raise "Unexpected case while removing constant assignment"
138
+ end
139
+ end
140
+
141
+ sig { params(context: NodeContext).void }
142
+ def delete_attr_accessor(context)
143
+ args_context = context.parent_context
144
+ send_context = args_context.parent_context
145
+ send_context = send_context.parent_context if send_context.node.is_a?(SyntaxTree::ArgParen)
146
+
147
+ send_node = T.cast(send_context.node, T.any(SyntaxTree::Command, SyntaxTree::CallNode))
148
+ need_accessor = context.node_string(send_node.message) == "attr_accessor"
149
+
150
+ if args_context.node.child_nodes.size == 1
151
+ # Only one accessor is defined, we can remove the whole node
152
+ delete_node_and_comments_and_sigs(send_context)
153
+ insert_accessor(context.node, send_context, was_removed: true) if need_accessor
154
+ return
155
+ end
156
+
157
+ prev_node = context.previous_node
158
+ next_node = context.next_node
159
+
160
+ if (prev_node && prev_node.location.end_line != context.node.location.start_line) &&
161
+ (next_node && next_node.location.start_line != context.node.location.end_line)
162
+ # We have a node before and after, but on different lines, we need to remove the whole line
163
+ #
164
+ # ~~~
165
+ # attr_reader(
166
+ # :foo,
167
+ # :bar, # attr to remove
168
+ # :baz,
169
+ # )
170
+ # ~~~
171
+ delete_lines(context.node.location.start_line, context.node.location.end_line)
172
+ elsif prev_node && next_node
173
+ # We have a node before and after one the same line, just remove the part of the line
174
+ #
175
+ # ~~~
176
+ # attr_reader :foo, :bar, :baz # we need to remove bar
177
+ # ~~~
178
+ replace_chars(prev_node.location.end_char, next_node.location.start_char, ", ")
179
+ elsif prev_node
180
+ # We have a node before, on the same line, but no node after, just remove the part of the line
181
+ #
182
+ # ~~~
183
+ # attr_reader :foo, :bar, :baz # we need to remove baz
184
+ # ~~~
185
+ delete_chars(prev_node.location.end_char, context.node.location.end_char)
186
+ elsif next_node
187
+ # We don't have a node before but a node after on the same line, just remove the part of the line
188
+ #
189
+ # ~~~
190
+ # attr_reader :foo, :bar, :baz # we need to remove foo
191
+ # ~~~
192
+ delete_chars(context.node.location.start_char, next_node.location.start_char)
193
+ else
194
+ raise "Unexpected case while removing attr_accessor"
195
+ end
196
+
197
+ insert_accessor(context.node, send_context, was_removed: false) if need_accessor
198
+ end
199
+
200
+ sig do
201
+ params(
202
+ node: SyntaxTree::Node,
203
+ send_context: NodeContext,
204
+ was_removed: T::Boolean,
205
+ ).void
206
+ end
207
+ def insert_accessor(node, send_context, was_removed:)
208
+ name = @node_context.node_string(node)
209
+ code = case @kind
210
+ when Definition::Kind::AttrReader
211
+ "attr_writer #{name}"
212
+ when Definition::Kind::AttrWriter
213
+ "attr_reader #{name}"
214
+ end
215
+
216
+ indent = " " * send_context.node.location.start_column
217
+
218
+ sig = send_context.attached_sig
219
+ sig_string = transform_sig(sig, name: name, kind: @kind) if sig
220
+
221
+ node_after = send_context.next_node
222
+
223
+ if was_removed
224
+ first_node = send_context.attached_comments_and_sigs.first || send_context.node
225
+ at_line = first_node.location.start_line - 1
226
+
227
+ prev_context = NodeContext.new(@old_source, first_node, send_context.nesting)
228
+ node_before = prev_context.previous_node
229
+
230
+ new_line_before = node_before && send_context.node.location.start_line - node_before.location.end_line < 2
231
+ new_line_after = node_after && node_after.location.start_line - send_context.node.location.end_line <= 2
232
+ else
233
+ at_line = send_context.node.location.end_line
234
+ new_line_before = true
235
+ new_line_after = node_after && node_after.location.start_line - send_context.node.location.end_line < 2
236
+ end
237
+
238
+ lines_to_insert = String.new
239
+ lines_to_insert << "\n" if new_line_before
240
+ lines_to_insert << "#{indent}#{sig_string}\n" if sig_string
241
+ lines_to_insert << "#{indent}#{code}\n"
242
+ lines_to_insert << "\n" if new_line_after
243
+
244
+ lines = @new_source.lines
245
+ lines.insert(at_line, lines_to_insert)
246
+ @new_source = lines.join
247
+ end
248
+
249
+ sig { params(context: NodeContext).void }
250
+ def delete_node_and_comments_and_sigs(context)
251
+ start_line = context.node.location.start_line
252
+ end_line = context.node.location.end_line
253
+
254
+ # Adjust the lines to remove to include the comments
255
+ nodes = context.attached_comments_and_sigs
256
+ if nodes.any?
257
+ start_line = T.must(nodes.first).location.start_line
258
+ end
259
+
260
+ # Adjust the lines to remove to include previous blank lines
261
+ prev_context = NodeContext.new(@old_source, nodes.first || context.node, context.nesting)
262
+ before = prev_context.previous_node
263
+ if before && before.location.end_line < start_line - 1
264
+ # There is a node before and a blank line
265
+ start_line = before.location.end_line + 1
266
+ elsif before.nil? && context.parent_node.location.start_line < start_line - 1
267
+ # There is no node before, but a blank line
268
+ start_line = context.parent_node.location.start_line + 1
269
+ end
270
+
271
+ # Adjust the lines to remove to include following blank lines
272
+ after = context.next_node
273
+ if before.nil? && after && after.location.start_line > end_line + 1
274
+ end_line = after.location.end_line - 1
275
+ elsif after.nil? && context.parent_node.location.end_line > end_line + 1
276
+ end_line = context.parent_node.location.end_line - 1
277
+ end
278
+
279
+ delete_lines(start_line, end_line)
280
+ end
281
+
282
+ sig { params(start_line: Integer, end_line: Integer).void }
283
+ def delete_lines(start_line, end_line)
284
+ lines = @new_source.lines
285
+ lines[start_line - 1...end_line] = []
286
+ @new_source = lines.join
287
+ end
288
+
289
+ sig { params(start_char: Integer, end_char: Integer).void }
290
+ def delete_chars(start_char, end_char)
291
+ @new_source[start_char...end_char] = ""
292
+ end
293
+
294
+ sig { params(start_char: Integer, end_char: Integer, replacement: String).void }
295
+ def replace_chars(start_char, end_char, replacement)
296
+ @new_source[start_char...end_char] = replacement
297
+ end
298
+
299
+ sig { params(line_number: Integer, start_column: Integer, end_column: Integer).void }
300
+ def delete_line_part(line_number, start_column, end_column)
301
+ lines = []
302
+ @new_source.lines.each_with_index do |line, index|
303
+ current_line = index + 1
304
+
305
+ lines << if line_number == current_line
306
+ T.must(line[0...start_column]) + T.must(line[end_column..-1])
307
+ else
308
+ line
309
+ end
310
+ end
311
+ @new_source = lines.join
312
+ end
313
+
314
+ sig { params(node: SyntaxTree::MethodAddBlock, name: String, kind: Definition::Kind).returns(String) }
315
+ def transform_sig(node, name:, kind:)
316
+ type = T.let(nil, T.nilable(String))
317
+
318
+ statements = node.block.bodystmt
319
+ statements = statements.statements if statements.is_a?(SyntaxTree::BodyStmt)
320
+
321
+ statements.body.each do |call|
322
+ next unless call.is_a?(SyntaxTree::CallNode)
323
+ next unless @node_context.node_string(call.message) == "returns"
324
+
325
+ args = call.arguments
326
+ args = args.arguments if args.is_a?(SyntaxTree::ArgParen)
327
+
328
+ next unless args.is_a?(SyntaxTree::Args)
329
+
330
+ first = args.parts.first
331
+ next unless first
332
+
333
+ type = @node_context.node_string(first)
334
+ end
335
+
336
+ name = name.delete_prefix(":")
337
+ type = T.must(type)
338
+
339
+ case kind
340
+ when Definition::Kind::AttrReader
341
+ "sig { params(#{name}: #{type}).returns(#{type}) }"
342
+ else
343
+ "sig { returns(#{type}) }"
344
+ end
345
+ end
346
+ end
347
+
348
+ class NodeContext
349
+ extend T::Sig
350
+
351
+ sig { returns(SyntaxTree::Node) }
352
+ attr_reader :node
353
+
354
+ sig { returns(T::Array[SyntaxTree::Node]) }
355
+ attr_accessor :nesting
356
+
357
+ sig { params(source: String, node: SyntaxTree::Node, nesting: T::Array[SyntaxTree::Node]).void }
358
+ def initialize(source, node, nesting)
359
+ @source = source
360
+ @node = node
361
+ @nesting = nesting
362
+ end
363
+
364
+ sig { returns(SyntaxTree::Node) }
365
+ def parent_node
366
+ parent = @nesting.last
367
+ raise "No parent for node #{node}" unless parent
368
+
369
+ parent
370
+ end
371
+
372
+ sig { returns(NodeContext) }
373
+ def parent_context
374
+ nesting = @nesting.dup
375
+ parent = nesting.pop
376
+ raise "No parent context for node #{@node}" unless parent
377
+
378
+ NodeContext.new(@source, parent, nesting)
379
+ end
380
+
381
+ sig { returns(T::Array[SyntaxTree::Node]) }
382
+ def previous_nodes
383
+ parent = parent_node
384
+
385
+ index = parent.child_nodes.index(@node)
386
+ raise "Node #{@node} not found in parent #{parent}" unless index
387
+
388
+ parent.child_nodes[0...index].reject { |child| child.is_a?(SyntaxTree::VoidStmt) }
389
+ end
390
+
391
+ sig { returns(T.nilable(SyntaxTree::Node)) }
392
+ def previous_node
393
+ previous_nodes.last
394
+ end
395
+
396
+ sig { returns(T::Array[SyntaxTree::Node]) }
397
+ def next_nodes
398
+ parent = parent_node
399
+
400
+ index = parent.child_nodes.index(node)
401
+ raise "Node #{@node} not found in nesting node #{parent}" unless index
402
+
403
+ parent.child_nodes[(index + 1)..-1].reject { |node| node.is_a?(SyntaxTree::VoidStmt) }
404
+ end
405
+
406
+ sig { returns(T.nilable(SyntaxTree::Node)) }
407
+ def next_node
408
+ next_nodes.first
409
+ end
410
+
411
+ sig { returns(T.nilable(NodeContext)) }
412
+ def sclass_context
413
+ sclass = T.let(nil, T.nilable(SyntaxTree::SClass))
414
+
415
+ nesting = @nesting.dup
416
+ until nesting.empty? || sclass
417
+ node = nesting.pop
418
+ next unless node.is_a?(SyntaxTree::SClass)
419
+
420
+ sclass = node
421
+ end
422
+
423
+ return unless sclass.is_a?(SyntaxTree::SClass)
424
+
425
+ nodes = sclass.bodystmt.statements.body.reject do |node|
426
+ node.is_a?(SyntaxTree::VoidStmt) || node.is_a?(SyntaxTree::Comment) ||
427
+ sorbet_signature?(node) || sorbet_extend_sig?(node)
428
+ end
429
+
430
+ if nodes.size <= 1
431
+ return NodeContext.new(@source, sclass, nesting)
432
+ end
433
+
434
+ nil
435
+ end
436
+
437
+ sig { params(node: T.nilable(SyntaxTree::Node)).returns(T::Boolean) }
438
+ def sorbet_signature?(node)
439
+ return false unless node.is_a?(SyntaxTree::MethodAddBlock)
440
+
441
+ call = node.call
442
+ return false unless call.is_a?(SyntaxTree::CallNode)
443
+
444
+ ident = call.message
445
+ return false unless ident.is_a?(SyntaxTree::Ident)
446
+
447
+ ident.value == "sig"
448
+ end
449
+
450
+ sig { params(node: T.nilable(SyntaxTree::Node)).returns(T::Boolean) }
451
+ def sorbet_extend_sig?(node)
452
+ return false unless node.is_a?(SyntaxTree::Command)
453
+ return false unless node_string(node.message) == "extend"
454
+ return false unless node.arguments.parts.size == 1
455
+
456
+ node_string(T.must(node.arguments.parts.first)) == "T::Sig"
457
+ end
458
+
459
+ sig { params(comment: SyntaxTree::Node, node: SyntaxTree::Node).returns(T::Boolean) }
460
+ def comment_for_node?(comment, node)
461
+ return false unless comment.is_a?(SyntaxTree::Comment)
462
+
463
+ comment.location.end_line == node.location.start_line - 1
464
+ end
465
+
466
+ sig { returns(T::Array[SyntaxTree::Node]) }
467
+ def attached_comments_and_sigs
468
+ nodes = T.let([], T::Array[SyntaxTree::Node])
469
+
470
+ previous_nodes.reverse_each do |prev_node|
471
+ break unless comment_for_node?(prev_node, nodes.last || node) || sorbet_signature?(prev_node)
472
+
473
+ nodes << prev_node
474
+ end
475
+
476
+ nodes.reverse
477
+ end
478
+
479
+ sig { returns(T.nilable(SyntaxTree::MethodAddBlock)) }
480
+ def attached_sig
481
+ previous_nodes.reverse_each do |node|
482
+ if node.is_a?(SyntaxTree::Comment)
483
+ next
484
+ elsif sorbet_signature?(node)
485
+ return T.cast(node, SyntaxTree::MethodAddBlock)
486
+ else
487
+ break
488
+ end
489
+ end
490
+
491
+ nil
492
+ end
493
+
494
+ sig { params(node: T.any(Symbol, SyntaxTree::Node)).returns(String) }
495
+ def node_string(node)
496
+ case node
497
+ when Symbol
498
+ node.to_s
499
+ else
500
+ T.must(@source[node.location.start_char...node.location.end_char])
501
+ end
502
+ end
503
+ end
504
+
505
+ class NodeFinder < SyntaxTree::Visitor
506
+ extend T::Sig
507
+
508
+ class << self
509
+ extend T::Sig
510
+
511
+ sig { params(source: String, location: Location, kind: Definition::Kind).returns(NodeContext) }
512
+ def find(source, location, kind)
513
+ tree = SyntaxTree.parse(source)
514
+
515
+ visitor = new(location)
516
+ visitor.visit(tree)
517
+
518
+ node = visitor.node
519
+ unless node
520
+ raise Error, "Can't find node at #{location}"
521
+ end
522
+
523
+ unless node_match_kind?(node, kind)
524
+ raise Error, "Can't find node at #{location}, expected #{kind} but got #{node.class}"
525
+ end
526
+
527
+ NodeContext.new(source, node, visitor.nodes_nesting)
528
+ end
529
+
530
+ sig { params(node: SyntaxTree::Node, kind: Definition::Kind).returns(T::Boolean) }
531
+ def node_match_kind?(node, kind)
532
+ case kind
533
+ when Definition::Kind::AttrReader, Definition::Kind::AttrWriter
534
+ node.is_a?(SyntaxTree::SymbolLiteral)
535
+ when Definition::Kind::Class
536
+ node.is_a?(SyntaxTree::ClassDeclaration)
537
+ when Definition::Kind::Constant
538
+ node.is_a?(SyntaxTree::Const) || node.is_a?(SyntaxTree::ConstPathField)
539
+ when Definition::Kind::Method
540
+ node.is_a?(SyntaxTree::DefNode)
541
+ when Definition::Kind::Module
542
+ node.is_a?(SyntaxTree::ModuleDeclaration)
543
+ end
544
+ end
545
+ end
546
+
547
+ sig { returns(T.nilable(SyntaxTree::Node)) }
548
+ attr_reader :node
549
+
550
+ sig { returns(T::Array[SyntaxTree::Node]) }
551
+ attr_accessor :nodes_nesting
552
+
553
+ sig { params(location: Location).void }
554
+ def initialize(location)
555
+ super()
556
+ @location = location
557
+ @node = T.let(nil, T.nilable(SyntaxTree::Node))
558
+ @nodes_nesting = T.let([], T::Array[SyntaxTree::Node])
559
+ end
560
+
561
+ sig { override.params(node: T.nilable(SyntaxTree::Node)).void }
562
+ def visit(node)
563
+ return unless node
564
+
565
+ location = location_from_node(node)
566
+
567
+ if location == @location
568
+ # We found the node we're looking for at `@location`
569
+ @node = node
570
+
571
+ # There may be a more precise child inside the node that also matches `@location`, let's visit them
572
+ @nodes_nesting << node
573
+ super(node)
574
+ @nodes_nesting.pop if @nodes_nesting.last == @node
575
+ elsif location.include?(@location)
576
+ # The node we're looking for is inside `node`, let's visit it
577
+ @nodes_nesting << node
578
+ super(node)
579
+ end
580
+ end
581
+
582
+ private
583
+
584
+ # TODO: remove once SyntaxTree location are fixed
585
+ sig { params(node: SyntaxTree::Node).returns(Location) }
586
+ def location_from_node(node)
587
+ case node
588
+ when SyntaxTree::Program, SyntaxTree::BodyStmt
589
+ # Patch SyntaxTree node locations to use the one of their children
590
+ location_from_children(node, node.statements.body)
591
+ when SyntaxTree::Statements
592
+ # Patch SyntaxTree node locations to use the one of their children
593
+ location_from_children(node, node.body)
594
+ else
595
+ Location.from_syntax_tree(@location.file, node.location)
596
+ end
597
+ end
598
+
599
+ # TODO: remove once SyntaxTree location are fixed
600
+ sig { params(node: SyntaxTree::Node, nodes: T::Array[SyntaxTree::Node]).returns(Location) }
601
+ def location_from_children(node, nodes)
602
+ first = T.must(nodes.first)
603
+ last = T.must(nodes.last)
604
+
605
+ Location.new(
606
+ @location.file,
607
+ first.location.start_line,
608
+ first.location.start_column,
609
+ last.location.end_line,
610
+ last.location.end_column,
611
+ )
612
+ end
613
+ end
614
+ end
615
+ end
616
+ end
@@ -13,6 +13,28 @@ module Spoom
13
13
  const :recv, T.nilable(SyntaxTree::Node), default: nil
14
14
  const :args, T::Array[SyntaxTree::Node], default: []
15
15
  const :block, T.nilable(SyntaxTree::Node), default: nil
16
+
17
+ sig do
18
+ type_parameters(:T)
19
+ .params(arg_type: T::Class[T.type_parameter(:T)], block: T.proc.params(arg: T.type_parameter(:T)).void)
20
+ .void
21
+ end
22
+ def each_arg(arg_type, &block)
23
+ args.each do |arg|
24
+ yield(T.unsafe(arg)) if arg.is_a?(arg_type)
25
+ end
26
+ end
27
+
28
+ sig { params(block: T.proc.params(key: SyntaxTree::Node, value: T.nilable(SyntaxTree::Node)).void).void }
29
+ def each_arg_assoc(&block)
30
+ args.each do |arg|
31
+ next unless arg.is_a?(SyntaxTree::BareAssocHash) || arg.is_a?(SyntaxTree::HashLiteral)
32
+
33
+ arg.assocs.each do |assoc|
34
+ yield(assoc.key, assoc.value) if assoc.is_a?(SyntaxTree::Assoc)
35
+ end
36
+ end
37
+ end
16
38
  end
17
39
  end
18
40
  end
@@ -12,6 +12,8 @@ require_relative "deadcode/location"
12
12
  require_relative "deadcode/definition"
13
13
  require_relative "deadcode/reference"
14
14
  require_relative "deadcode/send"
15
+ require_relative "deadcode/plugins"
16
+ require_relative "deadcode/remover"
15
17
 
16
18
  module Spoom
17
19
  module Deadcode
@@ -34,10 +36,10 @@ module Spoom
34
36
  class << self
35
37
  extend T::Sig
36
38
 
37
- sig { params(index: Index, ruby: String, file: String).void }
38
- def index_ruby(index, ruby, file:)
39
+ sig { params(index: Index, ruby: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
40
+ def index_ruby(index, ruby, file:, plugins: [])
39
41
  node = SyntaxTree.parse(ruby)
40
- visitor = Spoom::Deadcode::Indexer.new(file, ruby, index)
42
+ visitor = Spoom::Deadcode::Indexer.new(file, ruby, index, plugins: plugins)
41
43
  visitor.visit(node)
42
44
  rescue SyntaxTree::Parser::ParseError => e
43
45
  raise ParserError.new("Error while parsing #{file} (#{e.message} at #{e.lineno}:#{e.column})", parent: e)
@@ -45,10 +47,10 @@ module Spoom
45
47
  raise IndexerError.new("Error while indexing #{file} (#{e.message})", parent: e)
46
48
  end
47
49
 
48
- sig { params(index: Index, erb: String, file: String).void }
49
- def index_erb(index, erb, file:)
50
+ sig { params(index: Index, erb: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
51
+ def index_erb(index, erb, file:, plugins: [])
50
52
  ruby = ERB.new(erb).src
51
- index_ruby(index, ruby, file: file)
53
+ index_ruby(index, ruby, file: file, plugins: plugins)
52
54
  end
53
55
  end
54
56
  end
@@ -53,7 +53,7 @@ module Spoom
53
53
  sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
54
54
  def read
55
55
  raw_string = read_raw
56
- return nil unless raw_string
56
+ return unless raw_string
57
57
 
58
58
  json = JSON.parse(raw_string)
59
59
 
@@ -101,7 +101,7 @@ module Spoom
101
101
  },
102
102
  ))
103
103
 
104
- return nil unless json && json["result"]
104
+ return unless json && json["result"]
105
105
 
106
106
  Hover.from_json(json["result"])
107
107
  end