Initial packaging for Tizen
[profile/ivi/gobject-introspection.git] / giscanner / annotationparser.py
1 # -*- Mode: Python -*-
2 # GObject-Introspection - a framework for introspecting GObject libraries
3 # Copyright (C) 2008-2010 Johan Dahlin
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 # 02110-1301, USA.
19 #
20
21 # AnnotationParser - extract annotations from gtk-doc comments
22
23 import re
24
25 from . import message
26 from .odict import odict
27
28 # Tags - annotations applied to comment blocks
29 TAG_VFUNC = 'virtual'
30 TAG_SINCE = 'since'
31 TAG_STABILITY = 'stability'
32 TAG_DEPRECATED = 'deprecated'
33 TAG_RETURNS = 'returns'
34 TAG_ATTRIBUTES = 'attributes'
35 TAG_RENAME_TO = 'rename to'
36 TAG_TYPE = 'type'
37 TAG_UNREF_FUNC = 'unref func'
38 TAG_REF_FUNC = 'ref func'
39 TAG_SET_VALUE_FUNC = 'set value func'
40 TAG_GET_VALUE_FUNC = 'get value func'
41 TAG_TRANSFER = 'transfer'
42 TAG_VALUE = 'value'
43 _ALL_TAGS = [TAG_VFUNC,
44              TAG_SINCE,
45              TAG_STABILITY,
46              TAG_DEPRECATED,
47              TAG_RETURNS,
48              TAG_ATTRIBUTES,
49              TAG_RENAME_TO,
50              TAG_TYPE,
51              TAG_UNREF_FUNC,
52              TAG_REF_FUNC,
53              TAG_SET_VALUE_FUNC,
54              TAG_GET_VALUE_FUNC,
55              TAG_TRANSFER,
56              TAG_VALUE]
57
58 # Options - annotations for parameters and return values
59 OPT_ALLOW_NONE = 'allow-none'
60 OPT_ARRAY = 'array'
61 OPT_ATTRIBUTE = 'attribute'
62 OPT_CLOSURE = 'closure'
63 OPT_DESTROY = 'destroy'
64 OPT_ELEMENT_TYPE = 'element-type'
65 OPT_FOREIGN = 'foreign'
66 OPT_IN = 'in'
67 OPT_INOUT = 'inout'
68 OPT_INOUT_ALT = 'in-out'
69 OPT_OUT = 'out'
70 OPT_SCOPE = 'scope'
71 OPT_TRANSFER = 'transfer'
72 OPT_TYPE = 'type'
73 OPT_SKIP = 'skip'
74 OPT_CONSTRUCTOR = 'constructor'
75 OPT_METHOD = 'method'
76
77 ALL_OPTIONS = [
78     OPT_ALLOW_NONE,
79     OPT_ARRAY,
80     OPT_ATTRIBUTE,
81     OPT_CLOSURE,
82     OPT_DESTROY,
83     OPT_ELEMENT_TYPE,
84     OPT_FOREIGN,
85     OPT_IN,
86     OPT_INOUT,
87     OPT_INOUT_ALT,
88     OPT_OUT,
89     OPT_SCOPE,
90     OPT_TRANSFER,
91     OPT_TYPE,
92     OPT_SKIP,
93     OPT_CONSTRUCTOR,
94     OPT_METHOD]
95
96 # Array options - array specific annotations
97 OPT_ARRAY_FIXED_SIZE = 'fixed-size'
98 OPT_ARRAY_LENGTH = 'length'
99 OPT_ARRAY_ZERO_TERMINATED = 'zero-terminated'
100
101 # Out options
102 OPT_OUT_CALLER_ALLOCATES = 'caller-allocates'
103 OPT_OUT_CALLEE_ALLOCATES = 'callee-allocates'
104
105 # Scope options
106 OPT_SCOPE_ASYNC = 'async'
107 OPT_SCOPE_CALL = 'call'
108 OPT_SCOPE_NOTIFIED = 'notified'
109
110 # Transfer options
111 OPT_TRANSFER_NONE = 'none'
112 OPT_TRANSFER_CONTAINER = 'container'
113 OPT_TRANSFER_FULL = 'full'
114 OPT_TRANSFER_FLOATING = 'floating'
115
116
117 class DocBlock(object):
118
119     def __init__(self, name):
120         self.name = name
121         self.options = DocOptions()
122         self.value = None
123         self.tags = odict()
124         self.comment = None
125         self.params = []
126         self.position = None
127
128     def __cmp__(self, other):
129         return cmp(self.name, other.name)
130
131     def __repr__(self):
132         return '<DocBlock %r %r>' % (self.name, self.options)
133
134     def set_position(self, position):
135         self.position = position
136         self.options.position = position
137
138     def get(self, name):
139         return self.tags.get(name)
140
141     def to_gtk_doc(self):
142         options = ''
143         if self.options:
144             options += ' '
145             options += ' '.join('(%s)' % o for o in self.options)
146         lines = [self.name]
147         if 'SECTION' not in self.name:
148             lines[0] += ':'
149         lines[0] += options
150         tags = []
151         for name, tag in self.tags.iteritems():
152             if name in self.params:
153                 lines.append(tag.to_gtk_doc_param())
154             else:
155                 tags.append(tag)
156
157         lines.append('')
158         for l in self.comment.split('\n'):
159             lines.append(l)
160         if tags:
161             lines.append('')
162             for tag in tags:
163                 lines.append(tag.to_gtk_doc_tag())
164
165         comment = ''
166         #comment += '# %d \"%s\"\n' % (
167         #    self.position.line,
168         #    self.position.filename)
169         comment += '/**\n'
170         for line in lines:
171             line = line.rstrip()
172             if line:
173                 comment += ' * %s\n' % (line, )
174             else:
175                 comment += ' *\n'
176         comment += ' */\n'
177         return comment
178
179     def validate(self):
180         for tag in self.tags.values():
181             tag.validate()
182
183
184 class DocTag(object):
185
186     def __init__(self, block, name):
187         self.block = block
188         self.name = name
189         self.options = DocOptions()
190         self.comment = None
191         self.value = ''
192         self.position = None
193
194     def __repr__(self):
195         return '<DocTag %r %r>' % (self.name, self.options)
196
197     def _validate_option(self, name, value, required=False,
198                          n_params=None, choices=None):
199         if required and value is None:
200             message.warn('%s annotation needs a value' % (
201                 name, ), self.position)
202             return
203
204         if n_params is not None:
205             if n_params == 0:
206                 s = 'no value'
207             elif n_params == 1:
208                 s = 'one value'
209             else:
210                 s = '%d values' % (n_params, )
211             if ((n_params > 0 and (value is None or value.length() != n_params)) or
212                 n_params == 0 and value is not None):
213                 if value is None:
214                     length = 0
215                 else:
216                     length = value.length()
217                 message.warn('%s annotation needs %s, not %d' % (
218                     name, s, length), self.position)
219                 return
220
221         if choices is not None:
222             valuestr = value.one()
223             if valuestr not in choices:
224                 message.warn('invalid %s annotation value: %r' % (
225                     name, valuestr, ), self.position)
226                 return
227
228     def set_position(self, position):
229         self.position = position
230         self.options.position = position
231
232     def _get_gtk_doc_value(self):
233         def serialize_one(option, value, fmt, fmt2):
234             if value:
235                 if type(value) != str:
236                     value = ' '.join((serialize_one(k, v, '%s=%s', '%s')
237                                       for k, v in value.all().iteritems()))
238                 return fmt % (option, value)
239             else:
240                 return fmt2 % (option, )
241         annotations = []
242         for option, value in self.options.iteritems():
243             annotations.append(
244                 serialize_one(option, value, '(%s %s)', '(%s)'))
245         if annotations:
246             return ' '.join(annotations) + ': '
247         else:
248             return self.value
249
250     def to_gtk_doc_param(self):
251         return '@%s: %s%s' % (self.name, self._get_gtk_doc_value(), self.comment)
252
253     def to_gtk_doc_tag(self):
254         return '%s: %s%s' % (self.name.capitalize(),
255                              self._get_gtk_doc_value(),
256                              self.comment or '')
257
258     def validate(self):
259         for option in self.options:
260             value = self.options[option]
261             if option == OPT_ALLOW_NONE:
262                 self._validate_option('allow-none', value, n_params=0)
263             elif option == OPT_ARRAY:
264                 if value is None:
265                     continue
266                 for name, v in value.all().iteritems():
267                     if name in [OPT_ARRAY_ZERO_TERMINATED, OPT_ARRAY_FIXED_SIZE]:
268                         try:
269                             int(v)
270                         except (TypeError, ValueError):
271                             if v is None:
272                                 message.warn(
273                                     'array option %s needs a value' % (
274                                     name, ),
275                                     positions=self.position)
276                             else:
277                                 message.warn(
278                                     'invalid array %s option value %r, '
279                                     'must be an integer' % (name, v, ),
280                                     positions=self.position)
281                             continue
282                     elif name == OPT_ARRAY_LENGTH:
283                         if v is None:
284                             message.warn(
285                                 'array option length needs a value',
286                                 positions=self.position)
287                             continue
288                     else:
289                         message.warn(
290                             'invalid array annotation value: %r' % (
291                             name, ), self.position)
292
293             elif option == OPT_ATTRIBUTE:
294                 self._validate_option('attribute', value, n_params=2)
295             elif option == OPT_CLOSURE:
296                 if value is not None and value.length() > 1:
297                     message.warn(
298                         'closure takes at maximium 1 value, %d given' % (
299                         value.length()), self.position)
300                     continue
301             elif option == OPT_DESTROY:
302                 self._validate_option('destroy', value, n_params=1)
303             elif option == OPT_ELEMENT_TYPE:
304                 self._validate_option('element-type', value, required=True)
305                 if value is None:
306                     message.warn(
307                         'element-type takes at least one value, none given',
308                         self.position)
309                     continue
310                 if value.length() > 2:
311                     message.warn(
312                         'element-type takes at maximium 2 values, %d given' % (
313                         value.length()), self.position)
314                     continue
315             elif option == OPT_FOREIGN:
316                 self._validate_option('foreign', value, n_params=0)
317             elif option == OPT_IN:
318                 self._validate_option('in', value, n_params=0)
319             elif option in [OPT_INOUT, OPT_INOUT_ALT]:
320                 self._validate_option('inout', value, n_params=0)
321             elif option == OPT_OUT:
322                 if value is None:
323                     continue
324                 if value.length() > 1:
325                     message.warn(
326                         'out annotation takes at maximium 1 value, %d given' % (
327                         value.length()), self.position)
328                     continue
329                 value_str = value.one()
330                 if value_str not in [OPT_OUT_CALLEE_ALLOCATES,
331                                      OPT_OUT_CALLER_ALLOCATES]:
332                     message.warn("out annotation value is invalid: %r" % (
333                         value_str), self.position)
334                     continue
335             elif option == OPT_SCOPE:
336                 self._validate_option(
337                     'scope', value, required=True,
338                     n_params=1,
339                     choices=[OPT_SCOPE_ASYNC,
340                              OPT_SCOPE_CALL,
341                              OPT_SCOPE_NOTIFIED])
342             elif option == OPT_SKIP:
343                 self._validate_option('skip', value, n_params=0)
344             elif option == OPT_TRANSFER:
345                 self._validate_option(
346                     'transfer', value, required=True,
347                     n_params=1,
348                     choices=[OPT_TRANSFER_FULL,
349                              OPT_TRANSFER_CONTAINER,
350                              OPT_TRANSFER_NONE,
351                              OPT_TRANSFER_FLOATING])
352             elif option == OPT_TYPE:
353                 self._validate_option('type', value, required=True,
354                                       n_params=1)
355             elif option == OPT_CONSTRUCTOR:
356                 self._validate_option('constructor', value, n_params=0)
357             elif option == OPT_METHOD:
358                 self._validate_option('method', value, n_params=0)
359             else:
360                 message.warn('invalid annotation option: %s' % (option, ),
361                              self.position)
362
363
364 class DocOptions(object):
365     def __init__(self):
366         self.values = []
367
368     def __getitem__(self, item):
369         for key, value in self.values:
370             if key == item:
371                 return value
372         raise KeyError
373
374     def __nonzero__(self):
375         return bool(self.values)
376
377     def __iter__(self):
378         return (k for k, v in self.values)
379
380     def add(self, name, value):
381         self.values.append((name, value))
382
383     def get(self, item, default=None):
384         for key, value in self.values:
385             if key == item:
386                 return value
387         return default
388
389     def getall(self, item):
390         for key, value in self.values:
391             if key == item:
392                 yield value
393
394     def iteritems(self):
395         return iter(self.values)
396
397
398 class DocOption(object):
399
400     def __init__(self, tag, option):
401         self.tag = tag
402         self._array = []
403         self._dict = {}
404         # (annotation option1=value1 option2=value2) etc
405         for p in option.split(' '):
406             if '=' in p:
407                 name, value = p.split('=', 1)
408             else:
409                 name = p
410                 value = None
411             self._dict[name] = value
412             if value is None:
413                 self._array.append(name)
414             else:
415                 self._array.append((name, value))
416
417     def __repr__(self):
418         return '<DocOption %r>' % (self._array, )
419
420     def length(self):
421         return len(self._array)
422
423     def one(self):
424         assert len(self._array) == 1
425         return self._array[0]
426
427     def flat(self):
428         return self._array
429
430     def all(self):
431         return self._dict
432
433
434 class AnnotationParser(object):
435     COMMENT_HEADER_RE = re.compile(r'^\*[ \t]*\n[\t ]')
436     COMMENT_HEADER_START_RE = re.compile(r'\n[\t ]')
437     WHITESPACE_RE = re.compile(r'^\s*$')
438     OPTION_RE = re.compile(r'\([A-Za-z]+[^(]*\)')
439     RETURNS_RE = re.compile(r'^return(s?)( value)?:', re.IGNORECASE)
440
441     def __init__(self):
442         self._blocks = {}
443
444     def parse(self, comments):
445         for comment in comments:
446             self._parse_comment(comment)
447         return self._blocks
448
449     def _parse_comment(self, cmt):
450         # We're looking for gtk-doc comments here, they look like this:
451         # /**
452         #   * symbol:
453         #
454         # Or, alternatively, with options:
455         # /**
456         #   * symbol: (name value) ...
457         #
458         # symbol is currently one of:
459         #  - function: gtk_widget_show
460         #  - signal:   GtkWidget::destroy
461         #  - property: GtkWidget:visible
462         #
463         comment, filename, lineno = cmt
464         comment = comment.lstrip()
465         if not self.COMMENT_HEADER_RE.search(comment):
466             return
467         comment = self.COMMENT_HEADER_RE.sub('', comment, count=1)
468         comment = comment.strip()
469         if not comment.startswith('* '):
470             return
471         comment = comment[2:]
472
473         match = self.COMMENT_HEADER_START_RE.search(comment)
474         if match is None:
475             return
476         pos = match.start()
477         block_header = comment[:pos]
478         block_header = block_header.strip()
479         cpos = block_header.find(': ')
480         block_name = block_header
481         raw_name = block_header
482         if cpos != -1:
483             block_name = block_name[:cpos].strip()
484         if block_name.endswith(':'):
485             block_name = block_name[:-1]
486         block = DocBlock(block_name)
487         block.set_position(message.Position(filename, lineno))
488
489         if cpos:
490             block.options = self.parse_options(block, block_header[cpos+2:])
491         comment_lines = []
492         parsing_parameters = True
493         last_param_tag = None
494
495         # Second phase: parse parameters, return values, Tag: format
496         # annotations.
497         #
498         # Valid lines look like:
499         # * @foo: some comment here
500         # * @baz: (inout): This has an annotation
501         # * @bar: (out) (allow-none): this is a long parameter comment
502         # *  that gets wrapped to the next line.
503         # *
504         # * Some documentation for the function.
505         # *
506         # * Returns: (transfer none): A value
507
508         # offset of the first doctag in relation to the start of
509         # the docblock, we parsed /** and the xxx: lines already
510         lineno = 2
511         for line in comment[pos+1:].split('\n'):
512             line = line.lstrip()
513             if not line.startswith('*'):
514                 lineno += 1
515                 continue
516             nostar_line = line[1:]
517             is_whitespace = self.WHITESPACE_RE.match(nostar_line) is not None
518             if parsing_parameters and is_whitespace:
519                 # As soon as we find a line that's just whitespace,
520                 # we're done parsing the parameters.
521                 parsing_parameters = False
522                 lineno += 1
523                 continue
524             elif is_whitespace:
525                 comment_lines.append('')
526                 lineno += 1
527                 continue
528
529             # Explicitly only accept parameters of the form "* @foo" with one space.
530             is_parameter = nostar_line.startswith(' @')
531
532             # Strip the rest of the leading whitespace for the rest of
533             # the code; may not actually be necessary, but still doing
534             # it to avoid regressions.
535             line = nostar_line.lstrip()
536
537             # Look for a parameter or return value.  Both of these can
538             # have parenthesized options.
539             first_colonspace_index = line.find(': ')
540             is_return_value = self.RETURNS_RE.search(line)
541             parse_options = True
542             if ((is_parameter or is_return_value)
543                 and first_colonspace_index > 0):
544                 # Skip lines which has non-whitespace before first (
545                 first_paren = line[first_colonspace_index+1:].find('(')
546                 if (first_paren != -1 and
547                     line[first_colonspace_index+1:first_paren].strip()):
548                     parse_options = False
549
550                 if is_parameter:
551                     argname = line[1:first_colonspace_index]
552                 else:
553                     argname = TAG_RETURNS
554                 tag = DocTag(block, argname)
555                 tag.set_position(block.position.offset(lineno))
556                 line_after_first_colon_space = line[first_colonspace_index + 2:]
557                 second_colon_index = line_after_first_colon_space.find(':')
558                 if second_colon_index >= 0:
559                     second_colon_index += first_colonspace_index + 2
560                     assert line[second_colon_index] == ':'
561                 found_options = False
562                 if second_colon_index > first_colonspace_index:
563                     value_line = \
564                       line[first_colonspace_index+2:second_colon_index]
565                     if ')' in value_line:
566                         after_last_paren = value_line[value_line.rfind(')'):]
567                         if not after_last_paren.rstrip().endswith(')'):
568                             parse_options = False
569                     if parse_options and self.OPTION_RE.search(value_line):
570                         # The OPTION_RE is a little bit heuristic.  If
571                         # we found two colons, we scan inside for something
572                         # that looks like (foo).
573                         # *Ideally* we'd change the gtk-doc format to
574                         # require double colons, and then there'd be
575                         # no ambiguity.  I.e.:
576                         # @foo:: Some documentation here
577                         # But that'd be a rather incompatible change.
578                         found_options = True
579                         tag.comment = line[second_colon_index+1:].strip()
580                         tag.options = self.parse_options(tag, value_line)
581                 if not found_options:
582                     # We didn't find any options, so just take the whole thing
583                     # as documentation.
584                     tag.comment = line[first_colonspace_index+2:].strip()
585                 block.tags[argname] = tag
586                 last_param_tag = tag
587                 if is_parameter:
588                     block.params.append(argname)
589             elif (not is_parameter) and parsing_parameters and last_param_tag:
590                 # We need to handle continuation lines on parameters.  The
591                 # conditional above - if a line doesn't start with '@', we're
592                 # not yet in the documentation block for the whole function,
593                 # and we've seen at least one parameter.
594                 last_param_tag.comment += (' ' + line.strip())
595             elif first_colonspace_index > 0:
596                 # The line is of the form "Tag: some value here", like:
597                 # Since: 0.8
598                 tag_name = line[:first_colonspace_index]
599                 if tag_name.lower() in _ALL_TAGS:
600                     tag_name = tag_name.lower()
601                     tag = DocTag(block, tag_name)
602                     tag.value = line[first_colonspace_index+2:]
603                     tag.position = block.position.offset(lineno)
604                     block.tags[tag_name] = tag
605                 else:
606                     comment_lines.append(line)
607             elif not parsing_parameters:
608                 comment_lines.append(line)
609             lineno += 1
610         block.comment = '\n'.join(comment_lines).strip()
611         block.validate()
612         self._blocks[block.name] = block
613
614     @classmethod
615     def parse_options(cls, tag, value):
616         # (foo)
617         # (bar opt1 opt2...)
618         opened = -1
619         options = DocOptions()
620         options.position = tag.position
621         last = None
622         for i, c in enumerate(value):
623             if c == '(' and opened == -1:
624                 opened = i+1
625             if c == ')' and opened != -1:
626                 segment = value[opened:i]
627                 parts = segment.split(' ', 1)
628                 if len(parts) == 2:
629                     name, option = parts
630                 elif len(parts) == 1:
631                     name = parts[0]
632                     option = None
633                 else:
634                     raise AssertionError
635                 if option is not None:
636                     option = DocOption(tag, option)
637                 options.add(name, option)
638                 last = i + 2
639                 opened = -1
640
641         return options