Merge tag 'block-pull-request' of https://gitlab.com/stefanha/qemu into staging
[qemu/ar7.git] / scripts / qapi / parser.py
blob1b006cdc133be4b60fe54ceca8fa2d1f52bc6a71
1 # -*- coding: utf-8 -*-
3 # QAPI schema parser
5 # Copyright IBM, Corp. 2011
6 # Copyright (c) 2013-2019 Red Hat Inc.
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>
14 # This work is licensed under the terms of the GNU GPL, version 2.
15 # See the COPYING file in the top-level directory.
17 from collections import OrderedDict
18 import os
19 import re
20 from typing import (
21 TYPE_CHECKING,
22 Dict,
23 List,
24 Optional,
25 Set,
26 Union,
29 from .common import must_match
30 from .error import QAPISemError, QAPISourceError
31 from .source import QAPISourceInfo
34 if TYPE_CHECKING:
35 # pylint: disable=cyclic-import
36 # TODO: Remove cycle. [schema -> expr -> parser -> schema]
37 from .schema import QAPISchemaFeature, QAPISchemaMember
40 #: Represents a single Top Level QAPI schema expression.
41 TopLevelExpr = Dict[str, object]
43 # Return value alias for get_expr().
44 _ExprValue = Union[List[object], Dict[str, object], str, bool]
46 # FIXME: Consolidate and centralize definitions for TopLevelExpr,
47 # _ExprValue, _JSONValue, and _JSONObject; currently scattered across
48 # several modules.
51 class QAPIParseError(QAPISourceError):
52 """Error class for all QAPI schema parsing errors."""
53 def __init__(self, parser: 'QAPISchemaParser', msg: str):
54 col = 1
55 for ch in parser.src[parser.line_pos:parser.pos]:
56 if ch == '\t':
57 col = (col + 7) % 8 + 1
58 else:
59 col += 1
60 super().__init__(parser.info, msg, col)
63 class QAPISchemaParser:
64 """
65 Parse QAPI schema source.
67 Parse a JSON-esque schema file and process directives. See
68 qapi-code-gen.txt section "Schema Syntax" for the exact syntax.
69 Grammatical validation is handled later by `expr.check_exprs()`.
71 :param fname: Source file name.
72 :param previously_included:
73 The absolute names of previously included source files,
74 if being invoked from another parser.
75 :param incl_info:
76 `QAPISourceInfo` belonging to the parent module.
77 ``None`` implies this is the root module.
79 :ivar exprs: Resulting parsed expressions.
80 :ivar docs: Resulting parsed documentation blocks.
82 :raise OSError: For problems reading the root schema document.
83 :raise QAPIError: For errors in the schema source.
84 """
85 def __init__(self,
86 fname: str,
87 previously_included: Optional[Set[str]] = None,
88 incl_info: Optional[QAPISourceInfo] = None):
89 self._fname = fname
90 self._included = previously_included or set()
91 self._included.add(os.path.abspath(self._fname))
92 self.src = ''
94 # Lexer state (see `accept` for details):
95 self.info = QAPISourceInfo(self._fname, incl_info)
96 self.tok: Union[None, str] = None
97 self.pos = 0
98 self.cursor = 0
99 self.val: Optional[Union[bool, str]] = None
100 self.line_pos = 0
102 # Parser output:
103 self.exprs: List[Dict[str, object]] = []
104 self.docs: List[QAPIDoc] = []
106 # Showtime!
107 self._parse()
109 def _parse(self) -> None:
111 Parse the QAPI schema document.
113 :return: None. Results are stored in ``.exprs`` and ``.docs``.
115 cur_doc = None
117 # May raise OSError; allow the caller to handle it.
118 with open(self._fname, 'r', encoding='utf-8') as fp:
119 self.src = fp.read()
120 if self.src == '' or self.src[-1] != '\n':
121 self.src += '\n'
123 # Prime the lexer:
124 self.accept()
126 # Parse until done:
127 while self.tok is not None:
128 info = self.info
129 if self.tok == '#':
130 self.reject_expr_doc(cur_doc)
131 for cur_doc in self.get_doc(info):
132 self.docs.append(cur_doc)
133 continue
135 expr = self.get_expr()
136 if not isinstance(expr, dict):
137 raise QAPISemError(
138 info, "top-level expression must be an object")
140 if 'include' in expr:
141 self.reject_expr_doc(cur_doc)
142 if len(expr) != 1:
143 raise QAPISemError(info, "invalid 'include' directive")
144 include = expr['include']
145 if not isinstance(include, str):
146 raise QAPISemError(info,
147 "value of 'include' must be a string")
148 incl_fname = os.path.join(os.path.dirname(self._fname),
149 include)
150 self.exprs.append({'expr': {'include': incl_fname},
151 'info': info})
152 exprs_include = self._include(include, info, incl_fname,
153 self._included)
154 if exprs_include:
155 self.exprs.extend(exprs_include.exprs)
156 self.docs.extend(exprs_include.docs)
157 elif "pragma" in expr:
158 self.reject_expr_doc(cur_doc)
159 if len(expr) != 1:
160 raise QAPISemError(info, "invalid 'pragma' directive")
161 pragma = expr['pragma']
162 if not isinstance(pragma, dict):
163 raise QAPISemError(
164 info, "value of 'pragma' must be an object")
165 for name, value in pragma.items():
166 self._pragma(name, value, info)
167 else:
168 expr_elem = {'expr': expr,
169 'info': info}
170 if cur_doc:
171 if not cur_doc.symbol:
172 raise QAPISemError(
173 cur_doc.info, "definition documentation required")
174 expr_elem['doc'] = cur_doc
175 self.exprs.append(expr_elem)
176 cur_doc = None
177 self.reject_expr_doc(cur_doc)
179 @staticmethod
180 def reject_expr_doc(doc: Optional['QAPIDoc']) -> None:
181 if doc and doc.symbol:
182 raise QAPISemError(
183 doc.info,
184 "documentation for '%s' is not followed by the definition"
185 % doc.symbol)
187 @staticmethod
188 def _include(include: str,
189 info: QAPISourceInfo,
190 incl_fname: str,
191 previously_included: Set[str]
192 ) -> Optional['QAPISchemaParser']:
193 incl_abs_fname = os.path.abspath(incl_fname)
194 # catch inclusion cycle
195 inf: Optional[QAPISourceInfo] = info
196 while inf:
197 if incl_abs_fname == os.path.abspath(inf.fname):
198 raise QAPISemError(info, "inclusion loop for %s" % include)
199 inf = inf.parent
201 # skip multiple include of the same file
202 if incl_abs_fname in previously_included:
203 return None
205 try:
206 return QAPISchemaParser(incl_fname, previously_included, info)
207 except OSError as err:
208 raise QAPISemError(
209 info,
210 f"can't read include file '{incl_fname}': {err.strerror}"
211 ) from err
213 @staticmethod
214 def _pragma(name: str, value: object, info: QAPISourceInfo) -> None:
216 def check_list_str(name: str, value: object) -> List[str]:
217 if (not isinstance(value, list) or
218 any(not isinstance(elt, str) for elt in value)):
219 raise QAPISemError(
220 info,
221 "pragma %s must be a list of strings" % name)
222 return value
224 pragma = info.pragma
226 if name == 'doc-required':
227 if not isinstance(value, bool):
228 raise QAPISemError(info,
229 "pragma 'doc-required' must be boolean")
230 pragma.doc_required = value
231 elif name == 'command-name-exceptions':
232 pragma.command_name_exceptions = check_list_str(name, value)
233 elif name == 'command-returns-exceptions':
234 pragma.command_returns_exceptions = check_list_str(name, value)
235 elif name == 'member-name-exceptions':
236 pragma.member_name_exceptions = check_list_str(name, value)
237 else:
238 raise QAPISemError(info, "unknown pragma '%s'" % name)
240 def accept(self, skip_comment: bool = True) -> None:
242 Read and store the next token.
244 :param skip_comment:
245 When false, return COMMENT tokens ("#").
246 This is used when reading documentation blocks.
248 :return:
249 None. Several instance attributes are updated instead:
251 - ``.tok`` represents the token type. See below for values.
252 - ``.info`` describes the token's source location.
253 - ``.val`` is the token's value, if any. See below.
254 - ``.pos`` is the buffer index of the first character of
255 the token.
257 * Single-character tokens:
259 These are "{", "}", ":", ",", "[", and "]".
260 ``.tok`` holds the single character and ``.val`` is None.
262 * Multi-character tokens:
264 * COMMENT:
266 This token is not normally returned by the lexer, but it can
267 be when ``skip_comment`` is False. ``.tok`` is "#", and
268 ``.val`` is a string including all chars until end-of-line,
269 including the "#" itself.
271 * STRING:
273 ``.tok`` is "'", the single quote. ``.val`` contains the
274 string, excluding the surrounding quotes.
276 * TRUE and FALSE:
278 ``.tok`` is either "t" or "f", ``.val`` will be the
279 corresponding bool value.
281 * EOF:
283 ``.tok`` and ``.val`` will both be None at EOF.
285 while True:
286 self.tok = self.src[self.cursor]
287 self.pos = self.cursor
288 self.cursor += 1
289 self.val = None
291 if self.tok == '#':
292 if self.src[self.cursor] == '#':
293 # Start of doc comment
294 skip_comment = False
295 self.cursor = self.src.find('\n', self.cursor)
296 if not skip_comment:
297 self.val = self.src[self.pos:self.cursor]
298 return
299 elif self.tok in '{}:,[]':
300 return
301 elif self.tok == "'":
302 # Note: we accept only printable ASCII
303 string = ''
304 esc = False
305 while True:
306 ch = self.src[self.cursor]
307 self.cursor += 1
308 if ch == '\n':
309 raise QAPIParseError(self, "missing terminating \"'\"")
310 if esc:
311 # Note: we recognize only \\ because we have
312 # no use for funny characters in strings
313 if ch != '\\':
314 raise QAPIParseError(self,
315 "unknown escape \\%s" % ch)
316 esc = False
317 elif ch == '\\':
318 esc = True
319 continue
320 elif ch == "'":
321 self.val = string
322 return
323 if ord(ch) < 32 or ord(ch) >= 127:
324 raise QAPIParseError(
325 self, "funny character in string")
326 string += ch
327 elif self.src.startswith('true', self.pos):
328 self.val = True
329 self.cursor += 3
330 return
331 elif self.src.startswith('false', self.pos):
332 self.val = False
333 self.cursor += 4
334 return
335 elif self.tok == '\n':
336 if self.cursor == len(self.src):
337 self.tok = None
338 return
339 self.info = self.info.next_line()
340 self.line_pos = self.cursor
341 elif not self.tok.isspace():
342 # Show up to next structural, whitespace or quote
343 # character
344 match = must_match('[^[\\]{}:,\\s\'"]+',
345 self.src[self.cursor-1:])
346 raise QAPIParseError(self, "stray '%s'" % match.group(0))
348 def get_members(self) -> Dict[str, object]:
349 expr: Dict[str, object] = OrderedDict()
350 if self.tok == '}':
351 self.accept()
352 return expr
353 if self.tok != "'":
354 raise QAPIParseError(self, "expected string or '}'")
355 while True:
356 key = self.val
357 assert isinstance(key, str) # Guaranteed by tok == "'"
359 self.accept()
360 if self.tok != ':':
361 raise QAPIParseError(self, "expected ':'")
362 self.accept()
363 if key in expr:
364 raise QAPIParseError(self, "duplicate key '%s'" % key)
365 expr[key] = self.get_expr()
366 if self.tok == '}':
367 self.accept()
368 return expr
369 if self.tok != ',':
370 raise QAPIParseError(self, "expected ',' or '}'")
371 self.accept()
372 if self.tok != "'":
373 raise QAPIParseError(self, "expected string")
375 def get_values(self) -> List[object]:
376 expr: List[object] = []
377 if self.tok == ']':
378 self.accept()
379 return expr
380 if self.tok not in tuple("{['tf"):
381 raise QAPIParseError(
382 self, "expected '{', '[', ']', string, or boolean")
383 while True:
384 expr.append(self.get_expr())
385 if self.tok == ']':
386 self.accept()
387 return expr
388 if self.tok != ',':
389 raise QAPIParseError(self, "expected ',' or ']'")
390 self.accept()
392 def get_expr(self) -> _ExprValue:
393 expr: _ExprValue
394 if self.tok == '{':
395 self.accept()
396 expr = self.get_members()
397 elif self.tok == '[':
398 self.accept()
399 expr = self.get_values()
400 elif self.tok in tuple("'tf"):
401 assert isinstance(self.val, (str, bool))
402 expr = self.val
403 self.accept()
404 else:
405 raise QAPIParseError(
406 self, "expected '{', '[', string, or boolean")
407 return expr
409 def get_doc(self, info: QAPISourceInfo) -> List['QAPIDoc']:
410 if self.val != '##':
411 raise QAPIParseError(
412 self, "junk after '##' at start of documentation comment")
414 docs = []
415 cur_doc = QAPIDoc(self, info)
416 self.accept(False)
417 while self.tok == '#':
418 assert isinstance(self.val, str)
419 if self.val.startswith('##'):
420 # End of doc comment
421 if self.val != '##':
422 raise QAPIParseError(
423 self,
424 "junk after '##' at end of documentation comment")
425 cur_doc.end_comment()
426 docs.append(cur_doc)
427 self.accept()
428 return docs
429 if self.val.startswith('# ='):
430 if cur_doc.symbol:
431 raise QAPIParseError(
432 self,
433 "unexpected '=' markup in definition documentation")
434 if cur_doc.body.text:
435 cur_doc.end_comment()
436 docs.append(cur_doc)
437 cur_doc = QAPIDoc(self, info)
438 cur_doc.append(self.val)
439 self.accept(False)
441 raise QAPIParseError(self, "documentation comment must end with '##'")
444 class QAPIDoc:
446 A documentation comment block, either definition or free-form
448 Definition documentation blocks consist of
450 * a body section: one line naming the definition, followed by an
451 overview (any number of lines)
453 * argument sections: a description of each argument (for commands
454 and events) or member (for structs, unions and alternates)
456 * features sections: a description of each feature flag
458 * additional (non-argument) sections, possibly tagged
460 Free-form documentation blocks consist only of a body section.
463 class Section:
464 # pylint: disable=too-few-public-methods
465 def __init__(self, parser: QAPISchemaParser,
466 name: Optional[str] = None, indent: int = 0):
468 # parser, for error messages about indentation
469 self._parser = parser
470 # optional section name (argument/member or section name)
471 self.name = name
472 self.text = ''
473 # the expected indent level of the text of this section
474 self._indent = indent
476 def append(self, line: str) -> None:
477 # Strip leading spaces corresponding to the expected indent level
478 # Blank lines are always OK.
479 if line:
480 indent = must_match(r'\s*', line).end()
481 if indent < self._indent:
482 raise QAPIParseError(
483 self._parser,
484 "unexpected de-indent (expected at least %d spaces)" %
485 self._indent)
486 line = line[self._indent:]
488 self.text += line.rstrip() + '\n'
490 class ArgSection(Section):
491 def __init__(self, parser: QAPISchemaParser,
492 name: str, indent: int = 0):
493 super().__init__(parser, name, indent)
494 self.member: Optional['QAPISchemaMember'] = None
496 def connect(self, member: 'QAPISchemaMember') -> None:
497 self.member = member
499 class NullSection(Section):
501 Immutable dummy section for use at the end of a doc block.
503 # pylint: disable=too-few-public-methods
504 def append(self, line: str) -> None:
505 assert False, "Text appended after end_comment() called."
507 def __init__(self, parser: QAPISchemaParser, info: QAPISourceInfo):
508 # self._parser is used to report errors with QAPIParseError. The
509 # resulting error position depends on the state of the parser.
510 # It happens to be the beginning of the comment. More or less
511 # servicable, but action at a distance.
512 self._parser = parser
513 self.info = info
514 self.symbol: Optional[str] = None
515 self.body = QAPIDoc.Section(parser)
516 # dicts mapping parameter/feature names to their ArgSection
517 self.args: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
518 self.features: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
519 self.sections: List[QAPIDoc.Section] = []
520 # the current section
521 self._section = self.body
522 self._append_line = self._append_body_line
524 def has_section(self, name: str) -> bool:
525 """Return True if we have a section with this name."""
526 for i in self.sections:
527 if i.name == name:
528 return True
529 return False
531 def append(self, line: str) -> None:
533 Parse a comment line and add it to the documentation.
535 The way that the line is dealt with depends on which part of
536 the documentation we're parsing right now:
537 * The body section: ._append_line is ._append_body_line
538 * An argument section: ._append_line is ._append_args_line
539 * A features section: ._append_line is ._append_features_line
540 * An additional section: ._append_line is ._append_various_line
542 line = line[1:]
543 if not line:
544 self._append_freeform(line)
545 return
547 if line[0] != ' ':
548 raise QAPIParseError(self._parser, "missing space after #")
549 line = line[1:]
550 self._append_line(line)
552 def end_comment(self) -> None:
553 self._switch_section(QAPIDoc.NullSection(self._parser))
555 @staticmethod
556 def _is_section_tag(name: str) -> bool:
557 return name in ('Returns:', 'Since:',
558 # those are often singular or plural
559 'Note:', 'Notes:',
560 'Example:', 'Examples:',
561 'TODO:')
563 def _append_body_line(self, line: str) -> None:
565 Process a line of documentation text in the body section.
567 If this a symbol line and it is the section's first line, this
568 is a definition documentation block for that symbol.
570 If it's a definition documentation block, another symbol line
571 begins the argument section for the argument named by it, and
572 a section tag begins an additional section. Start that
573 section and append the line to it.
575 Else, append the line to the current section.
577 name = line.split(' ', 1)[0]
578 # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
579 # recognized, and get silently treated as ordinary text
580 if not self.symbol and not self.body.text and line.startswith('@'):
581 if not line.endswith(':'):
582 raise QAPIParseError(self._parser, "line should end with ':'")
583 self.symbol = line[1:-1]
584 # Invalid names are not checked here, but the name provided MUST
585 # match the following definition, which *is* validated in expr.py.
586 if not self.symbol:
587 raise QAPIParseError(
588 self._parser, "name required after '@'")
589 elif self.symbol:
590 # This is a definition documentation block
591 if name.startswith('@') and name.endswith(':'):
592 self._append_line = self._append_args_line
593 self._append_args_line(line)
594 elif line == 'Features:':
595 self._append_line = self._append_features_line
596 elif self._is_section_tag(name):
597 self._append_line = self._append_various_line
598 self._append_various_line(line)
599 else:
600 self._append_freeform(line)
601 else:
602 # This is a free-form documentation block
603 self._append_freeform(line)
605 def _append_args_line(self, line: str) -> None:
607 Process a line of documentation text in an argument section.
609 A symbol line begins the next argument section, a section tag
610 section or a non-indented line after a blank line begins an
611 additional section. Start that section and append the line to
614 Else, append the line to the current section.
617 name = line.split(' ', 1)[0]
619 if name.startswith('@') and name.endswith(':'):
620 # If line is "@arg: first line of description", find
621 # the index of 'f', which is the indent we expect for any
622 # following lines. We then remove the leading "@arg:"
623 # from line and replace it with spaces so that 'f' has the
624 # same index as it did in the original line and can be
625 # handled the same way we will handle following lines.
626 indent = must_match(r'@\S*:\s*', line).end()
627 line = line[indent:]
628 if not line:
629 # Line was just the "@arg:" header; following lines
630 # are not indented
631 indent = 0
632 else:
633 line = ' ' * indent + line
634 self._start_args_section(name[1:-1], indent)
635 elif self._is_section_tag(name):
636 self._append_line = self._append_various_line
637 self._append_various_line(line)
638 return
639 elif (self._section.text.endswith('\n\n')
640 and line and not line[0].isspace()):
641 if line == 'Features:':
642 self._append_line = self._append_features_line
643 else:
644 self._start_section()
645 self._append_line = self._append_various_line
646 self._append_various_line(line)
647 return
649 self._append_freeform(line)
651 def _append_features_line(self, line: str) -> None:
652 name = line.split(' ', 1)[0]
654 if name.startswith('@') and name.endswith(':'):
655 # If line is "@arg: first line of description", find
656 # the index of 'f', which is the indent we expect for any
657 # following lines. We then remove the leading "@arg:"
658 # from line and replace it with spaces so that 'f' has the
659 # same index as it did in the original line and can be
660 # handled the same way we will handle following lines.
661 indent = must_match(r'@\S*:\s*', line).end()
662 line = line[indent:]
663 if not line:
664 # Line was just the "@arg:" header; following lines
665 # are not indented
666 indent = 0
667 else:
668 line = ' ' * indent + line
669 self._start_features_section(name[1:-1], indent)
670 elif self._is_section_tag(name):
671 self._append_line = self._append_various_line
672 self._append_various_line(line)
673 return
674 elif (self._section.text.endswith('\n\n')
675 and line and not line[0].isspace()):
676 self._start_section()
677 self._append_line = self._append_various_line
678 self._append_various_line(line)
679 return
681 self._append_freeform(line)
683 def _append_various_line(self, line: str) -> None:
685 Process a line of documentation text in an additional section.
687 A symbol line is an error.
689 A section tag begins an additional section. Start that
690 section and append the line to it.
692 Else, append the line to the current section.
694 name = line.split(' ', 1)[0]
696 if name.startswith('@') and name.endswith(':'):
697 raise QAPIParseError(self._parser,
698 "'%s' can't follow '%s' section"
699 % (name, self.sections[0].name))
700 if self._is_section_tag(name):
701 # If line is "Section: first line of description", find
702 # the index of 'f', which is the indent we expect for any
703 # following lines. We then remove the leading "Section:"
704 # from line and replace it with spaces so that 'f' has the
705 # same index as it did in the original line and can be
706 # handled the same way we will handle following lines.
707 indent = must_match(r'\S*:\s*', line).end()
708 line = line[indent:]
709 if not line:
710 # Line was just the "Section:" header; following lines
711 # are not indented
712 indent = 0
713 else:
714 line = ' ' * indent + line
715 self._start_section(name[:-1], indent)
717 self._append_freeform(line)
719 def _start_symbol_section(
720 self,
721 symbols_dict: Dict[str, 'QAPIDoc.ArgSection'],
722 name: str,
723 indent: int) -> None:
724 # FIXME invalid names other than the empty string aren't flagged
725 if not name:
726 raise QAPIParseError(self._parser, "invalid parameter name")
727 if name in symbols_dict:
728 raise QAPIParseError(self._parser,
729 "'%s' parameter name duplicated" % name)
730 assert not self.sections
731 new_section = QAPIDoc.ArgSection(self._parser, name, indent)
732 self._switch_section(new_section)
733 symbols_dict[name] = new_section
735 def _start_args_section(self, name: str, indent: int) -> None:
736 self._start_symbol_section(self.args, name, indent)
738 def _start_features_section(self, name: str, indent: int) -> None:
739 self._start_symbol_section(self.features, name, indent)
741 def _start_section(self, name: Optional[str] = None,
742 indent: int = 0) -> None:
743 if name in ('Returns', 'Since') and self.has_section(name):
744 raise QAPIParseError(self._parser,
745 "duplicated '%s' section" % name)
746 new_section = QAPIDoc.Section(self._parser, name, indent)
747 self._switch_section(new_section)
748 self.sections.append(new_section)
750 def _switch_section(self, new_section: 'QAPIDoc.Section') -> None:
751 text = self._section.text = self._section.text.strip()
753 # Only the 'body' section is allowed to have an empty body.
754 # All other sections, including anonymous ones, must have text.
755 if self._section != self.body and not text:
756 # We do not create anonymous sections unless there is
757 # something to put in them; this is a parser bug.
758 assert self._section.name
759 raise QAPIParseError(
760 self._parser,
761 "empty doc section '%s'" % self._section.name)
763 self._section = new_section
765 def _append_freeform(self, line: str) -> None:
766 match = re.match(r'(@\S+:)', line)
767 if match:
768 raise QAPIParseError(self._parser,
769 "'%s' not allowed in free-form documentation"
770 % match.group(1))
771 self._section.append(line)
773 def connect_member(self, member: 'QAPISchemaMember') -> None:
774 if member.name not in self.args:
775 # Undocumented TODO outlaw
776 self.args[member.name] = QAPIDoc.ArgSection(self._parser,
777 member.name)
778 self.args[member.name].connect(member)
780 def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
781 if feature.name not in self.features:
782 raise QAPISemError(feature.info,
783 "feature '%s' lacks documentation"
784 % feature.name)
785 self.features[feature.name].connect(feature)
787 def check_expr(self, expr: TopLevelExpr) -> None:
788 if self.has_section('Returns') and 'command' not in expr:
789 raise QAPISemError(self.info,
790 "'Returns:' is only valid for commands")
792 def check(self) -> None:
794 def check_args_section(
795 args: Dict[str, QAPIDoc.ArgSection], what: str
796 ) -> None:
797 bogus = [name for name, section in args.items()
798 if not section.member]
799 if bogus:
800 raise QAPISemError(
801 self.info,
802 "documented %s%s '%s' %s not exist" % (
803 what,
804 "s" if len(bogus) > 1 else "",
805 "', '".join(bogus),
806 "do" if len(bogus) > 1 else "does"
809 check_args_section(self.args, 'member')
810 check_args_section(self.features, 'feature')