]> git.proxmox.com Git - mirror_qemu.git/blob - scripts/qapi/parser.py
Merge remote-tracking branch 'remotes/xtensa/tags/20191023-xtensa' into staging
[mirror_qemu.git] / scripts / qapi / parser.py
1 # -*- coding: utf-8 -*-
2 #
3 # QAPI schema parser
4 #
5 # Copyright IBM, Corp. 2011
6 # Copyright (c) 2013-2019 Red Hat Inc.
7 #
8 # Authors:
9 # Anthony Liguori <aliguori@us.ibm.com>
10 # Markus Armbruster <armbru@redhat.com>
11 # Marc-André Lureau <marcandre.lureau@redhat.com>
12 # Kevin Wolf <kwolf@redhat.com>
13 #
14 # This work is licensed under the terms of the GNU GPL, version 2.
15 # See the COPYING file in the top-level directory.
16
17 import os
18 import re
19 import sys
20 from collections import OrderedDict
21
22 from qapi.error import QAPIParseError, QAPISemError
23 from qapi.source import QAPISourceInfo
24
25
26 class QAPISchemaParser(object):
27
28 def __init__(self, fname, previously_included=None, incl_info=None):
29 previously_included = previously_included or set()
30 previously_included.add(os.path.abspath(fname))
31
32 try:
33 if sys.version_info[0] >= 3:
34 fp = open(fname, 'r', encoding='utf-8')
35 else:
36 fp = open(fname, 'r')
37 self.src = fp.read()
38 except IOError as e:
39 raise QAPISemError(incl_info or QAPISourceInfo(None, None, None),
40 "can't read %s file '%s': %s"
41 % ("include" if incl_info else "schema",
42 fname,
43 e.strerror))
44
45 if self.src == '' or self.src[-1] != '\n':
46 self.src += '\n'
47 self.cursor = 0
48 self.info = QAPISourceInfo(fname, 1, incl_info)
49 self.line_pos = 0
50 self.exprs = []
51 self.docs = []
52 self.accept()
53 cur_doc = None
54
55 while self.tok is not None:
56 info = self.info
57 if self.tok == '#':
58 self.reject_expr_doc(cur_doc)
59 cur_doc = self.get_doc(info)
60 self.docs.append(cur_doc)
61 continue
62
63 expr = self.get_expr(False)
64 if 'include' in expr:
65 self.reject_expr_doc(cur_doc)
66 if len(expr) != 1:
67 raise QAPISemError(info, "invalid 'include' directive")
68 include = expr['include']
69 if not isinstance(include, str):
70 raise QAPISemError(info,
71 "value of 'include' must be a string")
72 incl_fname = os.path.join(os.path.dirname(fname),
73 include)
74 self.exprs.append({'expr': {'include': incl_fname},
75 'info': info})
76 exprs_include = self._include(include, info, incl_fname,
77 previously_included)
78 if exprs_include:
79 self.exprs.extend(exprs_include.exprs)
80 self.docs.extend(exprs_include.docs)
81 elif "pragma" in expr:
82 self.reject_expr_doc(cur_doc)
83 if len(expr) != 1:
84 raise QAPISemError(info, "invalid 'pragma' directive")
85 pragma = expr['pragma']
86 if not isinstance(pragma, dict):
87 raise QAPISemError(
88 info, "value of 'pragma' must be an object")
89 for name, value in pragma.items():
90 self._pragma(name, value, info)
91 else:
92 expr_elem = {'expr': expr,
93 'info': info}
94 if cur_doc:
95 if not cur_doc.symbol:
96 raise QAPISemError(
97 cur_doc.info, "definition documentation required")
98 expr_elem['doc'] = cur_doc
99 self.exprs.append(expr_elem)
100 cur_doc = None
101 self.reject_expr_doc(cur_doc)
102
103 @staticmethod
104 def reject_expr_doc(doc):
105 if doc and doc.symbol:
106 raise QAPISemError(
107 doc.info,
108 "documentation for '%s' is not followed by the definition"
109 % doc.symbol)
110
111 def _include(self, include, info, incl_fname, previously_included):
112 incl_abs_fname = os.path.abspath(incl_fname)
113 # catch inclusion cycle
114 inf = info
115 while inf:
116 if incl_abs_fname == os.path.abspath(inf.fname):
117 raise QAPISemError(info, "inclusion loop for %s" % include)
118 inf = inf.parent
119
120 # skip multiple include of the same file
121 if incl_abs_fname in previously_included:
122 return None
123
124 return QAPISchemaParser(incl_fname, previously_included, info)
125
126 def _pragma(self, name, value, info):
127 if name == 'doc-required':
128 if not isinstance(value, bool):
129 raise QAPISemError(info,
130 "pragma 'doc-required' must be boolean")
131 info.pragma.doc_required = value
132 elif name == 'returns-whitelist':
133 if (not isinstance(value, list)
134 or any([not isinstance(elt, str) for elt in value])):
135 raise QAPISemError(
136 info,
137 "pragma returns-whitelist must be a list of strings")
138 info.pragma.returns_whitelist = value
139 elif name == 'name-case-whitelist':
140 if (not isinstance(value, list)
141 or any([not isinstance(elt, str) for elt in value])):
142 raise QAPISemError(
143 info,
144 "pragma name-case-whitelist must be a list of strings")
145 info.pragma.name_case_whitelist = value
146 else:
147 raise QAPISemError(info, "unknown pragma '%s'" % name)
148
149 def accept(self, skip_comment=True):
150 while True:
151 self.tok = self.src[self.cursor]
152 self.pos = self.cursor
153 self.cursor += 1
154 self.val = None
155
156 if self.tok == '#':
157 if self.src[self.cursor] == '#':
158 # Start of doc comment
159 skip_comment = False
160 self.cursor = self.src.find('\n', self.cursor)
161 if not skip_comment:
162 self.val = self.src[self.pos:self.cursor]
163 return
164 elif self.tok in '{}:,[]':
165 return
166 elif self.tok == "'":
167 # Note: we accept only printable ASCII
168 string = ''
169 esc = False
170 while True:
171 ch = self.src[self.cursor]
172 self.cursor += 1
173 if ch == '\n':
174 raise QAPIParseError(self, "missing terminating \"'\"")
175 if esc:
176 # Note: we recognize only \\ because we have
177 # no use for funny characters in strings
178 if ch != '\\':
179 raise QAPIParseError(self,
180 "unknown escape \\%s" % ch)
181 esc = False
182 elif ch == '\\':
183 esc = True
184 continue
185 elif ch == "'":
186 self.val = string
187 return
188 if ord(ch) < 32 or ord(ch) >= 127:
189 raise QAPIParseError(
190 self, "funny character in string")
191 string += ch
192 elif self.src.startswith('true', self.pos):
193 self.val = True
194 self.cursor += 3
195 return
196 elif self.src.startswith('false', self.pos):
197 self.val = False
198 self.cursor += 4
199 return
200 elif self.tok == '\n':
201 if self.cursor == len(self.src):
202 self.tok = None
203 return
204 self.info = self.info.next_line()
205 self.line_pos = self.cursor
206 elif not self.tok.isspace():
207 # Show up to next structural, whitespace or quote
208 # character
209 match = re.match('[^[\\]{}:,\\s\'"]+',
210 self.src[self.cursor-1:])
211 raise QAPIParseError(self, "stray '%s'" % match.group(0))
212
213 def get_members(self):
214 expr = OrderedDict()
215 if self.tok == '}':
216 self.accept()
217 return expr
218 if self.tok != "'":
219 raise QAPIParseError(self, "expected string or '}'")
220 while True:
221 key = self.val
222 self.accept()
223 if self.tok != ':':
224 raise QAPIParseError(self, "expected ':'")
225 self.accept()
226 if key in expr:
227 raise QAPIParseError(self, "duplicate key '%s'" % key)
228 expr[key] = self.get_expr(True)
229 if self.tok == '}':
230 self.accept()
231 return expr
232 if self.tok != ',':
233 raise QAPIParseError(self, "expected ',' or '}'")
234 self.accept()
235 if self.tok != "'":
236 raise QAPIParseError(self, "expected string")
237
238 def get_values(self):
239 expr = []
240 if self.tok == ']':
241 self.accept()
242 return expr
243 if self.tok not in "{['tfn":
244 raise QAPIParseError(
245 self, "expected '{', '[', ']', string, boolean or 'null'")
246 while True:
247 expr.append(self.get_expr(True))
248 if self.tok == ']':
249 self.accept()
250 return expr
251 if self.tok != ',':
252 raise QAPIParseError(self, "expected ',' or ']'")
253 self.accept()
254
255 def get_expr(self, nested):
256 if self.tok != '{' and not nested:
257 raise QAPIParseError(self, "expected '{'")
258 if self.tok == '{':
259 self.accept()
260 expr = self.get_members()
261 elif self.tok == '[':
262 self.accept()
263 expr = self.get_values()
264 elif self.tok in "'tfn":
265 expr = self.val
266 self.accept()
267 else:
268 raise QAPIParseError(
269 self, "expected '{', '[', string, boolean or 'null'")
270 return expr
271
272 def get_doc(self, info):
273 if self.val != '##':
274 raise QAPIParseError(
275 self, "junk after '##' at start of documentation comment")
276
277 doc = QAPIDoc(self, info)
278 self.accept(False)
279 while self.tok == '#':
280 if self.val.startswith('##'):
281 # End of doc comment
282 if self.val != '##':
283 raise QAPIParseError(
284 self,
285 "junk after '##' at end of documentation comment")
286 doc.end_comment()
287 self.accept()
288 return doc
289 else:
290 doc.append(self.val)
291 self.accept(False)
292
293 raise QAPIParseError(self, "documentation comment must end with '##'")
294
295
296 class QAPIDoc(object):
297 """
298 A documentation comment block, either definition or free-form
299
300 Definition documentation blocks consist of
301
302 * a body section: one line naming the definition, followed by an
303 overview (any number of lines)
304
305 * argument sections: a description of each argument (for commands
306 and events) or member (for structs, unions and alternates)
307
308 * features sections: a description of each feature flag
309
310 * additional (non-argument) sections, possibly tagged
311
312 Free-form documentation blocks consist only of a body section.
313 """
314
315 class Section(object):
316 def __init__(self, name=None):
317 # optional section name (argument/member or section name)
318 self.name = name
319 # the list of lines for this section
320 self.text = ''
321
322 def append(self, line):
323 self.text += line.rstrip() + '\n'
324
325 class ArgSection(Section):
326 def __init__(self, name):
327 QAPIDoc.Section.__init__(self, name)
328 self.member = None
329
330 def connect(self, member):
331 self.member = member
332
333 def __init__(self, parser, info):
334 # self._parser is used to report errors with QAPIParseError. The
335 # resulting error position depends on the state of the parser.
336 # It happens to be the beginning of the comment. More or less
337 # servicable, but action at a distance.
338 self._parser = parser
339 self.info = info
340 self.symbol = None
341 self.body = QAPIDoc.Section()
342 # dict mapping parameter name to ArgSection
343 self.args = OrderedDict()
344 self.features = OrderedDict()
345 # a list of Section
346 self.sections = []
347 # the current section
348 self._section = self.body
349 self._append_line = self._append_body_line
350
351 def has_section(self, name):
352 """Return True if we have a section with this name."""
353 for i in self.sections:
354 if i.name == name:
355 return True
356 return False
357
358 def append(self, line):
359 """
360 Parse a comment line and add it to the documentation.
361
362 The way that the line is dealt with depends on which part of
363 the documentation we're parsing right now:
364 * The body section: ._append_line is ._append_body_line
365 * An argument section: ._append_line is ._append_args_line
366 * A features section: ._append_line is ._append_features_line
367 * An additional section: ._append_line is ._append_various_line
368 """
369 line = line[1:]
370 if not line:
371 self._append_freeform(line)
372 return
373
374 if line[0] != ' ':
375 raise QAPIParseError(self._parser, "missing space after #")
376 line = line[1:]
377 self._append_line(line)
378
379 def end_comment(self):
380 self._end_section()
381
382 @staticmethod
383 def _is_section_tag(name):
384 return name in ('Returns:', 'Since:',
385 # those are often singular or plural
386 'Note:', 'Notes:',
387 'Example:', 'Examples:',
388 'TODO:')
389
390 def _append_body_line(self, line):
391 """
392 Process a line of documentation text in the body section.
393
394 If this a symbol line and it is the section's first line, this
395 is a definition documentation block for that symbol.
396
397 If it's a definition documentation block, another symbol line
398 begins the argument section for the argument named by it, and
399 a section tag begins an additional section. Start that
400 section and append the line to it.
401
402 Else, append the line to the current section.
403 """
404 name = line.split(' ', 1)[0]
405 # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
406 # recognized, and get silently treated as ordinary text
407 if not self.symbol and not self.body.text and line.startswith('@'):
408 if not line.endswith(':'):
409 raise QAPIParseError(self._parser, "line should end with ':'")
410 self.symbol = line[1:-1]
411 # FIXME invalid names other than the empty string aren't flagged
412 if not self.symbol:
413 raise QAPIParseError(self._parser, "invalid name")
414 elif self.symbol:
415 # This is a definition documentation block
416 if name.startswith('@') and name.endswith(':'):
417 self._append_line = self._append_args_line
418 self._append_args_line(line)
419 elif line == 'Features:':
420 self._append_line = self._append_features_line
421 elif self._is_section_tag(name):
422 self._append_line = self._append_various_line
423 self._append_various_line(line)
424 else:
425 self._append_freeform(line.strip())
426 else:
427 # This is a free-form documentation block
428 self._append_freeform(line.strip())
429
430 def _append_args_line(self, line):
431 """
432 Process a line of documentation text in an argument section.
433
434 A symbol line begins the next argument section, a section tag
435 section or a non-indented line after a blank line begins an
436 additional section. Start that section and append the line to
437 it.
438
439 Else, append the line to the current section.
440
441 """
442 name = line.split(' ', 1)[0]
443
444 if name.startswith('@') and name.endswith(':'):
445 line = line[len(name)+1:]
446 self._start_args_section(name[1:-1])
447 elif self._is_section_tag(name):
448 self._append_line = self._append_various_line
449 self._append_various_line(line)
450 return
451 elif (self._section.text.endswith('\n\n')
452 and line and not line[0].isspace()):
453 if line == 'Features:':
454 self._append_line = self._append_features_line
455 else:
456 self._start_section()
457 self._append_line = self._append_various_line
458 self._append_various_line(line)
459 return
460
461 self._append_freeform(line.strip())
462
463 def _append_features_line(self, line):
464 name = line.split(' ', 1)[0]
465
466 if name.startswith('@') and name.endswith(':'):
467 line = line[len(name)+1:]
468 self._start_features_section(name[1:-1])
469 elif self._is_section_tag(name):
470 self._append_line = self._append_various_line
471 self._append_various_line(line)
472 return
473 elif (self._section.text.endswith('\n\n')
474 and line and not line[0].isspace()):
475 self._start_section()
476 self._append_line = self._append_various_line
477 self._append_various_line(line)
478 return
479
480 self._append_freeform(line.strip())
481
482 def _append_various_line(self, line):
483 """
484 Process a line of documentation text in an additional section.
485
486 A symbol line is an error.
487
488 A section tag begins an additional section. Start that
489 section and append the line to it.
490
491 Else, append the line to the current section.
492 """
493 name = line.split(' ', 1)[0]
494
495 if name.startswith('@') and name.endswith(':'):
496 raise QAPIParseError(self._parser,
497 "'%s' can't follow '%s' section"
498 % (name, self.sections[0].name))
499 elif self._is_section_tag(name):
500 line = line[len(name)+1:]
501 self._start_section(name[:-1])
502
503 if (not self._section.name or
504 not self._section.name.startswith('Example')):
505 line = line.strip()
506
507 self._append_freeform(line)
508
509 def _start_symbol_section(self, symbols_dict, name):
510 # FIXME invalid names other than the empty string aren't flagged
511 if not name:
512 raise QAPIParseError(self._parser, "invalid parameter name")
513 if name in symbols_dict:
514 raise QAPIParseError(self._parser,
515 "'%s' parameter name duplicated" % name)
516 assert not self.sections
517 self._end_section()
518 self._section = QAPIDoc.ArgSection(name)
519 symbols_dict[name] = self._section
520
521 def _start_args_section(self, name):
522 self._start_symbol_section(self.args, name)
523
524 def _start_features_section(self, name):
525 self._start_symbol_section(self.features, name)
526
527 def _start_section(self, name=None):
528 if name in ('Returns', 'Since') and self.has_section(name):
529 raise QAPIParseError(self._parser,
530 "duplicated '%s' section" % name)
531 self._end_section()
532 self._section = QAPIDoc.Section(name)
533 self.sections.append(self._section)
534
535 def _end_section(self):
536 if self._section:
537 text = self._section.text = self._section.text.strip()
538 if self._section.name and (not text or text.isspace()):
539 raise QAPIParseError(
540 self._parser,
541 "empty doc section '%s'" % self._section.name)
542 self._section = None
543
544 def _append_freeform(self, line):
545 match = re.match(r'(@\S+:)', line)
546 if match:
547 raise QAPIParseError(self._parser,
548 "'%s' not allowed in free-form documentation"
549 % match.group(1))
550 self._section.append(line)
551
552 def connect_member(self, member):
553 if member.name not in self.args:
554 # Undocumented TODO outlaw
555 self.args[member.name] = QAPIDoc.ArgSection(member.name)
556 self.args[member.name].connect(member)
557
558 def check_expr(self, expr):
559 if self.has_section('Returns') and 'command' not in expr:
560 raise QAPISemError(self.info,
561 "'Returns:' is only valid for commands")
562
563 def check(self):
564 bogus = [name for name, section in self.args.items()
565 if not section.member]
566 if bogus:
567 raise QAPISemError(
568 self.info,
569 "the following documented members are not in "
570 "the declaration: %s" % ", ".join(bogus))