spoom 1.2.4 → 1.3.1

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +54 -55
  3. data/lib/spoom/cli/deadcode.rb +172 -0
  4. data/lib/spoom/cli/helper.rb +20 -0
  5. data/lib/spoom/cli/srb/bump.rb +200 -0
  6. data/lib/spoom/cli/srb/coverage.rb +224 -0
  7. data/lib/spoom/cli/srb/lsp.rb +159 -0
  8. data/lib/spoom/cli/srb/tc.rb +150 -0
  9. data/lib/spoom/cli/srb.rb +27 -0
  10. data/lib/spoom/cli.rb +72 -32
  11. data/lib/spoom/context/git.rb +2 -2
  12. data/lib/spoom/context/sorbet.rb +2 -2
  13. data/lib/spoom/deadcode/definition.rb +11 -0
  14. data/lib/spoom/deadcode/indexer.rb +222 -224
  15. data/lib/spoom/deadcode/location.rb +2 -2
  16. data/lib/spoom/deadcode/plugins/action_mailer.rb +2 -2
  17. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +19 -0
  18. data/lib/spoom/deadcode/plugins/actionpack.rb +4 -6
  19. data/lib/spoom/deadcode/plugins/active_model.rb +8 -8
  20. data/lib/spoom/deadcode/plugins/active_record.rb +9 -12
  21. data/lib/spoom/deadcode/plugins/active_support.rb +11 -0
  22. data/lib/spoom/deadcode/plugins/base.rb +1 -1
  23. data/lib/spoom/deadcode/plugins/graphql.rb +4 -4
  24. data/lib/spoom/deadcode/plugins/namespaces.rb +2 -4
  25. data/lib/spoom/deadcode/plugins/ruby.rb +8 -17
  26. data/lib/spoom/deadcode/plugins/sorbet.rb +4 -10
  27. data/lib/spoom/deadcode/plugins.rb +1 -0
  28. data/lib/spoom/deadcode/remover.rb +210 -175
  29. data/lib/spoom/deadcode/send.rb +9 -10
  30. data/lib/spoom/deadcode/visitor.rb +755 -0
  31. data/lib/spoom/deadcode.rb +40 -10
  32. data/lib/spoom/file_tree.rb +0 -16
  33. data/lib/spoom/sorbet/errors.rb +1 -1
  34. data/lib/spoom/sorbet/lsp/structures.rb +2 -2
  35. data/lib/spoom/version.rb +1 -1
  36. metadata +19 -15
  37. data/lib/spoom/cli/bump.rb +0 -198
  38. data/lib/spoom/cli/coverage.rb +0 -222
  39. data/lib/spoom/cli/lsp.rb +0 -168
  40. data/lib/spoom/cli/run.rb +0 -148
@@ -13,7 +13,7 @@ module Spoom
13
13
  @context = context
14
14
  end
15
15
 
16
- sig { params(kind: Definition::Kind, location: Location).void }
16
+ sig { params(kind: T.nilable(Definition::Kind), location: Location).returns(String) }
17
17
  def remove_location(kind, location)
18
18
  file = location.file
19
19
 
@@ -23,7 +23,7 @@ module Spoom
23
23
 
24
24
  node_remover = NodeRemover.new(@context.read(file), kind, location)
25
25
  node_remover.apply_edit
26
- @context.write!(file, node_remover.new_source)
26
+ node_remover.new_source
27
27
  end
28
28
 
29
29
  class NodeRemover
@@ -32,7 +32,7 @@ module Spoom
32
32
  sig { returns(String) }
33
33
  attr_reader :new_source
34
34
 
35
- sig { params(source: String, kind: Definition::Kind, location: Location).void }
35
+ sig { params(source: String, kind: T.nilable(Definition::Kind), location: Location).void }
36
36
  def initialize(source, kind, location)
37
37
  @old_source = source
38
38
  @new_source = T.let(source.dup, String)
@@ -52,11 +52,15 @@ module Spoom
52
52
 
53
53
  node = @node_context.node
54
54
  case node
