qemu

FORK: QEMU emulator
git clone https://git.neptards.moe/neptards/qemu.git
Log | Files | Refs | Submodules | LICENSE

expr.py (24561B)


      1 # -*- coding: utf-8 -*-
      2 #
      3 # Copyright IBM, Corp. 2011
      4 # Copyright (c) 2013-2021 Red Hat Inc.
      5 #
      6 # Authors:
      7 #  Anthony Liguori <aliguori@us.ibm.com>
      8 #  Markus Armbruster <armbru@redhat.com>
      9 #  Eric Blake <eblake@redhat.com>
     10 #  Marc-André Lureau <marcandre.lureau@redhat.com>
     11 #  John Snow <jsnow@redhat.com>
     12 #
     13 # This work is licensed under the terms of the GNU GPL, version 2.
     14 # See the COPYING file in the top-level directory.
     15 
     16 """
     17 Normalize and validate (context-free) QAPI schema expression structures.
     18 
     19 `QAPISchemaParser` parses a QAPI schema into abstract syntax trees
     20 consisting of dict, list, str, bool, and int nodes.  This module ensures
     21 that these nested structures have the correct type(s) and key(s) where
     22 appropriate for the QAPI context-free grammar.
     23 
     24 The QAPI schema expression language allows for certain syntactic sugar;
     25 this module also handles the normalization process of these nested
     26 structures.
     27 
     28 See `check_exprs` for the main entry point.
     29 
     30 See `schema.QAPISchema` for processing into native Python data
     31 structures and contextual semantic validation.
     32 """
     33 
     34 import re
     35 from typing import (
     36     Collection,
     37     Dict,
     38     Iterable,
     39     List,
     40     Optional,
     41     Union,
     42     cast,
     43 )
     44 
     45 from .common import c_name
     46 from .error import QAPISemError
     47 from .parser import QAPIDoc
     48 from .source import QAPISourceInfo
     49 
     50 
     51 # Deserialized JSON objects as returned by the parser.
     52 # The values of this mapping are not necessary to exhaustively type
     53 # here (and also not practical as long as mypy lacks recursive
     54 # types), because the purpose of this module is to interrogate that
     55 # type.
     56 _JSONObject = Dict[str, object]
     57 
     58 
     59 # See check_name_str(), below.
     60 valid_name = re.compile(r'(__[a-z0-9.-]+_)?'
     61                         r'(x-)?'
     62                         r'([a-z][a-z0-9_-]*)$', re.IGNORECASE)
     63 
     64 
     65 def check_name_is_str(name: object,
     66                       info: QAPISourceInfo,
     67                       source: str) -> None:
     68     """
     69     Ensure that ``name`` is a ``str``.
     70 
     71     :raise QAPISemError: When ``name`` fails validation.
     72     """
     73     if not isinstance(name, str):
     74         raise QAPISemError(info, "%s requires a string name" % source)
     75 
     76 
     77 def check_name_str(name: str, info: QAPISourceInfo, source: str) -> str:
     78     """
     79     Ensure that ``name`` is a valid QAPI name.
     80 
     81     A valid name consists of ASCII letters, digits, ``-``, and ``_``,
     82     starting with a letter.  It may be prefixed by a downstream prefix
     83     of the form __RFQDN_, or the experimental prefix ``x-``.  If both
     84     prefixes are present, the __RFDQN_ prefix goes first.
     85 
     86     A valid name cannot start with ``q_``, which is reserved.
     87 
     88     :param name: Name to check.
     89     :param info: QAPI schema source file information.
     90     :param source: Error string describing what ``name`` belongs to.
     91 
     92     :raise QAPISemError: When ``name`` fails validation.
     93     :return: The stem of the valid name, with no prefixes.
     94     """
     95     # Reserve the entire 'q_' namespace for c_name(), and for 'q_empty'
     96     # and 'q_obj_*' implicit type names.
     97     match = valid_name.match(name)
     98     if not match or c_name(name, False).startswith('q_'):
     99         raise QAPISemError(info, "%s has an invalid name" % source)
    100     return match.group(3)
    101 
    102 
    103 def check_name_upper(name: str, info: QAPISourceInfo, source: str) -> None:
    104     """
    105     Ensure that ``name`` is a valid event name.
    106 
    107     This means it must be a valid QAPI name as checked by
    108     `check_name_str()`, but where the stem prohibits lowercase
    109     characters and ``-``.
    110 
    111     :param name: Name to check.
    112     :param info: QAPI schema source file information.
    113     :param source: Error string describing what ``name`` belongs to.
    114 
    115     :raise QAPISemError: When ``name`` fails validation.
    116     """
    117     stem = check_name_str(name, info, source)
    118     if re.search(r'[a-z-]', stem):
    119         raise QAPISemError(
    120             info, "name of %s must not use lowercase or '-'" % source)
    121 
    122 
    123 def check_name_lower(name: str, info: QAPISourceInfo, source: str,
    124                      permit_upper: bool = False,
    125                      permit_underscore: bool = False) -> None:
    126     """
    127     Ensure that ``name`` is a valid command or member name.
    128 
    129     This means it must be a valid QAPI name as checked by
    130     `check_name_str()`, but where the stem prohibits uppercase
    131     characters and ``_``.
    132 
    133     :param name: Name to check.
    134     :param info: QAPI schema source file information.
    135     :param source: Error string describing what ``name`` belongs to.
    136     :param permit_upper: Additionally permit uppercase.
    137     :param permit_underscore: Additionally permit ``_``.
    138 
    139     :raise QAPISemError: When ``name`` fails validation.
    140     """
    141     stem = check_name_str(name, info, source)
    142     if ((not permit_upper and re.search(r'[A-Z]', stem))
    143             or (not permit_underscore and '_' in stem)):
    144         raise QAPISemError(
    145             info, "name of %s must not use uppercase or '_'" % source)
    146 
    147 
    148 def check_name_camel(name: str, info: QAPISourceInfo, source: str) -> None:
    149     """
    150     Ensure that ``name`` is a valid user-defined type name.
    151 
    152     This means it must be a valid QAPI name as checked by
    153     `check_name_str()`, but where the stem must be in CamelCase.
    154 
    155     :param name: Name to check.
    156     :param info: QAPI schema source file information.
    157     :param source: Error string describing what ``name`` belongs to.
    158 
    159     :raise QAPISemError: When ``name`` fails validation.
    160     """
    161     stem = check_name_str(name, info, source)
    162     if not re.match(r'[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$', stem):
    163         raise QAPISemError(info, "name of %s must use CamelCase" % source)
    164 
    165 
    166 def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None:
    167     """
    168     Ensure that ``name`` is a valid definition name.
    169 
    170     Based on the value of ``meta``, this means that:
    171       - 'event' names adhere to `check_name_upper()`.
    172       - 'command' names adhere to `check_name_lower()`.
    173       - Else, meta is a type, and must pass `check_name_camel()`.
    174         These names must not end with ``List``.
    175 
    176     :param name: Name to check.
    177     :param info: QAPI schema source file information.
    178     :param meta: Meta-type name of the QAPI expression.
    179 
    180     :raise QAPISemError: When ``name`` fails validation.
    181     """
    182     if meta == 'event':
    183         check_name_upper(name, info, meta)
    184     elif meta == 'command':
    185         check_name_lower(
    186             name, info, meta,
    187             permit_underscore=name in info.pragma.command_name_exceptions)
    188     else:
    189         check_name_camel(name, info, meta)
    190         if name.endswith('List'):
    191             raise QAPISemError(
    192                 info, "%s name should not end in 'List'" % meta)
    193 
    194 
    195 def check_keys(value: _JSONObject,
    196                info: QAPISourceInfo,
    197                source: str,
    198                required: Collection[str],
    199                optional: Collection[str]) -> None:
    200     """
    201     Ensure that a dict has a specific set of keys.
    202 
    203     :param value: The dict to check.
    204     :param info: QAPI schema source file information.
    205     :param source: Error string describing this ``value``.
    206     :param required: Keys that *must* be present.
    207     :param optional: Keys that *may* be present.
    208 
    209     :raise QAPISemError: When unknown keys are present.
    210     """
    211 
    212     def pprint(elems: Iterable[str]) -> str:
    213         return ', '.join("'" + e + "'" for e in sorted(elems))
    214 
    215     missing = set(required) - set(value)
    216     if missing:
    217         raise QAPISemError(
    218             info,
    219             "%s misses key%s %s"
    220             % (source, 's' if len(missing) > 1 else '',
    221                pprint(missing)))
    222     allowed = set(required) | set(optional)
    223     unknown = set(value) - allowed
    224     if unknown:
    225         raise QAPISemError(
    226             info,
    227             "%s has unknown key%s %s\nValid keys are %s."
    228             % (source, 's' if len(unknown) > 1 else '',
    229                pprint(unknown), pprint(allowed)))
    230 
    231 
    232 def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
    233     """
    234     Ensure flag members (if present) have valid values.
    235 
    236     :param expr: The expression to validate.
    237     :param info: QAPI schema source file information.
    238 
    239     :raise QAPISemError:
    240         When certain flags have an invalid value, or when
    241         incompatible flags are present.
    242     """
    243     for key in ('gen', 'success-response'):
    244         if key in expr and expr[key] is not False:
    245             raise QAPISemError(
    246                 info, "flag '%s' may only use false value" % key)
    247     for key in ('boxed', 'allow-oob', 'allow-preconfig', 'coroutine'):
    248         if key in expr and expr[key] is not True:
    249             raise QAPISemError(
    250                 info, "flag '%s' may only use true value" % key)
    251     if 'allow-oob' in expr and 'coroutine' in expr:
    252         # This is not necessarily a fundamental incompatibility, but
    253         # we don't have a use case and the desired semantics isn't
    254         # obvious.  The simplest solution is to forbid it until we get
    255         # a use case for it.
    256         raise QAPISemError(info, "flags 'allow-oob' and 'coroutine' "
    257                                  "are incompatible")
    258 
    259 
    260 def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
    261     """
    262     Validate the ``if`` member of an object.
    263 
    264     The ``if`` member may be either a ``str`` or a dict.
    265 
    266     :param expr: The expression containing the ``if`` member to validate.
    267     :param info: QAPI schema source file information.
    268     :param source: Error string describing ``expr``.
    269 
    270     :raise QAPISemError:
    271         When the "if" member fails validation, or when there are no
    272         non-empty conditions.
    273     :return: None
    274     """
    275 
    276     def _check_if(cond: Union[str, object]) -> None:
    277         if isinstance(cond, str):
    278             if not re.fullmatch(r'[A-Z][A-Z0-9_]*', cond):
    279                 raise QAPISemError(
    280                     info,
    281                     "'if' condition '%s' of %s is not a valid identifier"
    282                     % (cond, source))
    283             return
    284 
    285         if not isinstance(cond, dict):
    286             raise QAPISemError(
    287                 info,
    288                 "'if' condition of %s must be a string or an object" % source)
    289         check_keys(cond, info, "'if' condition of %s" % source, [],
    290                    ["all", "any", "not"])
    291         if len(cond) != 1:
    292             raise QAPISemError(
    293                 info,
    294                 "'if' condition of %s has conflicting keys" % source)
    295 
    296         if 'not' in cond:
    297             _check_if(cond['not'])
    298         elif 'all' in cond:
    299             _check_infix('all', cond['all'])
    300         else:
    301             _check_infix('any', cond['any'])
    302 
    303     def _check_infix(operator: str, operands: object) -> None:
    304         if not isinstance(operands, list):
    305             raise QAPISemError(
    306                 info,
    307                 "'%s' condition of %s must be an array"
    308                 % (operator, source))
    309         if not operands:
    310             raise QAPISemError(
    311                 info, "'if' condition [] of %s is useless" % source)
    312         for operand in operands:
    313             _check_if(operand)
    314 
    315     ifcond = expr.get('if')
    316     if ifcond is None:
    317         return
    318 
    319     _check_if(ifcond)
    320 
    321 
    322 def normalize_members(members: object) -> None:
    323     """
    324     Normalize a "members" value.
    325 
    326     If ``members`` is a dict, for every value in that dict, if that
    327     value is not itself already a dict, normalize it to
    328     ``{'type': value}``.
    329 
    330     :forms:
    331       :sugared: ``Dict[str, Union[str, TypeRef]]``
    332       :canonical: ``Dict[str, TypeRef]``
    333 
    334     :param members: The members value to normalize.
    335 
    336     :return: None, ``members`` is normalized in-place as needed.
    337     """
    338     if isinstance(members, dict):
    339         for key, arg in members.items():
    340             if isinstance(arg, dict):
    341                 continue
    342             members[key] = {'type': arg}
    343 
    344 
    345 def check_type(value: Optional[object],
    346                info: QAPISourceInfo,
    347                source: str,
    348                allow_array: bool = False,
    349                allow_dict: Union[bool, str] = False) -> None:
    350     """
    351     Normalize and validate the QAPI type of ``value``.
    352 
    353     Python types of ``str`` or ``None`` are always allowed.
    354 
    355     :param value: The value to check.
    356     :param info: QAPI schema source file information.
    357     :param source: Error string describing this ``value``.
    358     :param allow_array:
    359         Allow a ``List[str]`` of length 1, which indicates an array of
    360         the type named by the list element.
    361     :param allow_dict:
    362         Allow a dict.  Its members can be struct type members or union
    363         branches.  When the value of ``allow_dict`` is in pragma
    364         ``member-name-exceptions``, the dict's keys may violate the
    365         member naming rules.  The dict members are normalized in place.
    366 
    367     :raise QAPISemError: When ``value`` fails validation.
    368     :return: None, ``value`` is normalized in-place as needed.
    369     """
    370     if value is None:
    371         return
    372 
    373     # Type name
    374     if isinstance(value, str):
    375         return
    376 
    377     # Array type
    378     if isinstance(value, list):
    379         if not allow_array:
    380             raise QAPISemError(info, "%s cannot be an array" % source)
    381         if len(value) != 1 or not isinstance(value[0], str):
    382             raise QAPISemError(info,
    383                                "%s: array type must contain single type name" %
    384                                source)
    385         return
    386 
    387     # Anonymous type
    388 
    389     if not allow_dict:
    390         raise QAPISemError(info, "%s should be a type name" % source)
    391 
    392     if not isinstance(value, dict):
    393         raise QAPISemError(info,
    394                            "%s should be an object or type name" % source)
    395 
    396     permissive = False
    397     if isinstance(allow_dict, str):
    398         permissive = allow_dict in info.pragma.member_name_exceptions
    399 
    400     # value is a dictionary, check that each member is okay
    401     for (key, arg) in value.items():
    402         key_source = "%s member '%s'" % (source, key)
    403         if key.startswith('*'):
    404             key = key[1:]
    405         check_name_lower(key, info, key_source,
    406                          permit_upper=permissive,
    407                          permit_underscore=permissive)
    408         if c_name(key, False) == 'u' or c_name(key, False).startswith('has_'):
    409             raise QAPISemError(info, "%s uses reserved name" % key_source)
    410         check_keys(arg, info, key_source, ['type'], ['if', 'features'])
    411         check_if(arg, info, key_source)
    412         check_features(arg.get('features'), info)
    413         check_type(arg['type'], info, key_source, allow_array=True)
    414 
    415 
    416 def check_features(features: Optional[object],
    417                    info: QAPISourceInfo) -> None:
    418     """
    419     Normalize and validate the ``features`` member.
    420 
    421     ``features`` may be a ``list`` of either ``str`` or ``dict``.
    422     Any ``str`` element will be normalized to ``{'name': element}``.
    423 
    424     :forms:
    425       :sugared: ``List[Union[str, Feature]]``
    426       :canonical: ``List[Feature]``
    427 
    428     :param features: The features member value to validate.
    429     :param info: QAPI schema source file information.
    430 
    431     :raise QAPISemError: When ``features`` fails validation.
    432     :return: None, ``features`` is normalized in-place as needed.
    433     """
    434     if features is None:
    435         return
    436     if not isinstance(features, list):
    437         raise QAPISemError(info, "'features' must be an array")
    438     features[:] = [f if isinstance(f, dict) else {'name': f}
    439                    for f in features]
    440     for feat in features:
    441         source = "'features' member"
    442         assert isinstance(feat, dict)
    443         check_keys(feat, info, source, ['name'], ['if'])
    444         check_name_is_str(feat['name'], info, source)
    445         source = "%s '%s'" % (source, feat['name'])
    446         check_name_lower(feat['name'], info, source)
    447         check_if(feat, info, source)
    448 
    449 
    450 def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
    451     """
    452     Normalize and validate this expression as an ``enum`` definition.
    453 
    454     :param expr: The expression to validate.
    455     :param info: QAPI schema source file information.
    456 
    457     :raise QAPISemError: When ``expr`` is not a valid ``enum``.
    458     :return: None, ``expr`` is normalized in-place as needed.
    459     """
    460     name = expr['enum']
    461     members = expr['data']
    462     prefix = expr.get('prefix')
    463 
    464     if not isinstance(members, list):
    465         raise QAPISemError(info, "'data' must be an array")
    466     if prefix is not None and not isinstance(prefix, str):
    467         raise QAPISemError(info, "'prefix' must be a string")
    468 
    469     permissive = name in info.pragma.member_name_exceptions
    470 
    471     members[:] = [m if isinstance(m, dict) else {'name': m}
    472                   for m in members]
    473     for member in members:
    474         source = "'data' member"
    475         check_keys(member, info, source, ['name'], ['if', 'features'])
    476         member_name = member['name']
    477         check_name_is_str(member_name, info, source)
    478         source = "%s '%s'" % (source, member_name)
    479         # Enum members may start with a digit
    480         if member_name[0].isdigit():
    481             member_name = 'd' + member_name  # Hack: hide the digit
    482         check_name_lower(member_name, info, source,
    483                          permit_upper=permissive,
    484                          permit_underscore=permissive)
    485         check_if(member, info, source)
    486         check_features(member.get('features'), info)
    487 
    488 
    489 def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None:
    490     """
    491     Normalize and validate this expression as a ``struct`` definition.
    492 
    493     :param expr: The expression to validate.
    494     :param info: QAPI schema source file information.
    495 
    496     :raise QAPISemError: When ``expr`` is not a valid ``struct``.
    497     :return: None, ``expr`` is normalized in-place as needed.
    498     """
    499     name = cast(str, expr['struct'])  # Checked in check_exprs
    500     members = expr['data']
    501 
    502     check_type(members, info, "'data'", allow_dict=name)
    503     check_type(expr.get('base'), info, "'base'")
    504 
    505 
    506 def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None:
    507     """
    508     Normalize and validate this expression as a ``union`` definition.
    509 
    510     :param expr: The expression to validate.
    511     :param info: QAPI schema source file information.
    512 
    513     :raise QAPISemError: when ``expr`` is not a valid ``union``.
    514     :return: None, ``expr`` is normalized in-place as needed.
    515     """
    516     name = cast(str, expr['union'])  # Checked in check_exprs
    517     base = expr['base']
    518     discriminator = expr['discriminator']
    519     members = expr['data']
    520 
    521     check_type(base, info, "'base'", allow_dict=name)
    522     check_name_is_str(discriminator, info, "'discriminator'")
    523 
    524     if not isinstance(members, dict):
    525         raise QAPISemError(info, "'data' must be an object")
    526 
    527     for (key, value) in members.items():
    528         source = "'data' member '%s'" % key
    529         check_keys(value, info, source, ['type'], ['if'])
    530         check_if(value, info, source)
    531         check_type(value['type'], info, source, allow_array=not base)
    532 
    533 
    534 def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None:
    535     """
    536     Normalize and validate this expression as an ``alternate`` definition.
    537 
    538     :param expr: The expression to validate.
    539     :param info: QAPI schema source file information.
    540 
    541     :raise QAPISemError: When ``expr`` is not a valid ``alternate``.
    542     :return: None, ``expr`` is normalized in-place as needed.
    543     """
    544     members = expr['data']
    545 
    546     if not members:
    547         raise QAPISemError(info, "'data' must not be empty")
    548 
    549     if not isinstance(members, dict):
    550         raise QAPISemError(info, "'data' must be an object")
    551 
    552     for (key, value) in members.items():
    553         source = "'data' member '%s'" % key
    554         check_name_lower(key, info, source)
    555         check_keys(value, info, source, ['type'], ['if'])
    556         check_if(value, info, source)
    557         check_type(value['type'], info, source, allow_array=True)
    558 
    559 
    560 def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None:
    561     """
    562     Normalize and validate this expression as a ``command`` definition.
    563 
    564     :param expr: The expression to validate.
    565     :param info: QAPI schema source file information.
    566 
    567     :raise QAPISemError: When ``expr`` is not a valid ``command``.
    568     :return: None, ``expr`` is normalized in-place as needed.
    569     """
    570     args = expr.get('data')
    571     rets = expr.get('returns')
    572     boxed = expr.get('boxed', False)
    573 
    574     if boxed and args is None:
    575         raise QAPISemError(info, "'boxed': true requires 'data'")
    576     check_type(args, info, "'data'", allow_dict=not boxed)
    577     check_type(rets, info, "'returns'", allow_array=True)
    578 
    579 
    580 def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None:
    581     """
    582     Normalize and validate this expression as an ``event`` definition.
    583 
    584     :param expr: The expression to validate.
    585     :param info: QAPI schema source file information.
    586 
    587     :raise QAPISemError: When ``expr`` is not a valid ``event``.
    588     :return: None, ``expr`` is normalized in-place as needed.
    589     """
    590     args = expr.get('data')
    591     boxed = expr.get('boxed', False)
    592 
    593     if boxed and args is None:
    594         raise QAPISemError(info, "'boxed': true requires 'data'")
    595     check_type(args, info, "'data'", allow_dict=not boxed)
    596 
    597 
    598 def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]:
    599     """
    600     Validate and normalize a list of parsed QAPI schema expressions.
    601 
    602     This function accepts a list of expressions and metadata as returned
    603     by the parser.  It destructively normalizes the expressions in-place.
    604 
    605     :param exprs: The list of expressions to normalize and validate.
    606 
    607     :raise QAPISemError: When any expression fails validation.
    608     :return: The same list of expressions (now modified).
    609     """
    610     for expr_elem in exprs:
    611         # Expression
    612         assert isinstance(expr_elem['expr'], dict)
    613         for key in expr_elem['expr'].keys():
    614             assert isinstance(key, str)
    615         expr: _JSONObject = expr_elem['expr']
    616 
    617         # QAPISourceInfo
    618         assert isinstance(expr_elem['info'], QAPISourceInfo)
    619         info: QAPISourceInfo = expr_elem['info']
    620 
    621         # Optional[QAPIDoc]
    622         tmp = expr_elem.get('doc')
    623         assert tmp is None or isinstance(tmp, QAPIDoc)
    624         doc: Optional[QAPIDoc] = tmp
    625 
    626         if 'include' in expr:
    627             continue
    628 
    629         metas = expr.keys() & {'enum', 'struct', 'union', 'alternate',
    630                                'command', 'event'}
    631         if len(metas) != 1:
    632             raise QAPISemError(
    633                 info,
    634                 "expression must have exactly one key"
    635                 " 'enum', 'struct', 'union', 'alternate',"
    636                 " 'command', 'event'")
    637         meta = metas.pop()
    638 
    639         check_name_is_str(expr[meta], info, "'%s'" % meta)
    640         name = cast(str, expr[meta])
    641         info.set_defn(meta, name)
    642         check_defn_name_str(name, info, meta)
    643 
    644         if doc:
    645             if doc.symbol != name:
    646                 raise QAPISemError(
    647                     info, "documentation comment is for '%s'" % doc.symbol)
    648             doc.check_expr(expr)
    649         elif info.pragma.doc_required:
    650             raise QAPISemError(info,
    651                                "documentation comment required")
    652 
    653         if meta == 'enum':
    654             check_keys(expr, info, meta,
    655                        ['enum', 'data'], ['if', 'features', 'prefix'])
    656             check_enum(expr, info)
    657         elif meta == 'union':
    658             check_keys(expr, info, meta,
    659                        ['union', 'base', 'discriminator', 'data'],
    660                        ['if', 'features'])
    661             normalize_members(expr.get('base'))
    662             normalize_members(expr['data'])
    663             check_union(expr, info)
    664         elif meta == 'alternate':
    665             check_keys(expr, info, meta,
    666                        ['alternate', 'data'], ['if', 'features'])
    667             normalize_members(expr['data'])
    668             check_alternate(expr, info)
    669         elif meta == 'struct':
    670             check_keys(expr, info, meta,
    671                        ['struct', 'data'], ['base', 'if', 'features'])
    672             normalize_members(expr['data'])
    673             check_struct(expr, info)
    674         elif meta == 'command':
    675             check_keys(expr, info, meta,
    676                        ['command'],
    677                        ['data', 'returns', 'boxed', 'if', 'features',
    678                         'gen', 'success-response', 'allow-oob',
    679                         'allow-preconfig', 'coroutine'])
    680             normalize_members(expr.get('data'))
    681             check_command(expr, info)
    682         elif meta == 'event':
    683             check_keys(expr, info, meta,
    684                        ['event'], ['data', 'boxed', 'if', 'features'])
    685             normalize_members(expr.get('data'))
    686             check_event(expr, info)
    687         else:
    688             assert False, 'unexpected meta type'
    689 
    690         check_if(expr, info, meta)
    691         check_features(expr.get('features'), info)
    692         check_flags(expr, info)
    693 
    694     return exprs