lib/yard/handlers/base.rb



# frozen_string_literal: true
module YARD
  module Handlers
    # Raise this error when a handler should exit before completing.
    # The exception will be silenced, allowing the next handler(s) in the
    # queue to be executed.
    # @since 0.8.4
    class HandlerAborted < ::RuntimeError; end

    # Raised during processing phase when a handler needs to perform
    # an operation on an object's namespace but the namespace could
    # not be resolved.
    class NamespaceMissingError < Parser::UndocumentableError
      # The object the error occurred on
      # @return [CodeObjects::Base] a code object
      attr_accessor :object

      def initialize(object) @object = object end
    end

    # Handlers are pluggable semantic parsers for YARD's code generation
    # phase. They allow developers to control what information gets
    # generated by YARD, giving them the ability to, for instance, document
    # any Ruby DSLs that a customized framework may use. A good example
    # of this would be the ability to document and generate meta data for
    # the 'describe' declaration of the RSpec testing framework by simply
    # adding a handler for such a keyword. Similarly, any Ruby API that
    # takes advantage of class level declarations could add these to the
    # documentation in a very explicit format by treating them as first-
    # class objects in any outputted documentation.
    #
    # == Overview of a Typical Handler Scenario
    #
    # Generally, a handler class will declare a set of statements which
    # it will handle using the {handles} class declaration. It will then
    # implement the {#process} method to do the work. The processing would
    # usually involve the manipulation of the {#namespace}, {#owner}
    # {CodeObjects::Base code objects} or the creation of new ones, in
    # which case they should be registered by {#register}, a method that
    # sets some basic attributes for the new objects.
    #
    # Handlers are usually simple and take up to a page of code to process
    # and register a new object or add new attributes to the current +namespace+.
    #
    # == Setting up a Handler for Use
    #
    # A Handler is automatically registered when it is subclassed from the
    # base class. The only other thing that needs to be done is to specify
    # which statement the handler will process. This is done with the +handles+
    # declaration, taking either a {Parser::Ruby::Legacy::RubyToken}, {String} or `Regexp`.
    # Here is a simple example which processes module statements.
    #
    #   class MyModuleHandler < YARD::Handlers::Base
    #     handles TkMODULE
    #
    #     def process
    #       # do something
    #     end
    #   end
    #
    # == Processing Handler Data
    #
    # The goal of a specific handler is really up to the developer, and as
    # such there is no real guideline on how to process the data. However,
    # it is important to know where the data is coming from to be able to use
    # it.
    #
    # === +statement+ Attribute
    #
    # The +statement+ attribute pertains to the {Parser::Ruby::Legacy::Statement} object
    # containing a set of tokens parsed in by the parser. This is the main set
    # of data to be analyzed and processed. The comments attached to the statement
    # can be accessed by the {Parser::Ruby::Legacy::Statement#comments} method, but generally
    # the data to be processed will live in the +tokens+ attribute. This list
    # can be converted to a +String+ using +#to_s+ to parse the data with
    # regular expressions (or other text processing mechanisms), if needed.
    #
    # === +namespace+ Attribute
    #
    # The +namespace+ attribute is a {CodeObjects::NamespaceObject namespace object}
    # which represents the current namespace that the parser is in. For instance:
    #
    #   module SomeModule
    #     class MyClass
    #       def mymethod; end
    #     end
    #   end
    #
    # If a handler was to parse the 'class MyClass' statement, it would
    # be necessary to know that it belonged inside the SomeModule module.
    # This is the value that +namespace+ would return when processing such
    # a statement. If the class was then entered and another handler was
    # called on the method, the +namespace+ would be set to the 'MyClass'
    # code object.
    #
    # === +owner+ Attribute
    #
    # The +owner+ attribute is similar to the +namespace+ attribute in that
    # it also follows the scope of the code during parsing. However, a namespace
    # object is loosely defined as a module or class and YARD has the ability
    # to parse beyond module and class blocks (inside methods, for instance),
    # so the +owner+ attribute would not be limited to modules and classes.
    #
    # To put this into context, the example from above will be used. If a method
    # handler was added to the mix and decided to parse inside the method body,
    # the +owner+ would be set to the method object but the namespace would remain
    # set to the class. This would allow the developer to process any method
    # definitions set inside a method (def x; def y; 2 end end) by adding them
    # to the correct namespace (the class, not the method).
    #
    # In summary, the distinction between +namespace+ and +owner+ can be thought
    # of as the difference between first-class Ruby objects (namespaces) and
    # second-class Ruby objects (methods).
    #
    # === +visibility+ and +scope+ Attributes
    #
    # Mainly needed for parsing methods, the +visibility+ and +scope+ attributes
    # refer to the public/protected/private and class/instance values (respectively)
    # of the current parsing position.
    #
    # == Parsing Blocks in Statements
    #
    # In addition to parsing a statement and creating new objects, some
    # handlers may wish to continue parsing the code inside the statement's
    # block (if there is one). In this context, a block means the inside
    # of any statement, be it class definition, module definition, if
    # statement or classic 'Ruby block'.
    #
    # For example, a class statement would be "class MyClass" and the block
    # would be a list of statements including the method definitions inside
    # the class. For a class handler, the programmer would execute the
    # {#parse_block} method to continue parsing code inside the block, with
    # the +namespace+ now pointing to the class object the handler created.
    #
    # YARD has the ability to continue into any block: class, module, method,
    # even if statements. For this reason, the block parsing method must be
    # invoked explicitly out of efficiency sake.
    #
    # @abstract Subclass this class to provide a handler for YARD to use
    #   during the processing phase.
    #
    # @see CodeObjects::Base
    # @see CodeObjects::NamespaceObject
    # @see handles
    # @see #namespace
    # @see #owner
    # @see #register
    # @see #parse_block
    class Base
      # For accessing convenience, eg. "MethodObject"
      # instead of the full qualified namespace
      include YARD::CodeObjects

      include Parser

      class << self
        # Clear all registered subclasses. Testing purposes only
        # @return [void]
        def clear_subclasses
          @@subclasses = []
        end

        # Returns all registered handler subclasses.
        # @return [Array<Base>] a list of handlers
        def subclasses
          @@subclasses ||= []
        end

        def inherited(subclass)
          @@subclasses ||= []
          @@subclasses << subclass
        end

        # Declares the statement type which will be processed
        # by this handler.
        #
        # A match need not be unique to a handler. Multiple
        # handlers can process the same statement. However,
        # in this case, care should be taken to make sure that
        # {#parse_block} would only be executed by one of
        # the handlers, otherwise the same code will be parsed
        # multiple times and slow YARD down.
        #
        # @param [Parser::Ruby::Legacy::RubyToken, Symbol, String, Regexp] matches
        #   statements that match the declaration will be
        #   processed by this handler. A {String} match is
        #   equivalent to a +/\Astring/+ regular expression
        #   (match from the beginning of the line), and all
        #   token matches match only the first token of the
        #   statement.
        #
        def handles(*matches)
          (@handlers ||= []).concat(matches)
        end

        # This class is implemented by {Ruby::Base} and {Ruby::Legacy::Base}.
        # To implement a base handler class for another language, implement
        # this method to return true if the handler should process the given
        # statement object. Use {handlers} to enumerate the matchers declared
        # for the handler class.
        #
        # @param statement a statement object or node (depends on language type)
        # @return [Boolean] whether or not this handler object should process
        #   the given statement
        def handles?(statement) # rubocop:disable Lint/UnusedMethodArgument
          raise NotImplementedError, "override #handles? in a subclass"
        end

        # @return [Array] a list of matchers for the handler object.
        # @see handles?
        def handlers
          @handlers ||= []
        end

        # Declares that the handler should only be called when inside a
        # {CodeObjects::NamespaceObject}, not a method body.
        #
        # @return [void]
        def namespace_only
          @namespace_only = true
        end

        # @return [Boolean] whether the handler should only be processed inside
        #   a namespace.
        def namespace_only?
          @namespace_only ||= false
        end

        # Declares that a handler should only be called when inside a filename
        # by its basename or a regex match for the full path.
        #
        # @param [String, Regexp] filename a matching filename or regex
        # @return [void]
        # @since 0.6.2
        def in_file(filename)
          (@in_files ||= []) << filename
        end

        # @return [Boolean] whether the filename matches the declared file
        #   match for a handler. If no file match is specified, returns true.
        # @since 0.6.2
        def matches_file?(filename)
          @in_files ||= nil # avoid ruby warnings
          return true unless @in_files
          @in_files.any? do |in_file|
            case in_file
            when String
              File.basename(filename) == in_file
            when Regexp
              filename =~ in_file
            else
              true
            end
          end
        end

        # Generates a +process+ method, equivalent to +def process; ... end+.
        # Blocks defined with this syntax will be wrapped inside an anonymous
        # module so that the handler class can be extended with mixins that
        # override the +process+ method without alias chaining.
        #
        # @!macro yard.handlers.process
        #   @!method process
        #   Main processing callback
        #   @return [void]
        # @see #process
        # @return [void]
        # @since 0.5.4
        def process(&block)
          mod = Module.new
          mod.send(:define_method, :process, &block)
          include mod
        end
      end

      def initialize(source_parser, stmt)
        @parser = source_parser
        @statement = stmt
      end

      # The main handler method called by the parser on a statement
      # that matches the {handles} declaration.
      #
      # Subclasses should override this method to provide the handling
      # functionality for the class.
      #
      # @return [Array<CodeObjects::Base>, CodeObjects::Base, Object]
      #   If this method returns a code object (or a list of them),
      #   they are passed to the +#register+ method which adds basic
      #   attributes. It is not necessary to return any objects and in
      #   some cases you may want to explicitly avoid the returning of
      #   any objects for post-processing by the register method.
      #
      # @see handles
      # @see #register
      #
      def process
        raise NotImplementedError, "#{self} did not implement a #process method for handling."
      end

      # Parses the semantic "block" contained in the statement node.
      #
      # @abstract Subclasses should call {Processor#process parser.process}
      def parse_block(*)
        raise NotImplementedError, "#{self} did not implement a #parse_block method for handling"
      end

      # @return [Processor] the processor object that manages all global state
      #   during handling.
      attr_reader :parser

      # @return [Object] the statement object currently being processed. Usually
      #   refers to one semantic language statement, though the strict definition
      #   depends on the parser used.
      attr_reader :statement

      # (see Processor#owner)
      attr_accessor :owner

      # (see Processor#namespace)
      attr_accessor :namespace

      # (see Processor#visibility)
      attr_accessor :visibility

      # (see Processor#scope)
      attr_accessor :scope

      # (see Processor#globals)
      attr_reader :globals

      # (see Processor#extra_state)
      attr_reader :extra_state

      undef owner, owner=, namespace, namespace=
      undef visibility, visibility=, scope, scope=
      undef globals, extra_state

      def owner; parser.owner end
      def owner=(v) parser.owner = v end
      def namespace; parser.namespace end
      def namespace=(v); parser.namespace = v end
      def visibility; parser.visibility end
      def visibility=(v); parser.visibility = v end
      def scope; parser.scope end
      def scope=(v); parser.scope = v end
      def globals; parser.globals end
      def extra_state; parser.extra_state end

      # Aborts a handler by raising {Handlers::HandlerAborted}.
      # An exception will only be logged in debugging mode for
      # this kind of handler exit.
      #
      # @since 0.8.4
      def abort!
        raise Handlers::HandlerAborted
      end

      # Executes a given block with specific state values for {#owner},
      # {#namespace} and {#scope}.
      #
      # @option opts [CodeObjects::NamespaceObject] :namespace (value of #namespace)
      #   the namespace object that {#namespace} will be equal to for the
      #   duration of the block.
      # @option opts [Symbol] :scope (:instance)
      #   the scope for the duration of the block.
      # @option opts [CodeObjects::Base] :owner (value of #owner)
      #   the owner object (method) for the duration of the block
      # @yield a block to execute with the given state values.
      def push_state(opts = {})
        opts = {
          :namespace => namespace,
          :scope => :instance,
          :owner => owner || namespace,
          :visibility => nil
        }.update(opts)

        ns = namespace
        vis = visibility
        sc = scope
        oo = owner
        self.namespace = opts[:namespace]
        self.visibility = opts[:visibility] || :public
        self.scope = opts[:scope]
        self.owner = opts[:owner]

        yield

        self.namespace = ns
        self.visibility = vis
        self.scope = sc
        self.owner = oo
      end

      # Do some post processing on a list of code objects.
      # Adds basic attributes to the list of objects like
      # the filename, line number, {CodeObjects::Base#dynamic},
      # source code and {CodeObjects::Base#docstring},
      # but only if they don't exist.
      #
      # @param [Array<CodeObjects::Base>] objects
      #   the list of objects to post-process.
      #
      # @return [CodeObjects::Base, Array<CodeObjects::Base>]
      #   returns whatever is passed in, for chainability.
      #
      def register(*objects)
        objects.flatten.each do |object|
          next unless object.is_a?(CodeObjects::Base)
          register_ensure_loaded(object)
          yield(object) if block_given?
          register_file_info(object)
          register_source(object)
          register_visibility(object)
          register_docstring(object)
          register_group(object)
          register_dynamic(object)
          register_module_function(object)
        end
        objects.size == 1 ? objects.first : objects
      end

      # Ensures that the object's namespace is loaded before attaching it
      # to the namespace.
      #
      # @param [CodeObjects::Base] object the object to register
      # @return [void]
      # @since 0.8.0
      def register_ensure_loaded(object)
        ensure_loaded!(object.namespace)
        object.namespace.children << object
      rescue NamespaceMissingError
        nil # noop
      end

      # Registers the file/line of the declaration with the object
      #
      # @param [CodeObjects::Base] object the object to register
      # @return [void]
      # @since 0.8.0
      def register_file_info(object, file = parser.file, line = statement.line, comments = statement.comments)
        object.add_file(file, line, comments)
      end

      # Registers any docstring found for the object and expands macros
      #
      # @param [CodeObjects::Base] object the object to register
      # @return [void]
      # @since 0.8.0
      def register_docstring(object, docstring = statement.comments, stmt = statement)
        docstring = docstring.join("\n") if Array === docstring
        parser = Docstring.parser
        parser.parse(docstring || "", object, self)

        if object && docstring
          object.docstring = parser.to_docstring

          # Add hash_flag/line_range
          if stmt
            object.docstring.hash_flag = stmt.comments_hash_flag
            object.docstring.line_range = stmt.comments_range
          end
        end

        register_transitive_tags(object)
      end

      # Registers the object as being inside a specific group
      #
      # @param [CodeObjects::Base] object the object to register
      # @return [void]
      # @since 0.8.0
      def register_group(object, group = extra_state.group)
        if group
          unless object.namespace.is_a?(Proxy)
            object.namespace.groups |= [group]
          end
          object.group = group
        end
      end

      # Registers any transitive tags from the namespace on the object
      #
      # @param [CodeObjects::Base, nil] object the object to register
      # @return [void]
      # @since 0.8.0
      def register_transitive_tags(object)
        return unless object && !object.namespace.is_a?(Proxy)
        Tags::Library.transitive_tags.each do |tag|
          next unless object.namespace.has_tag?(tag)
          next if object.has_tag?(tag)
          object.add_tag(*object.namespace.tags(tag))
        end
      end

      # @param [CodeObjects::Base] object the object to register
      # @return [void]
      # @since 0.8.0
      def register_source(object, source = statement, type = parser.parser_type)
        return unless object.is_a?(MethodObject)
        object.source ||= source
        object.source_type = type
      end

      # Registers visibility on a method object. If the object does not
      # respond to setting visibility, nothing is done.
      #
      # @param [#visibility=] object the object to register
      # @param [Symbol] visibility the visibility to set on the object
      # @since 0.8.0
      def register_visibility(object, visibility = self.visibility)
        return unless object.respond_to?(:visibility=)
        return if object.is_a?(NamespaceObject)
        object.visibility = visibility
      end

      # Registers the same method information on the module function, if
      # the object was defined as a module function.
      #
      # @param [CodeObjects::Base] object the possible module function object
      #   to copy data for
      # @since 0.8.0
      def register_module_function(object)
        return unless object.is_a?(MethodObject)
        return unless object.module_function?
        modobj = MethodObject.new(object.namespace, object.name)
        object.copy_to(modobj)
        modobj.visibility = :private # rubocop:disable Lint/UselessSetterCall
      end

      # Registers the object as dynamic if the object is defined inside
      # a method or block (owner != namespace)
      #
      # @param [CodeObjects::Base] object the object to register
      # @return [void]
      # @since 0.8.0
      def register_dynamic(object)
        object.dynamic = true if owner != namespace
      end

      # Ensures that a specific +object+ has been parsed and loaded into the
      # registry. This is necessary when adding data to a namespace, for instance,
      # since the namespace may not have been processed yet (it can be located
      # in a file that has not been handled).
      #
      # Calling this method defers the handler until all other files have been
      # processed. If the object gets resolved, the rest of the handler continues,
      # otherwise an exception is raised.
      #
      # @example Adding a mixin to the String class programmatically
      #   ensure_loaded! P('String')
      #   # "String" is now guaranteed to be loaded
      #   P('String').mixins << P('MyMixin')
      #
      # @param [Proxy, CodeObjects::Base] object the object to resolve.
      # @param [Integer] max_retries the number of times to defer the handler
      #   before raising a +NamespaceMissingError+.
      # @raise [NamespaceMissingError] if the object is not resolved within
      #   +max_retries+ attempts, this exception is raised and the handler
      #   finishes processing.
      def ensure_loaded!(object, max_retries = 1)
        return if object.root?
        return object unless object.is_a?(Proxy)

        retries = 0
        while object.is_a?(Proxy)
          raise NamespaceMissingError, object if retries > max_retries
          log.debug "Missing object #{object} in file `#{parser.file}', moving it to the back of the line."
          parser.parse_remaining_files
          retries += 1
        end
        object
      end

      # @group Macro Support

      # @abstract Implement this method to return the parameters in a method call
      #   statement. It should return an empty list if the statement is not a
      #   method call.
      # @return [Array<String>] a list of argument names
      def call_params
        raise NotImplementedError
      end

      # @abstract Implement this method to return the method being called in
      #   a method call. It should return nil if the statement is not a method
      #   call.
      # @return [String] the method name being called
      # @return [nil] if the statement is not a method call
      def caller_method
        raise NotImplementedError
      end
    end
  end
end