Bug 563794 - Redo annotation parsing & applying
[gnome.gobject-introspection] / giscanner / annotationparser.py
1 # -*- Mode: Python -*-
2 # GObject-Introspection - a framework for introspecting GObject libraries
3 # Copyright (C) 2008  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 - parses gtk-doc annotations
22
23 # All gtk-doc comments needs to start with this:
24 _COMMENT_HEADER = '*\n * '
25
26 from .ast import (Array, Callback, Class, Enum, Field, Function, Interface,
27                   List, Map, Parameter, Record, Return, Type, Union, Varargs,
28                   default_array_types,
29                   BASIC_GIR_TYPES,
30                   PARAM_DIRECTION_INOUT,
31                   PARAM_DIRECTION_IN,
32                   PARAM_DIRECTION_OUT,
33                   PARAM_TRANSFER_NONE,
34                   PARAM_TRANSFER_CONTAINER,
35                   PARAM_TRANSFER_FULL,
36                   TYPE_ANY, TYPE_NONE)
37 from .glibast import GLibBoxed
38
39
40 class InvalidAnnotationError(Exception):
41     pass
42
43
44 class DocBlock(object):
45
46     def __init__(self, name):
47         self.name = name
48         self.value = None
49         self.tags = {}
50
51     def __repr__(self):
52         return '<Directive %r>' % (self.name, )
53
54     def get(self, name):
55         if name == 'Returns':
56             value = self.tags.get(name)
57             if value is None:
58                 return self.tags.get('Return value')
59             else:
60                 return value
61         else:
62             return self.tags.get(name)
63
64
65 class DocTag(object):
66
67     def __init__(self, name):
68         self.name = name
69         self.options = []
70
71
72 class Option(object):
73
74     def __init__(self, option):
75         self._array = []
76         self._dict = {}
77         for p in option.split(' '):
78             if '=' in p:
79                 name, value = p.split('=', 1)
80             else:
81                 name = p
82                 value = None
83             self._dict[name] = value
84             if value is None:
85                 self._array.append(name)
86             else:
87                 self._array.append((name, value))
88
89     def __repr__(self):
90         return '<Option %r>' % (self._array, )
91
92     def one(self):
93         assert len(self._array) == 1
94         return self._array[0]
95
96     def flat(self):
97         return self._array
98
99     def all(self):
100         return self._dict
101
102
103 class AnnotationParser(object):
104
105     def __init__(self, namespace, source_scanner, transformer):
106         self._blocks = {}
107         self._namespace = namespace
108         self._transformer = transformer
109         for comment in source_scanner.get_comments():
110             self._parse_comment(comment)
111
112     def parse(self):
113         aa = AnnotationApplier(self._blocks, self._transformer)
114         aa.parse(self._namespace)
115
116     def _parse_comment(self, comment):
117         if not comment.startswith(_COMMENT_HEADER):
118             return
119         comment = comment[len(_COMMENT_HEADER):]
120         pos = comment.index('\n ')
121
122         block_name = comment[:pos]
123         block_name = block_name.strip()
124         if not block_name.endswith(':'):
125             return
126         block = DocBlock(block_name[:-1])
127         content = comment[pos+1:]
128         for line in content.split('\n'):
129             line = line[2:].strip() # Skip ' *'
130             if not line:
131                 continue
132
133             if line.startswith('@'):
134                 line = line[1:]
135             elif not ': ' in line:
136                 continue
137             tag = self._parse_tag(line)
138             block.tags[tag.name] = tag
139
140         self._blocks[block.name] = block
141
142     def _parse_tag(self, value):
143         # Tag: bar
144         # Tag: bar opt1 opt2
145         parts = value.split(': ', 1)
146         if len(parts) == 1:
147             tag_name = parts[0]
148             options = ''
149         else:
150             tag_name, options = parts
151         tag = DocTag(tag_name)
152         tag.value = options
153         tag.options = self._parse_options(options)
154         return tag
155
156     def _parse_options(self, value):
157         # (foo)
158         # (bar opt1 opt2...)
159         opened = -1
160         options = {}
161         for i, c in enumerate(value):
162             if c == '(' and opened == -1:
163                 opened = i+1
164             if c == ')' and opened != -1:
165                 segment = value[opened:i]
166                 parts = segment.split(' ', 1)
167                 if len(parts) == 2:
168                     name, option = parts
169                 elif len(parts) == 1:
170                     name = parts[0]
171                     option = None
172                 else:
173                     raise AssertionError
174                 if option is not None:
175                     option = Option(option)
176                 options[name] = option
177                 opened = -1
178         return options
179
180
181 class AnnotationApplier(object):
182
183     def __init__(self, blocks, transformer):
184         self._blocks = blocks
185         self._transformer = transformer
186
187     def _get_tag(self, block, tag_name):
188         if block is None:
189             return None
190
191         return block.get(tag_name)
192
193     def parse(self, namespace):
194         for node in namespace.nodes:
195             self._parse_node(node)
196
197     # Boring parsing boilerplate.
198
199     def _parse_node(self, node):
200         if isinstance(node, Function):
201             self._parse_function(node)
202         elif isinstance(node, Enum):
203             self._parse_enum(node)
204         elif isinstance(node, Class):
205             self._parse_class(node)
206         elif isinstance(node, Interface):
207             self._parse_interface(node)
208         elif isinstance(node, Callback):
209             self._parse_callback(node)
210         elif isinstance(node, Record):
211             self._parse_record(node)
212         elif isinstance(node, Union):
213             self._parse_union(node)
214         elif isinstance(node, GLibBoxed):
215             self._parse_boxed(node)
216
217     def _parse_class(self, class_):
218         block = self._blocks.get(class_.name)
219         self._parse_version(class_, block)
220         self._parse_constructors(class_.constructors)
221         self._parse_methods(class_.methods)
222         self._parse_methods(class_.static_methods)
223         self._parse_properties(class_.properties)
224         self._parse_signals(class_.signals)
225         self._parse_fields(class_, class_.fields)
226
227     def _parse_interface(self, interface):
228         block = self._blocks.get(interface.name)
229         self._parse_version(interface, block)
230         self._parse_methods(interface.methods)
231         self._parse_properties(interface.properties)
232         self._parse_signals(interface.signals)
233         self._parse_fields(interface, interface.fields)
234
235     def _parse_record(self, record):
236         block = self._blocks.get(record.symbol)
237         self._parse_version(record, block)
238         self._parse_constructors(record.constructors)
239         self._parse_fields(record, record.fields)
240         if isinstance(record, GLibBoxed):
241             self._parse_methods(record.methods)
242
243     def _parse_boxed(self, boxed):
244         block = self._blocks.get(boxed.name)
245         self._parse_version(boxed, block)
246         self._parse_constructors(boxed.constructors)
247         self._parse_methods(boxed.methods)
248
249     def _parse_union(self, union):
250         block = self._blocks.get(union.name)
251         self._parse_fields(union, union.fields)
252         self._parse_constructors(union.constructors)
253         if isinstance(union, GLibBoxed):
254             self._parse_methods(union.methods)
255
256     def _parse_enum(self, enum):
257         block = self._blocks.get(enum.symbol)
258         self._parse_version(enum, block)
259
260     def _parse_constructors(self, constructors):
261         for ctor in constructors:
262             self._parse_function(ctor)
263
264     def _parse_fields(self, parent, fields):
265         for field in fields:
266             self._parse_field(parent, field)
267
268     def _parse_properties(self, properties):
269         for prop in properties:
270             self._parse_property(property)
271
272     def _parse_methods(self, methods):
273         for method in methods:
274             self._parse_function(method)
275
276     def _parse_signals(self, signals):
277         for signal in signals:
278             self._parse_signal(signal)
279
280     def _parse_property(self, prop):
281         pass
282
283     def _parse_callback(self, callback):
284         block = self._blocks.get(callback.ctype)
285         self._parse_version(callback, block)
286         self._parse_params(callback, callback.parameters, block)
287         self._parse_return(callback, callback.retval, block)
288
289     def _parse_function(self, func):
290         block = self._blocks.get(func.symbol)
291         self._parse_version(func, block)
292         self._parse_deprecated(func, block)
293         self._parse_params(func, func.parameters, block)
294         self._parse_return(func, func.retval, block)
295
296     def _parse_signal(self, signal):
297         block = self._blocks.get(signal.name)
298         self._parse_version(signal, block)
299         self._parse_params(signal, signal.parameters, block)
300         self._parse_return(signal, signal.retval, block)
301
302     def _parse_field(self, parent, field):
303         if isinstance(field, Callback):
304             self._parse_callback(field)
305
306     def _parse_params(self, parent, params, block):
307         for param in params:
308             self._parse_param(parent, param, block)
309
310     def _parse_return(self, parent, return_, block):
311         tag = self._get_tag(block, 'Returns')
312         options = getattr(tag, 'options', {})
313         self._parse_param_ret_common(parent, return_, options)
314
315     def _parse_param(self, parent, param, block):
316         tag = self._get_tag(block, param.name)
317         options = getattr(tag, 'options', {})
318
319         if isinstance(parent, Function):
320             scope = options.get('scope')
321             if scope:
322                 param.scope = scope.one()
323                 param.transfer = PARAM_TRANSFER_NONE
324         self._parse_param_ret_common(parent, param, options)
325
326     def _parse_param_ret_common(self, parent, node, options):
327         node.direction = self._extract_direction(node, options)
328         container_type = self._extract_container_type(
329             parent, node, options)
330         if container_type is not None:
331             node.type = container_type
332         if not node.direction:
333             node.direction = self._guess_direction(node)
334         node.transfer = self._extract_transfer(parent, node, options)
335         if 'allow-none' in options:
336             node.allow_none = True
337
338         assert node.transfer is not None
339
340     def _extract_direction(self, node, options):
341         if ('inout' in options or
342             'in-out' in options):
343             direction = PARAM_DIRECTION_INOUT
344         elif 'out' in options:
345             direction = PARAM_DIRECTION_OUT
346         elif 'in' in options:
347             direction = PARAM_DIRECTION_IN
348         else:
349             direction = node.direction
350         return direction
351
352     def _guess_array(self, node):
353         ctype = node.type.ctype
354         if ctype is None:
355             return False
356         if not ctype.endswith('*'):
357             return False
358         if node.type.canonical in default_array_types:
359             return True
360         return False
361
362     def _extract_container_type(self, parent, node, options):
363         has_element_type = 'element-type' in options
364         has_array = 'array' in options
365
366         # FIXME: This is a hack :-(
367         if (not isinstance(node, Field) and
368             (not has_element_type and
369              (node.direction is None
370               or node.direction == PARAM_DIRECTION_IN))):
371             if self._guess_array(node):
372                 has_array = True
373
374         if has_array:
375             container_type = self._parse_array(parent, node, options)
376         elif has_element_type:
377             container_type = self._parse_element_type(parent, node, options)
378         else:
379             container_type = None
380
381         return container_type
382
383     def _parse_array(self, parent, node, options):
384         array_opt = options.get('array')
385         if array_opt:
386             values = array_opt.all()
387         else:
388             values = {}
389         container_type = Array(node.type.ctype, node.type.name)
390         if 'zero-terminated' in values:
391             container_type.zeroterminated = values.get(
392                 'zero-terminated') == '1'
393         length = values.get('length')
394         if length is not None:
395             param_index = parent.get_parameter_index(length)
396             container_type.length_param_index = param_index
397         container_type.size = values.get('fized-size')
398         return container_type
399
400     def _parse_element_type(self, parent, node, options):
401         element_type_opt = options.get('element-type')
402         element_type = element_type_opt.flat()
403         if node.type.name in ['GLib.List', 'GLib.SList']:
404             assert len(element_type) == 1
405             etype = Type(element_type[0])
406             container_type = List(
407                 node.type.name,
408                 node.type.ctype,
409                 self._transformer.resolve_param_type(etype))
410         elif node.type.name in ['GLib.HashTable']:
411             assert len(element_type) == 2
412             key_type = Type(element_type[0])
413             value_type = Type(element_type[1])
414             container_type = Map(
415                 node.type.name,
416                 node.type.ctype,
417                 self._transformer.resolve_param_type(key_type),
418                 self._transformer.resolve_param_type(value_type))
419         else:
420             print 'FIXME: unhandled element-type container:', node
421         return container_type
422
423     def _extract_transfer(self, parent, node, options):
424         transfer_opt = options.get('transfer')
425         if transfer_opt is None:
426             transfer = self._guess_transfer(node, options)
427         else:
428             transfer = transfer_opt.one()
429             if transfer is None:
430                 transfer = PARAM_TRANSFER_FULL
431             if transfer not in [PARAM_TRANSFER_NONE,
432                                 PARAM_TRANSFER_CONTAINER,
433                                 PARAM_TRANSFER_FULL]:
434                 raise InvalidAnnotationError(
435                     "transfer for %s of %r is invalid (%r), must be one of "
436                     "none, container, full." % (node, parent.name, transfer))
437         return transfer
438
439     def _parse_version(self, node, block):
440         since_tag = self._get_tag(block, 'Since')
441         if since_tag is None:
442             return
443         node.version = since_tag.value
444
445     def _parse_deprecated(self, node, block):
446         deprecated_tag = self._get_tag(block, 'Deprecated')
447         if deprecated_tag is None:
448             return
449         value = deprecated_tag.value
450         if ': ' in value:
451             version, desc = value.split(': ')
452         else:
453             desc = value
454             version = None
455         node.deprecated = desc
456         if version is not None:
457             node.deprecated_version = version
458
459     def _guess_direction(self, node):
460         if node.direction:
461             return node.direction
462         is_pointer = False
463         if node.type.ctype:
464             is_pointer = '*' in node.type.ctype
465
466         if is_pointer and node.type.name in BASIC_GIR_TYPES:
467             return PARAM_DIRECTION_OUT
468
469         return PARAM_DIRECTION_IN
470
471     def _guess_transfer(self, node, options):
472         if node.transfer is not None:
473             return node.transfer
474
475         if isinstance(node.type, Array):
476             return PARAM_TRANSFER_NONE
477         # Anything with 'const' gets none
478         if node.type.is_const:
479             return PARAM_TRANSFER_NONE
480
481         elif node.type.name in [TYPE_NONE, TYPE_ANY]:
482             return PARAM_TRANSFER_NONE
483         elif isinstance(node.type, Varargs):
484             return PARAM_TRANSFER_NONE
485         elif isinstance(node, Parameter):
486             if node.direction in [PARAM_DIRECTION_INOUT,
487                                   PARAM_DIRECTION_OUT]:
488                 return PARAM_TRANSFER_FULL
489             # This one is a hack for compatibility; the transfer
490             # for string parameters really has no defined meaning.
491             elif node.type.canonical == 'utf8':
492                 return PARAM_TRANSFER_FULL
493             else:
494                 return PARAM_TRANSFER_NONE
495         elif isinstance(node, Return):
496             if (node.type.canonical in BASIC_GIR_TYPES or
497                 (node.type.canonical in [TYPE_NONE, TYPE_ANY] and
498                  node.type.is_const)):
499                 return PARAM_TRANSFER_NONE
500             else:
501                 return PARAM_TRANSFER_FULL
502         elif isinstance(node, Field):
503             return PARAM_TRANSFER_NONE
504         else:
505             raise AssertionError(node)