# 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] 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::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, 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] objects # the list of objects to post-process. # # @return [CodeObjects::Base, Array] # 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] 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