spoom 1.2.2 → 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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/lib/spoom/backtrace_filter/minitest.rb +21 -0
  4. data/lib/spoom/cli/coverage.rb +1 -1
  5. data/lib/spoom/context/git.rb +4 -4
  6. data/lib/spoom/context/sorbet.rb +6 -6
  7. data/lib/spoom/deadcode/erb.rb +4 -4
  8. data/lib/spoom/deadcode/indexer.rb +83 -6
  9. data/lib/spoom/deadcode/location.rb +29 -1
  10. data/lib/spoom/deadcode/plugins/action_mailer.rb +21 -0
  11. data/lib/spoom/deadcode/plugins/actionpack.rb +61 -0
  12. data/lib/spoom/deadcode/plugins/active_job.rb +13 -0
  13. data/lib/spoom/deadcode/plugins/active_model.rb +46 -0
  14. data/lib/spoom/deadcode/plugins/active_record.rb +111 -0
  15. data/lib/spoom/deadcode/plugins/active_support.rb +21 -0
  16. data/lib/spoom/deadcode/plugins/base.rb +354 -0
  17. data/lib/spoom/deadcode/plugins/graphql.rb +47 -0
  18. data/lib/spoom/deadcode/plugins/minitest.rb +28 -0
  19. data/lib/spoom/deadcode/plugins/namespaces.rb +34 -0
  20. data/lib/spoom/deadcode/plugins/rails.rb +31 -0
  21. data/lib/spoom/deadcode/plugins/rake.rb +12 -0
  22. data/lib/spoom/deadcode/plugins/rspec.rb +19 -0
  23. data/lib/spoom/deadcode/plugins/rubocop.rb +41 -0
  24. data/lib/spoom/deadcode/plugins/ruby.rb +65 -0
  25. data/lib/spoom/deadcode/plugins/sorbet.rb +46 -0
  26. data/lib/spoom/deadcode/plugins/thor.rb +21 -0
  27. data/lib/spoom/deadcode/plugins.rb +95 -0
  28. data/lib/spoom/deadcode/remover.rb +616 -0
  29. data/lib/spoom/deadcode/send.rb +22 -0
  30. data/lib/spoom/deadcode.rb +8 -6
  31. data/lib/spoom/sorbet/lsp.rb +2 -2
  32. data/lib/spoom/sorbet/sigils.rb +2 -2
  33. data/lib/spoom/sorbet.rb +1 -0
  34. data/lib/spoom/version.rb +1 -1
  35. metadata +24 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5154e37a81129c70e93925cc2a545a0d7861fd645a3ec2422e6549efc0cb76b5
4
- data.tar.gz: 30c9ecdc3599a37309180fea890a7888d53b430c8701e0970f503bf5731979f9
3
+ metadata.gz: 23df97e815cc01b35dbc960ec1921bbc1c65ae4c6f0588c690a0d9156877a013
4
+ data.tar.gz: d82fdb3fb66404b0c2d98ea9206be9d86ec2284bbae2ff24a5653a521ec9155e
5
5
  SHA512:
6
- metadata.gz: a3246a42fa965e3ef75d0178ffe958a8cc5723812ecb86799b5e2e406d5da7487ffd738696ccb3f6d76ae140de281dab73dba3ec3d8af9fdef696820654ed869
7
- data.tar.gz: ba4c8ab2d87ff4451f420e2d062c7cebdd19202bc91226fd131ff9ab9f838e7494fa43560ffa317fc0565d023ecceef184c02f762e215ba6451d3ec164e3c75a
6
+ metadata.gz: 67b6209ba61fbc285fc89dcf64adcde27cfecf44bb37d644cfb8aef50b7fca6e53a15e8eec5bc45030e4c838a35d3777c50a2d75fb32461792a1bc5957d7b2e3
7
+ data.tar.gz: c3e06e5140aac671ff8489243dee9feba3912ec12681332b2abb37ddd5c367246dc180ea80f6207afe28f81f499626f4dcf4b9628baa263854600d19d593f2f5
data/README.md CHANGED
@@ -348,6 +348,16 @@ Find all the symbols for a file:
348
348
  puts client.document_symbols("file://path/to/my/file.rb")