55
- when SyntaxTree::ClassDeclaration, SyntaxTree::ModuleDeclaration, SyntaxTree::DefNode
55
+ when Prism::ClassNode, Prism::ModuleNode, Prism::DefNode
56
56
  delete_node_and_comments_and_sigs(@node_context)
57
- when SyntaxTree::Const, SyntaxTree::ConstPathField
57
+ when Prism::ConstantWriteNode, Prism::ConstantOperatorWriteNode,
58
+ Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode,
59
+ Prism::ConstantPathWriteNode, Prism::ConstantPathOperatorWriteNode,
60
+ Prism::ConstantPathAndWriteNode, Prism::ConstantPathOrWriteNode,
61
+ Prism::ConstantTargetNode
58
62
  delete_constant_assignment(@node_context)
59
- when SyntaxTree::SymbolLiteral # for attr accessors
63
+ when Prism::SymbolNode # for attr accessors
60
64
  delete_attr_accessor(@node_context)
61
65
  else
62
66
  raise Error, "Unsupported node type: #{node.class}"
@@ -67,19 +71,26 @@ module Spoom
67
71
 
68
72
  sig { params(context: NodeContext).void }
69
73
  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
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
73
83
  end
74
84
 
85
+ # We're assigning multiple constants, we need to remove only the useless node
75
86
  parent_context = context.parent_context
76
87
  parent_node = parent_context.node
77
- if parent_node.is_a?(SyntaxTree::Assign)
88
+ if parent_node.is_a?(Prism::ConstantWriteNode)
78
89
  # Nesting node is an assign, it means only one constant is assigned on the line
79
90
  # so we can remove the whole assign
80
91
  delete_node_and_comments_and_sigs(parent_context)
81
92
  return
82
- elsif parent_node.is_a?(SyntaxTree::MLHS) && parent_node.parts.size == 1
93
+ elsif parent_node.is_a?(Prism::MultiWriteNode) && parent_node.lefts.size == 1
83
94
  # Nesting node is a single left hand side, it means only one constant is assigned
84
95
  # so we can remove the whole line
85
96
  delete_node_and_comments_and_sigs(parent_context.parent_context)
@@ -102,36 +113,36 @@ module Spoom
102
113
  # BAZ = 42
103
114
  # ~~~
104
115
  delete_lines(node.location.start_line, node.location.end_line)
105
- elsif prev_node && next_node
116
+ elsif prev_node && next_node.is_a?(Prism::ConstantTargetNode)
106
117
  # We have a node before and after one the same line, just remove the part of the line
107
118
  #
108
119
  # ~~~
109
120
  # FOO, BAR, BAZ = 42 # we need to remove BAR
110
121
  # ~~~
111
- replace_chars(prev_node.location.end_char, next_node.location.start_char, ", ")
122
+ replace_chars(prev_node.location.end_offset, next_node.location.start_offset, ", ")
112
123
  elsif prev_node
113
124
  # We have a node before, on the same line, but no node after, just remove the part of the line
114
125
  #
115
126
  # ~~~
116
127
  # FOO, BAR = 42 # we need to remove BAR
117
128
  # ~~~
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, ") = ")
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, " ")
127
138
  end
128
- elsif next_node
139
+ elsif next_node.is_a?(Prism::ConstantTargetNode)
129
140
  # We don't have a node before but a node after on the same line, just remove the part of the line
130
141
  #
131
142
  # ~~~
132
143
  # FOO, BAR = 42 # we need to remove FOO
133
144
  # ~~~
134
- delete_chars(node.location.start_char, next_node.location.start_char)
145
+ delete_chars(node.location.start_offset, next_node.location.start_offset)
135
146
  else
136
147
  # Should have been removed as a single MLHS node
137
148
  raise "Unexpected case while removing constant assignment"
@@ -142,10 +153,10 @@ module Spoom
142
153
  def delete_attr_accessor(context)
143
154
  args_context = context.parent_context
144
155
  send_context = args_context.parent_context
