# 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] 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) unless namespace.value =~ /\A#{NAMESPACEMATCH}\Z/ raise Parser::UndocumentableError, "constant mapping" end 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') # => # # @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, '' should be used to associate code that comes form standard input. # # @param [String] file the filename ('' 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 # @return [nil] if there is no file associated with the object 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) @source = format_source(statement.source.strip) else @source = format_source(statement.to_s) end if statement.respond_to?(:signature) self.signature = statement.signature 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, 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 obj_name = self.class.name.split('::').last obj_name.gsub!(/Object$/, '') obj_name.downcase! obj_name.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 "#" 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] 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