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