145
- send_context = send_context.parent_context if send_context.node.is_a?(SyntaxTree::ArgParen)
156
+ send_context = send_context.parent_context if send_context.node.is_a?(Prism::ArgumentsNode)
146
157
 
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"
158
+ send_node = T.cast(send_context.node, Prism::CallNode)
159
+ need_accessor = send_node.name == :attr_accessor
149
160
 
150
161
  if args_context.node.child_nodes.size == 1
151
162
  # Only one accessor is defined, we can remove the whole node
@@ -175,21 +186,21 @@ module Spoom
175
186
  # ~~~
176
187
  # attr_reader :foo, :bar, :baz # we need to remove bar
177
188
  # ~~~
178
- replace_chars(prev_node.location.end_char, next_node.location.start_char, ", ")
189
+ replace_chars(prev_node.location.end_offset, next_node.location.start_offset, ", ")
179
190
  elsif prev_node
180
191
  # We have a node before, on the same line, but no node after, just remove the part of the line
181
192
  #
182
193
  # ~~~
183
194
  # attr_reader :foo, :bar, :baz # we need to remove baz
184
195
  # ~~~
185
- delete_chars(prev_node.location.end_char, context.node.location.end_char)
196
+ delete_chars(prev_node.location.end_offset, context.node.location.end_offset)
186
197
  elsif next_node
187
198
  # We don't have a node before but a node after on the same line, just remove the part of the line
188
199
  #
189
200
  # ~~~
190
201
  # attr_reader :foo, :bar, :baz # we need to remove foo
191
202
  # ~~~
192
- delete_chars(context.node.location.start_char, next_node.location.start_char)
203
+ delete_chars(context.node.location.start_offset, next_node.location.start_offset)
193
204
  else
194
205
  raise "Unexpected case while removing attr_accessor"
195
206
  end
@@ -199,13 +210,13 @@ module Spoom
199
210
 
200
211
  sig do
201
212
  params(
202
- node: SyntaxTree::Node,
213
+ node: Prism::Node,
203
214
  send_context: NodeContext,
204
215
  was_removed: T::Boolean,
205
216
  ).void
206
217
  end
207
218
  def insert_accessor(node, send_context, was_removed:)
208
- name = @node_context.node_string(node)
219
+ name = node.slice
209
220
  code = case @kind
210
221
  when Definition::Kind::AttrReader
211
222
  "attr_writer #{name}"
@@ -221,10 +232,10 @@ module Spoom
221
232
  node_after = send_context.next_node
222
233
 
223
234
  if was_removed
224
- first_node = send_context.attached_comments_and_sigs.first || send_context.node
235
+ first_node = send_context.attached_sigs.first || send_context.node
225
236
  at_line = first_node.location.start_line - 1
226
237
 
227
- prev_context = NodeContext.new(@old_source, first_node, send_context.nesting)
238
+ prev_context = NodeContext.new(@old_source, @node_context.comments, first_node, send_context.nesting)
228
239
  node_before = prev_context.previous_node
229
240
 
230
241
  new_line_before = node_before && send_context.node.location.start_line - node_before.location.end_line < 2
@@ -251,27 +262,57 @@ module Spoom
251
262
  start_line = context.node.location.start_line
252
263
  end_line = context.node.location.end_line
253
264
 
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
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
258
276
  end
259
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
+
260
286
  # 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
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
+
263
298
  if before && before.location.end_line < start_line - 1
264
299
  # There is a node before and a blank line
265
300
  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
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
269
310
  end
270
311
 
271
312
  # Adjust the lines to remove to include following blank lines
272
313
  after = context.next_node
273
314
  if before.nil? && after && after.location.start_line > end_line + 1
274
- end_line = after.location.end_line - 1
315
+ end_line = after.location.start_line - 1
275
316
  elsif after.nil? && context.parent_node.location.end_line > end_line + 1
276
317
  end_line = context.parent_node.location.end_line - 1
277
318
  end
@@ -296,41 +337,24 @@ module Spoom
296
337
  @new_source[start_char...end_char] = replacement
297
338
  end
298
339
 
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) }
340
+ sig { params(node: Prism::CallNode, name: String, kind: T.nilable(Definition::Kind)).returns(String) }
315
341
  def transform_sig(node, name:, kind:)
