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