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