316
342
  type = T.let(nil, T.nilable(String))
317
343
 
318
- statements = node.block.bodystmt
319
- statements = statements.statements if statements.is_a?(SyntaxTree::BodyStmt)
344
+ block = T.cast(node.block, Prism::BlockNode)
345
+ statements = T.cast(block.body, Prism::StatementsNode)
320
346
 
321
347
  statements.body.each do |call|
322
- next unless call.is_a?(SyntaxTree::CallNode)
323
- next unless @node_context.node_string(call.message) == "returns"
348
+ next unless call.is_a?(Prism::CallNode)
349
+ next unless call.name == :returns
324
350
 
325
351
  args = call.arguments
326
- args = args.arguments if args.is_a?(SyntaxTree::ArgParen)
327
-
328
- next unless args.is_a?(SyntaxTree::Args)
352
+ next unless args
329
353
 
330
- first = args.parts.first
354
+ first = args.arguments.first
331
355
  next unless first
332
356
 
333
- type = @node_context.node_string(first)
357
+ type = first.slice
334
358
  end
335
359
 
336
360
  name = name.delete_prefix(":")
@@ -348,20 +372,31 @@ module Spoom
348
372
  class NodeContext
349
373
  extend T::Sig
350
374
 
351
- sig { returns(SyntaxTree::Node) }
375
+ sig { returns(T::Hash[Integer, Prism::Comment]) }
376
+ attr_reader :comments
377
+
378
+ sig { returns(Prism::Node) }
352
379
  attr_reader :node
353
380
 
354
- sig { returns(T::Array[SyntaxTree::Node]) }
381
+ sig { returns(T::Array[Prism::Node]) }
355
382
  attr_accessor :nesting
356
383
 
357
- sig { params(source: String, node: SyntaxTree::Node, nesting: T::Array[SyntaxTree::Node]).void }
358
- def initialize(source, node, nesting)
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)
359
393
  @source = source
394
+ @comments = comments
360
395
  @node = node
361
396
  @nesting = nesting
362
397
  end
363
398
 
364
- sig { returns(SyntaxTree::Node) }
399
+ sig { returns(Prism::Node) }
365
400
  def parent_node
366
401
  parent = @nesting.last
367
402
  raise "No parent for node #{node}" unless parent
@@ -375,100 +410,119 @@ module Spoom
375
410
  parent = nesting.pop
376
411
  raise "No parent context for node #{@node}" unless parent
377
412
 
378
- NodeContext.new(@source, parent, nesting)
413
+ NodeContext.new(@source, @comments, parent, nesting)
379
414
  end
380
415
 
381
- sig { returns(T::Array[SyntaxTree::Node]) }
416
+ sig { returns(T::Array[Prism::Node]) }
382
417
  def previous_nodes
383
418
  parent = parent_node
419
+ child_nodes = parent.child_nodes.compact
384
420
 
385
- index = parent.child_nodes.index(@node)
421
+ index = child_nodes.index(@node)
386
422
  raise "Node #{@node} not found in parent #{parent}" unless index
387
423
 
388
- parent.child_nodes[0...index].reject { |child| child.is_a?(SyntaxTree::VoidStmt) }
424
+ T.must(child_nodes[0...index])
389
425
  end
390
426
 
391
- sig { returns(T.nilable(SyntaxTree::Node)) }
427
+ sig { returns(T.nilable(Prism::Node)) }
392
428
  def previous_node
393
429
  previous_nodes.last
394
430
  end
395
431
 
396
- sig { returns(T::Array[SyntaxTree::Node]) }
432
+ sig { returns(T::Array[Prism::Node]) }
397
433
  def next_nodes
398
434
  parent = parent_node
435
+ child_nodes = parent.child_nodes.compact
399
436
 
400
- index = parent.child_nodes.index(node)
437
+ index = child_nodes.index(node)
401
438
  raise "Node #{@node} not found in nesting node #{parent}" unless index
402
439
 
403
- parent.child_nodes[(index + 1)..-1].reject { |node| node.is_a?(SyntaxTree::VoidStmt) }
440
+ T.must(child_nodes.compact[(index + 1)..-1])
404
441
  end
