spoom 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +64 -55
  3. data/lib/spoom/backtrace_filter/minitest.rb +21 -0
  4. data/lib/spoom/cli/deadcode.rb +172 -0
  5. data/lib/spoom/cli/helper.rb +20 -0
  6. data/lib/spoom/cli/srb/bump.rb +200 -0
  7. data/lib/spoom/cli/srb/coverage.rb +224 -0
  8. data/lib/spoom/cli/srb/lsp.rb +159 -0
  9. data/lib/spoom/cli/srb/tc.rb +150 -0
  10. data/lib/spoom/cli/srb.rb +27 -0
  11. data/lib/spoom/cli.rb +72 -32
  12. data/lib/spoom/context/git.rb +2 -2
  13. data/lib/spoom/context/sorbet.rb +2 -2
  14. data/lib/spoom/deadcode/definition.rb +11 -0
  15. data/lib/spoom/deadcode/erb.rb +4 -4
  16. data/lib/spoom/deadcode/indexer.rb +266 -200
  17. data/lib/spoom/deadcode/location.rb +30 -2
  18. data/lib/spoom/deadcode/plugins/action_mailer.rb +21 -0
  19. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +19 -0
  20. data/lib/spoom/deadcode/plugins/actionpack.rb +59 -0
  21. data/lib/spoom/deadcode/plugins/active_job.rb +13 -0
  22. data/lib/spoom/deadcode/plugins/active_model.rb +46 -0
  23. data/lib/spoom/deadcode/plugins/active_record.rb +108 -0
  24. data/lib/spoom/deadcode/plugins/active_support.rb +32 -0
  25. data/lib/spoom/deadcode/plugins/base.rb +165 -12
  26. data/lib/spoom/deadcode/plugins/graphql.rb +47 -0
  27. data/lib/spoom/deadcode/plugins/minitest.rb +28 -0
  28. data/lib/spoom/deadcode/plugins/namespaces.rb +32 -0
  29. data/lib/spoom/deadcode/plugins/rails.rb +31 -0
  30. data/lib/spoom/deadcode/plugins/rake.rb +12 -0
  31. data/lib/spoom/deadcode/plugins/rspec.rb +19 -0
  32. data/lib/spoom/deadcode/plugins/rubocop.rb +41 -0
  33. data/lib/spoom/deadcode/plugins/ruby.rb +10 -18
  34. data/lib/spoom/deadcode/plugins/sorbet.rb +40 -0
  35. data/lib/spoom/deadcode/plugins/thor.rb +21 -0
  36. data/lib/spoom/deadcode/plugins.rb +91 -0
  37. data/lib/spoom/deadcode/remover.rb +651 -0
  38. data/lib/spoom/deadcode/send.rb +27 -6
  39. data/lib/spoom/deadcode/visitor.rb +755 -0
  40. data/lib/spoom/deadcode.rb +41 -10
  41. data/lib/spoom/file_tree.rb +0 -16
  42. data/lib/spoom/sorbet/errors.rb +1 -1
  43. data/lib/spoom/sorbet/lsp/structures.rb +2 -2
  44. data/lib/spoom/version.rb +1 -1
  45. metadata +36 -15
  46. data/lib/spoom/cli/bump.rb +0 -198
  47. data/lib/spoom/cli/coverage.rb +0 -222
  48. data/lib/spoom/cli/lsp.rb +0 -168
  49. data/lib/spoom/cli/run.rb +0 -148
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class Namespaces < Base
8
+ extend T::Sig
9
+
10
+ sig { override.params(indexer: Indexer, definition: Definition).void }
11
+ def on_define_class(indexer, definition)
12
+ definition.ignored! if used_as_namespace?(indexer)
13
+ end
14
+
15
+ sig { override.params(indexer: Indexer, definition: Definition).void }
16
+ def on_define_module(indexer, definition)
17
+ definition.ignored! if used_as_namespace?(indexer)
18
+ end
19
+
20
+ private
21
+
22
+ sig { params(indexer: Indexer).returns(T::Boolean) }
23
+ def used_as_namespace?(indexer)
24
+ node = indexer.current_node
25
+ return false unless node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
26
+
27
+ !!node.body
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class Rails < Base
8
+ extend T::Sig
9
+
10
+ ignore_constants_named("APP_PATH", "ENGINE_PATH", "ENGINE_ROOT")
11
+
12
+ sig { override.params(indexer: Indexer, definition: Definition).void }
13
+ def on_define_class(indexer, definition)
14
+ definition.ignored! if file_is_helper?(indexer)
15
+ end
16
+
17
+ sig { override.params(indexer: Indexer, definition: Definition).void }
18
+ def on_define_module(indexer, definition)
19
+ definition.ignored! if file_is_helper?(indexer)
20
+ end
21
+
22
+ private
23
+
24
+ sig { params(indexer: Indexer).returns(T::Boolean) }
25
+ def file_is_helper?(indexer)
26
+ indexer.path.match?(%r{app/helpers/.*\.rb$})
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class Rake < Base
8
+ ignore_constants_named("APP_RAKEFILE")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class RSpec < Base
8
+ ignore_classes_named(/Spec$/)
9
+
10
+ ignore_methods_named(
11
+ "after_setup",
12
+ "after_teardown",
13
+ "before_setup",
14
+ "before_teardown",
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class Rubocop < Base
8
+ extend T::Sig
9
+
10
+ RUBOCOP_CONSTANTS = T.let(["MSG", "RESTRICT_ON_SEND"].to_set.freeze, T::Set[String])
11
+
12
+ ignore_classes_inheriting_from(
13
+ /^(::)?RuboCop::Cop::Cop$/,
14
+ /^(::)?RuboCop::Cop::Base$/,
15
+ )
16
+
17
+ sig { override.params(indexer: Indexer, definition: Definition).void }
18
+ def on_define_constant(indexer, definition)
19
+ definition.ignored! if rubocop_constant?(indexer, definition)
20
+ end
21
+
22
+ sig { override.params(indexer: Indexer, definition: Definition).void }
23
+ def on_define_method(indexer, definition)
24
+ definition.ignored! if rubocop_method?(indexer, definition)
25
+ end
26
+
27
+ private
28
+
29
+ sig { params(indexer: Indexer, definition: Definition).returns(T::Boolean) }
30
+ def rubocop_constant?(indexer, definition)
31
+ ignored_subclass?(indexer.nesting_class_superclass_name) && RUBOCOP_CONSTANTS.include?(definition.name)
32
+ end
33
+
34
+ sig { params(indexer: Indexer, definition: Definition).returns(T::Boolean) }
35
+ def rubocop_method?(indexer, definition)
36
+ ignored_subclass?(indexer.nesting_class_superclass_name) && definition.name == "on_send"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -7,7 +7,7 @@ module Spoom
7
7
  class Ruby < Base
8
8
  extend T::Sig
9
9
 
10
- ignore_method_names(
10
+ ignore_methods_named(
11
11
  "==",
12
12
  "extended",
13
13
  "included",
@@ -26,34 +26,26 @@ module Spoom
26
26
  when "const_defined?", "const_get", "const_source_location"
27
27
  reference_symbol_as_constant(indexer, send, T.must(send.args.first))
28
28
  when "send", "__send__", "try"
29
- reference_send_first_symbol_as_method(indexer, send)
29
+ arg = send.args.first
30
+ indexer.reference_method(arg.unescaped, send.node) if arg.is_a?(Prism::SymbolNode)
30
31
  when "alias_method"
31
32
  last_arg = send.args.last
32
33
 
33
- name = case last_arg
34
- when SyntaxTree::SymbolLiteral
35
- indexer.node_string(last_arg.value)
36
- when SyntaxTree::StringLiteral
37
- last_arg.parts.map { |part| indexer.node_string(part) }.join
34
+ if last_arg.is_a?(Prism::SymbolNode) || last_arg.is_a?(Prism::StringNode)
35
+ indexer.reference_method(last_arg.unescaped, send.node)
38
36
  end
39
-
40
- return unless name
41
-
42
- indexer.reference_method(name, send.node)
43
37
  end
44
38
  end
45
39
 
46
40
  private
47
41
 
48
- sig { params(indexer: Indexer, send: Send, node: SyntaxTree::Node).void }
42
+ sig { params(indexer: Indexer, send: Send, node: Prism::Node).void }
49
43
  def reference_symbol_as_constant(indexer, send, node)
50
44
  case node
51
- when SyntaxTree::SymbolLiteral
52
- name = indexer.node_string(node.value)
53
- indexer.reference_constant(name, send.node)
54
- when SyntaxTree::StringLiteral
55
- string = T.must(indexer.node_string(node)[1..-2])
56
- string.split("::").each do |name|
45
+ when Prism::SymbolNode
46
+ indexer.reference_constant(node.unescaped, send.node)
47
+ when Prism::StringNode
48
+ node.unescaped.split("::").each do |name|
57
49
  indexer.reference_constant(name, send.node) unless name.empty?
58
50
  end
59
51
  end
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ module Plugins
7
+ class Sorbet < Base
8
+ extend T::Sig
9
+
10
+ sig { override.params(indexer: Indexer, definition: Definition).void }
11
+ def on_define_constant(indexer, definition)
12
+ definition.ignored! if sorbet_type_member?(indexer, definition) || sorbet_enum_constant?(indexer, definition)
13
+ end
14
+
15
+ sig { override.params(indexer: Indexer, definition: Definition).void }
16
+ def on_define_method(indexer, definition)
17
+ definition.ignored! if indexer.last_sig =~ /(override|overridable)/
18
+ end
19
+
20
+ private
21
+
22
+ sig { params(indexer: Indexer, definition: Definition).returns(T::Boolean) }
23
+ def sorbet_type_member?(indexer, definition)
24
+ assign = indexer.nesting_node(Prism::ConstantWriteNode)
25
+ return false unless assign
26
+
27
+ value = assign.value
28
+ return false unless value.is_a?(Prism::CallNode)
29
+
30
+ value.name == :type_member || value.name == :type_template
31
+ end
32
+
33
+ sig { params(indexer: Indexer, definition: Definition).returns(T::Boolean) }
34
+ def sorbet_enum_constant?(indexer, definition)
35
+ /^(::)?T::Enum$/.match?(indexer.nesting_class_superclass_name) && indexer.nesting_call&.name == :enums
36
+ end
37
+ end
38
+ end
39
+ end
40
+ 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 Thor < Base
8
+ extend T::Sig
9
+
10
+ ignore_methods_named("exit_on_failure?")
11
+
12
+ sig { override.params(indexer: Indexer, definition: Definition).void }
13
+ def on_define_method(indexer, definition)
14
+ return if indexer.nesting_block # method defined in `no_commands do ... end`, we don't want to ignore it
15
+
16
+ definition.ignored! if indexer.nesting_class_superclass_name =~ /^(::)?Thor$/
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,4 +2,95 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "plugins/base"
5
+
6
+ require_relative "plugins/actionpack"
7
+ require_relative "plugins/active_job"
8
+ require_relative "plugins/action_mailer_preview"
9
+ require_relative "plugins/action_mailer"
10
+ require_relative "plugins/active_model"
11
+ require_relative "plugins/active_record"
12
+ require_relative "plugins/active_support"
13
+ require_relative "plugins/graphql"
14
+ require_relative "plugins/minitest"
15
+ require_relative "plugins/namespaces"
16
+ require_relative "plugins/rails"
17
+ require_relative "plugins/rake"
18
+ require_relative "plugins/rspec"
19
+ require_relative "plugins/rubocop"
5
20
  require_relative "plugins/ruby"
21
+ require_relative "plugins/sorbet"
22
+ require_relative "plugins/thor"
23
+
24
+ module Spoom
25
+ module Deadcode
26
+ DEFAULT_CUSTOM_PLUGINS_PATH = ".spoom/deadcode/plugins"
27
+
28
+ DEFAULT_PLUGINS = T.let(
29
+ Set.new([
30
+ Spoom::Deadcode::Plugins::Namespaces,
31
+ Spoom::Deadcode::Plugins::Ruby,
32
+ ]).freeze,
33
+ T::Set[T.class_of(Plugins::Base)],
34
+ )
35
+
36
+ PLUGINS_FOR_GEM = T.let(
37
+ {
38
+ "actionmailer" => Spoom::Deadcode::Plugins::ActionMailer,
39
+ "actionpack" => Spoom::Deadcode::Plugins::ActionPack,
40
+ "activejob" => Spoom::Deadcode::Plugins::ActiveJob,
41
+ "activemodel" => Spoom::Deadcode::Plugins::ActiveModel,
42
+ "activerecord" => Spoom::Deadcode::Plugins::ActiveRecord,
43
+ "activesupport" => Spoom::Deadcode::Plugins::ActiveSupport,
44
+ "graphql" => Spoom::Deadcode::Plugins::GraphQL,
45
+ "minitest" => Spoom::Deadcode::Plugins::Minitest,
46
+ "rails" => Spoom::Deadcode::Plugins::Rails,
47
+ "rake" => Spoom::Deadcode::Plugins::Rake,
48
+ "rspec" => Spoom::Deadcode::Plugins::RSpec,
49
+ "rubocop" => Spoom::Deadcode::Plugins::Rubocop,
50
+ "sorbet-runtime" => Spoom::Deadcode::Plugins::Sorbet,
51
+ "sorbet-static" => Spoom::Deadcode::Plugins::Sorbet,
52
+ "thor" => Spoom::Deadcode::Plugins::Thor,
53
+ }.freeze,
54
+ T::Hash[String, T.class_of(Plugins::Base)],
55
+ )
56
+
57
+ class << self
58
+ extend T::Sig
59
+
60
+ sig { params(context: Context).returns(T::Array[Plugins::Base]) }
61
+ def plugins_from_gemfile_lock(context)
62
+ # These plugins are always loaded
63
+ plugin_classes = DEFAULT_PLUGINS.dup
64
+
65
+ # These plugins depends on the gems used by the project
66
+ context.gemfile_lock_specs.keys.each do |name|
67
+ plugin_class = PLUGINS_FOR_GEM[name]
68
+ plugin_classes << plugin_class if plugin_class
69
+ end
70
+
71
+ plugin_classes.map(&:new)
72
+ end
73
+
74
+ sig { params(context: Context).returns(T::Array[Plugins::Base]) }
75
+ def load_custom_plugins(context)
76
+ context.glob("#{DEFAULT_CUSTOM_PLUGINS_PATH}/*.rb").each do |path|
77
+ require("#{context.absolute_path}/#{path}")
78
+ end
79
+
80
+ ObjectSpace
81
+ .each_object(Class)
82
+ .select do |klass|
83
+ next unless T.unsafe(klass).name # skip anonymous classes, we only use them in tests
84
+ next unless T.unsafe(klass) < Plugins::Base
85
+
86
+ location = Object.const_source_location(T.unsafe(klass).to_s)&.first
87
+ next unless location
88
+ next unless location.start_with?("#{context.absolute_path}/#{DEFAULT_CUSTOM_PLUGINS_PATH}")
89
+
90
+ true
91
+ end
92
+ .map { |klass| T.unsafe(klass).new }
93
+ end
94
+ end
95
+ end
96
+ end