spoom 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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