405
442
 
406
- sig { returns(T.nilable(SyntaxTree::Node)) }
443
+ sig { returns(T.nilable(Prism::Node)) }
407
444
  def next_node
408
445
  next_nodes.first
409
446
  end
410
447
 
411
448
  sig { returns(T.nilable(NodeContext)) }
412
449
  def sclass_context
413
- sclass = T.let(nil, T.nilable(SyntaxTree::SClass))
450
+ sclass = T.let(nil, T.nilable(Prism::SingletonClassNode))
414
451
 
415
452
  nesting = @nesting.dup
416
453
  until nesting.empty? || sclass
417
454
  node = nesting.pop
418
- next unless node.is_a?(SyntaxTree::SClass)
455
+ next unless node.is_a?(Prism::SingletonClassNode)
419
456
 
420
457
  sclass = node
421
458
  end
422
459
 
423
- return unless sclass.is_a?(SyntaxTree::SClass)
460
+ return unless sclass.is_a?(Prism::SingletonClassNode)
424
461
 
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)
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)
428
467
  end
429
468
 
430
469
  if nodes.size <= 1
431
- return NodeContext.new(@source, sclass, nesting)
470
+ return NodeContext.new(@source, @comments, sclass, nesting)
432
471
  end
433
472
 
434
473
  nil
435
474
  end
436
475
 
437
- sig { params(node: T.nilable(SyntaxTree::Node)).returns(T::Boolean) }
476
+ sig { params(node: T.nilable(Prism::Node)).returns(T::Boolean) }
438
477
  def sorbet_signature?(node)
439
- return false unless node.is_a?(SyntaxTree::MethodAddBlock)
478
+ node.is_a?(Prism::CallNode) && node.name == :sig
479
+ end
440
480
 
441
- call = node.call
442
- return false unless call.is_a?(SyntaxTree::CallNode)
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
443
485
 
444
- ident = call.message
445
- return false unless ident.is_a?(SyntaxTree::Ident)
486
+ args = node.arguments
487
+ return false unless args
488
+ return false unless args.arguments.size == 1
446
489
 
447
- ident.value == "sig"
490
+ args.arguments.first&.slice == "T::Sig"
448
491
  end
449
492
 
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
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])
455
496
 
456
- node_string(T.must(node.arguments.parts.first)) == "T::Sig"
497
+ (start_line + 1).upto(end_line - 1) do |line|
498
+ comment = @comments[line]
499
+ comments << comment if comment
500
+ end
501
+
502
+ comments
457
503
  end
458
504
 
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)
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
462
513
 
463
- comment.location.end_line == node.location.start_line - 1
514
+ comments << comment
515
+ end
516
+
517
+ comments.reverse
464
518
  end
465
519
 
466
- sig { returns(T::Array[SyntaxTree::Node]) }
467
- def attached_comments_and_sigs
468
- nodes = T.let([], T::Array[SyntaxTree::Node])
520
+ sig { returns(T::Array[Prism::Node]) }
521
+ def attached_sigs
522
+ nodes = T.let([], T::Array[Prism::Node])
469
523
 
470
524
  previous_nodes.reverse_each do |prev_node|
471
- break unless comment_for_node?(prev_node, nodes.last || node) || sorbet_signature?(prev_node)
525
+ break unless sorbet_signature?(prev_node)
472
526
 
473
527
  nodes << prev_node
474
528
  end
@@ -476,13 +530,13 @@ module Spoom
476
530
  nodes.reverse
477
531
  end
478
532
 
479
- sig { returns(T.nilable(SyntaxTree::MethodAddBlock)) }
533
+ sig { returns(T.nilable(Prism::CallNode)) }
480
534
  def attached_sig
481
535
  previous_nodes.reverse_each do |node|
482
- if node.is_a?(SyntaxTree::Comment)
536
+ if node.is_a?(Prism::Comment)
483
537
  next
484
538
  elsif sorbet_signature?(node)
485
- return T.cast(node, SyntaxTree::MethodAddBlock)
539
+ return T.cast(node, Prism::CallNode)
486
540
  else
