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