cachepc-qemu

Fork of AMDESE/qemu with changes for cachepc side-channel attack
git clone https://git.sinitax.com/sinitax/cachepc-qemu
Log | Files | Refs | Submodules | LICENSE | sfeed.txt

parser.py (30970B)


      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')