487
541
  break
488
542
  end
@@ -490,79 +544,92 @@ module Spoom
490
544
 
491
545
  nil
492
546
  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
547
  end
504
548
 
505
- class NodeFinder < SyntaxTree::Visitor
549
+ class NodeFinder < Visitor
506
550
  extend T::Sig
507
551
 
508
552
  class << self
509
553
  extend T::Sig
510
554
 
511
- sig { params(source: String, location: Location, kind: Definition::Kind).returns(NodeContext) }
555
+ sig { params(source: String, location: Location, kind: T.nilable(Definition::Kind)).returns(NodeContext) }
512
556
  def find(source, location, kind)
513
- tree = SyntaxTree.parse(source)
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
514
566
 
515
567
  visitor = new(location)
516
- visitor.visit(tree)
568
+ visitor.visit(result.value)
517
569
 
518
570
  node = visitor.node
519
571
  unless node
520
572
  raise Error, "Can't find node at #{location}"
521
573
  end
522
574
 
523
- unless node_match_kind?(node, kind)
575
+ if kind && !node_match_kind?(node, kind)
524
576
  raise Error, "Can't find node at #{location}, expected #{kind} but got #{node.class}"
525
577
  end
526
578
 
527
- NodeContext.new(source, node, visitor.nodes_nesting)
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)
528
587
  end
529
588
 
530
- sig { params(node: SyntaxTree::Node, kind: Definition::Kind).returns(T::Boolean) }
589
+ sig { params(node: Prism::Node, kind: Definition::Kind).returns(T::Boolean) }
531
590
  def node_match_kind?(node, kind)
532
591
  case kind
533
592
  when Definition::Kind::AttrReader, Definition::Kind::AttrWriter
534
- node.is_a?(SyntaxTree::SymbolLiteral)
593
+ node.is_a?(Prism::SymbolNode)
535
594
  when Definition::Kind::Class
536
- node.is_a?(SyntaxTree::ClassDeclaration)
595
+ node.is_a?(Prism::ClassNode)
537
596
  when Definition::Kind::Constant
538
- node.is_a?(SyntaxTree::Const) || node.is_a?(SyntaxTree::ConstPathField)
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)
539
606
  when Definition::Kind::Method
540
- node.is_a?(SyntaxTree::DefNode)
607
+ node.is_a?(Prism::DefNode)
541
608
  when Definition::Kind::Module
542
- node.is_a?(SyntaxTree::ModuleDeclaration)
609
+ node.is_a?(Prism::ModuleNode)
543
610
  end
544
611
  end
545
612
  end
546
613
 
547
- sig { returns(T.nilable(SyntaxTree::Node)) }
614
+ sig { returns(T.nilable(Prism::Node)) }
548
615
  attr_reader :node
549
616
 
550
- sig { returns(T::Array[SyntaxTree::Node]) }
551
- attr_accessor :nodes_nesting
617
+ sig { returns(T::Array[Prism::Node]) }
618
+ attr_reader :nodes_nesting
552
619
 
553
620
  sig { params(location: Location).void }
554
621
  def initialize(location)
555
622
  super()
556
623
  @location = location
557
- @node = T.let(nil, T.nilable(SyntaxTree::Node))
558
- @nodes_nesting = T.let([], T::Array[SyntaxTree::Node])
624
+ @node = T.let(nil, T.nilable(Prism::Node))
625
+ @nodes_nesting = T.let([], T::Array[Prism::Node])
559
626
  end
560
627
 
561
- sig { override.params(node: T.nilable(SyntaxTree::Node)).void }
628
+ sig { override.params(node: T.nilable(Prism::Node)).void }
562
629
  def visit(node)
563
630
  return unless node
564
631
 
565
- location = location_from_node(node)
632
+ location = Location.from_prism(@location.file, node.location)
566
633
 
567
634
  if location == @location
568
635
  # We found the node we're looking for at `@location`
@@ -578,38 +645,6 @@ module Spoom
578
645
  super(node)
579
646
  end
580
647
  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
648
  end
614
649
  end
615
650
  end