lib/yard/handlers/base.rb in yard-0.2.2 vs lib/yard/handlers/base.rb in yard-0.2.3

- old
+ new

@@ -1,8 +1,11 @@ module YARD module Handlers - class UndocumentableError < Exception; end + class NamespaceMissingError < Parser::UndocumentableError + attr_accessor :object + def initialize(object) @object = object end + end # = Handlers # # Handlers are pluggable semantic parsers for YARD's code generation # phase. They allow developers to control what information gets @@ -31,11 +34,11 @@ # == 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::RubyToken}, {String} or {Regexp}. + # 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 # @@ -51,14 +54,14 @@ # 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::Statement} object + # 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::Statement#comments} method, but generally + # 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 @@ -129,26 +132,23 @@ # @see #owner # @see #register # @see #parse_block # class Base - attr_accessor :__context__ - # For accessing convenience, eg. "MethodObject" # instead of the full qualified namespace include YARD::CodeObjects - # For tokens like TkDEF, TkCLASS, etc. - include YARD::Parser::RubyToken + include Parser class << self def clear_subclasses @@subclasses = [] end def subclasses - @@subclasses || [] + @@subclasses ||= [] end def inherited(subclass) @@subclasses ||= [] @@subclasses << subclass @@ -162,32 +162,37 @@ # 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, String, Regexp] match + # @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(match) - @handler = match + def handles(*matches) + (@handlers ||= []).push(*matches) end - def handles?(tokens) - case @handler - when String - tokens.first.text == @handler - when Regexp - tokens.to_s =~ @handler ? true : false - else - @handler == tokens.first.class - end + def handles?(statement) + raise NotImplementedError, "override #handles? in a subclass" end + + def handlers + @handlers ||= [] + end + + def namespace_only + @namespace_only = true + end + + def namespace_only? + @namespace_only ? true : false + end end def initialize(source_parser, stmt) @parser = source_parser @statement = stmt @@ -211,15 +216,54 @@ # def process raise NotImplementedError, "#{self} did not implement a #process method for handling." end + def parse_block(*args) + raise NotImplementedError, "#{self} did not implement a #parse_block method for handling" + end + protected attr_reader :parser, :statement attr_accessor :owner, :namespace, :visibility, :scope + 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 push_state(opts = {}, &block) + opts = { + :namespace => nil, + :scope => :instance, + :owner => nil + }.update(opts) + + if opts[:namespace] + ns, vis, sc = namespace, visibility, scope + self.namespace = opts[:namespace] + self.visibility = :public + self.scope = opts[:scope] + end + + oldowner, self.owner = self.owner, opts[:owner] ? opts[:owner] : namespace + yield + self.owner = oldowner + + if opts[:namespace] + self.namespace = ns + self.owner = namespace + self.visibility = vis + self.scope = sc + end + 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. @@ -232,278 +276,69 @@ # def register(*objects) objects.flatten.each do |object| next unless object.is_a?(CodeObjects::Base) - ensure_namespace_loaded!(object) + begin + ensure_loaded!(object.namespace) + object.namespace.children << object + rescue NamespaceMissingError + end # Yield the object to the calling block because ruby will parse the syntax # # register obj = ClassObject.new {|o| ... } # # as the block for #register. We need to make sure this gets to the object. yield(object) if block_given? - # Add file and line number, but for class/modules this is - # only done if there is a docstring for this specific definition. - if (object.is_a?(NamespaceObject) && statement.comments) || !object.is_a?(NamespaceObject) - object.file = parser.file - object.line = statement.tokens.first.line_no - elsif object.is_a?(NamespaceObject) && !statement.comments - object.file ||= parser.file - object.line ||= statement.tokens.first.line_no - end - + object.add_file(parser.file, statement.line, statement.comments) + # Add docstring if there is one. object.docstring = statement.comments if statement.comments # Add source only to non-class non-module objects unless object.is_a?(NamespaceObject) - object.source ||= statement + object.source ||= statement end - # Make it dynamic if it's owner is not it's namespace. + # Make it dynamic if its owner is not its namespace. # This generally means it was defined in a method (or block of some sort) object.dynamic = true if owner != namespace end objects.size == 1 ? objects.first : objects end - - def parse_block(opts = nil) - opts = { - :namespace => nil, - :scope => :instance, - :owner => nil - }.update(opts || {}) - - if opts[:namespace] - ns, vis, sc = namespace, visibility, scope - self.namespace = opts[:namespace] - self.visibility = :public - self.scope = opts[:scope] - end - self.owner = opts[:owner] ? opts[:owner] : namespace - parser.parse(statement.block) if statement.block - - if opts[:namespace] - self.namespace = ns - self.owner = namespace - self.visibility = vis - self.scope = sc - end - end - - 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 ensure_namespace_loaded!(object, max_retries = 1) + def ensure_loaded!(object, max_retries = 1) unless parser.load_order_errors - return object.parent.is_a?(Proxy) ? load_order_warn(object.parent) : nil + if object.is_a?(Proxy) + raise NamespaceMissingError, object + else + nil + end end - raise NotImplementedError if RUBY_PLATFORM =~ /java/ - return unless object.parent.is_a?(Proxy) + if RUBY_PLATFORM =~ /java/ + log.warn "JRuby does not implement Kernel#callcc and cannot load files in order. You must specify the correct order manually." + raise NamespaceMissingError, object + end retries, context = 0, nil callcc {|c| context = c } retries += 1 - if object.parent.is_a?(Proxy) + if object.is_a?(Proxy) if retries <= max_retries log.debug "Missing object #{object.parent} in file `#{parser.file}', moving it to the back of the line." raise Parser::LoadOrderError, context + else + raise NamespaceMissingError, object end - - if retries > max_retries && !object.parent.is_a?(Proxy) && !BUILTIN_ALL.include?(object.path) - load_order_warn(object.parent) - end else log.debug "Object #{object} successfully resolved. Adding item to #{object.parent}'s children" - object.namespace.children << object end - - rescue NotImplementedError - log.warn "JRuby does not implement Kernel#callcc and cannot load files in order. You must specify the correct order manually." - load_order_warn(object.parent) - end - - def load_order_warn(object) - log.warn "The #{object.type} #{object.path} has not yet been recognized." - log.warn "If this class/method is part of your source tree, this will affect your documentation results." - log.warn "You can correct this issue by loading the source file for this object before `#{parser.file}'" - log.warn - end - - # The string value of a token. For example, the return value for the symbol :sym - # would be :sym. The return value for a string "foo #{bar}" would be the literal - # "foo #{bar}" without any interpolation. The return value of the identifier - # 'test' would be the same value: 'test'. Here is a list of common types and - # their return values: - # - # @example - # tokval(TokenList.new('"foo"').first) => "foo" - # tokval(TokenList.new(':foo').first) => :foo - # tokval(TokenList.new('CONSTANT').first, RubyToken::TkId) => "CONSTANT" - # tokval(TokenList.new('identifier').first, RubyToken::TkId) => "identifier" - # tokval(TokenList.new('3.25').first) => 3.25 - # tokval(TokenList.new('/xyz/i').first) => /xyz/i - # - # @param [Token] token The token of the class - # - # @param [Array<Class<Token>>, Symbol] accepted_types - # The allowed token types that this token can be. Defaults to [{TkVal}]. - # A list of types would be, for example, [{TkSTRING}, {TkSYMBOL}], to return - # the token's value if it is either of those types. If +TkVal+ is accepted, - # +TkNode+ is also accepted. - # - # Certain symbol keys are allowed to specify multiple types in one fell swoop. - # These symbols are: - # :string => +TkSTRING+, +TkDSTRING+, +TkDXSTRING+ and +TkXSTRING+ - # :attr => +TkSYMBOL+ and +TkSTRING+ - # :identifier => +TkIDENTIFIER, +TkFID+ and +TkGVAR+. - # :number => +TkFLOAT+, +TkINTEGER+ - # - # @return [Object] if the token is one of the accepted types, in its real value form. - # It should be noted that identifiers and constants are kept in String form. - # @return [nil] if the token is not any of the specified accepted types - def tokval(token, *accepted_types) - accepted_types = [TkVal] if accepted_types.empty? - accepted_types.push(TkNode) if accepted_types.include? TkVal - - if accepted_types.include?(:attr) - accepted_types.push(TkSTRING, TkSYMBOL) - end - - if accepted_types.include?(:string) - accepted_types.push(TkSTRING, TkDSTRING, TkXSTRING, TkDXSTRING) - end - - if accepted_types.include?(:identifier) - accepted_types.push(TkIDENTIFIER, TkFID, TkGVAR) - end - - if accepted_types.include?(:number) - accepted_types.push(TkFLOAT, TkINTEGER) - end - - return unless accepted_types.any? {|t| t === token } - - case token - when TkSTRING, TkDSTRING, TkXSTRING, TkDXSTRING - token.text[1..-2] - when TkSYMBOL - token.text[1..-1].to_sym - when TkFLOAT - token.text.to_f - when TkINTEGER - token.text.to_i - when TkREGEXP - token.text =~ /\A\/(.+)\/([^\/])\Z/ - Regexp.new($1, $2) - when TkTRUE - true - when TkFALSE - false - when TkNIL - nil - else - token.text - end - end - - # Returns a list of symbols or string values from a statement. - # The list must be a valid comma delimited list, and values - # will only be returned to the end of the list only. - # - # Example: - # attr_accessor :a, 'b', :c, :d => ['a', 'b', 'c', 'd'] - # attr_accessor 'a', UNACCEPTED_TYPE, 'c' => ['a', 'c'] - # - # The tokval list of a {TokenList} of the above - # code would be the {#tokval} value of :a, 'b', - # :c and :d. - # - # It should also be noted that this function stops immediately at - # any ruby keyword encountered: - # "attr_accessor :a, :b, :c if x == 5" => ['a', 'b', 'c'] - # - # @param [TokenList] tokenlist The list of tokens to process. - # @param [Array<Class<Token>>] accepted_types passed to {#tokval} - # @return [Array<String>] the list of tokvalues in the list. - # @return [Array<EMPTY>] if there are no symbols or Strings in the list - # @see #tokval - def tokval_list(tokenlist, *accepted_types) - return [] unless tokenlist - out = [[]] - parencount, beforeparen = 0, 0 - needcomma = false - seen_comma = true - tokenlist.each do |token| - tokval = tokval(token, *accepted_types) - parencond = !out.last.empty? && tokval != nil - #puts "#{seen_comma.inspect} #{parencount} #{token.class.class_name} #{out.inspect}" - case token - when TkCOMMA - if parencount == 0 - out << [] unless out.last.empty? - needcomma = false - seen_comma = true - else - out.last << token.text if parencond - end - when TkLPAREN - if seen_comma - beforeparen += 1 - else - parencount += 1 - out.last << token.text if parencond - end - when TkRPAREN - if beforeparen > 0 - beforeparen -= 1 - else - out.last << token.text if parencount > 0 && tokval != nil - parencount -= 1 - end - when TkLBRACE, TkLBRACK, TkDO - parencount += 1 - out.last << token.text if tokval != nil - when TkRBRACE, TkRBRACK, TkEND - out.last << token.text if tokval != nil - parencount -= 1 - else - break if TkKW === token && ![TkTRUE, TkFALSE, TkSUPER, TkSELF, TkNIL].include?(token.class) - - seen_comma = false unless TkWhitespace === token - if parencount == 0 - next if needcomma - next if TkWhitespace === token - if tokval != nil - out.last << tokval - else - out.last.clear - needcomma = true - end - elsif parencond - needcomma = true - out.last << token.text - end - end - - if beforeparen == 0 && parencount < 0 - break - end - end - # Flatten any single element lists - out.map {|e| e.empty? ? nil : (e.size == 1 ? e.pop : e.flatten.join) }.compact + object end end end end \ No newline at end of file