spoom 1.2.4 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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 +209 -174
  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,21 +262,51 @@ 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
@@ -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