lib/yard/docstring_parser.rb in yard-0.9.18 vs lib/yard/docstring_parser.rb in yard-0.9.19
- old
+ new
@@ -1,345 +1,345 @@
-# frozen_string_literal: true
-require 'ostruct'
-
-module YARD
- # Parses text and creates a {Docstring} object to represent documentation
- # for a {CodeObjects::Base}. To create a new docstring, you should initialize
- # the parser and call {#parse} followed by {#to_docstring}.
- #
- # == Subclassing Notes
- #
- # The DocstringParser can be subclassed and subtituted during parsing by
- # setting the {Docstring.default_parser} attribute with the name of the
- # subclass. This allows developers to change the way docstrings are
- # parsed, allowing for completely different docstring syntaxes.
- #
- # @example Creating a Docstring with a DocstringParser
- # DocstringParser.new.parse("text here").to_docstring
- # @example Creating a Custom DocstringParser
- # # Parses docstrings backwards!
- # class ReverseDocstringParser
- # def parse_content(content)
- # super(content.reverse)
- # end
- # end
- #
- # # Set the parser as default when parsing
- # YARD::Docstring.default_parser = ReverseDocstringParser
- # @see #parse_content
- # @since 0.8.0
- class DocstringParser
- # @return [String] the parsed text portion of the docstring,
- # with tags removed.
- attr_accessor :text
-
- # @return [String] the complete input string to the parser.
- attr_accessor :raw_text
-
- # @return [Array<Tags::Tag>] the list of meta-data tags identified
- # by the parser
- attr_accessor :tags
-
- # @return [Array<Tags::Directive>] a list of directives identified
- # by the parser. This list will not be passed on to the
- # Docstring object.
- attr_accessor :directives
-
- # @return [OpenStruct] any arbitrary state to be passed between
- # tags during parsing. Mainly used by directives to coordinate
- # behaviour (so that directives can be aware of other directives
- # used in a docstring).
- attr_accessor :state
-
- # @return [CodeObjects::Base, nil] the object associated with
- # the docstring being parsed. May be nil if the docstring is
- # not attached to any object.
- attr_accessor :object
-
- # @return [CodeObjects::Base, nil] the object referenced by
- # the docstring being parsed. May be nil if the docstring doesn't
- # refer to any object.
- attr_accessor :reference
-
- # @return [Handlers::Base, nil] the handler parsing this
- # docstring. May be nil if this docstring parser is not
- # initialized through
- attr_accessor :handler
-
- # @return [Tags::Library] the tag library being used to
- # identify registered tags in the docstring.
- attr_accessor :library
-
- # The regular expression to match the tag syntax
- META_MATCH = /^@(!)?((?:\w\.?)+)(?:\s+(.*))?$/i
-
- # @!group Creation and Conversion Methods
-
- # Creates a new parser to parse docstring data
- #
- # @param [Tags::Library] library a tag library for recognizing
- # tags.
- def initialize(library = Tags::Library.instance)
- @text = ""
- @raw_text = ""
- @tags = []
- @directives = []
- @library = library
- @object = nil
- @reference = nil
- @handler = nil
- @state = OpenStruct.new
- end
-
- # @return [Docstring] translates parsed text into
- # a Docstring object.
- def to_docstring
- Docstring.new!(text, tags, object, raw_text, reference)
- end
-
- # @!group Parsing Methods
-
- # Parses all content and returns itself.
- #
- # @param [String] content the docstring text to parse
- # @param [CodeObjects::Base] object the object that the docstring
- # is attached to. Will be passed to directives to act on
- # this object.
- # @param [Handlers::Base, nil] handler the handler object that is
- # parsing this object. May be nil if this parser is not being
- # called from a {Parser::SourceParser} context.
- # @return [self] the parser object. To get the docstring,
- # call {#to_docstring}.
- # @see #to_docstring
- def parse(content, object = nil, handler = nil)
- @object = object
- @handler = handler
- @reference, @raw_text = detect_reference(content)
- text = parse_content(@raw_text)
- @text = text.strip
- call_directives_after_parse
- post_process
- self
- end
-
- # Parses a given block of text.
- #
- # @param [String] content the content to parse
- # @note Subclasses can override this method to perform custom
- # parsing of content data.
- def parse_content(content)
- content = content.split(/\r?\n/) if content.is_a?(String)
- return '' if !content || content.empty?
- docstring = String.new("")
-
- indent = content.first[/^\s*/].length
- last_indent = 0
- orig_indent = 0
- directive = false
- last_line = ""
- tag_name = nil
- tag_buf = []
-
- (content + ['']).each_with_index do |line, index|
- indent = line[/^\s*/].length
- empty = (line =~ /^\s*$/ ? true : false)
- done = content.size == index
-
- if tag_name && (((indent < orig_indent && !empty) || done ||
- (indent == 0 && !empty)) || (indent <= last_indent && line =~ META_MATCH))
- buf = tag_buf.join("\n")
- if directive || tag_is_directive?(tag_name)
- directive = create_directive(tag_name, buf)
- if directive
- docstring << parse_content(directive.expanded_text).chomp
- end
- else
- create_tag(tag_name, buf)
- end
- tag_name = nil
- tag_buf = []
- directive = false
- orig_indent = 0
- end
-
- # Found a meta tag
- if line =~ META_MATCH
- directive = $1
- tag_name = $2
- tag_buf = [($3 || '')]
- elsif tag_name && indent >= orig_indent && !empty
- orig_indent = indent if orig_indent == 0
- # Extra data added to the tag on the next line
- last_empty = last_line =~ /^[ \t]*$/ ? true : false
-
- tag_buf << '' if last_empty
- tag_buf << line.gsub(/^[ \t]{#{orig_indent}}/, '')
- elsif !tag_name
- # Regular docstring text
- docstring << line
- docstring << "\n"
- end
-
- last_indent = indent
- last_line = line
- end
-
- docstring
- end
-
- # @!group Parser Callback Methods
-
- # Call post processing callbacks on parser.
- # This is called implicitly by parser. Use this when
- # manually configuring a {Docstring} object.
- #
- # @return [void]
- def post_process
- call_after_parse_callbacks
- end
-
- # @!group Tag Manipulation Methods
-
- # Creates a tag from the {Tags::DefaultFactory tag factory}.
- #
- # To add an already created tag object, append it to {#tags}.
- #
- # @param [String] tag_name the tag name
- # @param [String] tag_buf the text attached to the tag with newlines removed.
- # @return [Tags::Tag, Tags::RefTag] a tag
- def create_tag(tag_name, tag_buf = '')
- if tag_buf =~ /\A\s*(?:(\S+)\s+)?\(\s*see\s+(\S+)\s*\)\s*\Z/
- return create_ref_tag(tag_name, $1, $2)
- end
-
- if library.has_tag?(tag_name)
- @tags += [library.tag_create(tag_name, tag_buf)].flatten
- else
- log.warn "Unknown tag @#{tag_name}" +
- (object ? " in file `#{object.file}` near line #{object.line}" : "")
- end
- rescue Tags::TagFormatError
- log.warn "Invalid tag format for @#{tag_name}" +
- (object ? " in file `#{object.file}` near line #{object.line}" : "")
- end
-
- # Creates a {Tags::RefTag}
- def create_ref_tag(tag_name, name, object_name)
- @tags << Tags::RefTagList.new(tag_name, P(object, object_name), name)
- end
-
- # Creates a new directive using the registered {#library}
- # @return [Tags::Directive] the directive object that is created
- def create_directive(tag_name, tag_buf)
- if library.has_directive?(tag_name)
- dir = library.directive_create(tag_name, tag_buf, self)
- if dir.is_a?(Tags::Directive)
- @directives << dir
- dir
- end
- else
- log.warn "Unknown directive @!#{tag_name}" +
- (object ? " in file `#{object.file}` near line #{object.line}" : "")
- nil
- end
- rescue Tags::TagFormatError
- log.warn "Invalid directive format for @!#{tag_name}" +
- (object ? " in file `#{object.file}` near line #{object.line}" : "")
- nil
- end
-
- # Backward compatibility to detect old tags that should be specified
- # as directives in 0.8 and onward.
- def tag_is_directive?(tag_name)
- list = %w(attribute endgroup group macro method scope visibility)
- list.include?(tag_name)
- end
-
- # Creates a callback that is called after a docstring is successfully
- # parsed. Use this method to perform sanity checks on a docstring's
- # tag data, or add any extra tags automatically to a docstring.
- #
- # @yield [parser] a block to be called after a docstring is parsed
- # @yieldparam [DocstringParser] parser the docstring parser object
- # with all directives and tags created.
- # @yieldreturn [void]
- # @return [void]
- def self.after_parse(&block)
- after_parse_callbacks << block
- end
-
- # @return [Array<Proc>] the {after_parse} callback proc objects
- def self.after_parse_callbacks
- @after_parse_callbacks ||= []
- end
-
- # Define a callback to check that @param tags are properly named
- after_parse do |parser|
- next unless parser.object
- next unless parser.object.is_a?(CodeObjects::MethodObject)
- next if parser.object.is_alias?
- names = parser.object.parameters.map {|l| l.first.gsub(/\W/, '') }
- seen_names = []
- infile_info = "\n in file `#{parser.object.file}' " \
- "near line #{parser.object.line}"
- parser.tags.each do |tag|
- next if tag.is_a?(Tags::RefTagList) # we don't handle this yet
- next unless tag.tag_name == "param"
- if seen_names.include?(tag.name)
- log.warn "@param tag has duplicate parameter name: " \
- "#{tag.name} #{infile_info}"
- elsif names.include?(tag.name)
- seen_names << tag.name
- else
- log.warn "@param tag has unknown parameter name: " \
- "#{tag.name} #{infile_info}"
- end
- end
- end
-
- private
-
- def namespace
- object && object.namespace
- end
-
- def detect_reference(content)
- if content =~ /\A\s*\(see (\S+)\s*\)(?:\s|$)/
- path = $1
- extra = $'
- [CodeObjects::Proxy.new(namespace, path), extra]
- else
- [nil, content]
- end
- end
-
- # @!group Parser Callback Methods
-
- # Calls the {Tags::Directive#after_parse} callback on all the
- # created directives.
- def call_directives_after_parse
- directives.each(&:after_parse)
- end
-
- # Calls all {after_parse} callbacks
- def call_after_parse_callbacks
- self.class.after_parse_callbacks.each do |cb|
- cb.call(self)
- end
- end
-
- # Define a callback to check that @see tags do not use {}.
- after_parse do |parser|
- next unless parser.object
-
- parser.tags.each_with_index do |tag, i|
- next if tag.is_a?(Tags::RefTagList) # we don't handle this yet
- next unless tag.tag_name == "see"
- next unless "#{tag.name}#{tag.text}" =~ /\A\{.*\}\Z/
- infile_info = "\n in file `#{parser.object.file}' " \
- "near line #{parser.object.line}"
- log.warn "@see tag (##{i + 1}) should not be wrapped in {} " \
- "(causes rendering issues): #{infile_info}"
- end
- end
- end
-end
+# frozen_string_literal: true
+require 'ostruct'
+
+module YARD
+ # Parses text and creates a {Docstring} object to represent documentation
+ # for a {CodeObjects::Base}. To create a new docstring, you should initialize
+ # the parser and call {#parse} followed by {#to_docstring}.
+ #
+ # == Subclassing Notes
+ #
+ # The DocstringParser can be subclassed and subtituted during parsing by
+ # setting the {Docstring.default_parser} attribute with the name of the
+ # subclass. This allows developers to change the way docstrings are
+ # parsed, allowing for completely different docstring syntaxes.
+ #
+ # @example Creating a Docstring with a DocstringParser
+ # DocstringParser.new.parse("text here").to_docstring
+ # @example Creating a Custom DocstringParser
+ # # Parses docstrings backwards!
+ # class ReverseDocstringParser
+ # def parse_content(content)
+ # super(content.reverse)
+ # end
+ # end
+ #
+ # # Set the parser as default when parsing
+ # YARD::Docstring.default_parser = ReverseDocstringParser
+ # @see #parse_content
+ # @since 0.8.0
+ class DocstringParser
+ # @return [String] the parsed text portion of the docstring,
+ # with tags removed.
+ attr_accessor :text
+
+ # @return [String] the complete input string to the parser.
+ attr_accessor :raw_text
+
+ # @return [Array<Tags::Tag>] the list of meta-data tags identified
+ # by the parser
+ attr_accessor :tags
+
+ # @return [Array<Tags::Directive>] a list of directives identified
+ # by the parser. This list will not be passed on to the
+ # Docstring object.
+ attr_accessor :directives
+
+ # @return [OpenStruct] any arbitrary state to be passed between
+ # tags during parsing. Mainly used by directives to coordinate
+ # behaviour (so that directives can be aware of other directives
+ # used in a docstring).
+ attr_accessor :state
+
+ # @return [CodeObjects::Base, nil] the object associated with
+ # the docstring being parsed. May be nil if the docstring is
+ # not attached to any object.
+ attr_accessor :object
+
+ # @return [CodeObjects::Base, nil] the object referenced by
+ # the docstring being parsed. May be nil if the docstring doesn't
+ # refer to any object.
+ attr_accessor :reference
+
+ # @return [Handlers::Base, nil] the handler parsing this
+ # docstring. May be nil if this docstring parser is not
+ # initialized through
+ attr_accessor :handler
+
+ # @return [Tags::Library] the tag library being used to
+ # identify registered tags in the docstring.
+ attr_accessor :library
+
+ # The regular expression to match the tag syntax
+ META_MATCH = /^@(!)?((?:\w\.?)+)(?:\s+(.*))?$/i
+
+ # @!group Creation and Conversion Methods
+
+ # Creates a new parser to parse docstring data
+ #
+ # @param [Tags::Library] library a tag library for recognizing
+ # tags.
+ def initialize(library = Tags::Library.instance)
+ @text = ""
+ @raw_text = ""
+ @tags = []
+ @directives = []
+ @library = library
+ @object = nil
+ @reference = nil
+ @handler = nil
+ @state = OpenStruct.new
+ end
+
+ # @return [Docstring] translates parsed text into
+ # a Docstring object.
+ def to_docstring
+ Docstring.new!(text, tags, object, raw_text, reference)
+ end
+
+ # @!group Parsing Methods
+
+ # Parses all content and returns itself.
+ #
+ # @param [String] content the docstring text to parse
+ # @param [CodeObjects::Base] object the object that the docstring
+ # is attached to. Will be passed to directives to act on
+ # this object.
+ # @param [Handlers::Base, nil] handler the handler object that is
+ # parsing this object. May be nil if this parser is not being
+ # called from a {Parser::SourceParser} context.
+ # @return [self] the parser object. To get the docstring,
+ # call {#to_docstring}.
+ # @see #to_docstring
+ def parse(content, object = nil, handler = nil)
+ @object = object
+ @handler = handler
+ @reference, @raw_text = detect_reference(content)
+ text = parse_content(@raw_text)
+ @text = text.strip
+ call_directives_after_parse
+ post_process
+ self
+ end
+
+ # Parses a given block of text.
+ #
+ # @param [String] content the content to parse
+ # @note Subclasses can override this method to perform custom
+ # parsing of content data.
+ def parse_content(content)
+ content = content.split(/\r?\n/) if content.is_a?(String)
+ return '' if !content || content.empty?
+ docstring = String.new("")
+
+ indent = content.first[/^\s*/].length
+ last_indent = 0
+ orig_indent = 0
+ directive = false
+ last_line = ""
+ tag_name = nil
+ tag_buf = []
+
+ (content + ['']).each_with_index do |line, index|
+ indent = line[/^\s*/].length
+ empty = (line =~ /^\s*$/ ? true : false)
+ done = content.size == index
+
+ if tag_name && (((indent < orig_indent && !empty) || done ||
+ (indent == 0 && !empty)) || (indent <= last_indent && line =~ META_MATCH))
+ buf = tag_buf.join("\n")
+ if directive || tag_is_directive?(tag_name)
+ directive = create_directive(tag_name, buf)
+ if directive
+ docstring << parse_content(directive.expanded_text).chomp
+ end
+ else
+ create_tag(tag_name, buf)
+ end
+ tag_name = nil
+ tag_buf = []
+ directive = false
+ orig_indent = 0
+ end
+
+ # Found a meta tag
+ if line =~ META_MATCH
+ directive = $1
+ tag_name = $2
+ tag_buf = [($3 || '')]
+ elsif tag_name && indent >= orig_indent && !empty
+ orig_indent = indent if orig_indent == 0
+ # Extra data added to the tag on the next line
+ last_empty = last_line =~ /^[ \t]*$/ ? true : false
+
+ tag_buf << '' if last_empty
+ tag_buf << line.gsub(/^[ \t]{#{orig_indent}}/, '')
+ elsif !tag_name
+ # Regular docstring text
+ docstring << line
+ docstring << "\n"
+ end
+
+ last_indent = indent
+ last_line = line
+ end
+
+ docstring
+ end
+
+ # @!group Parser Callback Methods
+
+ # Call post processing callbacks on parser.
+ # This is called implicitly by parser. Use this when
+ # manually configuring a {Docstring} object.
+ #
+ # @return [void]
+ def post_process
+ call_after_parse_callbacks
+ end
+
+ # @!group Tag Manipulation Methods
+
+ # Creates a tag from the {Tags::DefaultFactory tag factory}.
+ #
+ # To add an already created tag object, append it to {#tags}.
+ #
+ # @param [String] tag_name the tag name
+ # @param [String] tag_buf the text attached to the tag with newlines removed.
+ # @return [Tags::Tag, Tags::RefTag] a tag
+ def create_tag(tag_name, tag_buf = '')
+ if tag_buf =~ /\A\s*(?:(\S+)\s+)?\(\s*see\s+(\S+)\s*\)\s*\Z/
+ return create_ref_tag(tag_name, $1, $2)
+ end
+
+ if library.has_tag?(tag_name)
+ @tags += [library.tag_create(tag_name, tag_buf)].flatten
+ else
+ log.warn "Unknown tag @#{tag_name}" +
+ (object ? " in file `#{object.file}` near line #{object.line}" : "")
+ end
+ rescue Tags::TagFormatError
+ log.warn "Invalid tag format for @#{tag_name}" +
+ (object ? " in file `#{object.file}` near line #{object.line}" : "")
+ end
+
+ # Creates a {Tags::RefTag}
+ def create_ref_tag(tag_name, name, object_name)
+ @tags << Tags::RefTagList.new(tag_name, P(object, object_name), name)
+ end
+
+ # Creates a new directive using the registered {#library}
+ # @return [Tags::Directive] the directive object that is created
+ def create_directive(tag_name, tag_buf)
+ if library.has_directive?(tag_name)
+ dir = library.directive_create(tag_name, tag_buf, self)
+ if dir.is_a?(Tags::Directive)
+ @directives << dir
+ dir
+ end
+ else
+ log.warn "Unknown directive @!#{tag_name}" +
+ (object ? " in file `#{object.file}` near line #{object.line}" : "")
+ nil
+ end
+ rescue Tags::TagFormatError
+ log.warn "Invalid directive format for @!#{tag_name}" +
+ (object ? " in file `#{object.file}` near line #{object.line}" : "")
+ nil
+ end
+
+ # Backward compatibility to detect old tags that should be specified
+ # as directives in 0.8 and onward.
+ def tag_is_directive?(tag_name)
+ list = %w(attribute endgroup group macro method scope visibility)
+ list.include?(tag_name)
+ end
+
+ # Creates a callback that is called after a docstring is successfully
+ # parsed. Use this method to perform sanity checks on a docstring's
+ # tag data, or add any extra tags automatically to a docstring.
+ #
+ # @yield [parser] a block to be called after a docstring is parsed
+ # @yieldparam [DocstringParser] parser the docstring parser object
+ # with all directives and tags created.
+ # @yieldreturn [void]
+ # @return [void]
+ def self.after_parse(&block)
+ after_parse_callbacks << block
+ end
+
+ # @return [Array<Proc>] the {after_parse} callback proc objects
+ def self.after_parse_callbacks
+ @after_parse_callbacks ||= []
+ end
+
+ # Define a callback to check that @param tags are properly named
+ after_parse do |parser|
+ next unless parser.object
+ next unless parser.object.is_a?(CodeObjects::MethodObject)
+ next if parser.object.is_alias?
+ names = parser.object.parameters.map {|l| l.first.gsub(/\W/, '') }
+ seen_names = []
+ infile_info = "\n in file `#{parser.object.file}' " \
+ "near line #{parser.object.line}"
+ parser.tags.each do |tag|
+ next if tag.is_a?(Tags::RefTagList) # we don't handle this yet
+ next unless tag.tag_name == "param"
+ if seen_names.include?(tag.name)
+ log.warn "@param tag has duplicate parameter name: " \
+ "#{tag.name} #{infile_info}"
+ elsif names.include?(tag.name)
+ seen_names << tag.name
+ else
+ log.warn "@param tag has unknown parameter name: " \
+ "#{tag.name} #{infile_info}"
+ end
+ end
+ end
+
+ private
+
+ def namespace
+ object && object.namespace
+ end
+
+ def detect_reference(content)
+ if content =~ /\A\s*\(see (\S+)\s*\)(?:\s|$)/
+ path = $1
+ extra = $'
+ [CodeObjects::Proxy.new(namespace, path), extra]
+ else
+ [nil, content]
+ end
+ end
+
+ # @!group Parser Callback Methods
+
+ # Calls the {Tags::Directive#after_parse} callback on all the
+ # created directives.
+ def call_directives_after_parse
+ directives.each(&:after_parse)
+ end
+
+ # Calls all {after_parse} callbacks
+ def call_after_parse_callbacks
+ self.class.after_parse_callbacks.each do |cb|
+ cb.call(self)
+ end
+ end
+
+ # Define a callback to check that @see tags do not use {}.
+ after_parse do |parser|
+ next unless parser.object
+
+ parser.tags.each_with_index do |tag, i|
+ next if tag.is_a?(Tags::RefTagList) # we don't handle this yet
+ next unless tag.tag_name == "see"
+ next unless "#{tag.name}#{tag.text}" =~ /\A\{.*\}\Z/
+ infile_info = "\n in file `#{parser.object.file}' " \
+ "near line #{parser.object.line}"
+ log.warn "@see tag (##{i + 1}) should not be wrapped in {} " \
+ "(causes rendering issues): #{infile_info}"
+ end
+ end
+ end
+end