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