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