spoom 1.2.3 → 1.3.0
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.
- checksums.yaml +4 -4
- data/README.md +64 -55
- data/lib/spoom/backtrace_filter/minitest.rb +21 -0
- data/lib/spoom/cli/deadcode.rb +172 -0
- data/lib/spoom/cli/helper.rb +20 -0
- data/lib/spoom/cli/srb/bump.rb +200 -0
- data/lib/spoom/cli/srb/coverage.rb +224 -0
- data/lib/spoom/cli/srb/lsp.rb +159 -0
- data/lib/spoom/cli/srb/tc.rb +150 -0
- data/lib/spoom/cli/srb.rb +27 -0
- data/lib/spoom/cli.rb +72 -32
- data/lib/spoom/context/git.rb +2 -2
- data/lib/spoom/context/sorbet.rb +2 -2
- data/lib/spoom/deadcode/definition.rb +11 -0
- data/lib/spoom/deadcode/erb.rb +4 -4
- data/lib/spoom/deadcode/indexer.rb +266 -200
- data/lib/spoom/deadcode/location.rb +30 -2
- data/lib/spoom/deadcode/plugins/action_mailer.rb +21 -0
- data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +19 -0
- data/lib/spoom/deadcode/plugins/actionpack.rb +59 -0
- data/lib/spoom/deadcode/plugins/active_job.rb +13 -0
- data/lib/spoom/deadcode/plugins/active_model.rb +46 -0
- data/lib/spoom/deadcode/plugins/active_record.rb +108 -0
- data/lib/spoom/deadcode/plugins/active_support.rb +32 -0
- data/lib/spoom/deadcode/plugins/base.rb +165 -12
- data/lib/spoom/deadcode/plugins/graphql.rb +47 -0
- data/lib/spoom/deadcode/plugins/minitest.rb +28 -0
- data/lib/spoom/deadcode/plugins/namespaces.rb +32 -0
- data/lib/spoom/deadcode/plugins/rails.rb +31 -0
- data/lib/spoom/deadcode/plugins/rake.rb +12 -0
- data/lib/spoom/deadcode/plugins/rspec.rb +19 -0
- data/lib/spoom/deadcode/plugins/rubocop.rb +41 -0
- data/lib/spoom/deadcode/plugins/ruby.rb +10 -18
- data/lib/spoom/deadcode/plugins/sorbet.rb +40 -0
- data/lib/spoom/deadcode/plugins/thor.rb +21 -0
- data/lib/spoom/deadcode/plugins.rb +91 -0
- data/lib/spoom/deadcode/remover.rb +651 -0
- data/lib/spoom/deadcode/send.rb +27 -6
- data/lib/spoom/deadcode/visitor.rb +755 -0
- data/lib/spoom/deadcode.rb +41 -10
- data/lib/spoom/file_tree.rb +0 -16
- data/lib/spoom/sorbet/errors.rb +1 -1
- data/lib/spoom/sorbet/lsp/structures.rb +2 -2
- data/lib/spoom/version.rb +1 -1
- metadata +36 -15
- data/lib/spoom/cli/bump.rb +0 -198
- data/lib/spoom/cli/coverage.rb +0 -222
- data/lib/spoom/cli/lsp.rb +0 -168
- 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
|
data/lib/spoom/deadcode/send.rb
CHANGED
@@ -3,16 +3,37 @@
|
|
3
3
|
|
4
4
|
module Spoom
|
5
5
|
module Deadcode
|
6
|
-
# An abstraction to simplify handling of
|
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,
|
10
|
+
const :node, Prism::CallNode
|
12
11
|
const :name, String
|
13
|
-
const :recv, T.nilable(
|
14
|
-
const :args, T::Array[
|
15
|
-
const :block, T.nilable(
|
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
|