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