xref: /openbmc/qemu/scripts/qapi/parser.py (revision 15acf48cfed15b37771922093693007d1ad09219)
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        def __init__(self, parser: QAPISchemaParser,
465                     name: Optional[str] = None, indent: int = 0):
466            # parser, for error messages about indentation
467            self._parser = parser
468            # optional section name (argument/member or section name)
469            self.name = name
470            self.text = ''
471            # the expected indent level of the text of this section
472            self._indent = indent
473
474        def append(self, line: str) -> None:
475            # Strip leading spaces corresponding to the expected indent level
476            # Blank lines are always OK.
477            if line:
478                indent = must_match(r'\s*', line).end()
479                if indent < self._indent:
480                    raise QAPIParseError(
481                        self._parser,
482                        "unexpected de-indent (expected at least %d spaces)" %
483                        self._indent)
484                line = line[self._indent:]
485
486            self.text += line.rstrip() + '\n'
487
488    class ArgSection(Section):
489        def __init__(self, parser: QAPISchemaParser,
490                     name: str, indent: int = 0):
491            super().__init__(parser, name, indent)
492            self.member: Optional['QAPISchemaMember'] = None
493
494        def connect(self, member: 'QAPISchemaMember') -> None:
495            self.member = member
496
497    class NullSection(Section):
498        """
499        Immutable dummy section for use at the end of a doc block.
500        """
501        def append(self, line: str) -> None:
502            assert False, "Text appended after end_comment() called."
503
504    def __init__(self, parser: QAPISchemaParser, info: QAPISourceInfo):
505        # self._parser is used to report errors with QAPIParseError.  The
506        # resulting error position depends on the state of the parser.
507        # It happens to be the beginning of the comment.  More or less
508        # servicable, but action at a distance.
509        self._parser = parser
510        self.info = info
511        self.symbol: Optional[str] = None
512        self.body = QAPIDoc.Section(parser)
513        # dicts mapping parameter/feature names to their ArgSection
514        self.args: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
515        self.features: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
516        self.sections: List[QAPIDoc.Section] = []
517        # the current section
518        self._section = self.body
519        self._append_line = self._append_body_line
520
521    def has_section(self, name: str) -> bool:
522        """Return True if we have a section with this name."""
523        for i in self.sections:
524            if i.name == name:
525                return True
526        return False
527
528    def append(self, line: str) -> None:
529        """
530        Parse a comment line and add it to the documentation.
531
532        The way that the line is dealt with depends on which part of
533        the documentation we're parsing right now:
534        * The body section: ._append_line is ._append_body_line
535        * An argument section: ._append_line is ._append_args_line
536        * A features section: ._append_line is ._append_features_line
537        * An additional section: ._append_line is ._append_various_line
538        """
539        line = line[1:]
540        if not line:
541            self._append_freeform(line)
542            return
543
544        if line[0] != ' ':
545            raise QAPIParseError(self._parser, "missing space after #")
546        line = line[1:]
547        self._append_line(line)
548
549    def end_comment(self) -> None:
550        self._switch_section(QAPIDoc.NullSection(self._parser))
551
552    @staticmethod
553    def _is_section_tag(name: str) -> bool:
554        return name in ('Returns:', 'Since:',
555                        # those are often singular or plural
556                        'Note:', 'Notes:',
557                        'Example:', 'Examples:',
558                        'TODO:')
559
560    def _append_body_line(self, line: str) -> None:
561        """
562        Process a line of documentation text in the body section.
563
564        If this a symbol line and it is the section's first line, this
565        is a definition documentation block for that symbol.
566
567        If it's a definition documentation block, another symbol line
568        begins the argument section for the argument named by it, and
569        a section tag begins an additional section.  Start that
570        section and append the line to it.
571
572        Else, append the line to the current section.
573        """
574        name = line.split(' ', 1)[0]
575        # FIXME not nice: things like '#  @foo:' and '# @foo: ' aren't
576        # recognized, and get silently treated as ordinary text
577        if not self.symbol and not self.body.text and line.startswith('@'):
578            if not line.endswith(':'):
579                raise QAPIParseError(self._parser, "line should end with ':'")
580            self.symbol = line[1:-1]
581            # Invalid names are not checked here, but the name provided MUST
582            # match the following definition, which *is* validated in expr.py.
583            if not self.symbol:
584                raise QAPIParseError(
585                    self._parser, "name required after '@'")
586        elif self.symbol:
587            # This is a definition documentation block
588            if name.startswith('@') and name.endswith(':'):
589                self._append_line = self._append_args_line
590                self._append_args_line(line)
591            elif line == 'Features:':
592                self._append_line = self._append_features_line
593            elif self._is_section_tag(name):
594                self._append_line = self._append_various_line
595                self._append_various_line(line)
596            else:
597                self._append_freeform(line)
598        else:
599            # This is a free-form documentation block
600            self._append_freeform(line)
601
602    def _append_args_line(self, line: str) -> None:
603        """
604        Process a line of documentation text in an argument section.
605
606        A symbol line begins the next argument section, a section tag
607        section or a non-indented line after a blank line begins an
608        additional section.  Start that section and append the line to
609        it.
610
611        Else, append the line to the current section.
612
613        """
614        name = line.split(' ', 1)[0]
615
616        if name.startswith('@') and name.endswith(':'):
617            # If line is "@arg:   first line of description", find
618            # the index of 'f', which is the indent we expect for any
619            # following lines.  We then remove the leading "@arg:"
620            # from line and replace it with spaces so that 'f' has the
621            # same index as it did in the original line and can be
622            # handled the same way we will handle following lines.
623            indent = must_match(r'@\S*:\s*', line).end()
624            line = line[indent:]
625            if not line:
626                # Line was just the "@arg:" header; following lines
627                # are not indented
628                indent = 0
629            else:
630                line = ' ' * indent + line
631            self._start_args_section(name[1:-1], indent)
632        elif self._is_section_tag(name):
633            self._append_line = self._append_various_line
634            self._append_various_line(line)
635            return
636        elif (self._section.text.endswith('\n\n')
637              and line and not line[0].isspace()):
638            if line == 'Features:':
639                self._append_line = self._append_features_line
640            else:
641                self._start_section()
642                self._append_line = self._append_various_line
643                self._append_various_line(line)
644            return
645
646        self._append_freeform(line)
647
648    def _append_features_line(self, line: str) -> None:
649        name = line.split(' ', 1)[0]
650
651        if name.startswith('@') and name.endswith(':'):
652            # If line is "@arg:   first line of description", find
653            # the index of 'f', which is the indent we expect for any
654            # following lines.  We then remove the leading "@arg:"
655            # from line and replace it with spaces so that 'f' has the
656            # same index as it did in the original line and can be
657            # handled the same way we will handle following lines.
658            indent = must_match(r'@\S*:\s*', line).end()
659            line = line[indent:]
660            if not line:
661                # Line was just the "@arg:" header; following lines
662                # are not indented
663                indent = 0
664            else:
665                line = ' ' * indent + line
666            self._start_features_section(name[1:-1], indent)
667        elif self._is_section_tag(name):
668            self._append_line = self._append_various_line
669            self._append_various_line(line)
670            return
671        elif (self._section.text.endswith('\n\n')
672              and line and not line[0].isspace()):
673            self._start_section()
674            self._append_line = self._append_various_line
675            self._append_various_line(line)
676            return
677
678        self._append_freeform(line)
679
680    def _append_various_line(self, line: str) -> None:
681        """
682        Process a line of documentation text in an additional section.
683
684        A symbol line is an error.
685
686        A section tag begins an additional section.  Start that
687        section and append the line to it.
688
689        Else, append the line to the current section.
690        """
691        name = line.split(' ', 1)[0]
692
693        if name.startswith('@') and name.endswith(':'):
694            raise QAPIParseError(self._parser,
695                                 "'%s' can't follow '%s' section"
696                                 % (name, self.sections[0].name))
697        if self._is_section_tag(name):
698            # If line is "Section:   first line of description", find
699            # the index of 'f', which is the indent we expect for any
700            # following lines.  We then remove the leading "Section:"
701            # from line and replace it with spaces so that 'f' has the
702            # same index as it did in the original line and can be
703            # handled the same way we will handle following lines.
704            indent = must_match(r'\S*:\s*', line).end()
705            line = line[indent:]
706            if not line:
707                # Line was just the "Section:" header; following lines
708                # are not indented
709                indent = 0
710            else:
711                line = ' ' * indent + line
712            self._start_section(name[:-1], indent)
713
714        self._append_freeform(line)
715
716    def _start_symbol_section(
717            self,
718            symbols_dict: Dict[str, 'QAPIDoc.ArgSection'],
719            name: str,
720            indent: int) -> None:
721        # FIXME invalid names other than the empty string aren't flagged
722        if not name:
723            raise QAPIParseError(self._parser, "invalid parameter name")
724        if name in symbols_dict:
725            raise QAPIParseError(self._parser,
726                                 "'%s' parameter name duplicated" % name)
727        assert not self.sections
728        new_section = QAPIDoc.ArgSection(self._parser, name, indent)
729        self._switch_section(new_section)
730        symbols_dict[name] = new_section
731
732    def _start_args_section(self, name: str, indent: int) -> None:
733        self._start_symbol_section(self.args, name, indent)
734
735    def _start_features_section(self, name: str, indent: int) -> None:
736        self._start_symbol_section(self.features, name, indent)
737
738    def _start_section(self, name: Optional[str] = None,
739                       indent: int = 0) -> None:
740        if name in ('Returns', 'Since') and self.has_section(name):
741            raise QAPIParseError(self._parser,
742                                 "duplicated '%s' section" % name)
743        new_section = QAPIDoc.Section(self._parser, name, indent)
744        self._switch_section(new_section)
745        self.sections.append(new_section)
746
747    def _switch_section(self, new_section: 'QAPIDoc.Section') -> None:
748        text = self._section.text = self._section.text.strip()
749
750        # Only the 'body' section is allowed to have an empty body.
751        # All other sections, including anonymous ones, must have text.
752        if self._section != self.body and not text:
753            # We do not create anonymous sections unless there is
754            # something to put in them; this is a parser bug.
755            assert self._section.name
756            raise QAPIParseError(
757                self._parser,
758                "empty doc section '%s'" % self._section.name)
759
760        self._section = new_section
761
762    def _append_freeform(self, line: str) -> None:
763        match = re.match(r'(@\S+:)', line)
764        if match:
765            raise QAPIParseError(self._parser,
766                                 "'%s' not allowed in free-form documentation"
767                                 % match.group(1))
768        self._section.append(line)
769
770    def connect_member(self, member: 'QAPISchemaMember') -> None:
771        if member.name not in self.args:
772            # Undocumented TODO outlaw
773            self.args[member.name] = QAPIDoc.ArgSection(self._parser,
774                                                        member.name)
775        self.args[member.name].connect(member)
776
777    def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
778        if feature.name not in self.features:
779            raise QAPISemError(feature.info,
780                               "feature '%s' lacks documentation"
781                               % feature.name)
782        self.features[feature.name].connect(feature)
783
784    def check_expr(self, expr: TopLevelExpr) -> None:
785        if self.has_section('Returns') and 'command' not in expr:
786            raise QAPISemError(self.info,
787                               "'Returns:' is only valid for commands")
788
789    def check(self) -> None:
790
791        def check_args_section(
792                args: Dict[str, QAPIDoc.ArgSection], what: str
793        ) -> None:
794            bogus = [name for name, section in args.items()
795                     if not section.member]
796            if bogus:
797                raise QAPISemError(
798                    self.info,
799                    "documented %s%s '%s' %s not exist" % (
800                        what,
801                        "s" if len(bogus) > 1 else "",
802                        "', '".join(bogus),
803                        "do" if len(bogus) > 1 else "does"
804                    ))
805
806        check_args_section(self.args, 'member')
807        check_args_section(self.features, 'feature')
808