# 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)
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') # => #<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
# @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<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
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
"#<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