diff options
author | Peter Maydell <peter.maydell@linaro.org> | 2021-04-30 16:02:00 +0100 |
---|---|---|
committer | Peter Maydell <peter.maydell@linaro.org> | 2021-04-30 16:02:00 +0100 |
commit | 8f860d2633baf9c2b6261f703f86e394c6bc22ca (patch) | |
tree | c030a1573785ee9ba9e5860aaca87ecb3e83e07d | |
parent | f38d1ea49711232651a817ec9d04c9d9e4816c44 (diff) | |
parent | b54626e0b8f423e91b2e31fa7741e4954cebd2d6 (diff) | |
download | qemu-8f860d2633baf9c2b6261f703f86e394c6bc22ca.zip qemu-8f860d2633baf9c2b6261f703f86e394c6bc22ca.tar.gz qemu-8f860d2633baf9c2b6261f703f86e394c6bc22ca.tar.bz2 |
Merge remote-tracking branch 'remotes/armbru/tags/pull-qapi-2021-04-30' into staging
QAPI patches patches for 2021-04-30
# gpg: Signature made Fri 30 Apr 2021 12:42:32 BST
# gpg: using RSA key 354BC8B3D7EB2A6B68674E5F3870B400EB918653
# gpg: issuer "armbru@redhat.com"
# gpg: Good signature from "Markus Armbruster <armbru@redhat.com>" [full]
# gpg: aka "Markus Armbruster <armbru@pond.sub.org>" [full]
# Primary key fingerprint: 354B C8B3 D7EB 2A6B 6867 4E5F 3870 B400 EB91 8653
* remotes/armbru/tags/pull-qapi-2021-04-30: (25 commits)
qapi/error.py: enable mypy checks
qapi/error: Add type hints
qapi/error.py: enable pylint checks
qapi/error.py: move QAPIParseError to parser.py
qapi/error: assert QAPISourceInfo is not None
qapi/error: Make QAPISourceError 'col' parameter optional
qapi/error: Use Python3-style super()
qapi/error: Repurpose QAPIError as an abstract base exception class
qapi/expr: Update authorship and copyright information
qapi/expr.py: Use tuples instead of lists for static data
qapi/expr.py: Add docstrings
qapi/expr: Only explicitly prohibit 'Kind' nor 'List' for type names
qapi/expr.py: enable pylint checks
qapi/expr.py: Remove single-letter variable
qapi/expr.py: Consolidate check_if_str calls in check_if
qapi/expr.py: add type hint annotations
qapi/expr.py: Modify check_keys to accept any Collection
qapi/expr.py: Add casts in a few select cases
qapi/expr.py: Check type of union and alternate 'data' member
qapi/expr.py: move string check upwards in check_type
...
Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
-rw-r--r-- | docs/sphinx/qapidoc.py | 3 | ||||
-rw-r--r-- | scripts/qapi/error.py | 47 | ||||
-rw-r--r-- | scripts/qapi/expr.py | 442 | ||||
-rw-r--r-- | scripts/qapi/mypy.ini | 10 | ||||
-rw-r--r-- | scripts/qapi/parser.py | 14 | ||||
-rw-r--r-- | scripts/qapi/pylintrc | 4 | ||||
-rw-r--r-- | scripts/qapi/schema.py | 4 | ||||
-rw-r--r-- | tests/qapi-schema/alternate-data-invalid.err | 2 | ||||
-rw-r--r-- | tests/qapi-schema/alternate-data-invalid.json | 4 | ||||
-rw-r--r-- | tests/qapi-schema/alternate-data-invalid.out | 0 | ||||
-rw-r--r-- | tests/qapi-schema/meson.build | 2 | ||||
-rw-r--r-- | tests/qapi-schema/union-invalid-data.err | 2 | ||||
-rw-r--r-- | tests/qapi-schema/union-invalid-data.json | 6 | ||||
-rw-r--r-- | tests/qapi-schema/union-invalid-data.out | 0 |
14 files changed, 429 insertions, 111 deletions
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py index b7a2d39..87c67ab 100644 --- a/docs/sphinx/qapidoc.py +++ b/docs/sphinx/qapidoc.py @@ -34,7 +34,8 @@ from sphinx.errors import ExtensionError from sphinx.util.nodes import nested_parse_with_titles import sphinx from qapi.gen import QAPISchemaVisitor -from qapi.schema import QAPIError, QAPISemError, QAPISchema +from qapi.error import QAPIError, QAPISemError +from qapi.schema import QAPISchema # Sphinx up to 1.6 uses AutodocReporter; 1.7 and later diff --git a/scripts/qapi/error.py b/scripts/qapi/error.py index ae60d9e..e35e4dd 100644 --- a/scripts/qapi/error.py +++ b/scripts/qapi/error.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # -# QAPI error classes -# # Copyright (c) 2017-2019 Red Hat Inc. # # Authors: @@ -11,15 +9,36 @@ # This work is licensed under the terms of the GNU GPL, version 2. # See the COPYING file in the top-level directory. +""" +QAPI error classes + +Common error classes used throughout the package. Additional errors may +be defined in other modules. At present, `QAPIParseError` is defined in +parser.py. +""" + +from typing import Optional + +from .source import QAPISourceInfo + class QAPIError(Exception): - def __init__(self, info, col, msg): - Exception.__init__(self) + """Base class for all exceptions from the QAPI package.""" + + +class QAPISourceError(QAPIError): + """Error class for all exceptions identifying a source location.""" + def __init__(self, + info: Optional[QAPISourceInfo], + msg: str, + col: Optional[int] = None): + super().__init__() self.info = info - self.col = col self.msg = msg + self.col = col - def __str__(self): + def __str__(self) -> str: + assert self.info is not None loc = str(self.info) if self.col is not None: assert self.info.line is not None @@ -27,17 +46,5 @@ class QAPIError(Exception): return loc + ': ' + self.msg -class QAPIParseError(QAPIError): - def __init__(self, parser, msg): - col = 1 - for ch in parser.src[parser.line_pos:parser.pos]: - if ch == '\t': - col = (col + 7) % 8 + 1 - else: - col += 1 - super().__init__(parser.info, col, msg) - - -class QAPISemError(QAPIError): - def __init__(self, info, msg): - super().__init__(info, None, msg) +class QAPISemError(QAPISourceError): + """Error class for semantic QAPI errors.""" diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py index 540b398..496f7e0 100644 --- a/scripts/qapi/expr.py +++ b/scripts/qapi/expr.py @@ -1,40 +1,97 @@ # -*- coding: utf-8 -*- # -# Check (context-free) QAPI schema expression structure -# # Copyright IBM, Corp. 2011 -# Copyright (c) 2013-2019 Red Hat Inc. +# Copyright (c) 2013-2021 Red Hat Inc. # # Authors: # Anthony Liguori <aliguori@us.ibm.com> # Markus Armbruster <armbru@redhat.com> # Eric Blake <eblake@redhat.com> # Marc-André Lureau <marcandre.lureau@redhat.com> +# John Snow <jsnow@redhat.com> # # This work is licensed under the terms of the GNU GPL, version 2. # See the COPYING file in the top-level directory. -from collections import OrderedDict +""" +Normalize and validate (context-free) QAPI schema expression structures. + +`QAPISchemaParser` parses a QAPI schema into abstract syntax trees +consisting of dict, list, str, bool, and int nodes. This module ensures +that these nested structures have the correct type(s) and key(s) where +appropriate for the QAPI context-free grammar. + +The QAPI schema expression language allows for certain syntactic sugar; +this module also handles the normalization process of these nested +structures. + +See `check_exprs` for the main entry point. + +See `schema.QAPISchema` for processing into native Python data +structures and contextual semantic validation. +""" + import re +from typing import ( + Collection, + Dict, + Iterable, + List, + Optional, + Union, + cast, +) from .common import c_name from .error import QAPISemError +from .parser import QAPIDoc +from .source import QAPISourceInfo -# Names consist of letters, digits, -, and _, starting with a letter. -# An experimental name is prefixed with x-. A name of a downstream -# extension is prefixed with __RFQDN_. The latter prefix goes first. +# 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-)?' r'([a-z][a-z0-9_-]*)$', re.IGNORECASE) -def check_name_is_str(name, info, source): +def check_name_is_str(name: object, + info: QAPISourceInfo, + source: str) -> None: + """ + Ensure that ``name`` is a ``str``. + + :raise QAPISemError: When ``name`` fails validation. + """ if not isinstance(name, str): raise QAPISemError(info, "%s requires a string name" % source) -def check_name_str(name, info, source): +def check_name_str(name: str, info: QAPISourceInfo, source: str) -> str: + """ + Ensure that ``name`` is a valid QAPI name. + + A valid name consists of ASCII letters, digits, ``-``, and ``_``, + starting with a letter. It may be prefixed by a downstream prefix + of the form __RFQDN_, or the experimental prefix ``x-``. If both + prefixes are present, the __RFDQN_ prefix goes first. + + A valid name cannot start with ``q_``, which is reserved. + + :param name: Name to check. + :param info: QAPI schema source file information. + :param source: Error string describing what ``name`` belongs to. + + :raise QAPISemError: When ``name`` fails validation. + :return: The stem of the valid name, with no prefixes. + """ # Reserve the entire 'q_' namespace for c_name(), and for 'q_empty' # and 'q_obj_*' implicit type names. match = valid_name.match(name) @@ -43,16 +100,44 @@ def check_name_str(name, info, source): return match.group(3) -def check_name_upper(name, info, source): +def check_name_upper(name: str, info: QAPISourceInfo, source: str) -> None: + """ + Ensure that ``name`` is a valid event name. + + This means it must be a valid QAPI name as checked by + `check_name_str()`, but where the stem prohibits lowercase + characters and ``-``. + + :param name: Name to check. + :param info: QAPI schema source file information. + :param source: Error string describing what ``name`` belongs to. + + :raise QAPISemError: When ``name`` fails validation. + """ stem = check_name_str(name, info, source) if re.search(r'[a-z-]', stem): raise QAPISemError( info, "name of %s must not use lowercase or '-'" % source) -def check_name_lower(name, info, source, - permit_upper=False, - permit_underscore=False): +def check_name_lower(name: str, info: QAPISourceInfo, source: str, + permit_upper: bool = False, + permit_underscore: bool = False) -> None: + """ + Ensure that ``name`` is a valid command or member name. + + This means it must be a valid QAPI name as checked by + `check_name_str()`, but where the stem prohibits uppercase + characters and ``_``. + + :param name: Name to check. + :param info: QAPI schema source file information. + :param source: Error string describing what ``name`` belongs to. + :param permit_upper: Additionally permit uppercase. + :param permit_underscore: Additionally permit ``_``. + + :raise QAPISemError: When ``name`` fails validation. + """ stem = check_name_str(name, info, source) if ((not permit_upper and re.search(r'[A-Z]', stem)) or (not permit_underscore and '_' in stem)): @@ -60,13 +145,40 @@ def check_name_lower(name, info, source, info, "name of %s must not use uppercase or '_'" % source) -def check_name_camel(name, info, source): +def check_name_camel(name: str, info: QAPISourceInfo, source: str) -> None: + """ + Ensure that ``name`` is a valid user-defined type name. + + This means it must be a valid QAPI name as checked by + `check_name_str()`, but where the stem must be in CamelCase. + + :param name: Name to check. + :param info: QAPI schema source file information. + :param source: Error string describing what ``name`` belongs to. + + :raise QAPISemError: When ``name`` fails validation. + """ stem = check_name_str(name, info, source) if not re.match(r'[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$', stem): raise QAPISemError(info, "name of %s must use CamelCase" % source) -def check_defn_name_str(name, info, meta): +def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None: + """ + Ensure that ``name`` is a valid definition name. + + Based on the value of ``meta``, this means that: + - 'event' names adhere to `check_name_upper()`. + - 'command' names adhere to `check_name_lower()`. + - Else, meta is a type, and must pass `check_name_camel()`. + These names must not end with ``Kind`` nor ``List``. + + :param name: Name to check. + :param info: QAPI schema source file information. + :param meta: Meta-type name of the QAPI expression. + + :raise QAPISemError: When ``name`` fails validation. + """ if meta == 'event': check_name_upper(name, info, meta) elif meta == 'command': @@ -75,14 +187,29 @@ def check_defn_name_str(name, info, meta): permit_underscore=name in info.pragma.command_name_exceptions) else: check_name_camel(name, info, meta) - if name.endswith('Kind') or name.endswith('List'): - raise QAPISemError( - info, "%s name should not end in '%s'" % (meta, name[-4:])) + if name.endswith('Kind') or name.endswith('List'): + raise QAPISemError( + info, "%s name should not end in '%s'" % (meta, name[-4:])) + +def check_keys(value: _JSONObject, + info: QAPISourceInfo, + source: str, + required: Collection[str], + optional: Collection[str]) -> None: + """ + Ensure that a dict has a specific set of keys. -def check_keys(value, info, source, required, optional): + :param value: The dict to check. + :param info: QAPI schema source file information. + :param source: Error string describing this ``value``. + :param required: Keys that *must* be present. + :param optional: Keys that *may* be present. - def pprint(elems): + :raise QAPISemError: When unknown keys are present. + """ + + def pprint(elems: Iterable[str]) -> str: return ', '.join("'" + e + "'" for e in sorted(elems)) missing = set(required) - set(value) @@ -92,7 +219,7 @@ def check_keys(value, info, source, required, optional): "%s misses key%s %s" % (source, 's' if len(missing) > 1 else '', pprint(missing))) - allowed = set(required + optional) + allowed = set(required) | set(optional) unknown = set(value) - allowed if unknown: raise QAPISemError( @@ -102,12 +229,22 @@ def check_keys(value, info, source, required, optional): pprint(unknown), pprint(allowed))) -def check_flags(expr, info): - for key in ['gen', 'success-response']: +def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> 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 + incompatible flags are present. + """ + 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) - for key in ['boxed', 'allow-oob', 'allow-preconfig', 'coroutine']: + 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) @@ -120,47 +257,106 @@ def check_flags(expr, info): "are incompatible") -def check_if(expr, info, source): +def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None: + """ + Normalize and validate the ``if`` member of an object. - def check_if_str(ifcond, info): - if not isinstance(ifcond, str): - raise QAPISemError( - info, - "'if' condition of %s must be a string or a list of strings" - % source) - if ifcond.strip() == '': - raise QAPISemError( - info, - "'if' condition '%s' of %s makes no sense" - % (ifcond, source)) + The ``if`` member may be either a ``str`` or a ``List[str]``. + A ``str`` value will be normalized to ``List[str]``. + :forms: + :sugared: ``Union[str, List[str]]`` + :canonical: ``List[str]`` + + :param expr: The expression containing the ``if`` member to validate. + :param info: QAPI schema source file information. + :param source: Error string describing ``expr``. + + :raise QAPISemError: + When the "if" member fails validation, or when there are no + non-empty conditions. + :return: None, ``expr`` is normalized in-place as needed. + """ ifcond = expr.get('if') if ifcond is None: return + if isinstance(ifcond, list): - if ifcond == []: + if not ifcond: raise QAPISemError( info, "'if' condition [] of %s is useless" % source) - for elt in ifcond: - check_if_str(elt, info) else: - check_if_str(ifcond, info) - expr['if'] = [ifcond] + # Normalize to a list + ifcond = expr['if'] = [ifcond] + + for elt in ifcond: + if not isinstance(elt, str): + raise QAPISemError( + info, + "'if' condition of %s must be a string or a list of strings" + % source) + if not elt.strip(): + raise QAPISemError( + info, + "'if' condition '%s' of %s makes no sense" + % (elt, source)) -def normalize_members(members): - if isinstance(members, OrderedDict): +def normalize_members(members: object) -> None: + """ + Normalize a "members" value. + + If ``members`` is a dict, for every value in that dict, if that + value is not itself already a dict, normalize it to + ``{'type': value}``. + + :forms: + :sugared: ``Dict[str, Union[str, TypeRef]]`` + :canonical: ``Dict[str, TypeRef]`` + + :param members: The members value to normalize. + + :return: None, ``members`` is normalized in-place as needed. + """ + if isinstance(members, dict): for key, arg in members.items(): if isinstance(arg, dict): continue members[key] = {'type': arg} -def check_type(value, info, source, - allow_array=False, allow_dict=False): +def check_type(value: Optional[object], + info: QAPISourceInfo, + source: str, + allow_array: bool = False, + allow_dict: Union[bool, str] = False) -> None: + """ + Normalize and validate the QAPI type of ``value``. + + Python types of ``str`` or ``None`` are always allowed. + + :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. + + :raise QAPISemError: When ``value`` fails validation. + :return: None, ``value`` is normalized in-place as needed. + """ if value is None: return + # Type name + if isinstance(value, str): + return + # Array type if isinstance(value, list): if not allow_array: @@ -171,20 +367,18 @@ def check_type(value, info, source, source) return - # Type name - if isinstance(value, str): - return - # Anonymous type if not allow_dict: raise QAPISemError(info, "%s should be a type name" % source) - if not isinstance(value, OrderedDict): + if not isinstance(value, dict): raise QAPISemError(info, "%s should be an object or type name" % source) - permissive = allow_dict in info.pragma.member_name_exceptions + permissive = False + if isinstance(allow_dict, str): + permissive = allow_dict in info.pragma.member_name_exceptions # value is a dictionary, check that each member is okay for (key, arg) in value.items(): @@ -202,24 +396,50 @@ def check_type(value, info, source, check_type(arg['type'], info, key_source, allow_array=True) -def check_features(features, info): +def check_features(features: Optional[object], + info: QAPISourceInfo) -> None: + """ + Normalize and validate the ``features`` member. + + ``features`` may be a ``list`` of either ``str`` or ``dict``. + Any ``str`` element will be normalized to ``{'name': element}``. + + :forms: + :sugared: ``List[Union[str, Feature]]`` + :canonical: ``List[Feature]`` + + :param features: The features member value to validate. + :param info: QAPI schema source file information. + + :raise QAPISemError: When ``features`` fails validation. + :return: None, ``features`` is normalized in-place as needed. + """ if features is None: return if not isinstance(features, list): raise QAPISemError(info, "'features' must be an array") features[:] = [f if isinstance(f, dict) else {'name': f} for f in features] - for f in features: + for feat in features: source = "'features' member" - assert isinstance(f, dict) - check_keys(f, info, source, ['name'], ['if']) - check_name_is_str(f['name'], info, source) - source = "%s '%s'" % (source, f['name']) - check_name_lower(f['name'], info, source) - check_if(f, info, source) + assert isinstance(feat, dict) + 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_if(feat, info, source) + + +def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None: + """ + Normalize and validate this expression as an ``enum`` definition. + :param expr: The expression to validate. + :param info: QAPI schema source file information. -def check_enum(expr, info): + :raise QAPISemError: When ``expr`` is not a valid ``enum``. + :return: None, ``expr`` is normalized in-place as needed. + """ name = expr['enum'] members = expr['data'] prefix = expr.get('prefix') @@ -241,23 +461,41 @@ def check_enum(expr, info): source = "%s '%s'" % (source, member_name) # Enum members may start with a digit if member_name[0].isdigit(): - member_name = 'd' + member_name # Hack: hide the digit + member_name = 'd' + member_name # Hack: hide the digit check_name_lower(member_name, info, source, permit_upper=permissive, permit_underscore=permissive) check_if(member, info, source) -def check_struct(expr, info): - name = expr['struct'] +def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> 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. + """ + 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'") -def check_union(expr, info): - name = expr['union'] +def check_union(expr: _JSONObject, info: QAPISourceInfo) -> 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. + """ + name = cast(str, expr['union']) # Checked in check_exprs base = expr.get('base') discriminator = expr.get('discriminator') members = expr['data'] @@ -271,6 +509,9 @@ def check_union(expr, info): raise QAPISemError(info, "'discriminator' requires 'base'") check_name_is_str(discriminator, info, "'discriminator'") + if not isinstance(members, dict): + raise QAPISemError(info, "'data' must be an object") + for (key, value) in members.items(): source = "'data' member '%s'" % key if discriminator is None: @@ -281,11 +522,24 @@ def check_union(expr, info): check_type(value['type'], info, source, allow_array=not base) -def check_alternate(expr, info): +def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> 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'] if not members: raise QAPISemError(info, "'data' must not be empty") + + if not isinstance(members, dict): + raise QAPISemError(info, "'data' must be an object") + for (key, value) in members.items(): source = "'data' member '%s'" % key check_name_lower(key, info, source) @@ -294,7 +548,16 @@ def check_alternate(expr, info): check_type(value['type'], info, source) -def check_command(expr, info): +def check_command(expr: _JSONObject, info: QAPISourceInfo) -> 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. + """ args = expr.get('data') rets = expr.get('returns') boxed = expr.get('boxed', False) @@ -305,7 +568,16 @@ def check_command(expr, info): check_type(rets, info, "'returns'", allow_array=True) -def check_event(expr, info): +def check_event(expr: _JSONObject, info: QAPISourceInfo) -> 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. + """ args = expr.get('data') boxed = expr.get('boxed', False) @@ -314,11 +586,33 @@ def check_event(expr, info): check_type(args, info, "'data'", allow_dict=not boxed) -def check_exprs(exprs): +def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]: + """ + Validate and normalize a list of parsed QAPI schema expressions. + + This function accepts a list of expressions and metadata as returned + by the parser. It destructively normalizes the expressions in-place. + + :param exprs: The list of expressions to normalize and validate. + + :raise QAPISemError: When any expression fails validation. + :return: The same list of expressions (now modified). + """ for expr_elem in exprs: - expr = expr_elem['expr'] - info = expr_elem['info'] - doc = expr_elem.get('doc') + # 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 if 'include' in expr: continue @@ -338,8 +632,8 @@ def check_exprs(exprs): else: raise QAPISemError(info, "expression is missing metatype") - name = expr[meta] - check_name_is_str(name, info, "'%s'" % meta) + check_name_is_str(expr[meta], info, "'%s'" % meta) + name = cast(str, expr[meta]) info.set_defn(meta, name) check_defn_name_str(name, info, meta) diff --git a/scripts/qapi/mypy.ini b/scripts/qapi/mypy.ini index 0a000d5..54ca448 100644 --- a/scripts/qapi/mypy.ini +++ b/scripts/qapi/mypy.ini @@ -3,16 +3,6 @@ strict = True disallow_untyped_calls = False python_version = 3.6 -[mypy-qapi.error] -disallow_untyped_defs = False -disallow_incomplete_defs = False -check_untyped_defs = False - -[mypy-qapi.expr] -disallow_untyped_defs = False -disallow_incomplete_defs = False -check_untyped_defs = False - [mypy-qapi.parser] disallow_untyped_defs = False disallow_incomplete_defs = False diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py index 58267c3..ca5e8e1 100644 --- a/scripts/qapi/parser.py +++ b/scripts/qapi/parser.py @@ -18,10 +18,22 @@ from collections import OrderedDict import os import re -from .error import QAPIParseError, QAPISemError +from .error import QAPISemError, QAPISourceError from .source import QAPISourceInfo +class QAPIParseError(QAPISourceError): + """Error class for all QAPI schema parsing errors.""" + def __init__(self, parser, msg): + col = 1 + for ch in parser.src[parser.line_pos:parser.pos]: + if ch == '\t': + col = (col + 7) % 8 + 1 + else: + col += 1 + super().__init__(parser.info, msg, col) + + class QAPISchemaParser: def __init__(self, fname, previously_included=None, incl_info=None): diff --git a/scripts/qapi/pylintrc b/scripts/qapi/pylintrc index b9e077a..88efbf7 100644 --- a/scripts/qapi/pylintrc +++ b/scripts/qapi/pylintrc @@ -2,9 +2,7 @@ # Add files or directories matching the regex patterns to the ignore list. # The regex matches against base names, not paths. -ignore-patterns=error.py, - expr.py, - parser.py, +ignore-patterns=parser.py, schema.py, diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py index 703b446..3a4172f 100644 --- a/scripts/qapi/schema.py +++ b/scripts/qapi/schema.py @@ -20,7 +20,7 @@ import re from typing import Optional from .common import POINTER_SUFFIX, c_name -from .error import QAPIError, QAPISemError +from .error import QAPISemError, QAPISourceError from .expr import check_exprs from .parser import QAPISchemaParser @@ -875,7 +875,7 @@ class QAPISchema: other_ent = self._entity_dict.get(ent.name) if other_ent: if other_ent.info: - where = QAPIError(other_ent.info, None, "previous definition") + where = QAPISourceError(other_ent.info, "previous definition") raise QAPISemError( ent.info, "'%s' is already defined\n%s" % (ent.name, where)) diff --git a/tests/qapi-schema/alternate-data-invalid.err b/tests/qapi-schema/alternate-data-invalid.err new file mode 100644 index 0000000..55f1033 --- /dev/null +++ b/tests/qapi-schema/alternate-data-invalid.err @@ -0,0 +1,2 @@ +alternate-data-invalid.json: In alternate 'Alt': +alternate-data-invalid.json:2: 'data' must be an object diff --git a/tests/qapi-schema/alternate-data-invalid.json b/tests/qapi-schema/alternate-data-invalid.json new file mode 100644 index 0000000..7d5d905 --- /dev/null +++ b/tests/qapi-schema/alternate-data-invalid.json @@ -0,0 +1,4 @@ +# Alternate type requires an object for 'data' +{ 'alternate': 'Alt', + 'data': ['rubbish', 'nonsense'] +} diff --git a/tests/qapi-schema/alternate-data-invalid.out b/tests/qapi-schema/alternate-data-invalid.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/qapi-schema/alternate-data-invalid.out diff --git a/tests/qapi-schema/meson.build b/tests/qapi-schema/meson.build index 8ba6917..d7163e6 100644 --- a/tests/qapi-schema/meson.build +++ b/tests/qapi-schema/meson.build @@ -14,6 +14,7 @@ schemas = [ 'alternate-conflict-string.json', 'alternate-conflict-bool-string.json', 'alternate-conflict-num-string.json', + 'alternate-data-invalid.json', 'alternate-empty.json', 'alternate-invalid-dict.json', 'alternate-nested.json', @@ -192,6 +193,7 @@ schemas = [ 'union-clash-branches.json', 'union-empty.json', 'union-invalid-base.json', + 'union-invalid-data.json', 'union-optional-branch.json', 'union-unknown.json', 'unknown-escape.json', diff --git a/tests/qapi-schema/union-invalid-data.err b/tests/qapi-schema/union-invalid-data.err new file mode 100644 index 0000000..e26cf76 --- /dev/null +++ b/tests/qapi-schema/union-invalid-data.err @@ -0,0 +1,2 @@ +union-invalid-data.json: In union 'TestUnion': +union-invalid-data.json:2: 'data' must be an object diff --git a/tests/qapi-schema/union-invalid-data.json b/tests/qapi-schema/union-invalid-data.json new file mode 100644 index 0000000..395ba24 --- /dev/null +++ b/tests/qapi-schema/union-invalid-data.json @@ -0,0 +1,6 @@ +# the union data type must be an object. +{ 'union': 'TestUnion', + 'base': 'int', + 'discriminator': 'int', + 'data': ['rubbish', 'nonsense'] +} diff --git a/tests/qapi-schema/union-invalid-data.out b/tests/qapi-schema/union-invalid-data.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/qapi-schema/union-invalid-data.out |