1 # -*- coding: utf-8 -*-
5 # Copyright IBM, Corp. 2011
6 # Copyright (c) 2013-2019 Red Hat Inc.
14 # This work is licensed under the terms of the GNU GPL, version 2.
15 # See the COPYING file in the top-level directory.
19 from collections import OrderedDict
21 from qapi.error import QAPIParseError, QAPISemError
22 from qapi.source import QAPISourceInfo
25 class QAPISchemaParser:
27 def __init__(self, fname, previously_included=None, incl_info=None):
28 previously_included = previously_included or set()
29 previously_included.add(os.path.abspath(fname))
32 fp = open(fname, 'r', encoding='utf-8')
35 raise QAPISemError(incl_info or QAPISourceInfo(None, None, None),
36 "can't read %s file '%s': %s"
37 % ("include" if incl_info else "schema",
41 if self.src == '' or self.src[-1] != '\n':
44 self.info = QAPISourceInfo(fname, 1, incl_info)
51 while self.tok is not None:
54 self.reject_expr_doc(cur_doc)
55 cur_doc = self.get_doc(info)
56 self.docs.append(cur_doc)
59 expr = self.get_expr(False)
61 self.reject_expr_doc(cur_doc)
63 raise QAPISemError(info, "invalid 'include' directive")
64 include = expr['include']
65 if not isinstance(include, str):
66 raise QAPISemError(info,
67 "value of 'include' must be a string")
68 incl_fname = os.path.join(os.path.dirname(fname),
70 self.exprs.append({'expr': {'include': incl_fname},
72 exprs_include = self._include(include, info, incl_fname,
75 self.exprs.extend(exprs_include.exprs)
76 self.docs.extend(exprs_include.docs)
77 elif "pragma" in expr:
78 self.reject_expr_doc(cur_doc)
80 raise QAPISemError(info, "invalid 'pragma' directive")
81 pragma = expr['pragma']
82 if not isinstance(pragma, dict):
84 info, "value of 'pragma' must be an object")
85 for name, value in pragma.items():
86 self._pragma(name, value, info)
88 expr_elem = {'expr': expr,
91 if not cur_doc.symbol:
93 cur_doc.info, "definition documentation required")
94 expr_elem['doc'] = cur_doc
95 self.exprs.append(expr_elem)
97 self.reject_expr_doc(cur_doc)
100 def reject_expr_doc(doc):
101 if doc and doc.symbol:
104 "documentation for '%s' is not followed by the definition"
107 def _include(self, include, info, incl_fname, previously_included):
108 incl_abs_fname = os.path.abspath(incl_fname)
109 # catch inclusion cycle
112 if incl_abs_fname == os.path.abspath(inf.fname):
113 raise QAPISemError(info, "inclusion loop for %s" % include)
116 # skip multiple include of the same file
117 if incl_abs_fname in previously_included:
120 return QAPISchemaParser(incl_fname, previously_included, info)
122 def _pragma(self, name, value, info):
123 if name == 'doc-required':
124 if not isinstance(value, bool):
125 raise QAPISemError(info,
126 "pragma 'doc-required' must be boolean")
127 info.pragma.doc_required = value
128 elif name == 'returns-whitelist':
129 if (not isinstance(value, list)
130 or any([not isinstance(elt, str) for elt in value])):
133 "pragma returns-whitelist must be a list of strings")
134 info.pragma.returns_whitelist = value
135 elif name == 'name-case-whitelist':
136 if (not isinstance(value, list)
137 or any([not isinstance(elt, str) for elt in value])):
140 "pragma name-case-whitelist must be a list of strings")
141 info.pragma.name_case_whitelist = value
143 raise QAPISemError(info, "unknown pragma '%s'" % name)
145 def accept(self, skip_comment=True):
147 self.tok = self.src[self.cursor]
148 self.pos = self.cursor
153 if self.src[self.cursor] == '#':
154 # Start of doc comment
156 self.cursor = self.src.find('\n', self.cursor)
158 self.val = self.src[self.pos:self.cursor]
160 elif self.tok in '{}:,[]':
162 elif self.tok == "'":
163 # Note: we accept only printable ASCII
167 ch = self.src[self.cursor]
170 raise QAPIParseError(self, "missing terminating \"'\"")
172 # Note: we recognize only \\ because we have
173 # no use for funny characters in strings
175 raise QAPIParseError(self,
176 "unknown escape \\%s" % ch)
184 if ord(ch) < 32 or ord(ch) >= 127:
185 raise QAPIParseError(
186 self, "funny character in string")
188 elif self.src.startswith('true', self.pos):
192 elif self.src.startswith('false', self.pos):
196 elif self.tok == '\n':
197 if self.cursor == len(self.src):
200 self.info = self.info.next_line()
201 self.line_pos = self.cursor
202 elif not self.tok.isspace():
203 # Show up to next structural, whitespace or quote
205 match = re.match('[^[\\]{}:,\\s\'"]+',
206 self.src[self.cursor-1:])
207 raise QAPIParseError(self, "stray '%s'" % match.group(0))
209 def get_members(self):
215 raise QAPIParseError(self, "expected string or '}'")
220 raise QAPIParseError(self, "expected ':'")
223 raise QAPIParseError(self, "duplicate key '%s'" % key)
224 expr[key] = self.get_expr(True)
229 raise QAPIParseError(self, "expected ',' or '}'")
232 raise QAPIParseError(self, "expected string")
234 def get_values(self):
239 if self.tok not in "{['tfn":
240 raise QAPIParseError(
241 self, "expected '{', '[', ']', string, boolean or 'null'")
243 expr.append(self.get_expr(True))
248 raise QAPIParseError(self, "expected ',' or ']'")
251 def get_expr(self, nested):
252 if self.tok != '{' and not nested:
253 raise QAPIParseError(self, "expected '{'")
256 expr = self.get_members()
257 elif self.tok == '[':
259 expr = self.get_values()
260 elif self.tok in "'tfn":
264 raise QAPIParseError(
265 self, "expected '{', '[', string, boolean or 'null'")
268 def get_doc(self, info):
270 raise QAPIParseError(
271 self, "junk after '##' at start of documentation comment")
273 doc = QAPIDoc(self, info)
275 while self.tok == '#':
276 if self.val.startswith('##'):
279 raise QAPIParseError(
281 "junk after '##' at end of documentation comment")
288 raise QAPIParseError(self, "documentation comment must end with '##'")
293 A documentation comment block, either definition or free-form
295 Definition documentation blocks consist of
297 * a body section: one line naming the definition, followed by an
298 overview (any number of lines)
300 * argument sections: a description of each argument (for commands
301 and events) or member (for structs, unions and alternates)
303 * features sections: a description of each feature flag
305 * additional (non-argument) sections, possibly tagged
307 Free-form documentation blocks consist only of a body section.
311 def __init__(self, name=None):
312 # optional section name (argument/member or section name)
314 # the list of lines for this section
317 def append(self, line):
318 self.text += line.rstrip() + '\n'
320 class ArgSection(Section):
321 def __init__(self, name):
322 super().__init__(name)
325 def connect(self, member):
328 def __init__(self, parser, info):
329 # self._parser is used to report errors with QAPIParseError. The
330 # resulting error position depends on the state of the parser.
331 # It happens to be the beginning of the comment. More or less
332 # servicable, but action at a distance.
333 self._parser = parser
336 self.body = QAPIDoc.Section()
337 # dict mapping parameter name to ArgSection
338 self.args = OrderedDict()
339 self.features = OrderedDict()
342 # the current section
343 self._section = self.body
344 self._append_line = self._append_body_line
346 def has_section(self, name):
347 """Return True if we have a section with this name."""
348 for i in self.sections:
353 def append(self, line):
355 Parse a comment line and add it to the documentation.
357 The way that the line is dealt with depends on which part of
358 the documentation we're parsing right now:
359 * The body section: ._append_line is ._append_body_line
360 * An argument section: ._append_line is ._append_args_line
361 * A features section: ._append_line is ._append_features_line
362 * An additional section: ._append_line is ._append_various_line
366 self._append_freeform(line)
370 raise QAPIParseError(self._parser, "missing space after #")
372 self._append_line(line)
374 def end_comment(self):
378 def _is_section_tag(name):
379 return name in ('Returns:', 'Since:',
380 # those are often singular or plural
382 'Example:', 'Examples:',
385 def _append_body_line(self, line):
387 Process a line of documentation text in the body section.
389 If this a symbol line and it is the section's first line, this
390 is a definition documentation block for that symbol.
392 If it's a definition documentation block, another symbol line
393 begins the argument section for the argument named by it, and
394 a section tag begins an additional section. Start that
395 section and append the line to it.
397 Else, append the line to the current section.
399 name = line.split(' ', 1)[0]
400 # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
401 # recognized, and get silently treated as ordinary text
402 if not self.symbol and not self.body.text and line.startswith('@'):
403 if not line.endswith(':'):
404 raise QAPIParseError(self._parser, "line should end with ':'")
405 self.symbol = line[1:-1]
406 # FIXME invalid names other than the empty string aren't flagged
408 raise QAPIParseError(self._parser, "invalid name")
410 # This is a definition documentation block
411 if name.startswith('@') and name.endswith(':'):
412 self._append_line = self._append_args_line
413 self._append_args_line(line)
414 elif line == 'Features:':
415 self._append_line = self._append_features_line
416 elif self._is_section_tag(name):
417 self._append_line = self._append_various_line
418 self._append_various_line(line)
420 self._append_freeform(line.strip())
422 # This is a free-form documentation block
423 self._append_freeform(line.strip())
425 def _append_args_line(self, line):
427 Process a line of documentation text in an argument section.
429 A symbol line begins the next argument section, a section tag
430 section or a non-indented line after a blank line begins an
431 additional section. Start that section and append the line to
434 Else, append the line to the current section.
437 name = line.split(' ', 1)[0]
439 if name.startswith('@') and name.endswith(':'):
440 line = line[len(name)+1:]
441 self._start_args_section(name[1:-1])
442 elif self._is_section_tag(name):
443 self._append_line = self._append_various_line
444 self._append_various_line(line)
446 elif (self._section.text.endswith('\n\n')
447 and line and not line[0].isspace()):
448 if line == 'Features:':
449 self._append_line = self._append_features_line
451 self._start_section()
452 self._append_line = self._append_various_line
453 self._append_various_line(line)
456 self._append_freeform(line.strip())
458 def _append_features_line(self, line):
459 name = line.split(' ', 1)[0]
461 if name.startswith('@') and name.endswith(':'):
462 line = line[len(name)+1:]
463 self._start_features_section(name[1:-1])
464 elif self._is_section_tag(name):
465 self._append_line = self._append_various_line
466 self._append_various_line(line)
468 elif (self._section.text.endswith('\n\n')
469 and line and not line[0].isspace()):
470 self._start_section()
471 self._append_line = self._append_various_line
472 self._append_various_line(line)
475 self._append_freeform(line.strip())
477 def _append_various_line(self, line):
479 Process a line of documentation text in an additional section.
481 A symbol line is an error.
483 A section tag begins an additional section. Start that
484 section and append the line to it.
486 Else, append the line to the current section.
488 name = line.split(' ', 1)[0]
490 if name.startswith('@') and name.endswith(':'):
491 raise QAPIParseError(self._parser,
492 "'%s' can't follow '%s' section"
493 % (name, self.sections[0].name))
494 if self._is_section_tag(name):
495 line = line[len(name)+1:]
496 self._start_section(name[:-1])
498 if (not self._section.name or
499 not self._section.name.startswith('Example')):
502 self._append_freeform(line)
504 def _start_symbol_section(self, symbols_dict, name):
505 # FIXME invalid names other than the empty string aren't flagged
507 raise QAPIParseError(self._parser, "invalid parameter name")
508 if name in symbols_dict:
509 raise QAPIParseError(self._parser,
510 "'%s' parameter name duplicated" % name)
511 assert not self.sections
513 self._section = QAPIDoc.ArgSection(name)
514 symbols_dict[name] = self._section
516 def _start_args_section(self, name):
517 self._start_symbol_section(self.args, name)
519 def _start_features_section(self, name):
520 self._start_symbol_section(self.features, name)
522 def _start_section(self, name=None):
523 if name in ('Returns', 'Since') and self.has_section(name):
524 raise QAPIParseError(self._parser,
525 "duplicated '%s' section" % name)
527 self._section = QAPIDoc.Section(name)
528 self.sections.append(self._section)
530 def _end_section(self):
532 text = self._section.text = self._section.text.strip()
533 if self._section.name and (not text or text.isspace()):
534 raise QAPIParseError(
536 "empty doc section '%s'" % self._section.name)
539 def _append_freeform(self, line):
540 match = re.match(r'(@\S+:)', line)
542 raise QAPIParseError(self._parser,
543 "'%s' not allowed in free-form documentation"
545 self._section.append(line)
547 def connect_member(self, member):
548 if member.name not in self.args:
549 # Undocumented TODO outlaw
550 self.args[member.name] = QAPIDoc.ArgSection(member.name)
551 self.args[member.name].connect(member)
553 def connect_feature(self, feature):
554 if feature.name not in self.features:
555 raise QAPISemError(feature.info,
556 "feature '%s' lacks documentation"
558 self.features[feature.name].connect(feature)
560 def check_expr(self, expr):
561 if self.has_section('Returns') and 'command' not in expr:
562 raise QAPISemError(self.info,
563 "'Returns:' is only valid for commands")
567 def check_args_section(args, info, what):
568 bogus = [name for name, section in args.items()
569 if not section.member]
573 "documented member%s '%s' %s not exist"
574 % ("s" if len(bogus) > 1 else "",
576 "do" if len(bogus) > 1 else "does"))
578 check_args_section(self.args, self.info, 'members')
579 check_args_section(self.features, self.info, 'features')