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