lib/yard/code_objects/base.rb in yard-0.9.18 vs lib/yard/code_objects/base.rb in yard-0.9.19

- old
+ new

@@ -1,615 +1,615 @@ -# frozen_string_literal: true -module YARD - module CodeObjects - # A list of code objects. This array acts like a set (no unique items) - # but also disallows any {Proxy} objects from being added. - class CodeObjectList < Array - # Creates a new object list associated with a namespace - # - # @param [NamespaceObject] owner the namespace the list should be associated with - # @return [CodeObjectList] - def initialize(owner = Registry.root) - @owner = owner - end - - # Adds a new value to the list - # - # @param [Base] value a code object to add - # @return [CodeObjectList] self - def push(value) - value = Proxy.new(@owner, value) if value.is_a?(String) || value.is_a?(Symbol) - if value.is_a?(CodeObjects::Base) || value.is_a?(Proxy) - super(value) unless include?(value) - else - raise ArgumentError, "#{value.class} is not a valid CodeObject" - end - self - end - alias << push - end - - extend NamespaceMapper - - # Namespace separator - NSEP = '::' - - # Regex-quoted namespace separator - NSEPQ = NSEP - - # Instance method separator - ISEP = '#' - - # Regex-quoted instance method separator - ISEPQ = ISEP - - # Class method separator - CSEP = '.' - - # Regex-quoted class method separator - CSEPQ = Regexp.quote CSEP - - # Regular expression to match constant name - CONSTANTMATCH = /[A-Z]\w*/ - - # Regular expression to match the beginning of a constant - CONSTANTSTART = /^[A-Z]/ - - # Regular expression to match namespaces (const A or complex path A::B) - NAMESPACEMATCH = /(?:(?:#{NSEPQ}\s*)?#{CONSTANTMATCH})+/ - - # Regular expression to match a method name - METHODNAMEMATCH = %r{[a-zA-Z_]\w*[!?=]?|[-+~]\@|<<|>>|=~|===?|![=~]?|<=>|[<>]=?|\*\*|[-/+%^&*~`|]|\[\]=?} - - # Regular expression to match a fully qualified method def (self.foo, Class.foo). - METHODMATCH = /(?:(?:#{NAMESPACEMATCH}|[a-z]\w*)\s*(?:#{CSEPQ}|#{NSEPQ})\s*)?#{METHODNAMEMATCH}/ - - # All builtin Ruby exception classes for inheritance tree. - BUILTIN_EXCEPTIONS = ["ArgumentError", "ClosedQueueError", "EncodingError", - "EOFError", "Exception", "FiberError", "FloatDomainError", "IndexError", - "Interrupt", "IOError", "KeyError", "LoadError", "LocalJumpError", - "NameError", "NoMemoryError", "NoMethodError", "NotImplementedError", - "RangeError", "RegexpError", "RuntimeError", "ScriptError", "SecurityError", - "SignalException", "StandardError", "StopIteration", "SyntaxError", - "SystemCallError", "SystemExit", "SystemStackError", "ThreadError", - "TypeError", "UncaughtThrowError", "ZeroDivisionError"] - - # All builtin Ruby classes for inheritance tree. - # @note MatchingData is a 1.8.x legacy class - BUILTIN_CLASSES = ["Array", "Bignum", "Binding", "Class", "Complex", - "ConditionVariable", "Data", "Dir", "Encoding", "Enumerator", "FalseClass", - "Fiber", "File", "Fixnum", "Float", "Hash", "IO", "Integer", "MatchData", - "Method", "Module", "NilClass", "Numeric", "Object", "Proc", "Queue", - "Random", "Range", "Rational", "Regexp", "RubyVM", "SizedQueue", "String", - "Struct", "Symbol", "Thread", "ThreadGroup", "Time", "TracePoint", - "TrueClass", "UnboundMethod"] + BUILTIN_EXCEPTIONS - - # All builtin Ruby modules for mixin handling. - BUILTIN_MODULES = ["Comparable", "Enumerable", "Errno", "FileTest", "GC", - "Kernel", "Marshal", "Math", "ObjectSpace", "Precision", "Process", "Signal"] - - # All builtin Ruby classes and modules. - BUILTIN_ALL = BUILTIN_CLASSES + BUILTIN_MODULES - - # Hash of {BUILTIN_EXCEPTIONS} as keys and true as value (for O(1) lookups) - BUILTIN_EXCEPTIONS_HASH = BUILTIN_EXCEPTIONS.inject({}) {|h, n| h.update(n => true) } - - # +Base+ is the superclass of all code objects recognized by YARD. A code - # object is any entity in the Ruby language (class, method, module). A - # DSL might subclass +Base+ to create a new custom object representing - # a new entity type. - # - # == Registry Integration - # Any created object associated with a namespace is immediately registered - # with the registry. This allows the Registry to act as an identity map - # to ensure that no object is represented by more than one Ruby object - # in memory. A unique {#path} is essential for this identity map to work - # correctly. - # - # == Custom Attributes - # Code objects allow arbitrary custom attributes to be set using the - # {#[]=} assignment method. - # - # == Namespaces - # There is a special type of object called a "namespace". These are subclasses - # of the {NamespaceObject} and represent Ruby entities that can have - # objects defined within them. Classically these are modules and classes, - # though a DSL might create a custom {NamespaceObject} to describe a - # specific set of objects. - # - # == Separators - # Custom classes with different separator tokens should define their own - # separators using the {NamespaceMapper.register_separator} method. The - # standard Ruby separators have already been defined ('::', '#', '.', etc). - # - # @abstract This class should not be used directly. Instead, create a - # subclass that implements {#path}, {#sep} or {#type}. You might also - # need to register custom separators if {#sep} uses alternate separator - # tokens. - # @see Registry - # @see #path - # @see #[]= - # @see NamespaceObject - # @see NamespaceMapper.register_separator - class Base - # The files the object was defined in. To add a file, use {#add_file}. - # @return [Array<Array(String, Integer)>] a list of files - # @see #add_file - attr_reader :files - - # The namespace the object is defined in. If the object is in the - # top level namespace, this is {Registry.root} - # @return [NamespaceObject] the namespace object - attr_reader :namespace - - # The source code associated with the object - # @return [String, nil] source, if present, or nil - attr_reader :source - - # Language of the source code associated with the object. Defaults to - # +:ruby+. - # - # @return [Symbol] the language type - attr_accessor :source_type - - # The one line signature representing an object. For a method, this will - # be of the form "def meth(arguments...)". This is usually the first - # source line. - # - # @return [String] a line of source - attr_accessor :signature - - # The non-localized documentation string associated with the object - # @return [Docstring] the documentation string - # @since 0.8.4 - attr_reader :base_docstring - undef base_docstring - def base_docstring; @docstring end - - # Marks whether or not the method is conditionally defined at runtime - # @return [Boolean] true if the method is conditionally defined at runtime - attr_accessor :dynamic - - # @return [String] the group this object is associated with - # @since 0.6.0 - attr_accessor :group - - # Is the object defined conditionally at runtime? - # @see #dynamic - def dynamic?; @dynamic end - - # @return [Symbol] the visibility of an object (:public, :private, :protected) - attr_accessor :visibility - undef visibility= - def visibility=(v) @visibility = v.to_sym end - - class << self - # Allocates a new code object - # @return [Base] - # @see #initialize - def new(namespace, name, *args, &block) - raise ArgumentError, "invalid empty object name" if name.to_s.empty? - if namespace.is_a?(ConstantObject) - namespace = Proxy.new(namespace.namespace, namespace.value) - end - - if name.to_s[0, 2] == NSEP - name = name.to_s[2..-1] - namespace = Registry.root - end - - if name =~ /(?:#{NSEPQ})([^:]+)$/ - return new(Proxy.new(namespace, $`), $1, *args, &block) - end - - obj = super(namespace, name, *args) - existing_obj = Registry.at(obj.path) - obj = existing_obj if existing_obj && existing_obj.class == self - yield(obj) if block_given? - obj - end - - # Compares the class with subclasses - # - # @param [Object] other the other object to compare classes with - # @return [Boolean] true if other is a subclass of self - def ===(other) - other.is_a?(self) - end - end - - # Creates a new code object - # - # @example Create a method in the root namespace - # CodeObjects::Base.new(:root, '#method') # => #<yardoc method #method> - # @example Create class Z inside namespace X::Y - # CodeObjects::Base.new(P("X::Y"), :Z) # or - # CodeObjects::Base.new(Registry.root, "X::Y") - # @param [NamespaceObject] namespace the namespace the object belongs in, - # {Registry.root} or :root should be provided if it is associated with - # the top level namespace. - # @param [Symbol, String] name the name (or complex path) of the object. - # @yield [self] a block to perform any extra initialization on the object - # @yieldparam [Base] self the newly initialized code object - # @return [Base] the newly created object - def initialize(namespace, name, *) - 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 - @source_type = :ruby - @visibility = :public - @tags = [] - @docstrings = {} - @docstring = Docstring.new!('', [], self) - @namespace = nil - self.namespace = namespace - yield(self) if block_given? - end - - # Copies all data in this object to another code object, except for - # uniquely identifying information (path, namespace, name, scope). - # - # @param [Base] other the object to copy data to - # @return [Base] the other object - # @since 0.8.0 - def copy_to(other) - copyable_attributes.each do |ivar| - ivar = "@#{ivar}" - other.instance_variable_set(ivar, instance_variable_get(ivar)) - end - other.docstring = @docstring.to_raw - other - end - - # The name of the object - # @param [Boolean] prefix whether to show a prefix. Implement - # this in a subclass to define how the prefix is showed. - # @return [Symbol] if prefix is false, the symbolized name - # @return [String] if prefix is true, prefix + the name as a String. - # This must be implemented by the subclass. - def name(prefix = false) - prefix ? @name.to_s : (defined?(@name) && @name) - 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] line the line number where the object lies in the file - # @param [Boolean] has_comments 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] - return if files.include?(obj) - 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 - - # Tests if another object is equal to this, including a proxy - # @param [Base, Proxy] other if other is a {Proxy}, tests if - # the paths are equal - # @return [Boolean] whether or not the objects are considered the same - def equal?(other) - if other.is_a?(Base) || other.is_a?(Proxy) - path == other.path - else - super - end - end - alias == equal? - alias eql? equal? - - # @return [Integer] the object's hash value (for equality checking) - def hash; path.hash end - - # @return [nil] this object does not turn into an array - def to_ary; nil end - - # Accesses a custom attribute on the object - # @param [#to_s] key the name of the custom attribute - # @return [Object, nil] the custom attribute or nil if not found. - # @see #[]= - def [](key) - if respond_to?(key) - send(key) - elsif instance_variable_defined?("@#{key}") - instance_variable_get("@#{key}") - end - end - - # Sets a custom attribute on the object - # @param [#to_s] key the name of the custom attribute - # @param [Object] value the value to associate - # @return [void] - # @see #[] - def []=(key, value) - if respond_to?("#{key}=") - send("#{key}=", value) - else - instance_variable_set("@#{key}", value) - end - end - - # @overload dynamic_attr_name - # @return the value of attribute named by the method attribute name - # @raise [NoMethodError] if no method or custom attribute exists by - # the attribute name - # @see #[] - # @overload dynamic_attr_name=(value) - # @param value a value to set - # @return +value+ - # @see #[]= - def method_missing(meth, *args, &block) - if meth.to_s =~ /=$/ - self[meth.to_s[0..-2]] = args.first - elsif instance_variable_get("@#{meth}") - self[meth] - else - super - end - end - - # Attaches source code to a code object with an optional file location - # - # @param [#source, 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.respond_to?(:source) - self.signature = statement.first_line - @source = format_source(statement.source.strip) - else - @source = format_source(statement.to_s) - end - end - - # The documentation string associated with the object - # - # @param [String, I18n::Locale] locale (I18n::Locale.default) - # the locale of the documentation string. - # @return [Docstring] the documentation string - def docstring(locale = I18n::Locale.default) - if locale.nil? - @docstring.resolve_reference - return @docstring - end - - if locale.is_a?(String) - locale_name = locale - locale = nil - else - locale_name = locale.name - end - @docstrings[locale_name] ||= - translate_docstring(locale || Registry.locale(locale_name)) - end - - # Attaches a docstring to a code object by parsing the comments attached to the statement - # and filling the {#tags} and {#docstring} methods with the parsed information. - # - # @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) - @docstrings.clear - @docstring = Docstring === comments ? - comments : Docstring.new(comments, self) - 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('::').last.gsub(/Object$/, '').downcase.to_sym - end - - # Represents the unique path of the object. The default implementation - # joins the path of {#namespace} with {#name} via the value of {#sep}. - # Custom code objects should ensure that the path is unique to the code - # object by either overriding {#sep} or this method. - # - # @example The path of an instance method - # MethodObject.new(P("A::B"), :c).path # => "A::B#c" - # @return [String] the unique path of the object - # @see #sep - def path - @path ||= if parent && !parent.root? - [parent.path, name.to_s].join(sep) - else - name.to_s - end - end - alias to_s path - - # @note - # Override this method if your object has a special title that does - # not match the {#path} attribute value. This title will be used - # when linking or displaying the object. - # @return [String] the display title for an object - # @see 0.8.4 - def title - path - end - - # @param [Base, String] other another code object (or object path) - # @return [String] the shortest relative path from this object to +other+ - # @since 0.5.3 - def relative_path(other) - other = Registry.at(other) if String === other && Registry.at(other) - same_parent = false - if other.respond_to?(:path) - same_parent = other.parent == parent - other = other.path - end - return other unless namespace - common = [path, other].join(" ").match(/^(\S*)\S*(?: \1\S*)*$/)[1] - common = path unless common =~ /(\.|::|#)$/ - common = common.sub(/(\.|::|#)[^:#\.]*?$/, '') if same_parent - suffix = %w(. :).include?(common[-1, 1]) || other[common.size, 1] == '#' ? - '' : '(::|\.)' - result = other.sub(/^#{Regexp.quote common}#{suffix}/, '') - result.empty? ? other : result - end - - # Renders the object using the {Templates::Engine templating system}. - # - # @example Formats a class in plaintext - # puts P('MyClass').format - # @example Formats a method in html with rdoc markup - # puts P('MyClass#meth').format(:format => :html, :markup => :rdoc) - # @param [Hash] options a set of options to pass to the template - # @option options [Symbol] :format (:text) :html, :text or another output format - # @option options [Symbol] :template (:default) a specific template to use - # @option options [Symbol] :markup (nil) the markup type (:rdoc, :markdown, :textile) - # @option options [Serializers::Base] :serializer (nil) see Serializers - # @return [String] the rendered template - # @see Templates::Engine#render - def format(options = {}) - options = options.merge(:object => self) - options = options.merge(:type => type) unless options[:type] - Templates::Engine.render(options) - end - - # Inspects the object, returning the type and path - # @return [String] a string describing the object - def inspect - "#<yardoc #{type} #{path}>" - end - - # Sets the namespace the object is defined in. - # - # @param [NamespaceObject, :root, nil] obj the new namespace (:root - # for {Registry.root}). If obj is nil, the object is unregistered - # from the Registry. - def namespace=(obj) - if @namespace - @namespace.children.delete(self) - Registry.delete(self) - end - - @namespace = (obj == :root ? Registry.root : obj) - - if @namespace - reg_obj = Registry.at(path) - return if reg_obj && reg_obj.class == self.class - - unless @namespace.is_a?(Proxy) - # remove prior objects from obj's children that match this one - @namespace.children.delete_if {|o| o.path == path } - @namespace.children << self - end - Registry.register(self) - end - end - - alias parent namespace - alias parent= namespace= - - # Gets a tag from the {#docstring} - # @see Docstring#tag - def tag(name); docstring.tag(name) end - - # Gets a list of tags from the {#docstring} - # @see Docstring#tags - def tags(name = nil); docstring.tags(name) end - - # Tests if the {#docstring} has a tag - # @see Docstring#has_tag? - def has_tag?(name); docstring.has_tag?(name) end - - # Add tags to the {#docstring} - # @see Docstring#add_tag - # @since 0.8.4 - def add_tag(*tags) - @docstrings.clear - @docstring.add_tag(*tags) - end - - # @return whether or not this object is a RootObject - def root?; false end - - # Override this method with a custom component separator. For instance, - # {MethodObject} implements sep as '#' or '.' (depending on if the - # method is instance or class respectively). {#path} depends on this - # value to generate the full path in the form: namespace.path + sep + name - # - # @return [String] the component that separates the namespace path - # and the name (default is {NSEP}) - def sep; NSEP end - - protected - - # Override this method if your code object subclass does not allow - # copying of certain attributes. - # - # @return [Array<String>] the list of instance variable names (without - # "@" prefix) that should be copied when {#copy_to} is called - # @see #copy_to - # @since 0.8.0 - def copyable_attributes - vars = instance_variables.map {|ivar| ivar.to_s[1..-1] } - vars -= %w(docstring docstrings namespace name path) - vars - end - - private - - # Formats source code by removing leading indentation - # - # @param [String] source the source code to format - # @return [String] formatted source - def format_source(source) - source = source.chomp - last = source.split(/\r?\n/).last - indent = last ? last[/^([ \t]*)/, 1].length : 0 - source.gsub(/^[ \t]{#{indent}}/, '') - end - - def translate_docstring(locale) - @docstring.resolve_reference - return @docstring if locale.nil? - - text = I18n::Text.new(@docstring) - localized_text = text.translate(locale) - docstring = Docstring.new(localized_text, self) - @docstring.tags.each do |tag| - if tag.is_a?(Tags::Tag) - localized_tag = tag.clone - localized_tag.text = I18n::Text.new(tag.text).translate(locale) - docstring.add_tag(localized_tag) - else - docstring.add_tag(tag) - end - end - docstring - end - end - end -end +# frozen_string_literal: true +module YARD + module CodeObjects + # A list of code objects. This array acts like a set (no unique items) + # but also disallows any {Proxy} objects from being added. + class CodeObjectList < Array + # Creates a new object list associated with a namespace + # + # @param [NamespaceObject] owner the namespace the list should be associated with + # @return [CodeObjectList] + def initialize(owner = Registry.root) + @owner = owner + end + + # Adds a new value to the list + # + # @param [Base] value a code object to add + # @return [CodeObjectList] self + def push(value) + value = Proxy.new(@owner, value) if value.is_a?(String) || value.is_a?(Symbol) + if value.is_a?(CodeObjects::Base) || value.is_a?(Proxy) + super(value) unless include?(value) + else + raise ArgumentError, "#{value.class} is not a valid CodeObject" + end + self + end + alias << push + end + + extend NamespaceMapper + + # Namespace separator + NSEP = '::' + + # Regex-quoted namespace separator + NSEPQ = NSEP + + # Instance method separator + ISEP = '#' + + # Regex-quoted instance method separator + ISEPQ = ISEP + + # Class method separator + CSEP = '.' + + # Regex-quoted class method separator + CSEPQ = Regexp.quote CSEP + + # Regular expression to match constant name + CONSTANTMATCH = /[A-Z]\w*/ + + # Regular expression to match the beginning of a constant + CONSTANTSTART = /^[A-Z]/ + + # Regular expression to match namespaces (const A or complex path A::B) + NAMESPACEMATCH = /(?:(?:#{NSEPQ}\s*)?#{CONSTANTMATCH})+/ + + # Regular expression to match a method name + METHODNAMEMATCH = %r{[a-zA-Z_]\w*[!?=]?|[-+~]\@|<<|>>|=~|===?|![=~]?|<=>|[<>]=?|\*\*|[-/+%^&*~`|]|\[\]=?} + + # Regular expression to match a fully qualified method def (self.foo, Class.foo). + METHODMATCH = /(?:(?:#{NAMESPACEMATCH}|[a-z]\w*)\s*(?:#{CSEPQ}|#{NSEPQ})\s*)?#{METHODNAMEMATCH}/ + + # All builtin Ruby exception classes for inheritance tree. + BUILTIN_EXCEPTIONS = ["ArgumentError", "ClosedQueueError", "EncodingError", + "EOFError", "Exception", "FiberError", "FloatDomainError", "IndexError", + "Interrupt", "IOError", "KeyError", "LoadError", "LocalJumpError", + "NameError", "NoMemoryError", "NoMethodError", "NotImplementedError", + "RangeError", "RegexpError", "RuntimeError", "ScriptError", "SecurityError", + "SignalException", "StandardError", "StopIteration", "SyntaxError", + "SystemCallError", "SystemExit", "SystemStackError", "ThreadError", + "TypeError", "UncaughtThrowError", "ZeroDivisionError"] + + # All builtin Ruby classes for inheritance tree. + # @note MatchingData is a 1.8.x legacy class + BUILTIN_CLASSES = ["Array", "Bignum", "Binding", "Class", "Complex", + "ConditionVariable", "Data", "Dir", "Encoding", "Enumerator", "FalseClass", + "Fiber", "File", "Fixnum", "Float", "Hash", "IO", "Integer", "MatchData", + "Method", "Module", "NilClass", "Numeric", "Object", "Proc", "Queue", + "Random", "Range", "Rational", "Regexp", "RubyVM", "SizedQueue", "String", + "Struct", "Symbol", "Thread", "ThreadGroup", "Time", "TracePoint", + "TrueClass", "UnboundMethod"] + BUILTIN_EXCEPTIONS + + # All builtin Ruby modules for mixin handling. + BUILTIN_MODULES = ["Comparable", "Enumerable", "Errno", "FileTest", "GC", + "Kernel", "Marshal", "Math", "ObjectSpace", "Precision", "Process", "Signal"] + + # All builtin Ruby classes and modules. + BUILTIN_ALL = BUILTIN_CLASSES + BUILTIN_MODULES + + # Hash of {BUILTIN_EXCEPTIONS} as keys and true as value (for O(1) lookups) + BUILTIN_EXCEPTIONS_HASH = BUILTIN_EXCEPTIONS.inject({}) {|h, n| h.update(n => true) } + + # +Base+ is the superclass of all code objects recognized by YARD. A code + # object is any entity in the Ruby language (class, method, module). A + # DSL might subclass +Base+ to create a new custom object representing + # a new entity type. + # + # == Registry Integration + # Any created object associated with a namespace is immediately registered + # with the registry. This allows the Registry to act as an identity map + # to ensure that no object is represented by more than one Ruby object + # in memory. A unique {#path} is essential for this identity map to work + # correctly. + # + # == Custom Attributes + # Code objects allow arbitrary custom attributes to be set using the + # {#[]=} assignment method. + # + # == Namespaces + # There is a special type of object called a "namespace". These are subclasses + # of the {NamespaceObject} and represent Ruby entities that can have + # objects defined within them. Classically these are modules and classes, + # though a DSL might create a custom {NamespaceObject} to describe a + # specific set of objects. + # + # == Separators + # Custom classes with different separator tokens should define their own + # separators using the {NamespaceMapper.register_separator} method. The + # standard Ruby separators have already been defined ('::', '#', '.', etc). + # + # @abstract This class should not be used directly. Instead, create a + # subclass that implements {#path}, {#sep} or {#type}. You might also + # need to register custom separators if {#sep} uses alternate separator + # tokens. + # @see Registry + # @see #path + # @see #[]= + # @see NamespaceObject + # @see NamespaceMapper.register_separator + class Base + # The files the object was defined in. To add a file, use {#add_file}. + # @return [Array<Array(String, Integer)>] a list of files + # @see #add_file + attr_reader :files + + # The namespace the object is defined in. If the object is in the + # top level namespace, this is {Registry.root} + # @return [NamespaceObject] the namespace object + attr_reader :namespace + + # The source code associated with the object + # @return [String, nil] source, if present, or nil + attr_reader :source + + # Language of the source code associated with the object. Defaults to + # +:ruby+. + # + # @return [Symbol] the language type + attr_accessor :source_type + + # The one line signature representing an object. For a method, this will + # be of the form "def meth(arguments...)". This is usually the first + # source line. + # + # @return [String] a line of source + attr_accessor :signature + + # The non-localized documentation string associated with the object + # @return [Docstring] the documentation string + # @since 0.8.4 + attr_reader :base_docstring + undef base_docstring + def base_docstring; @docstring end + + # Marks whether or not the method is conditionally defined at runtime + # @return [Boolean] true if the method is conditionally defined at runtime + attr_accessor :dynamic + + # @return [String] the group this object is associated with + # @since 0.6.0 + attr_accessor :group + + # Is the object defined conditionally at runtime? + # @see #dynamic + def dynamic?; @dynamic end + + # @return [Symbol] the visibility of an object (:public, :private, :protected) + attr_accessor :visibility + undef visibility= + def visibility=(v) @visibility = v.to_sym end + + class << self + # Allocates a new code object + # @return [Base] + # @see #initialize + def new(namespace, name, *args, &block) + raise ArgumentError, "invalid empty object name" if name.to_s.empty? + if namespace.is_a?(ConstantObject) + namespace = Proxy.new(namespace.namespace, namespace.value) + end + + if name.to_s[0, 2] == NSEP + name = name.to_s[2..-1] + namespace = Registry.root + end + + if name =~ /(?:#{NSEPQ})([^:]+)$/ + return new(Proxy.new(namespace, $`), $1, *args, &block) + end + + obj = super(namespace, name, *args) + existing_obj = Registry.at(obj.path) + obj = existing_obj if existing_obj && existing_obj.class == self + yield(obj) if block_given? + obj + end + + # Compares the class with subclasses + # + # @param [Object] other the other object to compare classes with + # @return [Boolean] true if other is a subclass of self + def ===(other) + other.is_a?(self) + end + end + + # Creates a new code object + # + # @example Create a method in the root namespace + # CodeObjects::Base.new(:root, '#method') # => #<yardoc method #method> + # @example Create class Z inside namespace X::Y + # CodeObjects::Base.new(P("X::Y"), :Z) # or + # CodeObjects::Base.new(Registry.root, "X::Y") + # @param [NamespaceObject] namespace the namespace the object belongs in, + # {Registry.root} or :root should be provided if it is associated with + # the top level namespace. + # @param [Symbol, String] name the name (or complex path) of the object. + # @yield [self] a block to perform any extra initialization on the object + # @yieldparam [Base] self the newly initialized code object + # @return [Base] the newly created object + def initialize(namespace, name, *) + 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 + @source_type = :ruby + @visibility = :public + @tags = [] + @docstrings = {} + @docstring = Docstring.new!('', [], self) + @namespace = nil + self.namespace = namespace + yield(self) if block_given? + end + + # Copies all data in this object to another code object, except for + # uniquely identifying information (path, namespace, name, scope). + # + # @param [Base] other the object to copy data to + # @return [Base] the other object + # @since 0.8.0 + def copy_to(other) + copyable_attributes.each do |ivar| + ivar = "@#{ivar}" + other.instance_variable_set(ivar, instance_variable_get(ivar)) + end + other.docstring = @docstring.to_raw + other + end + + # The name of the object + # @param [Boolean] prefix whether to show a prefix. Implement + # this in a subclass to define how the prefix is showed. + # @return [Symbol] if prefix is false, the symbolized name + # @return [String] if prefix is true, prefix + the name as a String. + # This must be implemented by the subclass. + def name(prefix = false) + prefix ? @name.to_s : (defined?(@name) && @name) + 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] line the line number where the object lies in the file + # @param [Boolean] has_comments 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] + return if files.include?(obj) + 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 + + # Tests if another object is equal to this, including a proxy + # @param [Base, Proxy] other if other is a {Proxy}, tests if + # the paths are equal + # @return [Boolean] whether or not the objects are considered the same + def equal?(other) + if other.is_a?(Base) || other.is_a?(Proxy) + path == other.path + else + super + end + end + alias == equal? + alias eql? equal? + + # @return [Integer] the object's hash value (for equality checking) + def hash; path.hash end + + # @return [nil] this object does not turn into an array + def to_ary; nil end + + # Accesses a custom attribute on the object + # @param [#to_s] key the name of the custom attribute + # @return [Object, nil] the custom attribute or nil if not found. + # @see #[]= + def [](key) + if respond_to?(key) + send(key) + elsif instance_variable_defined?("@#{key}") + instance_variable_get("@#{key}") + end + end + + # Sets a custom attribute on the object + # @param [#to_s] key the name of the custom attribute + # @param [Object] value the value to associate + # @return [void] + # @see #[] + def []=(key, value) + if respond_to?("#{key}=") + send("#{key}=", value) + else + instance_variable_set("@#{key}", value) + end + end + + # @overload dynamic_attr_name + # @return the value of attribute named by the method attribute name + # @raise [NoMethodError] if no method or custom attribute exists by + # the attribute name + # @see #[] + # @overload dynamic_attr_name=(value) + # @param value a value to set + # @return +value+ + # @see #[]= + def method_missing(meth, *args, &block) + if meth.to_s =~ /=$/ + self[meth.to_s[0..-2]] = args.first + elsif instance_variable_get("@#{meth}") + self[meth] + else + super + end + end + + # Attaches source code to a code object with an optional file location + # + # @param [#source, 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.respond_to?(:source) + self.signature = statement.first_line + @source = format_source(statement.source.strip) + else + @source = format_source(statement.to_s) + end + end + + # The documentation string associated with the object + # + # @param [String, I18n::Locale] locale (I18n::Locale.default) + # the locale of the documentation string. + # @return [Docstring] the documentation string + def docstring(locale = I18n::Locale.default) + if locale.nil? + @docstring.resolve_reference + return @docstring + end + + if locale.is_a?(String) + locale_name = locale + locale = nil + else + locale_name = locale.name + end + @docstrings[locale_name] ||= + translate_docstring(locale || Registry.locale(locale_name)) + end + + # Attaches a docstring to a code object by parsing the comments attached to the statement + # and filling the {#tags} and {#docstring} methods with the parsed information. + # + # @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) + @docstrings.clear + @docstring = Docstring === comments ? + comments : Docstring.new(comments, self) + 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('::').last.gsub(/Object$/, '').downcase.to_sym + end + + # Represents the unique path of the object. The default implementation + # joins the path of {#namespace} with {#name} via the value of {#sep}. + # Custom code objects should ensure that the path is unique to the code + # object by either overriding {#sep} or this method. + # + # @example The path of an instance method + # MethodObject.new(P("A::B"), :c).path # => "A::B#c" + # @return [String] the unique path of the object + # @see #sep + def path + @path ||= if parent && !parent.root? + [parent.path, name.to_s].join(sep) + else + name.to_s + end + end + alias to_s path + + # @note + # Override this method if your object has a special title that does + # not match the {#path} attribute value. This title will be used + # when linking or displaying the object. + # @return [String] the display title for an object + # @see 0.8.4 + def title + path + end + + # @param [Base, String] other another code object (or object path) + # @return [String] the shortest relative path from this object to +other+ + # @since 0.5.3 + def relative_path(other) + other = Registry.at(other) if String === other && Registry.at(other) + same_parent = false + if other.respond_to?(:path) + same_parent = other.parent == parent + other = other.path + end + return other unless namespace + common = [path, other].join(" ").match(/^(\S*)\S*(?: \1\S*)*$/)[1] + common = path unless common =~ /(\.|::|#)$/ + common = common.sub(/(\.|::|#)[^:#\.]*?$/, '') if same_parent + suffix = %w(. :).include?(common[-1, 1]) || other[common.size, 1] == '#' ? + '' : '(::|\.)' + result = other.sub(/^#{Regexp.quote common}#{suffix}/, '') + result.empty? ? other : result + end + + # Renders the object using the {Templates::Engine templating system}. + # + # @example Formats a class in plaintext + # puts P('MyClass').format + # @example Formats a method in html with rdoc markup + # puts P('MyClass#meth').format(:format => :html, :markup => :rdoc) + # @param [Hash] options a set of options to pass to the template + # @option options [Symbol] :format (:text) :html, :text or another output format + # @option options [Symbol] :template (:default) a specific template to use + # @option options [Symbol] :markup (nil) the markup type (:rdoc, :markdown, :textile) + # @option options [Serializers::Base] :serializer (nil) see Serializers + # @return [String] the rendered template + # @see Templates::Engine#render + def format(options = {}) + options = options.merge(:object => self) + options = options.merge(:type => type) unless options[:type] + Templates::Engine.render(options) + end + + # Inspects the object, returning the type and path + # @return [String] a string describing the object + def inspect + "#<yardoc #{type} #{path}>" + end + + # Sets the namespace the object is defined in. + # + # @param [NamespaceObject, :root, nil] obj the new namespace (:root + # for {Registry.root}). If obj is nil, the object is unregistered + # from the Registry. + def namespace=(obj) + if @namespace + @namespace.children.delete(self) + Registry.delete(self) + end + + @namespace = (obj == :root ? Registry.root : obj) + + if @namespace + reg_obj = Registry.at(path) + return if reg_obj && reg_obj.class == self.class + + unless @namespace.is_a?(Proxy) + # remove prior objects from obj's children that match this one + @namespace.children.delete_if {|o| o.path == path } + @namespace.children << self + end + Registry.register(self) + end + end + + alias parent namespace + alias parent= namespace= + + # Gets a tag from the {#docstring} + # @see Docstring#tag + def tag(name); docstring.tag(name) end + + # Gets a list of tags from the {#docstring} + # @see Docstring#tags + def tags(name = nil); docstring.tags(name) end + + # Tests if the {#docstring} has a tag + # @see Docstring#has_tag? + def has_tag?(name); docstring.has_tag?(name) end + + # Add tags to the {#docstring} + # @see Docstring#add_tag + # @since 0.8.4 + def add_tag(*tags) + @docstrings.clear + @docstring.add_tag(*tags) + end + + # @return whether or not this object is a RootObject + def root?; false end + + # Override this method with a custom component separator. For instance, + # {MethodObject} implements sep as '#' or '.' (depending on if the + # method is instance or class respectively). {#path} depends on this + # value to generate the full path in the form: namespace.path + sep + name + # + # @return [String] the component that separates the namespace path + # and the name (default is {NSEP}) + def sep; NSEP end + + protected + + # Override this method if your code object subclass does not allow + # copying of certain attributes. + # + # @return [Array<String>] the list of instance variable names (without + # "@" prefix) that should be copied when {#copy_to} is called + # @see #copy_to + # @since 0.8.0 + def copyable_attributes + vars = instance_variables.map {|ivar| ivar.to_s[1..-1] } + vars -= %w(docstring docstrings namespace name path) + vars + end + + private + + # Formats source code by removing leading indentation + # + # @param [String] source the source code to format + # @return [String] formatted source + def format_source(source) + source = source.chomp + last = source.split(/\r?\n/).last + indent = last ? last[/^([ \t]*)/, 1].length : 0 + source.gsub(/^[ \t]{#{indent}}/, '') + end + + def translate_docstring(locale) + @docstring.resolve_reference + return @docstring if locale.nil? + + text = I18n::Text.new(@docstring) + localized_text = text.translate(locale) + docstring = Docstring.new(localized_text, self) + @docstring.tags.each do |tag| + if tag.is_a?(Tags::Tag) + localized_tag = tag.clone + localized_tag.text = I18n::Text.new(tag.text).translate(locale) + docstring.add_tag(localized_tag) + else + docstring.add_tag(tag) + end + end + docstring + end + end + end +end