summaryrefslogtreecommitdiff
path: root/giscanner/annotationparser.py
diff options
context:
space:
mode:
Diffstat (limited to 'giscanner/annotationparser.py')
-rw-r--r--giscanner/annotationparser.py556
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)