spoom 1.2.3 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -13,6 +13,7 @@ require_relative "deadcode/definition"
13
13
  require_relative "deadcode/reference"
14
14
  require_relative "deadcode/send"
15
15
  require_relative "deadcode/plugins"
16
+ require_relative "deadcode/remover"
16
17
 
17
18
  module Spoom
18
19
  module Deadcode
data/lib/spoom/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.2.3"
5
+ VERSION = "1.2.4"
6
6
  end