lib/spoom/deadcode/plugins/base.rb



# typed: strict
# frozen_string_literal: true

require "set"

module Spoom
  module Deadcode
    module Plugins
      class Base
        extend T::Sig
        extend T::Helpers

        abstract!

        class << self
          extend T::Sig

          # Plugins DSL

          # Mark methods matching `names` as ignored.
          #
          # Names can be either strings or regexps:
          #
          # ~~~rb
          # class MyPlugin < Spoom::Deadcode::Plugins::Base
          #   ignore_method_names(
          #     "foo",
          #     "bar",
          #     /baz.*/,
          #   )
          # end
          # ~~~
          sig { params(names: T.any(String, Regexp)).void }
          def ignore_method_names(*names)
            save_names_and_patterns(names, :@ignored_method_names, :@ignored_method_patterns)
          end

          private

          sig { params(names: T::Array[T.any(String, Regexp)], names_variable: Symbol, patterns_variable: Symbol).void }
          def save_names_and_patterns(names, names_variable, patterns_variable)
            ignored_names = instance_variable_set(names_variable, Set.new)
            ignored_patterns = instance_variable_set(patterns_variable, [])

            names.each do |name|
              case name
              when String
                ignored_names << name
              when Regexp
                ignored_patterns << name
              end
            end
          end
        end

        # Indexing event methods

        # Called when an accessor is defined.
        #
        # Will be called when the indexer processes a `attr_reader`, `attr_writer` or `attr_accessor` node.
        # Note that when this method is called, the definition for the node has already been added to the index.
        # It is still possible to ignore it from the plugin:
        #
        # ~~~rb
        # class MyPlugin < Spoom::Deadcode::Plugins::Base
        #   def on_define_accessor(indexer, definition)
        #     definition.ignored! if definition.name == "foo"
        #   end
        # end
        # ~~~
        sig { params(indexer: Indexer, definition: Definition).void }
        def on_define_accessor(indexer, definition)
          # no-op
        end

        # Called when a class is defined.
        #
        # Will be called when the indexer processes a `class` node.
        # Note that when this method is called, the definition for the node has already been added to the index.
        # It is still possible to ignore it from the plugin:
        #
        # ~~~rb
        # class MyPlugin < Spoom::Deadcode::Plugins::Base
        #   def on_define_class(indexer, definition)
        #     definition.ignored! if definition.name == "Foo"
        #   end
        # end
        # ~~~
        sig { params(indexer: Indexer, definition: Definition).void }
        def on_define_class(indexer, definition)
          # no-op
        end

        # Called when a constant is defined.
        #
        # Will be called when the indexer processes a `CONST =` node.
        # Note that when this method is called, the definition for the node has already been added to the index.
        # It is still possible to ignore it from the plugin:
        #
        # ~~~rb
        # class MyPlugin < Spoom::Deadcode::Plugins::Base
        #   def on_define_constant(indexer, definition)
        #     definition.ignored! if definition.name == "FOO"
        #   end
        # end
        # ~~~
        sig { params(indexer: Indexer, definition: Definition).void }
        def on_define_constant(indexer, definition)
          # no-op
        end

        # Called when a method is defined.
        #
        # Will be called when the indexer processes a `def` or `defs` node.
        # Note that when this method is called, the definition for the node has already been added to the index.
        # It is still possible to ignore it from the plugin:
        #
        # ~~~rb
        # class MyPlugin < Spoom::Deadcode::Plugins::Base
        #   def on_define_method(indexer, definition)
        #     super # So the `ignore_method_names` DSL is still applied
        #
        #     definition.ignored! if definition.name == "foo"
        #   end
        # end
        # ~~~
        sig { params(indexer: Indexer, definition: Definition).void }
        def on_define_method(indexer, definition)
          definition.ignored! if ignored_method_name?(definition.name)
        end

        # Called when a module is defined.
        #
        # Will be called when the indexer processes a `module` node.
        # Note that when this method is called, the definition for the node has already been added to the index.
        # It is still possible to ignore it from the plugin:
        #
        # ~~~rb
        # class MyPlugin < Spoom::Deadcode::Plugins::Base
        #   def on_define_module(indexer, definition)
        #     definition.ignored! if definition.name == "Foo"
        #   end
        # end
        # ~~~
        sig { params(indexer: Indexer, definition: Definition).void }
        def on_define_module(indexer, definition)
          # no-op
        end

        # Called when a send is being processed
        #
        # ~~~rb
        # class MyPlugin < Spoom::Deadcode::Plugins::Base
        #   def on_send(indexer, send)
        #     return unless send.name == "dsl_method"
        #     return if send.args.empty?
        #
        #     method_name = indexer.node_string(send.args.first).delete_prefix(":")
        #     indexer.reference_method(method_name, send.node)
        #   end
        # end
        # ~~~
        sig { params(indexer: Indexer, send: Send).void }
        def on_send(indexer, send)
          # no-op
        end

        private

        sig { params(name: String).returns(T::Boolean) }
        def ignored_method_name?(name)
          ignored_name?(name, :@ignored_method_names, :@ignored_method_patterns)
        end

        sig { params(const: Symbol).returns(T::Set[String]) }
        def names(const)
          self.class.instance_variable_get(const) || Set.new
        end

        sig { params(name: String, names_variable: Symbol, patterns_variable: Symbol).returns(T::Boolean) }
        def ignored_name?(name, names_variable, patterns_variable)
          names(names_variable).include?(name) || patterns(patterns_variable).any? { |pattern| pattern.match?(name) }
        end

        sig { params(const: Symbol).returns(T::Array[Regexp]) }
        def patterns(const)
          self.class.instance_variable_get(const) || []
        end

        sig { params(indexer: Indexer, send: Send).void }
        def reference_send_first_symbol_as_method(indexer, send)
          first_arg = send.args.first
          return unless first_arg.is_a?(SyntaxTree::SymbolLiteral)

          name = indexer.node_string(first_arg.value)
          indexer.reference_method(name, send.node)
        end
      end
    end
  end
end