PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/yard/code_objects/base.rb

https://github.com/edward/yard
Ruby | 324 lines | 228 code | 42 blank | 54 comment | 59 complexity | 58a9ee088ddc2c0b259e29f98363e36b MD5 | raw file
  1. module YARD
  2. module CodeObjects
  3. class CodeObjectList < Array
  4. def initialize(owner = Registry.root)
  5. @owner = owner
  6. end
  7. def push(value)
  8. value = Proxy.new(@owner, value) if value.is_a?(String) || value.is_a?(Symbol)
  9. if value.is_a?(CodeObjects::Base) || value.is_a?(Proxy)
  10. super(value) unless include?(value)
  11. else
  12. raise ArgumentError, "#{value.class} is not a valid CodeObject"
  13. end
  14. self
  15. end
  16. alias_method :<<, :push
  17. end
  18. NSEP = '::'
  19. ISEP = '#'
  20. CONSTANTMATCH = /[A-Z]\w*/
  21. NAMESPACEMATCH = /(?:(?:#{Regexp.quote NSEP})?#{CONSTANTMATCH})+/
  22. METHODNAMEMATCH = /[a-zA-Z_]\w*[!?]?|[-+~]\@|<<|>>|=~|===?|[<>]=?|\*\*|[-\/+%^&*~`|]|\[\]=?/
  23. METHODMATCH = /(?:(?:#{NAMESPACEMATCH}|self)\s*(?:\.|#{Regexp.quote NSEP})\s*)?#{METHODNAMEMATCH}/
  24. BUILTIN_EXCEPTIONS = ["SecurityError", "Exception", "NoMethodError", "FloatDomainError",
  25. "IOError", "TypeError", "NotImplementedError", "SystemExit", "Interrupt", "SyntaxError",
  26. "RangeError", "NoMemoryError", "ArgumentError", "ThreadError", "EOFError", "RuntimeError",
  27. "ZeroDivisionError", "StandardError", "LoadError", "NameError", "LocalJumpError", "SystemCallError",
  28. "SignalException", "ScriptError", "SystemStackError", "RegexpError", "IndexError"]
  29. BUILTIN_CLASSES = ["TrueClass", "Array", "Dir", "Struct", "UnboundMethod", "Object", "Fixnum", "Float",
  30. "ThreadGroup", "MatchData", "Proc", "Binding", "Class", "Time", "Bignum", "NilClass", "Symbol",
  31. "Numeric", "String", "Data", "MatchingData", "Regexp", "Integer", "File", "IO", "Range", "FalseClass",
  32. "Method", "Continuation", "Thread", "Hash", "Module"] + BUILTIN_EXCEPTIONS
  33. BUILTIN_MODULES = ["ObjectSpace", "Signal", "Marshal", "Kernel", "Process", "GC", "FileTest", "Enumerable",
  34. "Comparable", "Errno", "Precision", "Math", "DTracer"]
  35. BUILTIN_ALL = BUILTIN_CLASSES + BUILTIN_MODULES
  36. BUILTIN_EXCEPTIONS_HASH = BUILTIN_EXCEPTIONS.inject({}) {|h,n| h.update(n => true) }
  37. class Base
  38. attr_reader :name
  39. attr_accessor :namespace
  40. attr_accessor :source, :signature, :file, :line, :docstring, :dynamic
  41. def dynamic?; @dynamic end
  42. class << self
  43. def new(namespace, name, *args, &block)
  44. if name.to_s[0,2] == "::"
  45. name = name.to_s[2..-1]
  46. namespace = Registry.root
  47. elsif name =~ /(?:#{NSEP}|#{ISEP})([^#{NSEP}#{ISEP}]+)$/
  48. return new(Proxy.new(namespace, $`), $1, *args, &block)
  49. end
  50. keyname = namespace && namespace.respond_to?(:path) ? namespace.path : ''
  51. if self == RootObject
  52. keyname = :root
  53. elsif keyname.empty?
  54. keyname = name.to_s
  55. elsif self == MethodObject
  56. keyname += (!args.first || args.first.to_sym == :instance ? ISEP : NSEP) + name.to_s
  57. else
  58. keyname += NSEP + name.to_s
  59. end
  60. if self != RootObject && obj = Registry[keyname]
  61. yield(obj) if block_given?
  62. obj
  63. else
  64. Registry.objects[keyname] = super(namespace, name, *args, &block)
  65. end
  66. end
  67. end
  68. def initialize(namespace, name, *args)
  69. if namespace && namespace != :root &&
  70. !namespace.is_a?(NamespaceObject) && !namespace.is_a?(Proxy)
  71. raise ArgumentError, "Invalid namespace object: #{namespace}"
  72. end
  73. @name = name.to_sym
  74. @tags = []
  75. @docstring = ""
  76. self.namespace = namespace
  77. yield(self) if block_given?
  78. end
  79. def ==(other)
  80. if other.is_a?(Proxy)
  81. path == other.path
  82. else
  83. super
  84. end
  85. end
  86. def [](key)
  87. if respond_to?(key)
  88. send(key)
  89. else
  90. instance_variable_get("@#{key}")
  91. end
  92. end
  93. def []=(key, value)
  94. if respond_to?("#{key}=")
  95. send("#{key}=", value)
  96. else
  97. instance_variable_set("@#{key}", value)
  98. end
  99. end
  100. def method_missing(meth, *args, &block)
  101. if meth.to_s =~ /=$/
  102. self[meth.to_s[0..-2]] = *args
  103. elsif instance_variable_get("@#{meth}")
  104. self[meth]
  105. else
  106. super
  107. end
  108. end
  109. ##
  110. # Attaches source code to a code object with an optional file location
  111. #
  112. # @param [Parser::Statement, String] statement
  113. # the +Parser::Statement+ holding the source code or the raw source
  114. # as a +String+ for the definition of the code object only (not the block)
  115. def source=(statement)
  116. if statement.is_a? Parser::Statement
  117. src = statement.tokens.to_s
  118. blk = statement.block ? statement.block.to_s : ""
  119. if src =~ /^def\s.*[^\)]$/ && blk[0,1] !~ /\r|\n/
  120. blk = ";" + blk
  121. end
  122. @source = format_source(src + blk)
  123. self.line = statement.tokens.first.line_no
  124. self.signature = src
  125. else
  126. @source = format_source(statement.to_s)
  127. end
  128. end
  129. ##
  130. # Attaches a docstring to a code oject by parsing the comments attached to the statement
  131. # and filling the {#tags} and {#docstring} methods with the parsed information.
  132. #
  133. # @param [String, Array<String>] comments
  134. # the comments attached to the code object to be parsed
  135. # into a docstring and meta tags.
  136. def docstring=(comments)
  137. @short_docstring = nil
  138. parse_comments(comments) if comments
  139. end
  140. ##
  141. # Gets the first line of a docstring to the period or the first paragraph.
  142. #
  143. # @return [String] The first line or paragraph of the docstring; always ends with a period.
  144. def short_docstring
  145. @short_docstring ||= (docstring.split(/\.|\r?\n\r?\n/).first || '')
  146. @short_docstring += '.' unless @short_docstring.empty?
  147. @short_docstring
  148. end
  149. ##
  150. # Default type is the lowercase class name without the "Object" suffix
  151. #
  152. # Override this method to provide a custom object type
  153. #
  154. # @return [Symbol] the type of code object this represents
  155. def type
  156. self.class.name.split(/#{NSEP}/).last.gsub(/Object$/, '').downcase.to_sym
  157. end
  158. def path
  159. if parent && parent != Registry.root
  160. [parent.path, name.to_s].join(sep)
  161. else
  162. name.to_s
  163. end
  164. end
  165. alias_method :to_s, :path
  166. def inspect
  167. "#<yardoc #{type} #{path}>"
  168. end
  169. def namespace=(obj)
  170. if @namespace
  171. @namespace.children.delete(self)
  172. Registry.delete(self)
  173. end
  174. @namespace = (obj == :root ? Registry.root : obj)
  175. if @namespace
  176. @namespace.children << self unless @namespace.is_a?(Proxy)
  177. Registry.register(self)
  178. end
  179. end
  180. alias_method :parent, :namespace
  181. alias_method :parent=, :namespace=
  182. ##
  183. # Convenience method to return the first tag
  184. # object in the list of tag objects of that name
  185. #
  186. # Example:
  187. # doc = YARD::Documentation.new("@return zero when nil")
  188. # doc.tag("return").text # => "zero when nil"
  189. #
  190. # @param [#to_s] name the tag name to return data for
  191. # @return [Tags::Tag] the first tag in the list of {#tags}
  192. def tag(name)
  193. @tags.find {|tag| tag.tag_name.to_s == name.to_s }
  194. end
  195. ##
  196. # Returns a list of tags specified by +name+ or all tags if +name+ is not specified.
  197. #
  198. # @param name the tag name to return data for, or nil for all tags
  199. # @return [Array<Tags::Tag>] the list of tags by the specified tag name
  200. def tags(name = nil)
  201. return @tags if name.nil?
  202. @tags.select {|tag| tag.tag_name.to_s == name.to_s }
  203. end
  204. ##
  205. # Returns true if at least one tag by the name +name+ was declared
  206. #
  207. # @param [String] name the tag name to search for
  208. # @return [Boolean] whether or not the tag +name+ was declared
  209. def has_tag?(name)
  210. @tags.any? {|tag| tag.tag_name.to_s == name.to_s }
  211. end
  212. protected
  213. def sep; NSEP end
  214. private
  215. ##
  216. # Parses out comments split by newlines into a new code object
  217. #
  218. # @param [Array<String>, String] comments
  219. # the newline delimited array of comments. If the comments
  220. # are passed as a String, they will be split by newlines.
  221. def parse_comments(comments)
  222. return if comments.empty?
  223. meta_match = /^@(\S+)\s*(.*)/
  224. comments = comments.split(/\r?\n/) if comments.is_a? String
  225. @tags, @docstring = [], ""
  226. indent, last_indent = comments.first[/^\s*/].length, 0
  227. orig_indent = 0
  228. last_line = ""
  229. tag_name, tag_klass, tag_buf, raw_buf = nil, nil, "", []
  230. (comments+['']).each_with_index do |line, index|
  231. indent = line[/^\s*/].length
  232. empty = (line =~ /^\s*$/ ? true : false)
  233. done = comments.size == index
  234. if tag_name && (((indent < orig_indent && !empty) || done) ||
  235. (indent <= last_indent && line =~ meta_match))
  236. tagfactory = Tags::Library.new
  237. tag_method = "#{tag_name}_tag"
  238. if tag_name && tagfactory.respond_to?(tag_method)
  239. if tagfactory.method(tag_method).arity == 2
  240. @tags << tagfactory.send(tag_method, tag_buf, raw_buf.join("\n"))
  241. else
  242. @tags << tagfactory.send(tag_method, tag_buf)
  243. end
  244. else
  245. log.warn "Unknown tag @#{tag_name} in documentation for `#{path}`"
  246. end
  247. tag_name, tag_buf, raw_buf = nil, '', []
  248. orig_indent = 0
  249. end
  250. # Found a meta tag
  251. if line =~ meta_match
  252. orig_indent = indent
  253. tag_name, tag_buf = $1, $2
  254. raw_buf = [tag_buf.dup]
  255. elsif tag_name && indent >= orig_indent && !empty
  256. # Extra data added to the tag on the next line
  257. last_empty = last_line =~ /^[ \t]*$/ ? true : false
  258. if last_empty
  259. tag_buf << "\n\n"
  260. raw_buf << ''
  261. end
  262. tag_buf << line.gsub(/^[ \t]{#{indent}}/, last_empty ? '' : ' ')
  263. raw_buf << line.gsub(/^[ \t]{#{orig_indent}}/, '')
  264. elsif !tag_name
  265. # Regular docstring text
  266. @docstring << line << "\n"
  267. end
  268. last_indent = indent
  269. last_line = line
  270. end
  271. # Remove trailing/leading whitespace / newlines
  272. @docstring.gsub!(/\A[\r\n\s]+|[\r\n\s]+\Z/, '')
  273. end
  274. # Formats source code by removing leading indentation
  275. def format_source(source)
  276. source.chomp!
  277. indent = source.split(/\r?\n/).last[/^([ \t]*)/, 1].length
  278. source.gsub(/^[ \t]{#{indent}}/, '')
  279. end
  280. end
  281. end
  282. end