spoom 1.2.2 → 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -0
- data/lib/spoom/backtrace_filter/minitest.rb +21 -0
- data/lib/spoom/cli/coverage.rb +1 -1
- data/lib/spoom/context/git.rb +4 -4
- data/lib/spoom/context/sorbet.rb +6 -6
- data/lib/spoom/deadcode/erb.rb +4 -4
- data/lib/spoom/deadcode/indexer.rb +83 -6
- data/lib/spoom/deadcode/location.rb +29 -1
- 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 +354 -0
- 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 +65 -0
- data/lib/spoom/deadcode/plugins/sorbet.rb +46 -0
- data/lib/spoom/deadcode/plugins/thor.rb +21 -0
- data/lib/spoom/deadcode/plugins.rb +95 -0
- data/lib/spoom/deadcode/remover.rb +616 -0
- data/lib/spoom/deadcode/send.rb +22 -0
- data/lib/spoom/deadcode.rb +8 -6
- data/lib/spoom/sorbet/lsp.rb +2 -2
- data/lib/spoom/sorbet/sigils.rb +2 -2
- data/lib/spoom/sorbet.rb +1 -0
- data/lib/spoom/version.rb +1 -1
- metadata +24 -18
@@ -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
@@ -12,6 +12,8 @@ require_relative "deadcode/location"
|
|
12
12
|
require_relative "deadcode/definition"
|
13
13
|
require_relative "deadcode/reference"
|
14
14
|
require_relative "deadcode/send"
|
15
|
+
require_relative "deadcode/plugins"
|
16
|
+
require_relative "deadcode/remover"
|
15
17
|
|
16
18
|
module Spoom
|
17
19
|
module Deadcode
|
@@ -34,10 +36,10 @@ module Spoom
|
|
34
36
|
class << self
|
35
37
|
extend T::Sig
|
36
38
|
|
37
|
-
sig { params(index: Index, ruby: String, file: String).void }
|
38
|
-
def index_ruby(index, ruby, file:)
|
39
|
+
sig { params(index: Index, ruby: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
|
40
|
+
def index_ruby(index, ruby, file:, plugins: [])
|
39
41
|
node = SyntaxTree.parse(ruby)
|
40
|
-
visitor = Spoom::Deadcode::Indexer.new(file, ruby, index)
|
42
|
+
visitor = Spoom::Deadcode::Indexer.new(file, ruby, index, plugins: plugins)
|
41
43
|
visitor.visit(node)
|
42
44
|
rescue SyntaxTree::Parser::ParseError => e
|
43
45
|
raise ParserError.new("Error while parsing #{file} (#{e.message} at #{e.lineno}:#{e.column})", parent: e)
|
@@ -45,10 +47,10 @@ module Spoom
|
|
45
47
|
raise IndexerError.new("Error while indexing #{file} (#{e.message})", parent: e)
|
46
48
|
end
|
47
49
|
|
48
|
-
sig { params(index: Index, erb: String, file: String).void }
|
49
|
-
def index_erb(index, erb, file:)
|
50
|
+
sig { params(index: Index, erb: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
|
51
|
+
def index_erb(index, erb, file:, plugins: [])
|
50
52
|
ruby = ERB.new(erb).src
|
51
|
-
index_ruby(index, ruby, file: file)
|
53
|
+
index_ruby(index, ruby, file: file, plugins: plugins)
|
52
54
|
end
|
53
55
|
end
|
54
56
|
end
|
data/lib/spoom/sorbet/lsp.rb
CHANGED
@@ -53,7 +53,7 @@ module Spoom
|
|
53
53
|
sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
|
54
54
|
def read
|
55
55
|
raw_string = read_raw
|
56
|
-
return
|
56
|
+
return unless raw_string
|
57
57
|
|
58
58
|
json = JSON.parse(raw_string)
|
59
59
|
|
@@ -101,7 +101,7 @@ module Spoom
|
|
101
101
|
},
|
102
102
|
))
|
103
103
|
|
104
|
-
return
|
104
|
+
return unless json && json["result"]
|
105
105
|
|
106
106
|
Hover.from_json(json["result"])
|
107
107
|
end
|