lib/yard/code_objects/base.rb in yard-0.2.2 vs lib/yard/code_objects/base.rb in yard-0.2.3
- old
+ new
@@ -15,78 +15,127 @@
self
end
alias_method :<<, :push
end
- NSEP = '::'
- ISEP = '#'
+ NSEPQ = NSEP = '::'
+ ISEPQ = ISEP = '#'
+ CSEP = '.'
+ CSEPQ = Regexp.quote CSEP
CONSTANTMATCH = /[A-Z]\w*/
- NAMESPACEMATCH = /(?:(?:#{Regexp.quote NSEP})?#{CONSTANTMATCH})+/
- METHODNAMEMATCH = /[a-zA-Z_]\w*[!?]?|[-+~]\@|<<|>>|=~|===?|[<>]=?|\*\*|[-\/+%^&*~`|]|\[\]=?/
- METHODMATCH = /(?:(?:#{NAMESPACEMATCH}|self)\s*(?:\.|#{Regexp.quote NSEP})\s*)?#{METHODNAMEMATCH}/
+ NAMESPACEMATCH = /(?:(?:#{NSEPQ})?#{CONSTANTMATCH})+/
+ METHODNAMEMATCH = /[a-zA-Z_]\w*[!?=]?|[-+~]\@|<<|>>|=~|===?|[<>]=?|\*\*|[-\/+%^&*~`|]|\[\]=?/
+ METHODMATCH = /(?:(?:#{NAMESPACEMATCH}|self)\s*(?:#{CSEPQ}|#{NSEPQ})\s*)?#{METHODNAMEMATCH}/
BUILTIN_EXCEPTIONS = ["SecurityError", "Exception", "NoMethodError", "FloatDomainError",
"IOError", "TypeError", "NotImplementedError", "SystemExit", "Interrupt", "SyntaxError",
"RangeError", "NoMemoryError", "ArgumentError", "ThreadError", "EOFError", "RuntimeError",
"ZeroDivisionError", "StandardError", "LoadError", "NameError", "LocalJumpError", "SystemCallError",
"SignalException", "ScriptError", "SystemStackError", "RegexpError", "IndexError"]
+ # Note: MatchingData is a 1.8.x legacy class
BUILTIN_CLASSES = ["TrueClass", "Array", "Dir", "Struct", "UnboundMethod", "Object", "Fixnum", "Float",
- "ThreadGroup", "MatchData", "Proc", "Binding", "Class", "Time", "Bignum", "NilClass", "Symbol",
- "Numeric", "String", "Data", "MatchingData", "Regexp", "Integer", "File", "IO", "Range", "FalseClass",
+ "ThreadGroup", "MatchingData", "MatchData", "Proc", "Binding", "Class", "Time", "Bignum", "NilClass", "Symbol",
+ "Numeric", "String", "Data", "MatchData", "Regexp", "Integer", "File", "IO", "Range", "FalseClass",
"Method", "Continuation", "Thread", "Hash", "Module"] + BUILTIN_EXCEPTIONS
BUILTIN_MODULES = ["ObjectSpace", "Signal", "Marshal", "Kernel", "Process", "GC", "FileTest", "Enumerable",
- "Comparable", "Errno", "Precision", "Math", "DTracer"]
+ "Comparable", "Errno", "Precision", "Math"]
BUILTIN_ALL = BUILTIN_CLASSES + BUILTIN_MODULES
BUILTIN_EXCEPTIONS_HASH = BUILTIN_EXCEPTIONS.inject({}) {|h,n| h.update(n => true) }
class Base
- attr_reader :name
- attr_accessor :namespace
- attr_accessor :source, :signature, :file, :line, :docstring, :dynamic
+ attr_reader :name, :files
+ attr_accessor :namespace, :source, :signature, :docstring, :dynamic
def dynamic?; @dynamic end
class << self
def new(namespace, name, *args, &block)
- if name =~ /(?:#{NSEP}|#{ISEP})([^#{NSEP}#{ISEP}]+)$/
+ if name.to_s[0,2] == NSEP
+ name = name.to_s[2..-1]
+ namespace = Registry.root
+ elsif name =~ /(?:#{NSEPQ}|#{ISEPQ}|#{CSEPQ})([^#{NSEPQ}#{ISEPQ}#{CSEPQ}]+)$/
return new(Proxy.new(namespace, $`), $1, *args, &block)
end
keyname = namespace && namespace.respond_to?(:path) ? namespace.path : ''
if self == RootObject
keyname = :root
+ elsif self == MethodObject
+ keyname += (args.first && args.first.to_sym == :class ? CSEP : ISEP) + name.to_s
elsif keyname.empty?
keyname = name.to_s
- elsif self == MethodObject
- keyname += (!args.first || args.first.to_sym == :instance ? ISEP : NSEP) + name.to_s
else
keyname += NSEP + name.to_s
end
- if self != RootObject && obj = Registry[keyname]
+ obj = Registry.objects[keyname]
+ obj = nil if obj && obj.class != self
+
+ if self != RootObject && obj
yield(obj) if block_given?
obj
else
Registry.objects[keyname] = super(namespace, name, *args, &block)
end
end
+
+ def ===(other)
+ self >= other.class ? true : false
+ end
end
def initialize(namespace, name, *args)
if namespace && namespace != :root &&
!namespace.is_a?(NamespaceObject) && !namespace.is_a?(Proxy)
raise ArgumentError, "Invalid namespace object: #{namespace}"
end
+ @files = []
+ @current_file_has_comments = false
@name = name.to_sym
@tags = []
- @docstring = ""
+ @docstring = Docstring.new('', self)
self.namespace = namespace
yield(self) if block_given?
end
+ # Associates a file with a code object, optionally adding the line where it was defined.
+ # By convention, '<STDIN>' should be used to associate code that comes form standard input.
+ #
+ # @param [String] file the filename ('<STDIN>' for standard input)
+ # @param [Fixnum, nil] the line number where the object lies in the file
+ # @param [Boolean] whether or not the definition has comments associated. This
+ # will allow {#file} to return the definition where the comments were made instead
+ # of any empty definitions that might have been parsed before (module namespaces for instance).
+ def add_file(file, line = nil, has_comments = false)
+ raise(ArgumentError, "file cannot be nil or empty") if file.nil? || file == ''
+ obj = [file.to_s, line]
+ if has_comments && !@current_file_has_comments
+ @current_file_has_comments = true
+ @files.unshift(obj)
+ else
+ @files << obj # back of the line
+ end
+ end
+
+ # Returns the filename the object was first parsed at, taking
+ # definitions with docstrings first.
+ #
+ # @return [String] a filename
+ def file
+ @files.first ? @files.first[0] : nil
+ end
+
+ # Returns the line the object was first parsed at (or nil)
+ #
+ # @return [Fixnum] the line where the object was first defined.
+ # @return [nil] if there is no line associated with the object
+ def line
+ @files.first ? @files.first[1] : nil
+ end
+
def ==(other)
if other.is_a?(Proxy)
path == other.path
else
super
@@ -109,11 +158,11 @@
end
end
def method_missing(meth, *args, &block)
if meth.to_s =~ /=$/
- self[meth.to_s[0..-2]] = *args
+ self[meth.to_s[0..-2]] = args.first
elsif instance_variable_get("@#{meth}")
self[meth]
else
super
end
@@ -124,55 +173,48 @@
#
# @param [Parser::Statement, String] statement
# the +Parser::Statement+ holding the source code or the raw source
# as a +String+ for the definition of the code object only (not the block)
def source=(statement)
- if statement.is_a? Parser::Statement
+ if statement.is_a? Parser::Ruby::Legacy::Statement
src = statement.tokens.to_s
blk = statement.block ? statement.block.to_s : ""
if src =~ /^def\s.*[^\)]$/ && blk[0,1] !~ /\r|\n/
blk = ";" + blk
end
@source = format_source(src + blk)
self.line = statement.tokens.first.line_no
self.signature = src
+ elsif statement.respond_to?(:source)
+ self.line = statement.line
+ self.signature = statement.first_line
+ @source = format_source(statement.source.strip)
else
@source = format_source(statement.to_s)
end
end
##
# Attaches a docstring to a code oject by parsing the comments attached to the statement
# and filling the {#tags} and {#docstring} methods with the parsed information.
#
- # @param [String, Array<String>] comments
+ # @param [String, Array<String>, Docstring] comments
# the comments attached to the code object to be parsed
# into a docstring and meta tags.
def docstring=(comments)
- @short_docstring = nil
- parse_comments(comments) if comments
+ @docstring = Docstring === comments ? comments : Docstring.new(comments, self)
end
##
- # Gets the first line of a docstring to the period or the first paragraph.
- #
- # @return [String] The first line or paragraph of the docstring; always ends with a period.
- def short_docstring
- @short_docstring ||= (docstring.split(/\.|\r?\n\r?\n/).first || '')
- @short_docstring += '.' unless @short_docstring.empty?
- @short_docstring
- end
-
- ##
# Default type is the lowercase class name without the "Object" suffix
#
# Override this method to provide a custom object type
#
# @return [Symbol] the type of code object this represents
def type
- self.class.name.split(/#{NSEP}/).last.gsub(/Object$/, '').downcase.to_sym
+ self.class.name.split(/#{NSEPQ}/).last.gsub(/Object$/, '').downcase.to_sym
end
def path
if parent && parent != Registry.root
[parent.path, name.to_s].join(sep)
@@ -201,121 +243,22 @@
end
alias_method :parent, :namespace
alias_method :parent=, :namespace=
- ##
- # Convenience method to return the first tag
- # object in the list of tag objects of that name
- #
- # Example:
- # doc = YARD::Documentation.new("@return zero when nil")
- # doc.tag("return").text # => "zero when nil"
- #
- # @param [#to_s] name the tag name to return data for
- # @return [Tags::Tag] the first tag in the list of {#tags}
- def tag(name)
- @tags.find {|tag| tag.tag_name.to_s == name.to_s }
- end
+ def tag(name); @docstring.tag(name) end
+ def tags(name = nil); @docstring.tags(name) end
+ def has_tag?(name); @docstring.has_tag?(name) end
- ##
- # Returns a list of tags specified by +name+ or all tags if +name+ is not specified.
- #
- # @param name the tag name to return data for, or nil for all tags
- # @return [Array<Tags::Tag>] the list of tags by the specified tag name
- def tags(name = nil)
- return @tags if name.nil?
- @tags.select {|tag| tag.tag_name.to_s == name.to_s }
- end
-
- ##
- # Returns true if at least one tag by the name +name+ was declared
- #
- # @param [String] name the tag name to search for
- # @return [Boolean] whether or not the tag +name+ was declared
- def has_tag?(name)
- @tags.any? {|tag| tag.tag_name.to_s == name.to_s }
- end
-
protected
def sep; NSEP end
- private
-
- ##
- # Parses out comments split by newlines into a new code object
- #
- # @param [Array<String>, String] comments
- # the newline delimited array of comments. If the comments
- # are passed as a String, they will be split by newlines.
- def parse_comments(comments)
- return if comments.empty?
- meta_match = /^@(\S+)\s*(.*)/
- comments = comments.split(/\r?\n/) if comments.is_a? String
- @tags, @docstring = [], ""
-
- indent, last_indent = comments.first[/^\s*/].length, 0
- orig_indent = 0
- last_line = ""
- tag_name, tag_klass, tag_buf, raw_buf = nil, nil, "", []
-
- (comments+['']).each_with_index do |line, index|
- indent = line[/^\s*/].length
- empty = (line =~ /^\s*$/ ? true : false)
- done = comments.size == index
-
- if tag_name && (((indent < orig_indent && !empty) || done) ||
- (indent <= last_indent && line =~ meta_match))
- tagfactory = Tags::Library.new
- tag_method = "#{tag_name}_tag"
- if tag_name && tagfactory.respond_to?(tag_method)
- if tagfactory.method(tag_method).arity == 2
- @tags << tagfactory.send(tag_method, tag_buf, raw_buf.join("\n"))
- else
- @tags << tagfactory.send(tag_method, tag_buf)
- end
- else
- log.warn "Unknown tag @#{tag_name} in documentation for `#{path}`"
- end
- tag_name, tag_buf, raw_buf = nil, '', []
- orig_indent = 0
- end
-
- # Found a meta tag
- if line =~ meta_match
- orig_indent = indent
- tag_name, tag_buf = $1, $2
- raw_buf = [tag_buf.dup]
- elsif tag_name && indent >= orig_indent && !empty
- # Extra data added to the tag on the next line
- last_empty = last_line =~ /^[ \t]*$/ ? true : false
-
- if last_empty
- tag_buf << "\n\n"
- raw_buf << ''
- end
-
- tag_buf << line.gsub(/^[ \t]{#{indent}}/, last_empty ? '' : ' ')
- raw_buf << line.gsub(/^[ \t]{#{orig_indent}}/, '')
- elsif !tag_name
- # Regular docstring text
- @docstring << line << "\n"
- end
-
- last_indent = indent
- last_line = line
- end
-
- # Remove trailing/leading whitespace / newlines
- @docstring.gsub!(/\A[\r\n\s]+|[\r\n\s]+\Z/, '')
- end
-
# Formats source code by removing leading indentation
def format_source(source)
source.chomp!
indent = source.split(/\r?\n/).last[/^([ \t]*)/, 1].length
source.gsub(/^[ \t]{#{indent}}/, '')
end
end
end
-end
\ No newline at end of file
+end