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