spoom 1.2.3 → 1.3.0

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