349
349
  ```
350
350
 
351
+ ### Backtrace Filtering
352
+
353
+ Spoom provides a backtrace filter for Minitest to remove the Sorbet frames from test failures, giving a more readable output. To enable it:
354
+
355
+ ```ruby
356
+ # test/test_helper.rb
357
+ require "spoom/backtrace_filter/minitest"
358
+ Minitest.backtrace_filter = Spoom::BacktraceFilter::Minitest.new
359
+ ```
360
+
351
361
  ## Development
352
362
 
353
363
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Don't forget to run `bin/sanity` before pushing your changes.
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "minitest"
5
+
6
+ module Spoom
7
+ module BacktraceFilter
8
+ class Minitest < ::Minitest::BacktraceFilter
9
+ extend T::Sig
10
+
11
+ SORBET_PATHS = T.let(Gem.loaded_specs["sorbet-runtime"].full_require_paths.freeze, T::Array[String])
12
+
13
+ sig { override.params(bt: T.nilable(T::Array[String])).returns(T::Array[String]) }
14
+ def filter(bt)
15
+ super.select do |line|
16
+ SORBET_PATHS.none? { |path| line.include?(path) }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -187,7 +187,7 @@ module Spoom
187
187
 
188
188
  no_commands do
189
189
  def parse_time(string, option)
190
- return nil unless string
190
+ return unless string
191
191
 
192
192
  Time.parse(string)
193
193
  rescue ArgumentError
@@ -13,7 +13,7 @@ module Spoom
13
13
  sig { params(string: String).returns(T.nilable(Commit)) }
14
14
  def parse_line(string)
15
15
  sha, epoch = string.split(" ", 2)
16
- return nil unless sha && epoch
16
+ return unless sha && epoch
17
17
 
18
18
  time = Time.strptime(epoch, "%s")
19
19
  Commit.new(sha: sha, time: time)
@@ -88,7 +88,7 @@ module Spoom
88
88
  sig { returns(T.nilable(String)) }
89
89
  def git_current_branch
90
90
  res = git("branch --show-current")
91
- return nil unless res.status
91
+ return unless res.status
92
92
 
93
93
  res.out.strip
94
94
  end
@@ -103,10 +103,10 @@ module Spoom
103
103
  sig { params(short_sha: T::Boolean).returns(T.nilable(Spoom::Git::Commit)) }
104
104
  def git_last_commit(short_sha: true)
105
105
  res = git_log("HEAD --format='%#{short_sha ? "h" : "H"} %at' -1")
106
- return nil unless res.status
106
+ return unless res.status
107
107
 
108
108
  out = res.out.strip
109
- return nil if out.empty?
109
+ return if out.empty?
110
110
 
111
111
  Spoom::Git::Commit.parse_line(out)
112
112
  end
@@ -52,7 +52,7 @@ module Spoom
52
52
  sorbet_bin: sorbet_bin,
53
53
  capture_err: capture_err,
54
54
  )
55
- return nil unless file?(metrics_file)
55
+ return unless file?(metrics_file)
56
56
 
57
57
  metrics_path = absolute_path_to(metrics_file)
58
58
  metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
@@ -109,7 +109,7 @@ module Spoom
109
109
  sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(T.nilable(String)) }
110
110
  def srb_version(*arg, sorbet_bin: nil, capture_err: true)
111
111
  res = T.unsafe(self).srb_tc("--no-config", "--version", *arg, sorbet_bin: sorbet_bin, capture_err: capture_err)
112
- return nil unless res.status
112
+ return unless res.status
113
113
 
114
114
  res.out.split(" ")[2]
115
115
  end
@@ -147,10 +147,10 @@ module Spoom
147
147
  sig { returns(T.nilable(Spoom::Git::Commit)) }
148
148
  def sorbet_intro_commit
149
149
  res = git_log("--diff-filter=A --format='%h %at' -1 -- sorbet/config")
150
- return nil unless res.status
150
+ return unless res.status
151
151
 
152
152
  out = res.out.strip
153
- return nil if out.empty?
153
+ return if out.empty?
154
154
 
155
155
  Spoom::Git::Commit.parse_line(out)
156
156
  end
@@ -159,10 +159,10 @@ module Spoom
159
159
  sig { returns(T.nilable(Spoom::Git::Commit)) }
160
160
  def sorbet_removal_commit
161
161
  res = git_log("--diff-filter=D --format='%h %at' -1 -- sorbet/config")
162
- return nil unless res.status
162
+ return unless res.status
163
163
 
164
164
  out = res.out.strip
165
- return nil if out.empty?
165
+ return if out.empty?
166
166
 
167
167
  Spoom::Git::Commit.parse_line(out)
168
168
  end
@@ -44,7 +44,7 @@ module Spoom
44
44
 
45
45
  private
46
46
 
47
- sig { params(text: T.untyped).void }
47
+ sig { override.params(text: T.untyped).void }
48
48
  def add_text(text)
49
49
  return if text.empty?
50
50
 
@@ -62,7 +62,7 @@ module Spoom
62
62
 
63
63
  BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
64
64
 
65
- sig { params(indicator: T.untyped, code: T.untyped).void }
65
+ sig { override.params(indicator: T.untyped, code: T.untyped).void }
66
66
  def add_expression(indicator, code)
67
67
  flush_newline_if_pending(src)
68
68
 
@@ -79,13 +79,13 @@ module Spoom
79
79
  end
80
80
  end
81
81
 
82
- sig { params(code: T.untyped).void }
82
+ sig { override.params(code: T.untyped).void }
83
83
  def add_code(code)
84
84
  flush_newline_if_pending(src)
85
85
  super
86
86
  end
87
87
 
88
- sig { params(_: T.untyped).void }
88
+ sig { override.params(_: T.untyped).void }
89
89
  def add_postamble(_)
90
90
  flush_newline_if_pending(src)
91
91
  super
@@ -12,14 +12,15 @@ module Spoom
12
12
  sig { returns(Index) }
13
13
  attr_reader :index
14
14
 
15
- sig { params(path: String, source: String, index: Index).void }
16
- def initialize(path, source, index)
15
+ sig { params(path: String, source: String, index: Index, plugins: T::Array[Plugins::Base]).void }
16
+ def initialize(path, source, index, plugins: [])
17
17
  super()
18
18
 
19
19
  @path = path
20
20
  @file_name = T.let(File.basename(path), String)
21
21
  @source = source
22
22
  @index = index
23
+ @plugins = plugins
23
24
  @previous_node = T.let(nil, T.nilable(SyntaxTree::Node))
24
25
  @names_nesting = T.let([], T::Array[String])
25
26
  @nodes_nesting = T.let([], T::Array[SyntaxTree::Node])
@@ -156,10 +157,10 @@ module Spoom
156
157
 
157
158
  sig { override.params(node: SyntaxTree::DefNode).void }
158
159
  def visit_def(node)
159
- super
160
-
161
160
  name = node_string(node.name)
162
161
  define_method(name, [*@names_nesting, name].join("::"), node)
162
+
163
+ super
163
164
  end
164
165
 
165
166
  sig { override.params(node: SyntaxTree::Field).void }
@@ -228,6 +229,10 @@ module Spoom
228
229
  define_attr_writer("#{name}=", "#{full_name}=", arg)
229
230
  end
230
231
  else
232
+ @plugins.each do |plugin|
233
+ plugin.internal_on_send(self, send)
234
+ end
235
+
231
236
  reference_method(send.name, send.node)
232
237
  visit_all(send.args)
233
238
  visit(send.block)
@@ -270,8 +275,6 @@ module Spoom
270
275
  visit_send(Send.new(node: node, name: node_string(node.value)))
271
276
  end
272
277
 
273
- private
274
-
275
278
  # Definition indexing
276
279
 
277
280
  sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
@@ -283,6 +286,7 @@ module Spoom
283
286
  location: node_location(node),
284
287
  )
285
288
  @index.define(definition)
289
+ @plugins.each { |plugin| plugin.internal_on_define_accessor(self, definition) }
286
290
  end
287
291
 
288
292
  sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
@@ -294,6 +298,7 @@ module Spoom
294
298
  location: node_location(node),
295
299
  )
296
300
  @index.define(definition)
301
+ @plugins.each { |plugin| plugin.internal_on_define_accessor(self, definition) }
297
302
  end
298
303
 
299
304
  sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
@@ -305,6 +310,7 @@ module Spoom
305
310
  location: node_location(node),
306
311
  )
307
312
  @index.define(definition)
313
+ @plugins.each { |plugin| plugin.internal_on_define_class(self, definition) }
308
314
  end
309
315
 
310
316
  sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
@@ -316,6 +322,7 @@ module Spoom
316
322
  location: node_location(node),
317
323
  )
318
324
  @index.define(definition)
325
+ @plugins.each { |plugin| plugin.internal_on_define_constant(self, definition) }
319
326
  end
320
327
 
321
328
  sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
@@ -327,6 +334,7 @@ module Spoom
327
334
  location: node_location(node),
328
335
  )
329
336
  @index.define(definition)
337
+ @plugins.each { |plugin| plugin.internal_on_define_method(self, definition) }
330
338
  end
331
339
 
332
340
  sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
@@ -338,6 +346,7 @@ module Spoom
338
346
  location: node_location(node),
339
347
  )
340
348
  @index.define(definition)
349
+ @plugins.each { |plugin| plugin.internal_on_define_module(self, definition) }
341
350
  end
342
351
 
343
352
  # Reference indexing
@@ -352,6 +361,74 @@ module Spoom
352
361
  @index.reference(Reference.new(name: name, kind: Reference::Kind::Method, location: node_location(node)))
353
362
  end
354
363
 
364
+ # Context
365
+
366
+ sig { returns(SyntaxTree::Node) }
367
+ def current_node
368
+ T.must(@nodes_nesting.last)
369
+ end
370
+
371
+ sig { type_parameters(:N).params(type: T::Class[T.type_parameter(:N)]).returns(T.nilable(T.type_parameter(:N))) }
372
+ def nesting_node(type)
373
+ @nodes_nesting.reverse_each do |node|
374
+ return T.unsafe(node) if node.is_a?(type)
375
+ end
376
+
377
+ nil
378
+ end
379
+
380
+ sig { returns(T.nilable(SyntaxTree::ClassDeclaration)) }
381
+ def nesting_class
382
+ nesting_node(SyntaxTree::ClassDeclaration)
383
+ end
384
+
385
+ sig { returns(T.nilable(SyntaxTree::BlockNode)) }
386
+ def nesting_block
387
+ nesting_node(SyntaxTree::BlockNode)
388
+ end
389
+
390
+ sig { returns(T.nilable(SyntaxTree::MethodAddBlock)) }
391
+ def nesting_block_call
392
+ nesting_node(SyntaxTree::MethodAddBlock)
393
+ end
394
+
395
+ sig { returns(T.nilable(String)) }
396
+ def nesting_block_call_name
397
+ block = nesting_block_call
398
+ return unless block.is_a?(SyntaxTree::MethodAddBlock)
399
+
400
+ call = block.call
401
+ case call
402
+ when SyntaxTree::ARef
403
+ node_string(call.collection)
404
+ when SyntaxTree::CallNode, SyntaxTree::Command, SyntaxTree::CommandCall
405
+ node_string(call.message)
406
+ end
407
+ end
408
+
409
+ sig { returns(T.nilable(String)) }
410
+ def nesting_class_name
411
+ nesting_class = self.nesting_class
412
+ return unless nesting_class
413
+
414
+ node_string(nesting_class.constant)
415
+ end
416
+
417
+ sig { returns(T.nilable(String)) }
418
+ def nesting_class_superclass_name
419
+ nesting_class_superclass = nesting_class&.superclass
420
+ return unless nesting_class_superclass
421
+
422
+ node_string(nesting_class_superclass).delete_prefix("::")
423
+ end
424
+
425
+ sig { returns(T.nilable(String)) }
426
+ def last_sig
427
+ return unless @previous_node.is_a?(SyntaxTree::MethodAddBlock)
428
+
429
+ node_string(@previous_node)
430
+ end
431
+
355
432
  # Node utils
356
433
 
357
434
  sig { params(node: T.any(Symbol, SyntaxTree::Node)).returns(String) }
@@ -13,6 +13,23 @@ module Spoom
13
13
  class << self
14
14
  extend T::Sig
15
15
 
16
+ sig { params(location_string: String).returns(Location) }
17
+ def from_string(location_string)
18
+ file, rest = location_string.split(":", 2)
19
+ raise LocationError, "Invalid location string: #{location_string}" unless file && rest
20
+
21
+ start_line, rest = rest.split(":", 2)
22
+ raise LocationError, "Invalid location string: #{location_string}" unless start_line && rest
23
+
24
+ start_column, rest = rest.split("-", 2)
25
+ raise LocationError, "Invalid location string: #{location_string}" unless start_column && rest
26
+
27
+ end_line, end_column = rest.split(":", 2)
28
+ raise LocationError, "Invalid location string: #{location_string}" unless end_line && end_column
29
+
30
+ new(file, start_line.to_i, start_column.to_i, end_line.to_i, end_column.to_i)
31
+ end
32
+
16
33
  sig { params(file: String, location: SyntaxTree::Location).returns(Location) }
17
34
  def from_syntax_tree(file, location)
18
35
  new(file, location.start_line, location.start_column, location.end_line, location.end_column)
@@ -42,9 +59,20 @@ module Spoom
42
59
  @end_column = end_column
43
60
  end
44
61
 
62
+ sig { params(other: Location).returns(T::Boolean) }
63
+ def include?(other)
64
+ return false unless @file == other.file
65
+ return false if @start_line > other.start_line
66
+ return false if @start_line == other.start_line && @start_column > other.start_column
67
+ return false if @end_line < other.end_line
68
+ return false if @end_line == other.end_line && @end_column < other.end_column
69
+
70
+ true
71
+ end
72
+
45
73
  sig { override.params(other: BasicObject).returns(T.nilable(Integer)) }
46
74
  def <=>(other)
47
- return nil unless Location === other
75
+ return unless Location === other
48
76
 
49
77
  to_s <=> other.to_s
50
78
  end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class ActionMailer < Base
8
+ extend T::Sig
9
+
10
+ sig { override.params(indexer: Indexer, send: Send).void }
11
+ def on_send(indexer, send)
12
+ return unless send.recv.nil? && ActionPack::CALLBACKS.include?(send.name)
13
+
14
+ send.each_arg(SyntaxTree::SymbolLiteral) do |arg|
15
+ indexer.reference_method(indexer.node_string(arg.value), send.node)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class ActionPack < Base
8
+ extend T::Sig
9
+
10
+ CALLBACKS = T.let(
11
+ [
12
+ "after_action",
13
+ "append_after_action",
14
+ "append_around_action",
15
+ "append_before_action",
16
+ "around_action",
17
+ "before_action",
18
+ "prepend_after_action",
19
+ "prepend_around_action",
20
+ "prepend_before_action",
21
+ "skip_after_action",
22
+ "skip_around_action",
23
+ "skip_before_action",
24
+ ].freeze,
25
+ T::Array[String],
26
+ )
27
+
28
+ ignore_classes_named(/Controller$/)
29
+
30
+ sig { override.params(indexer: Indexer, definition: Definition).void }
31
+ def on_define_method(indexer, definition)
32
+ definition.ignored! if ignored_class_name?(indexer.nesting_class_name)
33
+ end
34
+
35
+ sig { override.params(indexer: Indexer, send: Send).void }
36
+ def on_send(indexer, send)
37
+ return unless send.recv.nil? && CALLBACKS.include?(send.name)
38
+
39
+ arg = send.args.first
40
+ case arg
41
+ when SyntaxTree::SymbolLiteral
42
+ indexer.reference_method(indexer.node_string(arg.value), send.node)
43
+ when SyntaxTree::VarRef
44
+ indexer.reference_constant(indexer.node_string(arg), send.node)
45
+ end
46
+
47
+ send.each_arg_assoc do |key, value|
48
+ key = indexer.node_string(key).delete_suffix(":")
49
+
50
+ case key
51
+ when "if", "unless"
52
+ indexer.reference_method(indexer.symbol_string(value), send.node) if value
53
+ else
54
+ indexer.reference_constant(camelize(key), send.node)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class ActiveJob < Base
8
+ ignore_classes_named("ApplicationJob")
9
+ ignore_methods_named("perform", "build_enumerator", "each_iteration")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class ActiveModel < Base
8
+ extend T::Sig
9
+
10
+ ignore_classes_inheriting_from(/^(::)?ActiveModel::EachValidator$/)
11
+ ignore_methods_named("validate_each")
12
+
13
+ sig { override.params(indexer: Indexer, send: Send).void }
14
+ def on_send(indexer, send)
15
+ return if send.recv
16
+
17
+ case send.name
18
+ when "attribute", "attributes"
19
+ send.each_arg(SyntaxTree::SymbolLiteral) do |arg|
20
+ indexer.reference_method(indexer.node_string(arg.value), send.node)
21
+ end
22
+ when "validate", "validates", "validates!", "validates_each"
23
+ send.each_arg(SyntaxTree::SymbolLiteral) do |arg|
24
+ indexer.reference_method(indexer.node_string(arg.value), send.node)
25
+ end
26
+ send.each_arg_assoc do |key, value|
27
+ key = indexer.node_string(key).delete_suffix(":")
28
+
29
+ case key
30
+ when "if", "unless"
31
+ indexer.reference_method(indexer.symbol_string(value), send.node) if value
32
+ else
33
+ indexer.reference_constant(camelize(key), send.node)
34
+ end
35
+ end
36
+ when "validates_with"
37
+ arg = send.args.first
38
+ if arg.is_a?(SyntaxTree::SymbolLiteral)
39
+ indexer.reference_constant(indexer.node_string(arg.value), send.node)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,111 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class ActiveRecord < Base
8
+ extend T::Sig
9
+
10
+ ignore_classes_inheriting_from(/^(::)?ActiveRecord::Migration/)
11
+
12
+ ignore_methods_named(
13
+ "change",
14
+ "down",
15
+ "up",
16
+ "table_name_prefix",
17
+ "to_param",
18
+ )
19
+
20
+ CALLBACKS = T.let(
21
+ [
22
+ "after_commit",
23
+ "after_create_commit",
24
+ "after_create",
25
+ "after_destroy_commit",
26
+ "after_destroy",
27
+ "after_find",
28
+ "after_initialize",
29
+ "after_rollback",
30
+ "after_save_commit",
31
+ "after_save",
32
+ "after_touch",
33
+ "after_update_commit",
34
+ "after_update",
35
+ "after_validation",
36
+ "around_create",
37
+ "around_destroy",
38
+ "around_save",
39
+ "around_update",
40
+ "before_create",
41
+ "before_destroy",
42
+ "before_save",
43
+ "before_update",
44
+ "before_validation",
45
+ ].freeze,
46
+ T::Array[String],
47
+ )
48
+
49
+ CRUD_METHODS = T.let(
50
+ [
51
+ "assign_attributes",
52
+ "create",
53
+ "create!",
54
+ "insert",
55
+ "insert!",
56
+ "new",
57
+ "update",
58
+ "update!",
59
+ "upsert",
60
+ ].freeze,
61
+ T::Array[String],
62
+ )
63
+
64
+ ARRAY_METHODS = T.let(
65
+ [
66
+ "insert_all",
67
+ "insert_all!",
68
+ "upsert_all",
69
+ ].freeze,
70
+ T::Array[String],
71
+ )
72
+
73
+ sig { override.params(indexer: Indexer, send: Send).void }
74
+ def on_send(indexer, send)
75
+ if send.recv.nil? && CALLBACKS.include?(send.name)
76
+ send.each_arg(SyntaxTree::SymbolLiteral) do |arg|
77
+ indexer.reference_method(indexer.node_string(arg.value), send.node)
78
+ end
79
+ return
80
+ end
81
+
82
+ return unless send.recv
83
+
84
+ case send.name
85
+ when *CRUD_METHODS
86
+ send.each_arg_assoc do |key, _value|
87
+ key = indexer.symbol_string(key).delete_suffix(":")
88
+ indexer.reference_method("#{key}=", send.node)
89
+ end
90
+ when *ARRAY_METHODS
91
+ send.each_arg(SyntaxTree::ArrayLiteral) do |arg|
92
+ args = arg.contents
93
+ next unless args.is_a?(SyntaxTree::Args)
94
+
95
+ args.parts.each do |part|
96
+ next unless part.is_a?(SyntaxTree::HashLiteral)
97
+
98
+ part.assocs.each do |assoc|
99
+ next unless assoc.is_a?(SyntaxTree::Assoc)
100
+
101
+ key = indexer.symbol_string(assoc.key).delete_suffix(":")
102
+ indexer.reference_method("#{key}=", send.node)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class ActiveSupport < Base
8
+ ignore_classes_inheriting_from(/^(::)?ActiveSupport::TestCase$/)
9
+
10
+ ignore_methods_named(
11
+ "after_all",
12
+ "after_setup",
13
+ "after_teardown",
14
+ "before_all",
15
+ "before_setup",
16
+ "before_teardown",
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end