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