diff options
Diffstat (limited to 'scripts/qapi/expr.py')
-rw-r--r-- | scripts/qapi/expr.py | 204 |
1 files changed, 95 insertions, 109 deletions
diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py index 819ea6ad97..cae0a08359 100644 --- a/scripts/qapi/expr.py +++ b/scripts/qapi/expr.py @@ -33,7 +33,6 @@ structures and contextual semantic validation. import re from typing import ( - Collection, Dict, Iterable, List, @@ -44,18 +43,10 @@ from typing import ( from .common import c_name from .error import QAPISemError -from .parser import QAPIDoc +from .parser import QAPIExpression from .source import QAPISourceInfo -# Deserialized JSON objects as returned by the parser. -# The values of this mapping are not necessary to exhaustively type -# here (and also not practical as long as mypy lacks recursive -# types), because the purpose of this module is to interrogate that -# type. -_JSONObject = Dict[str, object] - - # See check_name_str(), below. valid_name = re.compile(r'(__[a-z0-9.-]+_)?' r'(x-)?' @@ -192,11 +183,11 @@ def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None: info, "%s name should not end in 'List'" % meta) -def check_keys(value: _JSONObject, +def check_keys(value: Dict[str, object], info: QAPISourceInfo, source: str, - required: Collection[str], - optional: Collection[str]) -> None: + required: List[str], + optional: List[str]) -> None: """ Ensure that a dict has a specific set of keys. @@ -229,12 +220,11 @@ def check_keys(value: _JSONObject, pprint(unknown), pprint(allowed))) -def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None: +def check_flags(expr: QAPIExpression) -> None: """ Ensure flag members (if present) have valid values. :param expr: The expression to validate. - :param info: QAPI schema source file information. :raise QAPISemError: When certain flags have an invalid value, or when @@ -243,21 +233,22 @@ def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None: for key in ('gen', 'success-response'): if key in expr and expr[key] is not False: raise QAPISemError( - info, "flag '%s' may only use false value" % key) + expr.info, "flag '%s' may only use false value" % key) for key in ('boxed', 'allow-oob', 'allow-preconfig', 'coroutine'): if key in expr and expr[key] is not True: raise QAPISemError( - info, "flag '%s' may only use true value" % key) + expr.info, "flag '%s' may only use true value" % key) if 'allow-oob' in expr and 'coroutine' in expr: # This is not necessarily a fundamental incompatibility, but # we don't have a use case and the desired semantics isn't # obvious. The simplest solution is to forbid it until we get # a use case for it. - raise QAPISemError(info, "flags 'allow-oob' and 'coroutine' " - "are incompatible") + raise QAPISemError( + expr.info, "flags 'allow-oob' and 'coroutine' are incompatible") -def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None: +def check_if(expr: Dict[str, object], + info: QAPISourceInfo, source: str) -> None: """ Validate the ``if`` member of an object. @@ -342,62 +333,56 @@ def normalize_members(members: object) -> None: members[key] = {'type': arg} -def check_type(value: Optional[object], - info: QAPISourceInfo, - source: str, - allow_array: bool = False, - allow_dict: Union[bool, str] = False) -> None: +def check_type_name(value: Optional[object], + info: QAPISourceInfo, source: str) -> None: + if value is not None and not isinstance(value, str): + raise QAPISemError(info, "%s should be a type name" % source) + + +def check_type_name_or_array(value: Optional[object], + info: QAPISourceInfo, source: str) -> None: + if value is None or isinstance(value, str): + return + + if not isinstance(value, list): + raise QAPISemError(info, + "%s should be a type name or array" % source) + + if len(value) != 1 or not isinstance(value[0], str): + raise QAPISemError(info, + "%s: array type must contain single type name" % + source) + + +def check_type_implicit(value: Optional[object], + info: QAPISourceInfo, source: str, + parent_name: Optional[str]) -> None: """ - Normalize and validate the QAPI type of ``value``. + Normalize and validate an optional implicit struct type. - Python types of ``str`` or ``None`` are always allowed. + Accept ``None`` or a ``dict`` defining an implicit struct type. + The latter is normalized in place. :param value: The value to check. :param info: QAPI schema source file information. :param source: Error string describing this ``value``. - :param allow_array: - Allow a ``List[str]`` of length 1, which indicates an array of - the type named by the list element. - :param allow_dict: - Allow a dict. Its members can be struct type members or union - branches. When the value of ``allow_dict`` is in pragma - ``member-name-exceptions``, the dict's keys may violate the - member naming rules. The dict members are normalized in place. + :param parent_name: + When the value of ``parent_name`` is in pragma + ``member-name-exceptions``, an implicit struct type may + violate the member naming rules. :raise QAPISemError: When ``value`` fails validation. - :return: None, ``value`` is normalized in-place as needed. + :return: None """ if value is None: return - # Type name - if isinstance(value, str): - return - - # Array type - if isinstance(value, list): - if not allow_array: - raise QAPISemError(info, "%s cannot be an array" % source) - if len(value) != 1 or not isinstance(value[0], str): - raise QAPISemError(info, - "%s: array type must contain single type name" % - source) - return - - # Anonymous type - - if not allow_dict: - raise QAPISemError(info, "%s should be a type name" % source) - if not isinstance(value, dict): raise QAPISemError(info, "%s should be an object or type name" % source) - permissive = False - if isinstance(allow_dict, str): - permissive = allow_dict in info.pragma.member_name_exceptions + permissive = parent_name in info.pragma.member_name_exceptions - # value is a dictionary, check that each member is okay for (key, arg) in value.items(): key_source = "%s member '%s'" % (source, key) if key.startswith('*'): @@ -410,7 +395,16 @@ def check_type(value: Optional[object], check_keys(arg, info, key_source, ['type'], ['if', 'features']) check_if(arg, info, key_source) check_features(arg.get('features'), info) - check_type(arg['type'], info, key_source, allow_array=True) + check_type_name_or_array(arg['type'], info, key_source) + + +def check_type_name_or_implicit(value: Optional[object], + info: QAPISourceInfo, source: str, + parent_name: Optional[str]) -> None: + if value is None or isinstance(value, str): + return + + check_type_implicit(value, info, source, parent_name) def check_features(features: Optional[object], @@ -443,16 +437,15 @@ def check_features(features: Optional[object], check_keys(feat, info, source, ['name'], ['if']) check_name_is_str(feat['name'], info, source) source = "%s '%s'" % (source, feat['name']) - check_name_str(feat['name'], info, source) + check_name_lower(feat['name'], info, source) check_if(feat, info, source) -def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None: +def check_enum(expr: QAPIExpression) -> None: """ Normalize and validate this expression as an ``enum`` definition. :param expr: The expression to validate. - :param info: QAPI schema source file information. :raise QAPISemError: When ``expr`` is not a valid ``enum``. :return: None, ``expr`` is normalized in-place as needed. @@ -460,6 +453,7 @@ def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None: name = expr['enum'] members = expr['data'] prefix = expr.get('prefix') + info = expr.info if not isinstance(members, list): raise QAPISemError(info, "'data' must be an array") @@ -472,7 +466,7 @@ def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None: for m in members] for member in members: source = "'data' member" - check_keys(member, info, source, ['name'], ['if']) + check_keys(member, info, source, ['name'], ['if', 'features']) member_name = member['name'] check_name_is_str(member_name, info, source) source = "%s '%s'" % (source, member_name) @@ -483,14 +477,14 @@ def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None: permit_upper=permissive, permit_underscore=permissive) check_if(member, info, source) + check_features(member.get('features'), info) -def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None: +def check_struct(expr: QAPIExpression) -> None: """ Normalize and validate this expression as a ``struct`` definition. :param expr: The expression to validate. - :param info: QAPI schema source file information. :raise QAPISemError: When ``expr`` is not a valid ``struct``. :return: None, ``expr`` is normalized in-place as needed. @@ -498,16 +492,15 @@ def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None: name = cast(str, expr['struct']) # Checked in check_exprs members = expr['data'] - check_type(members, info, "'data'", allow_dict=name) - check_type(expr.get('base'), info, "'base'") + check_type_implicit(members, expr.info, "'data'", name) + check_type_name(expr.get('base'), expr.info, "'base'") -def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None: +def check_union(expr: QAPIExpression) -> None: """ Normalize and validate this expression as a ``union`` definition. :param expr: The expression to validate. - :param info: QAPI schema source file information. :raise QAPISemError: when ``expr`` is not a valid ``union``. :return: None, ``expr`` is normalized in-place as needed. @@ -516,8 +509,9 @@ def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None: base = expr['base'] discriminator = expr['discriminator'] members = expr['data'] + info = expr.info - check_type(base, info, "'base'", allow_dict=name) + check_type_name_or_implicit(base, info, "'base'", name) check_name_is_str(discriminator, info, "'discriminator'") if not isinstance(members, dict): @@ -527,20 +521,20 @@ def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None: source = "'data' member '%s'" % key check_keys(value, info, source, ['type'], ['if']) check_if(value, info, source) - check_type(value['type'], info, source, allow_array=not base) + check_type_name(value['type'], info, source) -def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None: +def check_alternate(expr: QAPIExpression) -> None: """ Normalize and validate this expression as an ``alternate`` definition. :param expr: The expression to validate. - :param info: QAPI schema source file information. :raise QAPISemError: When ``expr`` is not a valid ``alternate``. :return: None, ``expr`` is normalized in-place as needed. """ members = expr['data'] + info = expr.info if not members: raise QAPISemError(info, "'data' must not be empty") @@ -553,15 +547,14 @@ def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None: check_name_lower(key, info, source) check_keys(value, info, source, ['type'], ['if']) check_if(value, info, source) - check_type(value['type'], info, source) + check_type_name_or_array(value['type'], info, source) -def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None: +def check_command(expr: QAPIExpression) -> None: """ Normalize and validate this expression as a ``command`` definition. :param expr: The expression to validate. - :param info: QAPI schema source file information. :raise QAPISemError: When ``expr`` is not a valid ``command``. :return: None, ``expr`` is normalized in-place as needed. @@ -570,18 +563,20 @@ def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None: rets = expr.get('returns') boxed = expr.get('boxed', False) - if boxed and args is None: - raise QAPISemError(info, "'boxed': true requires 'data'") - check_type(args, info, "'data'", allow_dict=not boxed) - check_type(rets, info, "'returns'", allow_array=True) + if boxed: + if args is None: + raise QAPISemError(expr.info, "'boxed': true requires 'data'") + check_type_name(args, expr.info, "'data'") + else: + check_type_name_or_implicit(args, expr.info, "'data'", None) + check_type_name_or_array(rets, expr.info, "'returns'") -def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None: +def check_event(expr: QAPIExpression) -> None: """ Normalize and validate this expression as an ``event`` definition. :param expr: The expression to validate. - :param info: QAPI schema source file information. :raise QAPISemError: When ``expr`` is not a valid ``event``. :return: None, ``expr`` is normalized in-place as needed. @@ -589,12 +584,15 @@ def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None: args = expr.get('data') boxed = expr.get('boxed', False) - if boxed and args is None: - raise QAPISemError(info, "'boxed': true requires 'data'") - check_type(args, info, "'data'", allow_dict=not boxed) + if boxed: + if args is None: + raise QAPISemError(expr.info, "'boxed': true requires 'data'") + check_type_name(args, expr.info, "'data'") + else: + check_type_name_or_implicit(args, expr.info, "'data'", None) -def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]: +def check_exprs(exprs: List[QAPIExpression]) -> List[QAPIExpression]: """ Validate and normalize a list of parsed QAPI schema expressions. @@ -606,21 +604,9 @@ def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]: :raise QAPISemError: When any expression fails validation. :return: The same list of expressions (now modified). """ - for expr_elem in exprs: - # Expression - assert isinstance(expr_elem['expr'], dict) - for key in expr_elem['expr'].keys(): - assert isinstance(key, str) - expr: _JSONObject = expr_elem['expr'] - - # QAPISourceInfo - assert isinstance(expr_elem['info'], QAPISourceInfo) - info: QAPISourceInfo = expr_elem['info'] - - # Optional[QAPIDoc] - tmp = expr_elem.get('doc') - assert tmp is None or isinstance(tmp, QAPIDoc) - doc: Optional[QAPIDoc] = tmp + for expr in exprs: + info = expr.info + doc = expr.doc if 'include' in expr: continue @@ -652,24 +638,24 @@ def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]: if meta == 'enum': check_keys(expr, info, meta, ['enum', 'data'], ['if', 'features', 'prefix']) - check_enum(expr, info) + check_enum(expr) elif meta == 'union': check_keys(expr, info, meta, ['union', 'base', 'discriminator', 'data'], ['if', 'features']) normalize_members(expr.get('base')) normalize_members(expr['data']) - check_union(expr, info) + check_union(expr) elif meta == 'alternate': check_keys(expr, info, meta, ['alternate', 'data'], ['if', 'features']) normalize_members(expr['data']) - check_alternate(expr, info) + check_alternate(expr) elif meta == 'struct': check_keys(expr, info, meta, ['struct', 'data'], ['base', 'if', 'features']) normalize_members(expr['data']) - check_struct(expr, info) + check_struct(expr) elif meta == 'command': check_keys(expr, info, meta, ['command'], @@ -677,17 +663,17 @@ def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]: 'gen', 'success-response', 'allow-oob', 'allow-preconfig', 'coroutine']) normalize_members(expr.get('data')) - check_command(expr, info) + check_command(expr) elif meta == 'event': check_keys(expr, info, meta, ['event'], ['data', 'boxed', 'if', 'features']) normalize_members(expr.get('data')) - check_event(expr, info) + check_event(expr) else: assert False, 'unexpected meta type' check_if(expr, info, meta) check_features(expr.get('features'), info) - check_flags(expr, info) + check_flags(expr) return exprs |