lib/yard/handlers/base.rb in yard-0.9.16 vs lib/yard/handlers/base.rb in yard-0.9.17

- old
+ new

@@ -1,595 +1,595 @@ -# 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 +# 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