diff options
Diffstat (limited to 'giscanner/annotationparser.py')
-rw-r--r-- | giscanner/annotationparser.py | 556 |
1 files changed, 417 insertions, 139 deletions
diff --git a/giscanner/annotationparser.py b/giscanner/annotationparser.py index 2ac1b0eb..a0657dc4 100644 --- a/giscanner/annotationparser.py +++ b/giscanner/annotationparser.py @@ -20,18 +20,13 @@ # -# AnnotationParser - extract annotations from gtk-doc comments +# AnnotationParser - extract annotations from GTK-Doc comment blocks import re from . import message -from .annotationpatterns import (COMMENT_START_RE, COMMENT_END_RE, - COMMENT_ASTERISK_RE, EMPTY_LINE_RE, - SECTION_RE, SYMBOL_RE, PROPERTY_RE, SIGNAL_RE, - PARAMETER_RE, DESCRIPTION_TAG_RE, TAG_RE, - MULTILINE_ANNOTATION_CONTINUATION_RE) -from .odict import odict +from .collections import OrderedDict # GTK-Doc comment block parts @@ -53,6 +48,7 @@ TAG_STABILITY = 'stability' TAG_DEPRECATED = 'deprecated' TAG_RETURNS = 'returns' TAG_RETURNVALUE = 'return value' +TAG_DESCRIPTION = 'description' TAG_ATTRIBUTES = 'attributes' TAG_RENAME_TO = 'rename to' TAG_TYPE = 'type' @@ -68,6 +64,7 @@ _ALL_TAGS = [TAG_VFUNC, TAG_DEPRECATED, TAG_RETURNS, TAG_RETURNVALUE, + TAG_DESCRIPTION, TAG_ATTRIBUTES, TAG_RENAME_TO, TAG_TYPE, @@ -137,15 +134,254 @@ OPT_TRANSFER_FULL = 'full' OPT_TRANSFER_FLOATING = 'floating' +#The following regular expression programs are built to: +# - match (or substitute) a single comment block line at a time; +# - support (but remains untested) LOCALE and UNICODE modes. + +# Program matching the start of a comment block. +# +# Results in 0 symbolic groups. +COMMENT_START_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + / # 1 forward slash character + \*{2} # exactly 2 asterisk characters + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE) + +# Program matching the end of a comment block. We need to take care +# of comment ends that aren't on their own line for legacy support +# reasons. See https://bugzilla.gnome.org/show_bug.cgi?id=689354 +# +# Results in 1 symbolic group: +# - group 1 = description +COMMENT_END_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + (?P<description>.*?) # description text + [^\S\n\r]* # 0 or more whitespace characters + \*+ # 1 or more asterisk characters + / # 1 forward slash character + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE) + +# Program matching the ' * ' at the beginning of every +# line inside a comment block. +# +# Results in 0 symbolic groups. +COMMENT_ASTERISK_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + \* # 1 asterisk character + [^\S\n\r]? # 0 or 1 whitespace characters. Careful, + # removing more than 1 whitespace + # character would break embedded + # example program indentation + ''', + re.VERBOSE) + +# Program matching the indentation at the beginning of every +# line (stripped from the ' * ') inside a comment block. +# +# Results in 1 symbolic group: +# - group 1 = indentation +COMMENT_INDENTATION_RE = re.compile( + r''' + ^ + (?P<indentation>[^\S\n\r]*) # 0 or more whitespace characters + .* + $ + ''', + re.VERBOSE) + +# Program matching an empty line. +# +# Results in 0 symbolic groups. +EMPTY_LINE_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE) + +# Program matching SECTION identifiers. +# +# Results in 2 symbolic groups: +# - group 1 = colon +# - group 2 = section_name +SECTION_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + SECTION # SECTION + [^\S\n\r]* # 0 or more whitespace characters + (?P<colon>:?) # colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<section_name>\w\S+)? # section name + [^\S\n\r]* # 0 or more whitespace characters + $ + ''', + re.VERBOSE) + +# Program matching symbol (function, constant, struct and enum) identifiers. +# +# Results in 3 symbolic groups: +# - group 1 = symbol_name +# - group 2 = colon +# - group 3 = annotations +SYMBOL_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + (?P<symbol_name>[\w-]*\w) # symbol name + [^\S\n\r]* # 0 or more whitespace characters + (?P<colon>:?) # colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<annotations>(?:\(.*?\)[^\S\n\r]*)*) # annotations + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE) + +# Program matching property identifiers. +# +# Results in 4 symbolic groups: +# - group 1 = class_name +# - group 2 = property_name +# - group 3 = colon +# - group 4 = annotations +PROPERTY_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + (?P<class_name>[\w]+) # class name + [^\S\n\r]* # 0 or more whitespace characters + :{1} # required colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<property_name>[\w-]*\w) # property name + [^\S\n\r]* # 0 or more whitespace characters + (?P<colon>:?) # colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<annotations>(?:\(.*?\)[^\S\n\r]*)*) # annotations + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE) + +# Program matching signal identifiers. +# +# Results in 4 symbolic groups: +# - group 1 = class_name +# - group 2 = signal_name +# - group 3 = colon +# - group 4 = annotations +SIGNAL_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + (?P<class_name>[\w]+) # class name + [^\S\n\r]* # 0 or more whitespace characters + :{2} # 2 required colons + [^\S\n\r]* # 0 or more whitespace characters + (?P<signal_name>[\w-]*\w) # signal name + [^\S\n\r]* # 0 or more whitespace characters + (?P<colon>:?) # colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<annotations>(?:\(.*?\)[^\S\n\r]*)*) # annotations + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE) + +# Program matching parameters. +# +# Results in 4 symbolic groups: +# - group 1 = parameter_name +# - group 2 = annotations +# - group 3 = colon +# - group 4 = description +PARAMETER_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + @ # @ character + (?P<parameter_name>[\w-]*\w|\.\.\.) # parameter name + [^\S\n\r]* # 0 or more whitespace characters + :{1} # required colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<annotations>(?:\(.*?\)[^\S\n\r]*)*) # annotations + (?P<colon>:?) # colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<description>.*?) # description + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE) + +# Program matching tags. +# +# Results in 4 symbolic groups: +# - group 1 = tag_name +# - group 2 = annotations +# - group 3 = colon +# - group 4 = description +_all_tags = '|'.join(_ALL_TAGS).replace(' ', '\\ ') +TAG_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + (?P<tag_name>''' + _all_tags + r''') # tag name + [^\S\n\r]* # 0 or more whitespace characters + :{1} # required colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<annotations>(?:\(.*?\)[^\S\n\r]*)*) # annotations + (?P<colon>:?) # colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<description>.*?) # description + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE | re.IGNORECASE) + +# Program matching multiline annotation continuations. +# This is used on multiline parameters and tags (but not on the first line) to +# generate warnings about invalid annotations spanning multiple lines. +# +# Results in 3 symbolic groups: +# - group 2 = annotations +# - group 3 = colon +# - group 4 = description +MULTILINE_ANNOTATION_CONTINUATION_RE = re.compile( + r''' + ^ # start + [^\S\n\r]* # 0 or more whitespace characters + (?P<annotations>(?:\(.*?\)[^\S\n\r]*)*) # annotations + (?P<colon>:) # colon + [^\S\n\r]* # 0 or more whitespace characters + (?P<description>.*?) # description + [^\S\n\r]* # 0 or more whitespace characters + $ # end + ''', + re.VERBOSE) + + class DocBlock(object): def __init__(self, name): self.name = name self.options = DocOptions() self.value = None - self.tags = odict() + self.tags = OrderedDict() self.comment = None - self.params = odict() + self.params = OrderedDict() self.position = None def __cmp__(self, other): @@ -154,16 +390,6 @@ class DocBlock(object): def __repr__(self): return '<DocBlock %r %r>' % (self.name, self.options) - def set_position(self, position): - self.position = position - self.options.position = position - - def get_tag(self, name): - return self.tags.get(name) - - def get_param(self, name): - return self.params.get(name) - def to_gtk_doc(self): options = '' if self.options: @@ -175,9 +401,10 @@ class DocBlock(object): lines[0] += options for param in self.params.values(): lines.append(param.to_gtk_doc_param()) - lines.append('') - for l in self.comment.split('\n'): - lines.append(l) + if self.comment: + lines.append('') + for l in self.comment.split('\n'): + lines.append(l) if self.tags: lines.append('') for tag in self.tags.values(): @@ -229,8 +456,8 @@ class DocTag(object): s = 'one value' else: s = '%d values' % (n_params, ) - if ((n_params > 0 and (value is None or value.length() != n_params)) or - n_params == 0 and value is not None): + if ((n_params > 0 and (value is None or value.length() != n_params)) + or n_params == 0 and value is not None): if value is None: length = 0 else: @@ -250,7 +477,7 @@ class DocTag(object): if value is None: return - for name, v in value.all().iteritems(): + for name, v in value.all().items(): if name in [OPT_ARRAY_ZERO_TERMINATED, OPT_ARRAY_FIXED_SIZE]: try: int(v) @@ -279,7 +506,7 @@ class DocTag(object): if value is not None and value.length() > 1: message.warn( 'closure takes at most 1 value, %d given' % ( - value.length()), self.position) + value.length(), ), self.position) def _validate_element_type(self, option, value): self._validate_option(option, value, required=True) @@ -291,7 +518,7 @@ class DocTag(object): if value.length() > 2: message.warn( 'element-type takes at most 2 values, %d given' % ( - value.length()), self.position) + value.length(), ), self.position) return def _validate_out(self, option, value): @@ -300,30 +527,26 @@ class DocTag(object): if value.length() > 1: message.warn( 'out annotation takes at most 1 value, %d given' % ( - value.length()), self.position) + value.length(), ), self.position) return value_str = value.one() if value_str not in [OPT_OUT_CALLEE_ALLOCATES, OPT_OUT_CALLER_ALLOCATES]: message.warn("out annotation value is invalid: %r" % ( - value_str), self.position) + value_str, ), self.position) return - def set_position(self, position): - self.position = position - self.options.position = position - def _get_gtk_doc_value(self): def serialize_one(option, value, fmt, fmt2): if value: if type(value) != str: value = ' '.join((serialize_one(k, v, '%s=%s', '%s') - for k, v in value.all().iteritems())) + for k, v in value.all().items())) return fmt % (option, value) else: return fmt2 % (option, ) annotations = [] - for option, value in self.options.iteritems(): + for option, value in self.options.items(): annotations.append( serialize_one(option, value, '(%s %s)', '(%s)')) if annotations: @@ -345,8 +568,7 @@ class DocTag(object): # validation below is most certainly going to fail. return - for option in self.options: - value = self.options[option] + for option, value in self.options.items(): if option == OPT_ALLOW_NONE: self._validate_option(option, value, n_params=0) elif option == OPT_ARRAY: @@ -399,6 +621,7 @@ class DocTag(object): class DocOptions(object): def __init__(self): self.values = [] + self.position = None def __repr__(self): return '<DocOptions %r>' % (self.values, ) @@ -429,7 +652,7 @@ class DocOptions(object): if key == item: yield value - def iteritems(self): + def items(self): return iter(self.values) @@ -438,7 +661,7 @@ class DocOption(object): def __init__(self, tag, option): self.tag = tag self._array = [] - self._dict = {} + self._dict = OrderedDict() # (annotation option1=value1 option2=value2) etc for p in option.split(' '): if '=' in p: @@ -477,7 +700,8 @@ class AnnotationParser(object): :class:`DocTag`, :class:`DocOptions` and :class:`DocOption` objects. This parser tries to accept malformed input whenever possible and does not emit syntax errors. However, it does emit warnings at the slightest indication - of malformed input when possible. + of malformed input when possible. It is usually a good idea to heed these + warnings as malformed input is known to result in invalid GTK-Doc output. A GTK-Doc comment block can be constructed out of multiple parts that can be combined to write different types of documentation. @@ -489,42 +713,52 @@ class AnnotationParser(object): .. code-block:: c /** - * identifier_name: [annotations] - * @parameter_name: [annotations:] [description] + * identifier_name [:annotations] + * @parameter_name [:annotations] [:description] * - * description - * tag_name: [annotations:] [description] + * comment_block_description + * tag_name [:annotations] [:description] */ - - Parts and fields cannot span multiple lines, except for parameter descriptions, - tag descriptions and comment block descriptions. - - There has to be exactly 1 `identifier` part on the first line of the - comment block which consists of: - * an `identifier_name` field - * an optional `annotations` field - - There can be 0 or more `parameter` parts following the `identifier` part, - each consisting of: - * a `parameter_name` filed - * an optional `annotations` field - * an optional `description` field - - An empty lines signals the end of the `parameter` parts and the beginning - of the (free form) comment block `description` part. - - There can be 0 or 1 `description` parts following the `description` part. - - There can be 0 or more `tag` parts following the `description` part, - each consisting of: - * a `tag_name` field - * an optional `annotations` field - * an optional `description` field + The order in which the different parts have to be specified is important:: + + - There has to be exactly 1 `identifier` part on the first line of the + comment block which consists of: + * an `identifier_name` field + * an optional `annotations` field + - Followed by 0 or more `parameters` parts, each consisting of: + * a `parameter_name` field + * an optional `annotations` field + * an optional `description` field + - Followed by at least 1 empty line signaling the beginning of + the `comment_block_description` part + - Followed by an optional `comment block description` part. + - Followed by 0 or more `tag` parts, each consisting of: + * a `tag_name` field + * an optional `annotations` field + * an optional `description` field + + Additionally, the following restrictions are in effect:: + + - Parts can optionally be separated by an empty line, except between + the `parameter` parts and the `comment block description` part where + an empty line is required (see above). + - Parts and fields cannot span multiple lines, except for + `parameter descriptions`, `tag descriptions` and the + `comment_block_description` fields. + - `parameter descriptions` fields can not span multiple paragraphs. + - `tag descriptions` and `comment block description` fields can + span multiple paragraphs. .. NOTE:: :class:`AnnotationParser` functionality is heavily based on gtkdoc-mkdb's - `ScanSourceFile()`_ function and is currently in sync with gtk-doc - commit `b41641b`_. + `ScanSourceFile()`_ function and is currently in sync with GTK-Doc + commit `47abcd5`_. - .. _types of documentation: + .. _GTK-Doc's documentation: http://developer.gnome.org/gtk-doc-manual/1.18/documenting.html.en .. _ScanSourceFile(): http://git.gnome.org/browse/gtk-doc/tree/gtkdoc-mkdb.in#n3722 - .. _b41641b: b41641bd75f870afff7561ceed8a08456da57565 + .. _47abcd5: 47abcd53b8489ebceec9e394676512a181c1f1f6 """ def parse(self, comments): @@ -532,22 +766,32 @@ class AnnotationParser(object): Parses multiple GTK-Doc comment blocks. :param comments: a list of (comment, filename, lineno) tuples - :returns: a list of :class:`DocBlock` or ``None`` objects + :returns: a dictionary mapping identifier names to :class:`DocBlock` objects """ comment_blocks = {} for comment in comments: - comment_block = self.parse_comment_block(comment) + try: + comment_block = self.parse_comment_block(comment) + except Exception: + message.warn('unrecoverable parse error, please file a GObject-Introspection ' + 'bug report including the complete comment block at the ' + 'indicated location.', message.Position(comment[1], comment[2])) + continue if comment_block is not None: + # Note: previous versions of this parser did not check + # if an identifier was already stored in comment_blocks, + # so when multiple comment blocks where encountered documenting + # the same identifier the last one seen "wins". + # Keep this behavior for backwards compatibility, but + # emit a warning. if comment_block.name in comment_blocks: message.warn("multiple comment blocks documenting '%s:' identifier." % - (comment_block.name), + (comment_block.name, ), comment_block.position) - # Always store the block even if it's a duplicate for - # backward compatibility... comment_blocks[comment_block.name] = comment_block return comment_blocks @@ -561,29 +805,48 @@ class AnnotationParser(object): """ comment, filename, lineno = comment + + # Assign line numbers to each line of the comment block, + # which will later be used as the offset to calculate the + # real line number in the source file comment_lines = list(enumerate(comment.split('\n'))) # Check for the start the comment block. - if COMMENT_START_RE.search(comment_lines[0][1]): + if COMMENT_START_RE.match(comment_lines[0][1]): del comment_lines[0] else: # Not a GTK-Doc comment block. return None # Check for the end the comment block. - if COMMENT_END_RE.search(comment_lines[-1][1]): - del comment_lines[-1] + line_offset, line = comment_lines[-1] + result = COMMENT_END_RE.match(line) + if result: + description = result.group('description') + if description: + comment_lines[-1] = (line_offset, description) + position = message.Position(filename, lineno + line_offset) + marker = ' ' * result.end('description') + '^' + message.warn("Comments should end with */ on a new line:\n%s\n%s" % + (line, marker), + position) + else: + del comment_lines[-1] + else: + # Not a GTK-Doc comment block. + return None # If we get this far, we are inside a GTK-Doc comment block. return self._parse_comment_block(comment_lines, filename, lineno) def _parse_comment_block(self, comment_lines, filename, lineno): """ - Parses a single GTK-Doc comment block stripped from it's + Parses a single GTK-Doc comment block already stripped from its comment start (/**) and comment end (*/) marker lines. - :param comment_lines: GTK-Doc comment block stripped from it's comment - start (/**) and comment end (*/) marker lines + :param comment_lines: list of (line_offset, line) tuples representing a + GTK-Doc comment block already stripped from it's + start (/**) and end (*/) marker lines :param filename: source file name where the comment block originated from :param lineno: line in the source file where the comment block starts :returns: a :class:`DocBlock` object or ``None`` @@ -601,6 +864,8 @@ class AnnotationParser(object): http://git.gnome.org/browse/gtk-doc/tree/gtkdoc-mkdb.in#n3722 """ comment_block = None + part_indent = None + line_indent = None in_part = None identifier = None current_param = None @@ -621,61 +886,55 @@ class AnnotationParser(object): column_offset = result.end(0) line = line[result.end(0):] + # Store indentation level of the line. + result = COMMENT_INDENTATION_RE.match(line) + line_indent = len(result.group('indentation').replace('\t', ' ')) + #################################################################### # Check for GTK-Doc comment block identifier. #################################################################### if not comment_block: - # The correct identifier name would have the colon at the end - # but maintransformer.py does not expect us to do that. So - # make sure to compute an identifier_name without the colon and - # a real_identifier_name with the colon. - if not identifier: - result = SECTION_RE.search(line) + result = SECTION_RE.match(line) if result: identifier = IDENTIFIER_SECTION - real_identifier_name = 'SECTION:%s' % (result.group('section_name')) - identifier_name = real_identifier_name + identifier_name = 'SECTION:%s' % (result.group('section_name'), ) column = result.start('section_name') + column_offset if not identifier: - result = SYMBOL_RE.search(line) + result = SYMBOL_RE.match(line) if result: identifier = IDENTIFIER_SYMBOL - real_identifier_name = '%s:' % (result.group('symbol_name')) - identifier_name = '%s' % (result.group('symbol_name')) + identifier_name = '%s' % (result.group('symbol_name'), ) column = result.start('symbol_name') + column_offset if not identifier: - result = PROPERTY_RE.search(line) + result = PROPERTY_RE.match(line) if result: identifier = IDENTIFIER_PROPERTY - real_identifier_name = '%s:%s:' % (result.group('class_name'), - result.group('property_name')) identifier_name = '%s:%s' % (result.group('class_name'), result.group('property_name')) column = result.start('property_name') + column_offset if not identifier: - result = SIGNAL_RE.search(line) + result = SIGNAL_RE.match(line) if result: identifier = IDENTIFIER_SIGNAL - real_identifier_name = '%s::%s:' % (result.group('class_name'), - result.group('signal_name')) identifier_name = '%s::%s' % (result.group('class_name'), result.group('signal_name')) column = result.start('signal_name') + column_offset if identifier: in_part = PART_IDENTIFIER + part_indent = line_indent comment_block = DocBlock(identifier_name) - comment_block.set_position(position) + comment_block.position = position if 'colon' in result.groupdict() and result.group('colon') != ':': colon_start = result.start('colon') colon_column = column_offset + colon_start - marker = ' '*colon_column + '^' + marker = ' ' * colon_column + '^' message.warn("missing ':' at column %s:\n%s\n%s" % (colon_column + 1, original_line, marker), position) @@ -695,7 +954,7 @@ class AnnotationParser(object): # right thing to do because sooner or later some long # descriptions will contain something matching an identifier # pattern by accident. - marker = ' '*column_offset + '^' + marker = ' ' * column_offset + '^' message.warn('ignoring unrecognized GTK-Doc comment block, identifier not ' 'found:\n%s\n%s' % (original_line, marker), position) @@ -705,7 +964,7 @@ class AnnotationParser(object): #################################################################### # Check for comment block parameters. #################################################################### - result = PARAMETER_RE.search(line) + result = PARAMETER_RE.match(line) if result: param_name = result.group('parameter_name') param_annotations = result.group('annotations') @@ -714,9 +973,11 @@ class AnnotationParser(object): if in_part == PART_IDENTIFIER: in_part = PART_PARAMETERS + part_indent = line_indent + if in_part != PART_PARAMETERS: column = result.start('parameter_name') + column_offset - marker = ' '*column + '^' + marker = ' ' * column + '^' message.warn("'@%s' parameter unexpected at this location:\n%s\n%s" % (param_name, original_line, marker), position) @@ -730,17 +991,17 @@ class AnnotationParser(object): returns_seen = True else: message.warn("encountered multiple 'Returns' parameters or tags for " - "'%s'." % (comment_block.name), + "'%s'." % (comment_block.name, ), position) elif param_name in comment_block.params.keys(): column = result.start('parameter_name') + column_offset - marker = ' '*column + '^' + marker = ' ' * column + '^' message.warn("multiple '@%s' parameters for identifier '%s':\n%s\n%s" % (param_name, comment_block.name, original_line, marker), position) tag = DocTag(comment_block, param_name) - tag.set_position(position) + tag.position = position tag.comment = param_description if param_annotations: tag.options = self.parse_options(tag, param_annotations) @@ -756,28 +1017,48 @@ class AnnotationParser(object): # # When we are parsing comment block parameters or the comment block # identifier (when there are no parameters) and encounter an empty - # line, we must be parsing the comment block description + # line, we must be parsing the comment block description. #################################################################### - if (EMPTY_LINE_RE.search(line) - and (in_part == PART_IDENTIFIER or in_part == PART_PARAMETERS)): + if (EMPTY_LINE_RE.match(line) and in_part in [PART_IDENTIFIER, PART_PARAMETERS]): in_part = PART_DESCRIPTION + part_indent = line_indent continue #################################################################### # Check for GTK-Doc comment block tags. #################################################################### - result = TAG_RE.search(line) - if result: + result = TAG_RE.match(line) + if result and line_indent <= part_indent: tag_name = result.group('tag_name') tag_annotations = result.group('annotations') tag_description = result.group('description') + marker = ' ' * (result.start('tag_name') + column_offset) + '^' + + # Deprecated GTK-Doc Description: tag + if tag_name.lower() == TAG_DESCRIPTION: + message.warn("GTK-Doc tag \"Description:\" has been deprecated:\n%s\n%s" % + (original_line, marker), + position) + + in_part = PART_DESCRIPTION + part_indent = line_indent + + if not comment_block.comment: + comment_block.comment = tag_description + else: + comment_block.comment += '\n' + tag_description + continue + + # Now that the deprecated stuff is out of the way, continue parsing real tags if in_part == PART_DESCRIPTION: in_part = PART_TAGS + part_indent = line_indent + if in_part != PART_TAGS: column = result.start('tag_name') + column_offset - marker = ' '*column + '^' + marker = ' ' * column + '^' message.warn("'%s:' tag unexpected at this location:\n%s\n%s" % (tag_name, original_line, marker), position) @@ -787,7 +1068,7 @@ class AnnotationParser(object): returns_seen = True else: message.warn("encountered multiple 'Returns' parameters or tags for " - "'%s'." % (comment_block.name), + "'%s'." % (comment_block.name, ), position) tag = DocTag(comment_block, TAG_RETURNS) @@ -801,7 +1082,7 @@ class AnnotationParser(object): else: if tag_name.lower() in comment_block.tags.keys(): column = result.start('tag_name') + column_offset - marker = ' '*column + '^' + marker = ' ' * column + '^' message.warn("multiple '%s:' tags for identifier '%s':\n%s\n%s" % (tag_name, comment_block.name, original_line, marker), position) @@ -814,7 +1095,7 @@ class AnnotationParser(object): tag.options = self.parse_options(tag, tag_annotations) else: message.warn("annotations not supported for tag '%s:'." % - (tag_name), + (tag_name, ), position) comment_block.tags[tag_name.lower()] = tag current_tag = tag @@ -824,12 +1105,8 @@ class AnnotationParser(object): # If we get here, we must be in the middle of a multiline # comment block, parameter or tag description. #################################################################### - if in_part == PART_DESCRIPTION or in_part == PART_IDENTIFIER: + if in_part in [PART_IDENTIFIER, PART_DESCRIPTION]: if not comment_block.comment: - # Backwards compatibility with old style GTK-Doc - # comment blocks where Description used to be a comment - # block tag. Simply get rid of 'Description:'. - line = re.sub(DESCRIPTION_TAG_RE, '', line) comment_block.comment = line else: comment_block.comment += '\n' + line @@ -837,38 +1114,39 @@ class AnnotationParser(object): elif in_part == PART_PARAMETERS: self._validate_multiline_annotation_continuation(line, original_line, column_offset, position) - # Append to parameter description. current_param.comment += ' ' + line.strip() + continue elif in_part == PART_TAGS: self._validate_multiline_annotation_continuation(line, original_line, column_offset, position) - # Append to tag description. if current_tag.name.lower() in [TAG_RETURNS, TAG_RETURNVALUE]: current_tag.comment += ' ' + line.strip() else: current_tag.value += ' ' + line.strip() + continue ######################################################################## # Finished parsing this comment block. ######################################################################## - # We have picked up a couple of \n characters that where not - # intended. Strip those. - if comment_block.comment: - comment_block.comment = comment_block.comment.strip() - else: - comment_block.comment = '' + if comment_block: + # We have picked up a couple of \n characters that where not + # intended. Strip those. + if comment_block.comment: + comment_block.comment = comment_block.comment.strip() - for tag in comment_block.tags.itervalues(): - self._clean_comment_block_part(tag) + for tag in comment_block.tags.values(): + self._clean_comment_block_part(tag) - for param in comment_block.params.itervalues(): - self._clean_comment_block_part(param) + for param in comment_block.params.values(): + self._clean_comment_block_part(param) - # Validate and store block. - comment_block.validate() - return comment_block + # Validate and store block. + comment_block.validate() + return comment_block + else: + return None def _clean_comment_block_part(self, part): if part.comment: @@ -882,7 +1160,7 @@ class AnnotationParser(object): part.value = '' def _validate_multiline_annotation_continuation(self, line, original_line, - column_offset, position): + column_offset, position): ''' Validate parameters and tags (except the first line) and generate warnings about invalid annotations spanning multiple lines. @@ -893,26 +1171,26 @@ class AnnotationParser(object): :param position: position of `line` in the source file ''' - result = MULTILINE_ANNOTATION_CONTINUATION_RE.search(line) + result = MULTILINE_ANNOTATION_CONTINUATION_RE.match(line) if result: - line = result.group('description') column = result.start('annotations') + column_offset - marker = ' '*column + '^' + marker = ' ' * column + '^' message.warn('ignoring invalid multiline annotation continuation:\n' '%s\n%s' % (original_line, marker), position) @classmethod def parse_options(cls, tag, value): - # (foo) - # (bar opt1 opt2 ...) + # (annotation) + # (annotation opt1 opt2 ...) + # (annotation opt1=value1 opt2=value2 ...) opened = -1 options = DocOptions() options.position = tag.position for i, c in enumerate(value): if c == '(' and opened == -1: - opened = i+1 + opened = i + 1 if c == ')' and opened != -1: segment = value[opened:i] parts = segment.split(' ', 1) |