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