qemu/scripts/qapi/schema.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1493 lines
50 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
#
# QAPI schema internal representation
#
# Copyright (c) 2015-2019 Red Hat Inc.
#
# Authors:
# Markus Armbruster <armbru@redhat.com>
# Eric Blake <eblake@redhat.com>
# Marc-André Lureau <marcandre.lureau@redhat.com>
#
# This work is licensed under the terms of the GNU GPL, version 2.
# See the COPYING file in the top-level directory.
# pylint: disable=too-many-lines
# TODO catching name collisions in generated code would be nice
from __future__ import annotations
from abc import ABC, abstractmethod
from collections import OrderedDict
import os
import re
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Union,
cast,
)
from .common import (
POINTER_SUFFIX,
c_name,
cgen_ifcond,
docgen_ifcond,
gen_endif,
gen_if,
)
qapi/parser: Don't try to handle file errors Fixes: f5d4361cda Fixes: 52a474180a Fixes: 46f49468c6 Remove the try/except block that handles file-opening errors in QAPISchemaParser.__init__() and add one each to QAPISchemaParser._include() and QAPISchema.__init__() respectively. This simultaneously fixes the typing of info.fname (f5d4361cda), A static typing violation in test-qapi (46f49468c6), and a regression of an error message (52a474180a). The short-ish version of what motivates this patch is: - It's hard to write a good error message in the init method, because we need to determine the context of our caller to do so. It's easier to just let the caller write the message. - We don't want to allow QAPISourceInfo(None, None, None) to exist. The typing introduced by commit f5d4361cda types the 'fname' field as (non-optional) str, which was premature until the removal of this construct. - Errors made using such an object are currently incorrect (since 52a474180a) - It's not technically a semantic error if we cannot open the schema. - There are various typing constraints that make mixing these two cases undesirable for a single special case. - test-qapi's code handling an fname of 'None' is now dead, drop it. Additionally, Not all QAPIError objects have an 'info' field (since 46f49468), so deleting this stanza corrects a typing oversight in test-qapi introduced by that commit. Other considerations: - open() is moved to a 'with' block to ensure file pointers are cleaned up deterministically. - Python 3.3 deprecated IOError and made it a synonym for OSError. Avoid the misleading perception these exception handlers are narrower than they really are. The long version: The error message here is incorrect (since commit 52a474180a): > python3 qapi-gen.py 'fake.json' qapi-gen.py: qapi-gen.py: can't read schema file 'fake.json': No such file or directory In pursuing it, we find that QAPISourceInfo has a special accommodation for when there's no filename. Meanwhile, the intent when QAPISourceInfo was typed (f5d4361cda) was non-optional 'str'. This usage was overlooked. To remove this, I'd want to avoid having a "fake" QAPISourceInfo object. I also don't want to explicitly begin accommodating QAPISourceInfo itself being None, because we actually want to eventually prove that this can never happen -- We don't want to confuse "The file isn't open yet" with "This error stems from a definition that wasn't defined in any file". (An earlier series tried to create a dummy info object, but it was tough to prove in review that it worked correctly without creating new regressions. This patch avoids that distraction. We would like to first prove that we never raise QAPISemError for any built-in object before we add "special" info objects. We aren't ready to do that yet.) So, which way out of the labyrinth? Here's one way: Don't try to handle errors at a level with "mixed" semantic contexts; i.e. don't mix inclusion errors (should report a source line where the include was triggered) and command line errors (where we specified a file we couldn't read). Remove the error handling from the initializer of the parser. Pythonic! Now it's the caller's job to figure out what to do about it. Handle the error in QAPISchemaParser._include() instead, where we can write a targeted error message where we are guaranteed to have an 'info' context to report with. The root level error can similarly move to QAPISchema.__init__(), where we know we'll never have an info context to report with, so we use a more abstract error type. Now the error looks sensible again: > python3 qapi-gen.py 'fake.json' qapi-gen.py: can't read schema file 'fake.json': No such file or directory With these error cases separated, QAPISourceInfo can be solidified as never having placeholder arguments that violate our desired types. Clean up test-qapi along similar lines. Signed-off-by: John Snow <jsnow@redhat.com> Message-Id: <20210519183951.3946870-2-jsnow@redhat.com> Reviewed-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
2021-05-19 18:39:37 +00:00
from .error import QAPIError, QAPISemError, QAPISourceError
qapi: Prefer explicit relative imports All of the QAPI include statements are changed to be package-aware, as explicit relative imports. A quirk of Python packages is that the name of the package exists only *outside* of the package. This means that to a module inside of the qapi folder, there is inherently no such thing as the "qapi" package. The reason these imports work is because the "qapi" package exists in the context of the caller -- the execution shim, where sys.path includes a directory that has a 'qapi' folder in it. When we write "from qapi import sibling", we are NOT referencing the folder 'qapi', but rather "any package named qapi in sys.path". If you should so happen to have a 'qapi' package in your path, it will use *that* package. When we write "from .sibling import foo", we always reference explicitly our sibling module; guaranteeing consistency in *where* we are importing these modules from. This can be useful when working with virtual environments and packages in development mode. In development mode, a package is installed as a series of symlinks that forwards to your same source files. The problem arises because code quality checkers will follow "import qapi.x" to the "installed" version instead of the sibling file and -- even though they are the same file -- they have different module paths, and this causes cyclic import problems, false positive type mismatch errors, and more. It can also be useful when dealing with hierarchical packages, e.g. if we allow qemu.core.qmp, qemu.qapi.parser, etc. Signed-off-by: John Snow <jsnow@redhat.com> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com> Reviewed-by: Cleber Rosa <crosa@redhat.com> Reviewed-by: Philippe Mathieu-Daudé <philmd@redhat.com> Message-Id: <20201009161558.107041-6-jsnow@redhat.com> Reviewed-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
2020-10-09 16:15:27 +00:00
from .expr import check_exprs
from .parser import QAPIDoc, QAPIExpression, QAPISchemaParser
from .source import QAPISourceInfo
class QAPISchemaIfCond:
def __init__(
self,
ifcond: Optional[Union[str, Dict[str, object]]] = None,
) -> None:
self.ifcond = ifcond
def _cgen(self) -> str:
return cgen_ifcond(self.ifcond)
def gen_if(self) -> str:
return gen_if(self._cgen())
def gen_endif(self) -> str:
return gen_endif(self._cgen())
def docgen(self) -> str:
return docgen_ifcond(self.ifcond)
def is_present(self) -> bool:
return bool(self.ifcond)
class QAPISchemaEntity:
"""
A schema entity.
This is either a directive, such as include, or a definition.
The latter uses sub-class `QAPISchemaDefinition`.
"""
def __init__(self, info: Optional[QAPISourceInfo]):
self._module: Optional[QAPISchemaModule] = None
# For explicitly defined entities, info points to the (explicit)
# definition. For builtins (and their arrays), info is None.
# For implicitly defined entities, info points to a place that
# triggered the implicit definition (there may be more than one
# such place).
self.info = info
self._checked = False
def __repr__(self) -> str:
return "<%s at 0x%x>" % (type(self).__name__, id(self))
def check(self, schema: QAPISchema) -> None:
# pylint: disable=unused-argument
self._checked = True
def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
pass
def _set_module(
self, schema: QAPISchema, info: Optional[QAPISourceInfo]
) -> None:
assert self._checked
fname = info.fname if info else QAPISchemaModule.BUILTIN_MODULE_NAME
self._module = schema.module_by_fname(fname)
self._module.add_entity(self)
def set_module(self, schema: QAPISchema) -> None:
self._set_module(schema, self.info)
def visit(self, visitor: QAPISchemaVisitor) -> None:
# pylint: disable=unused-argument
assert self._checked
class QAPISchemaDefinition(QAPISchemaEntity):
meta: str
def __init__(
self,
name: str,
info: Optional[QAPISourceInfo],
doc: Optional[QAPIDoc],
ifcond: Optional[QAPISchemaIfCond] = None,
features: Optional[List[QAPISchemaFeature]] = None,
):
super().__init__(info)
for f in features or []:
f.set_defined_in(name)
self.name = name
self.doc = doc
self._ifcond = ifcond or QAPISchemaIfCond()
self.features = features or []
def __repr__(self) -> str:
return "<%s:%s at 0x%x>" % (type(self).__name__, self.name,
id(self))
def c_name(self) -> str:
return c_name(self.name)
def check(self, schema: QAPISchema) -> None:
assert not self._checked
super().check(schema)
seen: Dict[str, QAPISchemaMember] = {}
for f in self.features:
f.check_clash(self.info, seen)
def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
super().connect_doc(doc)
doc = doc or self.doc
if doc:
for f in self.features:
doc.connect_feature(f)
@property
def ifcond(self) -> QAPISchemaIfCond:
assert self._checked
return self._ifcond
def is_implicit(self) -> bool:
return not self.info
def describe(self) -> str:
return "%s '%s'" % (self.meta, self.name)
class QAPISchemaVisitor:
def visit_begin(self, schema: QAPISchema) -> None:
pass
def visit_end(self) -> None:
pass
def visit_module(self, name: str) -> None:
pass
def visit_needed(self, entity: QAPISchemaEntity) -> bool:
# pylint: disable=unused-argument
# Default to visiting everything
return True
def visit_include(self, name: str, info: Optional[QAPISourceInfo]) -> None:
pass
def visit_builtin_type(
self, name: str, info: Optional[QAPISourceInfo], json_type: str
) -> None:
pass
def visit_enum_type(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
members: List[QAPISchemaEnumMember],
prefix: Optional[str],
) -> None:
pass
def visit_array_type(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
element_type: QAPISchemaType,
) -> None:
pass
def visit_object_type(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
base: Optional[QAPISchemaObjectType],
members: List[QAPISchemaObjectTypeMember],
branches: Optional[QAPISchemaBranches],
) -> None:
pass
def visit_object_type_flat(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
members: List[QAPISchemaObjectTypeMember],
branches: Optional[QAPISchemaBranches],
) -> None:
pass
def visit_alternate_type(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
alternatives: QAPISchemaAlternatives,
) -> None:
pass
def visit_command(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
arg_type: Optional[QAPISchemaObjectType],
ret_type: Optional[QAPISchemaType],
gen: bool,
success_response: bool,
boxed: bool,
allow_oob: bool,
allow_preconfig: bool,
coroutine: bool,
) -> None:
pass
def visit_event(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
arg_type: Optional[QAPISchemaObjectType],
boxed: bool,
) -> None:
pass
class QAPISchemaModule:
BUILTIN_MODULE_NAME = './builtin'
def __init__(self, name: str):
self.name = name
self._entity_list: List[QAPISchemaEntity] = []
@staticmethod
def is_system_module(name: str) -> bool:
"""
System modules are internally defined modules.
Their names start with the "./" prefix.
"""
return name.startswith('./')
@classmethod
def is_user_module(cls, name: str) -> bool:
"""
User modules are those defined by the user in qapi JSON files.
They do not start with the "./" prefix.
"""
return not cls.is_system_module(name)
@classmethod
def is_builtin_module(cls, name: str) -> bool:
"""
The built-in module is a single System module for the built-in types.
It is always "./builtin".
"""
return name == cls.BUILTIN_MODULE_NAME
def add_entity(self, ent: QAPISchemaEntity) -> None:
self._entity_list.append(ent)
def visit(self, visitor: QAPISchemaVisitor) -> None:
visitor.visit_module(self.name)
for entity in self._entity_list:
if visitor.visit_needed(entity):
entity.visit(visitor)
class QAPISchemaInclude(QAPISchemaEntity):
def __init__(self, sub_module: QAPISchemaModule, info: QAPISourceInfo):
super().__init__(info)
self._sub_module = sub_module
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_include(self._sub_module.name, self.info)
class QAPISchemaType(QAPISchemaDefinition, ABC):
# Return the C type for common use.
# For the types we commonly box, this is a pointer type.
@abstractmethod
def c_type(self) -> str:
pass
# Return the C type to be used in a parameter list.
def c_param_type(self) -> str:
return self.c_type()
# Return the C type to be used where we suppress boxing.
def c_unboxed_type(self) -> str:
return self.c_type()
@abstractmethod
def json_type(self) -> str:
pass
def alternate_qtype(self) -> Optional[str]:
json2qtype = {
'null': 'QTYPE_QNULL',
'string': 'QTYPE_QSTRING',
'number': 'QTYPE_QNUM',
'int': 'QTYPE_QNUM',
'boolean': 'QTYPE_QBOOL',
'array': 'QTYPE_QLIST',
'object': 'QTYPE_QDICT'
}
return json2qtype.get(self.json_type())
def doc_type(self) -> Optional[str]:
if self.is_implicit():
return None
return self.name
def need_has_if_optional(self) -> bool:
qapi: Start to elide redundant has_FOO in generated C In QAPI, absent optional members are distinct from any present value. We thus represent an optional schema member FOO as two C members: a FOO with the member's type, and a bool has_FOO. Likewise for function arguments. However, has_FOO is actually redundant for a pointer-valued FOO, which can be null only when has_FOO is false, i.e. has_FOO == !!FOO. Except for arrays, where we a null FOO can also be a present empty array. The redundant has_FOO are a nuisance to work with. Improve the generator to elide them. Uses of has_FOO need to be replaced as follows. Tests of has_FOO become the equivalent comparison of FOO with null. For brevity, this is commonly done by implicit conversion to bool. Assignments to has_FOO get dropped. Likewise for arguments to has_FOO parameters. Beware: code may violate the invariant has_FOO == !!FOO before the transformation, and get away with it. The above transformation can then break things. Two cases: * Absent: if code ignores FOO entirely when !has_FOO (except for freeing it if necessary), even non-null / uninitialized FOO works. Such code is known to exist. * Present: if code ignores FOO entirely when has_FOO, even null FOO works. Such code should not exist. In both cases, replacing tests of has_FOO by FOO reverts their sense. We have to fix the value of FOO then. To facilitate review of the necessary updates to handwritten code, add means to opt out of this change, and opt out for all QAPI schema modules where the change requires updates to handwritten code. The next few commits will remove these opt-outs in reviewable chunks, then drop the means to opt out. Signed-off-by: Markus Armbruster <armbru@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Message-Id: <20221104160712.3005652-5-armbru@redhat.com>
2022-11-04 16:06:46 +00:00
# When FOO is a pointer, has_FOO == !!FOO, i.e. has_FOO is redundant.
# Except for arrays; see QAPISchemaArrayType.need_has_if_optional().
return not self.c_type().endswith(POINTER_SUFFIX)
def check(self, schema: QAPISchema) -> None:
super().check(schema)
for feat in self.features:
if feat.is_special():
raise QAPISemError(
self.info,
f"feature '{feat.name}' is not supported for types")
def describe(self) -> str:
return "%s type '%s'" % (self.meta, self.name)
class QAPISchemaBuiltinType(QAPISchemaType):
meta = 'built-in'
def __init__(self, name: str, json_type: str, c_type: str):
super().__init__(name, None, None)
assert json_type in ('string', 'number', 'int', 'boolean', 'null',
'value')
self._json_type_name = json_type
self._c_type_name = c_type
def c_name(self) -> str:
return self.name
def c_type(self) -> str:
return self._c_type_name
def c_param_type(self) -> str:
if self.name == 'str':
return 'const ' + self._c_type_name
return self._c_type_name
def json_type(self) -> str:
return self._json_type_name
def doc_type(self) -> str:
return self.json_type()
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_builtin_type(self.name, self.info, self.json_type())
class QAPISchemaEnumType(QAPISchemaType):
meta = 'enum'
def __init__(
self,
name: str,
info: Optional[QAPISourceInfo],
doc: Optional[QAPIDoc],
ifcond: Optional[QAPISchemaIfCond],
features: Optional[List[QAPISchemaFeature]],
members: List[QAPISchemaEnumMember],
prefix: Optional[str],
):
super().__init__(name, info, doc, ifcond, features)
for m in members:
m.set_defined_in(name)
self.members = members
self.prefix = prefix
def check(self, schema: QAPISchema) -> None:
super().check(schema)
seen: Dict[str, QAPISchemaMember] = {}
for m in self.members:
m.check_clash(self.info, seen)
def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
super().connect_doc(doc)
qapi: Clean up doc comment checking for implicit union base An object type's doc comment describes the type's members, less the ones defined in a named base type. Cases: * Struct: the members are defined in 'data' and inherited from 'base'. Since the base type cannot be implicit, the doc comment describes just 'data'. * Simple union: the only member is the implicit tag member @type, and the doc comment describes it. * Flat union with implicit base type: the members are defined in 'base', and the doc comment describes it. * Flat union with named base type: the members are inherited from 'base'. The doc comment describes no members. Before we can check a doc comment with .check_doc(), we need .connect_doc() connect each of its "argument sections" to the member it documents. For structs and simple unions, this is straightforward: the members in question are in .local_members, and .connect_doc() connects them. For flat unions with a named base type, it's trivial: .local_members is empty, and .connect_doc() does nothing. For flat unions with an implicit base type, it's tricky. We have QAPISchema._make_implicit_object_type() forward the union's doc comment to the implicit base type, so that the base type's .connect_doc() connects the members. The union's .connect_doc() does nothing, as .local_members is empty. Dirt effect: we check the doc comment twice, once for the union type, and once for the implicit base type. This is needlessly brittle and hard to understand. Clean up as follows. Make the union's .connect_doc() connect an implicit base's members itself. Do not forward the union's doc comment to its implicit base type. Requires extending .connect_doc() so it can work with a doc comment other than self.doc. Add an optional argument for that. Signed-off-by: Markus Armbruster <armbru@redhat.com> Message-Id: <20191024110237.30963-11-armbru@redhat.com>
2019-10-24 11:02:28 +00:00
doc = doc or self.doc
for m in self.members:
m.connect_doc(doc)
def is_implicit(self) -> bool:
# See QAPISchema._def_predefineds()
return self.name == 'QType'
def c_type(self) -> str:
return c_name(self.name)
def member_names(self) -> List[str]:
return [m.name for m in self.members]
def json_type(self) -> str:
return 'string'
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_enum_type(
self.name, self.info, self.ifcond, self.features,
self.members, self.prefix)
class QAPISchemaArrayType(QAPISchemaType):
meta = 'array'
def __init__(
self, name: str, info: Optional[QAPISourceInfo], element_type: str
):
super().__init__(name, info, None)
self._element_type_name = element_type
self.element_type: QAPISchemaType
def need_has_if_optional(self) -> bool:
qapi: Start to elide redundant has_FOO in generated C In QAPI, absent optional members are distinct from any present value. We thus represent an optional schema member FOO as two C members: a FOO with the member's type, and a bool has_FOO. Likewise for function arguments. However, has_FOO is actually redundant for a pointer-valued FOO, which can be null only when has_FOO is false, i.e. has_FOO == !!FOO. Except for arrays, where we a null FOO can also be a present empty array. The redundant has_FOO are a nuisance to work with. Improve the generator to elide them. Uses of has_FOO need to be replaced as follows. Tests of has_FOO become the equivalent comparison of FOO with null. For brevity, this is commonly done by implicit conversion to bool. Assignments to has_FOO get dropped. Likewise for arguments to has_FOO parameters. Beware: code may violate the invariant has_FOO == !!FOO before the transformation, and get away with it. The above transformation can then break things. Two cases: * Absent: if code ignores FOO entirely when !has_FOO (except for freeing it if necessary), even non-null / uninitialized FOO works. Such code is known to exist. * Present: if code ignores FOO entirely when has_FOO, even null FOO works. Such code should not exist. In both cases, replacing tests of has_FOO by FOO reverts their sense. We have to fix the value of FOO then. To facilitate review of the necessary updates to handwritten code, add means to opt out of this change, and opt out for all QAPI schema modules where the change requires updates to handwritten code. The next few commits will remove these opt-outs in reviewable chunks, then drop the means to opt out. Signed-off-by: Markus Armbruster <armbru@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Message-Id: <20221104160712.3005652-5-armbru@redhat.com>
2022-11-04 16:06:46 +00:00
# When FOO is an array, we still need has_FOO to distinguish
# absent (!has_FOO) from present and empty (has_FOO && !FOO).
return True
def check(self, schema: QAPISchema) -> None:
super().check(schema)
self.element_type = schema.resolve_type(
self._element_type_name, self.info,
self.info.defn_meta if self.info else None)
assert not isinstance(self.element_type, QAPISchemaArrayType)
def set_module(self, schema: QAPISchema) -> None:
self._set_module(schema, self.element_type.info)
@property
def ifcond(self) -> QAPISchemaIfCond:
assert self._checked
return self.element_type.ifcond
def is_implicit(self) -> bool:
return True
def c_type(self) -> str:
return c_name(self.name) + POINTER_SUFFIX
def json_type(self) -> str:
return 'array'
def doc_type(self) -> Optional[str]:
elt_doc_type = self.element_type.doc_type()
if not elt_doc_type:
return None
return 'array of ' + elt_doc_type
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_array_type(self.name, self.info, self.ifcond,
self.element_type)
def describe(self) -> str:
return "%s type ['%s']" % (self.meta, self._element_type_name)
class QAPISchemaObjectType(QAPISchemaType):
def __init__(
self,
name: str,
info: Optional[QAPISourceInfo],
doc: Optional[QAPIDoc],
ifcond: Optional[QAPISchemaIfCond],
features: Optional[List[QAPISchemaFeature]],
base: Optional[str],
local_members: List[QAPISchemaObjectTypeMember],
branches: Optional[QAPISchemaBranches],
):
# struct has local_members, optional base, and no branches
# union has base, branches, and no local_members
super().__init__(name, info, doc, ifcond, features)
self.meta = 'union' if branches else 'struct'
for m in local_members:
m.set_defined_in(name)
if branches is not None:
branches.set_defined_in(name)
self._base_name = base
self.base = None
self.local_members = local_members
self.branches = branches
self.members: List[QAPISchemaObjectTypeMember]
self._check_complete = False
def check(self, schema: QAPISchema) -> None:
# This calls another type T's .check() exactly when the C
# struct emitted by gen_object() contains that T's C struct
# (pointers don't count).
if self._check_complete:
# A previous .check() completed: nothing to do
return
if self._checked:
# Recursed: C struct contains itself
raise QAPISemError(self.info,
"object %s contains itself" % self.name)
super().check(schema)
assert self._checked and not self._check_complete
seen = OrderedDict()
if self._base_name:
self.base = schema.resolve_type(self._base_name, self.info,
"'base'")
if (not isinstance(self.base, QAPISchemaObjectType)
or self.base.branches):
raise QAPISemError(
self.info,
"'base' requires a struct type, %s isn't"
% self.base.describe())
self.base.check(schema)
self.base.check_clash(self.info, seen)
for m in self.local_members:
m.check(schema)
m.check_clash(self.info, seen)
# self.check_clash() works in terms of the supertype, but
# self.members is declared List[QAPISchemaObjectTypeMember].
# Cast down to the subtype.
members = cast(List[QAPISchemaObjectTypeMember], list(seen.values()))
if self.branches:
self.branches.check(schema, seen)
self.branches.check_clash(self.info, seen)
self.members = members
self._check_complete = True # mark completed
# Check that the members of this type do not cause duplicate JSON members,
# and update seen to track the members seen so far. Report any errors
# on behalf of info, which is not necessarily self.info
def check_clash(
self,
info: Optional[QAPISourceInfo],
seen: Dict[str, QAPISchemaMember],
) -> None:
assert self._checked
for m in self.members:
m.check_clash(info, seen)
if self.branches:
self.branches.check_clash(info, seen)
def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
super().connect_doc(doc)
qapi: Clean up doc comment checking for implicit union base An object type's doc comment describes the type's members, less the ones defined in a named base type. Cases: * Struct: the members are defined in 'data' and inherited from 'base'. Since the base type cannot be implicit, the doc comment describes just 'data'. * Simple union: the only member is the implicit tag member @type, and the doc comment describes it. * Flat union with implicit base type: the members are defined in 'base', and the doc comment describes it. * Flat union with named base type: the members are inherited from 'base'. The doc comment describes no members. Before we can check a doc comment with .check_doc(), we need .connect_doc() connect each of its "argument sections" to the member it documents. For structs and simple unions, this is straightforward: the members in question are in .local_members, and .connect_doc() connects them. For flat unions with a named base type, it's trivial: .local_members is empty, and .connect_doc() does nothing. For flat unions with an implicit base type, it's tricky. We have QAPISchema._make_implicit_object_type() forward the union's doc comment to the implicit base type, so that the base type's .connect_doc() connects the members. The union's .connect_doc() does nothing, as .local_members is empty. Dirt effect: we check the doc comment twice, once for the union type, and once for the implicit base type. This is needlessly brittle and hard to understand. Clean up as follows. Make the union's .connect_doc() connect an implicit base's members itself. Do not forward the union's doc comment to its implicit base type. Requires extending .connect_doc() so it can work with a doc comment other than self.doc. Add an optional argument for that. Signed-off-by: Markus Armbruster <armbru@redhat.com> Message-Id: <20191024110237.30963-11-armbru@redhat.com>
2019-10-24 11:02:28 +00:00
doc = doc or self.doc
if self.base and self.base.is_implicit():
self.base.connect_doc(doc)
for m in self.local_members:
m.connect_doc(doc)
def is_implicit(self) -> bool:
# See QAPISchema._make_implicit_object_type(), as well as
# _def_predefineds()
return self.name.startswith('q_')
def is_empty(self) -> bool:
return not self.members and not self.branches
def has_conditional_members(self) -> bool:
2023-03-16 07:13:25 +00:00
return any(m.ifcond.is_present() for m in self.members)
def c_name(self) -> str:
assert self.name != 'q_empty'
return super().c_name()
def c_type(self) -> str:
assert not self.is_implicit()
return c_name(self.name) + POINTER_SUFFIX
def c_unboxed_type(self) -> str:
return c_name(self.name)
def json_type(self) -> str:
return 'object'
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_object_type(
self.name, self.info, self.ifcond, self.features,
self.base, self.local_members, self.branches)
visitor.visit_object_type_flat(
self.name, self.info, self.ifcond, self.features,
self.members, self.branches)
class QAPISchemaAlternateType(QAPISchemaType):
meta = 'alternate'
def __init__(
self,
name: str,
info: QAPISourceInfo,
doc: Optional[QAPIDoc],
ifcond: Optional[QAPISchemaIfCond],
features: List[QAPISchemaFeature],
alternatives: QAPISchemaAlternatives,
):
super().__init__(name, info, doc, ifcond, features)
assert alternatives.tag_member
alternatives.set_defined_in(name)
alternatives.tag_member.set_defined_in(self.name)
self.alternatives = alternatives
def check(self, schema: QAPISchema) -> None:
super().check(schema)
self.alternatives.tag_member.check(schema)
# Not calling self.alternatives.check_clash(), because there's
# nothing to clash with
self.alternatives.check(schema, {})
# Alternate branch names have no relation to the tag enum values;
# so we have to check for potential name collisions ourselves.
seen: Dict[str, QAPISchemaMember] = {}
types_seen: Dict[str, str] = {}
for v in self.alternatives.variants:
v.check_clash(self.info, seen)
qtype = v.type.alternate_qtype()
if not qtype:
raise QAPISemError(
self.info,
"%s cannot use %s"
% (v.describe(self.info), v.type.describe()))
conflicting = set([qtype])
if qtype == 'QTYPE_QSTRING':
if isinstance(v.type, QAPISchemaEnumType):
for m in v.type.members:
if m.name in ['on', 'off']:
conflicting.add('QTYPE_QBOOL')
if re.match(r'[-+0-9.]', m.name):
# lazy, could be tightened
conflicting.add('QTYPE_QNUM')
else:
conflicting.add('QTYPE_QNUM')
conflicting.add('QTYPE_QBOOL')
for qt in conflicting:
if qt in types_seen:
raise QAPISemError(
self.info,
"%s can't be distinguished from '%s'"
% (v.describe(self.info), types_seen[qt]))
types_seen[qt] = v.name
def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
super().connect_doc(doc)
doc = doc or self.doc
for v in self.alternatives.variants:
v.connect_doc(doc)
def c_type(self) -> str:
return c_name(self.name) + POINTER_SUFFIX
def json_type(self) -> str:
return 'value'
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_alternate_type(
self.name, self.info, self.ifcond, self.features,
self.alternatives)
class QAPISchemaVariants:
def __init__(
self,
info: QAPISourceInfo,
variants: List[QAPISchemaVariant],
):
self.info = info
self.tag_member: QAPISchemaObjectTypeMember
self.variants = variants
def set_defined_in(self, name: str) -> None:
for v in self.variants:
v.set_defined_in(name)
def check(
self, schema: QAPISchema, seen: Dict[str, QAPISchemaMember]
) -> None:
for v in self.variants:
v.check(schema)
class QAPISchemaBranches(QAPISchemaVariants):
def __init__(self,
info: QAPISourceInfo,
variants: List[QAPISchemaVariant],
tag_name: str):
super().__init__(info, variants)
self._tag_name = tag_name
def check(
self, schema: QAPISchema, seen: Dict[str, QAPISchemaMember]
) -> None:
# We need to narrow the member type:
tag_member = seen.get(c_name(self._tag_name))
assert (tag_member is None
or isinstance(tag_member, QAPISchemaObjectTypeMember))
base = "'base'"
# Pointing to the base type when not implicit would be
# nice, but we don't know it here
if not tag_member or self._tag_name != tag_member.name:
raise QAPISemError(
self.info,
"discriminator '%s' is not a member of %s"
% (self._tag_name, base))
self.tag_member = tag_member
# Here we do:
assert tag_member.defined_in
base_type = schema.lookup_type(tag_member.defined_in)
assert base_type
if not base_type.is_implicit():
base = "base type '%s'" % tag_member.defined_in
if not isinstance(tag_member.type, QAPISchemaEnumType):
raise QAPISemError(
self.info,
"discriminator member '%s' of %s must be of enum type"
% (self._tag_name, base))
if tag_member.optional:
raise QAPISemError(
self.info,
"discriminator member '%s' of %s must not be optional"
% (self._tag_name, base))
if tag_member.ifcond.is_present():
raise QAPISemError(
self.info,
"discriminator member '%s' of %s must not be conditional"
% (self._tag_name, base))
# branches that are not explicitly covered get an empty type
assert tag_member.defined_in
cases = {v.name for v in self.variants}
for m in tag_member.type.members:
if m.name not in cases:
v = QAPISchemaVariant(m.name, self.info,
'q_empty', m.ifcond)
v.set_defined_in(tag_member.defined_in)
self.variants.append(v)
if not self.variants:
raise QAPISemError(self.info, "union has no branches")
for v in self.variants:
v.check(schema)
# Union names must match enum values; alternate names are
# checked separately. Use 'seen' to tell the two apart.
if seen:
if v.name not in tag_member.type.member_names():
raise QAPISemError(
self.info,
"branch '%s' is not a value of %s"
% (v.name, tag_member.type.describe()))
if not isinstance(v.type, QAPISchemaObjectType):
raise QAPISemError(
self.info,
"%s cannot use %s"
% (v.describe(self.info), v.type.describe()))
v.type.check(schema)
def check_clash(
self,
info: Optional[QAPISourceInfo],
seen: Dict[str, QAPISchemaMember],
) -> None:
for v in self.variants:
# Reset seen map for each variant, since qapi names from one
# branch do not affect another branch.
#
# v.type's typing is enforced in check() above.
assert isinstance(v.type, QAPISchemaObjectType)
v.type.check_clash(info, dict(seen))
class QAPISchemaAlternatives(QAPISchemaVariants):
def __init__(self,
info: QAPISourceInfo,
variants: List[QAPISchemaVariant],
tag_member: QAPISchemaObjectTypeMember):
super().__init__(info, variants)
self.tag_member = tag_member
def check(
self, schema: QAPISchema, seen: Dict[str, QAPISchemaMember]
) -> None:
super().check(schema, seen)
assert isinstance(self.tag_member.type, QAPISchemaEnumType)
assert not self.tag_member.optional
assert not self.tag_member.ifcond.is_present()
class QAPISchemaMember:
""" Represents object members, enum members and features """
role = 'member'
def __init__(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: Optional[QAPISchemaIfCond] = None,
):
self.name = name
self.info = info
self.ifcond = ifcond or QAPISchemaIfCond()
self.defined_in: Optional[str] = None
def set_defined_in(self, name: str) -> None:
assert not self.defined_in
self.defined_in = name
def check_clash(
self,
info: Optional[QAPISourceInfo],
seen: Dict[str, QAPISchemaMember],
) -> None:
cname = c_name(self.name)
if cname in seen:
raise QAPISemError(
info,
"%s collides with %s"
% (self.describe(info), seen[cname].describe(info)))
seen[cname] = self
def connect_doc(self, doc: Optional[QAPIDoc]) -> None:
if doc:
doc.connect_member(self)
def describe(self, info: Optional[QAPISourceInfo]) -> str:
role = self.role
qapi: Improve specificity of type/member descriptions Error messages describe object members, enumeration values, features, and variants like ROLE 'NAME', where ROLE is "member", "value", "feature", or "branch", respectively. When the member is defined in another type, e.g. inherited from a base type, we add "of type 'TYPE'". Example: test case struct-base-clash-deep reports a member of type 'Sub' clashing with a member of its base type 'Base' as struct-base-clash-deep.json: In struct 'Sub': struct-base-clash-deep.json:10: member 'name' collides with member 'name' of type 'Base' Members of implicitly defined types need special treatment. We don't want to add "of type 'TYPE'" for them, because their named are made up and mean nothing to the user. Instead, we describe members of an implicitly defined base type as "base member 'NAME'", and command and event parameters as "parameter 'NAME'". Example: test case union-bad-base reports member of a variant's type clashing with a member of its implicitly defined base type as union-bad-base.json: In union 'TestUnion': union-bad-base.json:8: member 'string' of type 'TestTypeA' collides with base member 'string' The next commit will permit unions as variant types. "base member 'NAME' would then be ambigious: is it the union's base, or is it the union's variant's base? One of its test cases would report a clash between two such bases as "base member 'type' collides with base member 'type'". Confusing. Refine the special treatment: add "of TYPE" even for implicitly defined types, but massage TYPE and ROLE so they make sense for the user. Message-Id: <20230420102619.348173-3-berrange@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
2023-04-25 13:10:28 +00:00
meta = 'type'
defined_in = self.defined_in
assert defined_in
if defined_in.startswith('q_obj_'):
# See QAPISchema._make_implicit_object_type() - reverse the
# mapping there to create a nice human-readable description
defined_in = defined_in[6:]
if defined_in.endswith('-arg'):
# Implicit type created for a command's dict 'data'
assert role == 'member'
role = 'parameter'
qapi: Improve specificity of type/member descriptions Error messages describe object members, enumeration values, features, and variants like ROLE 'NAME', where ROLE is "member", "value", "feature", or "branch", respectively. When the member is defined in another type, e.g. inherited from a base type, we add "of type 'TYPE'". Example: test case struct-base-clash-deep reports a member of type 'Sub' clashing with a member of its base type 'Base' as struct-base-clash-deep.json: In struct 'Sub': struct-base-clash-deep.json:10: member 'name' collides with member 'name' of type 'Base' Members of implicitly defined types need special treatment. We don't want to add "of type 'TYPE'" for them, because their named are made up and mean nothing to the user. Instead, we describe members of an implicitly defined base type as "base member 'NAME'", and command and event parameters as "parameter 'NAME'". Example: test case union-bad-base reports member of a variant's type clashing with a member of its implicitly defined base type as union-bad-base.json: In union 'TestUnion': union-bad-base.json:8: member 'string' of type 'TestTypeA' collides with base member 'string' The next commit will permit unions as variant types. "base member 'NAME' would then be ambigious: is it the union's base, or is it the union's variant's base? One of its test cases would report a clash between two such bases as "base member 'type' collides with base member 'type'". Confusing. Refine the special treatment: add "of TYPE" even for implicitly defined types, but massage TYPE and ROLE so they make sense for the user. Message-Id: <20230420102619.348173-3-berrange@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
2023-04-25 13:10:28 +00:00
meta = 'command'
defined_in = defined_in[:-4]
elif defined_in.endswith('-base'):
# Implicit type created for a union's dict 'base'
role = 'base ' + role
qapi: Improve specificity of type/member descriptions Error messages describe object members, enumeration values, features, and variants like ROLE 'NAME', where ROLE is "member", "value", "feature", or "branch", respectively. When the member is defined in another type, e.g. inherited from a base type, we add "of type 'TYPE'". Example: test case struct-base-clash-deep reports a member of type 'Sub' clashing with a member of its base type 'Base' as struct-base-clash-deep.json: In struct 'Sub': struct-base-clash-deep.json:10: member 'name' collides with member 'name' of type 'Base' Members of implicitly defined types need special treatment. We don't want to add "of type 'TYPE'" for them, because their named are made up and mean nothing to the user. Instead, we describe members of an implicitly defined base type as "base member 'NAME'", and command and event parameters as "parameter 'NAME'". Example: test case union-bad-base reports member of a variant's type clashing with a member of its implicitly defined base type as union-bad-base.json: In union 'TestUnion': union-bad-base.json:8: member 'string' of type 'TestTypeA' collides with base member 'string' The next commit will permit unions as variant types. "base member 'NAME' would then be ambigious: is it the union's base, or is it the union's variant's base? One of its test cases would report a clash between two such bases as "base member 'type' collides with base member 'type'". Confusing. Refine the special treatment: add "of TYPE" even for implicitly defined types, but massage TYPE and ROLE so they make sense for the user. Message-Id: <20230420102619.348173-3-berrange@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
2023-04-25 13:10:28 +00:00
defined_in = defined_in[:-5]
else:
assert False
qapi: Improve specificity of type/member descriptions Error messages describe object members, enumeration values, features, and variants like ROLE 'NAME', where ROLE is "member", "value", "feature", or "branch", respectively. When the member is defined in another type, e.g. inherited from a base type, we add "of type 'TYPE'". Example: test case struct-base-clash-deep reports a member of type 'Sub' clashing with a member of its base type 'Base' as struct-base-clash-deep.json: In struct 'Sub': struct-base-clash-deep.json:10: member 'name' collides with member 'name' of type 'Base' Members of implicitly defined types need special treatment. We don't want to add "of type 'TYPE'" for them, because their named are made up and mean nothing to the user. Instead, we describe members of an implicitly defined base type as "base member 'NAME'", and command and event parameters as "parameter 'NAME'". Example: test case union-bad-base reports member of a variant's type clashing with a member of its implicitly defined base type as union-bad-base.json: In union 'TestUnion': union-bad-base.json:8: member 'string' of type 'TestTypeA' collides with base member 'string' The next commit will permit unions as variant types. "base member 'NAME' would then be ambigious: is it the union's base, or is it the union's variant's base? One of its test cases would report a clash between two such bases as "base member 'type' collides with base member 'type'". Confusing. Refine the special treatment: add "of TYPE" even for implicitly defined types, but massage TYPE and ROLE so they make sense for the user. Message-Id: <20230420102619.348173-3-berrange@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
2023-04-25 13:10:28 +00:00
assert info is not None
qapi: Improve specificity of type/member descriptions Error messages describe object members, enumeration values, features, and variants like ROLE 'NAME', where ROLE is "member", "value", "feature", or "branch", respectively. When the member is defined in another type, e.g. inherited from a base type, we add "of type 'TYPE'". Example: test case struct-base-clash-deep reports a member of type 'Sub' clashing with a member of its base type 'Base' as struct-base-clash-deep.json: In struct 'Sub': struct-base-clash-deep.json:10: member 'name' collides with member 'name' of type 'Base' Members of implicitly defined types need special treatment. We don't want to add "of type 'TYPE'" for them, because their named are made up and mean nothing to the user. Instead, we describe members of an implicitly defined base type as "base member 'NAME'", and command and event parameters as "parameter 'NAME'". Example: test case union-bad-base reports member of a variant's type clashing with a member of its implicitly defined base type as union-bad-base.json: In union 'TestUnion': union-bad-base.json:8: member 'string' of type 'TestTypeA' collides with base member 'string' The next commit will permit unions as variant types. "base member 'NAME' would then be ambigious: is it the union's base, or is it the union's variant's base? One of its test cases would report a clash between two such bases as "base member 'type' collides with base member 'type'". Confusing. Refine the special treatment: add "of TYPE" even for implicitly defined types, but massage TYPE and ROLE so they make sense for the user. Message-Id: <20230420102619.348173-3-berrange@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
2023-04-25 13:10:28 +00:00
if defined_in != info.defn_name:
return "%s '%s' of %s '%s'" % (role, self.name, meta, defined_in)
return "%s '%s'" % (role, self.name)
class QAPISchemaEnumMember(QAPISchemaMember):
role = 'value'
def __init__(
self,
name: str,
info: Optional[QAPISourceInfo],
ifcond: Optional[QAPISchemaIfCond] = None,
features: Optional[List[QAPISchemaFeature]] = None,
):
super().__init__(name, info, ifcond)
for f in features or []:
f.set_defined_in(name)
self.features = features or []
def connect_doc(self, doc: Optional[QAPIDoc]) -> None:
super().connect_doc(doc)
if doc:
for f in self.features:
doc.connect_feature(f)
class QAPISchemaFeature(QAPISchemaMember):
role = 'feature'
def is_special(self) -> bool:
return self.name in ('deprecated', 'unstable')
class QAPISchemaObjectTypeMember(QAPISchemaMember):
def __init__(
self,
name: str,
info: QAPISourceInfo,
typ: str,
optional: bool,
ifcond: Optional[QAPISchemaIfCond] = None,
features: Optional[List[QAPISchemaFeature]] = None,
):
super().__init__(name, info, ifcond)
for f in features or []:
f.set_defined_in(name)
self._type_name = typ
self.type: QAPISchemaType # set during check()
self.optional = optional
self.features = features or []
def need_has(self) -> bool:
qapi: Start to elide redundant has_FOO in generated C In QAPI, absent optional members are distinct from any present value. We thus represent an optional schema member FOO as two C members: a FOO with the member's type, and a bool has_FOO. Likewise for function arguments. However, has_FOO is actually redundant for a pointer-valued FOO, which can be null only when has_FOO is false, i.e. has_FOO == !!FOO. Except for arrays, where we a null FOO can also be a present empty array. The redundant has_FOO are a nuisance to work with. Improve the generator to elide them. Uses of has_FOO need to be replaced as follows. Tests of has_FOO become the equivalent comparison of FOO with null. For brevity, this is commonly done by implicit conversion to bool. Assignments to has_FOO get dropped. Likewise for arguments to has_FOO parameters. Beware: code may violate the invariant has_FOO == !!FOO before the transformation, and get away with it. The above transformation can then break things. Two cases: * Absent: if code ignores FOO entirely when !has_FOO (except for freeing it if necessary), even non-null / uninitialized FOO works. Such code is known to exist. * Present: if code ignores FOO entirely when has_FOO, even null FOO works. Such code should not exist. In both cases, replacing tests of has_FOO by FOO reverts their sense. We have to fix the value of FOO then. To facilitate review of the necessary updates to handwritten code, add means to opt out of this change, and opt out for all QAPI schema modules where the change requires updates to handwritten code. The next few commits will remove these opt-outs in reviewable chunks, then drop the means to opt out. Signed-off-by: Markus Armbruster <armbru@redhat.com> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Message-Id: <20221104160712.3005652-5-armbru@redhat.com>
2022-11-04 16:06:46 +00:00
return self.optional and self.type.need_has_if_optional()
def check(self, schema: QAPISchema) -> None:
assert self.defined_in
self.type = schema.resolve_type(self._type_name, self.info,
self.describe)
seen: Dict[str, QAPISchemaMember] = {}
for f in self.features:
f.check_clash(self.info, seen)
def connect_doc(self, doc: Optional[QAPIDoc]) -> None:
super().connect_doc(doc)
if doc:
for f in self.features:
doc.connect_feature(f)
class QAPISchemaVariant(QAPISchemaObjectTypeMember):
role = 'branch'
def __init__(
self,
name: str,
info: QAPISourceInfo,
typ: str,
ifcond: QAPISchemaIfCond,
):
super().__init__(name, info, typ, False, ifcond)
class QAPISchemaCommand(QAPISchemaDefinition):
meta = 'command'
def __init__(
self,
name: str,
info: QAPISourceInfo,
doc: Optional[QAPIDoc],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
arg_type: Optional[str],
ret_type: Optional[str],
gen: bool,
success_response: bool,
boxed: bool,
allow_oob: bool,
allow_preconfig: bool,
coroutine: bool,
):
super().__init__(name, info, doc, ifcond, features)
self._arg_type_name = arg_type
self.arg_type: Optional[QAPISchemaObjectType] = None
self._ret_type_name = ret_type
self.ret_type: Optional[QAPISchemaType] = None
self.gen = gen
self.success_response = success_response
self.boxed = boxed
self.allow_oob = allow_oob
self.allow_preconfig = allow_preconfig
self.coroutine = coroutine
def check(self, schema: QAPISchema) -> None:
assert self.info is not None
super().check(schema)
if self._arg_type_name:
arg_type = schema.resolve_type(
self._arg_type_name, self.info, "command's 'data'")
if not isinstance(arg_type, QAPISchemaObjectType):
raise QAPISemError(
self.info,
"command's 'data' cannot take %s"
% arg_type.describe())
self.arg_type = arg_type
if self.arg_type.branches and not self.boxed:
raise QAPISemError(
self.info,
"command's 'data' can take %s only with 'boxed': true"
% self.arg_type.describe())
2023-03-16 07:13:25 +00:00
self.arg_type.check(schema)
if self.arg_type.has_conditional_members() and not self.boxed:
raise QAPISemError(
self.info,
"conditional command arguments require 'boxed': true")
if self._ret_type_name:
self.ret_type = schema.resolve_type(
self._ret_type_name, self.info, "command's 'returns'")
if self.name not in self.info.pragma.command_returns_exceptions:
typ = self.ret_type
if isinstance(typ, QAPISchemaArrayType):
typ = typ.element_type
if not isinstance(typ, QAPISchemaObjectType):
raise QAPISemError(
self.info,
"command's 'returns' cannot take %s"
% self.ret_type.describe())
def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
super().connect_doc(doc)
doc = doc or self.doc
if doc:
if self.arg_type and self.arg_type.is_implicit():
self.arg_type.connect_doc(doc)
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_command(
self.name, self.info, self.ifcond, self.features,
self.arg_type, self.ret_type, self.gen, self.success_response,
self.boxed, self.allow_oob, self.allow_preconfig,
self.coroutine)
class QAPISchemaEvent(QAPISchemaDefinition):
meta = 'event'
def __init__(
self,
name: str,
info: QAPISourceInfo,
doc: Optional[QAPIDoc],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
arg_type: Optional[str],
boxed: bool,
):
super().__init__(name, info, doc, ifcond, features)
self._arg_type_name = arg_type
self.arg_type: Optional[QAPISchemaObjectType] = None
self.boxed = boxed
def check(self, schema: QAPISchema) -> None:
super().check(schema)
if self._arg_type_name:
typ = schema.resolve_type(
self._arg_type_name, self.info, "event's 'data'")
if not isinstance(typ, QAPISchemaObjectType):
raise QAPISemError(
self.info,
"event's 'data' cannot take %s"
% typ.describe())
self.arg_type = typ
if self.arg_type.branches and not self.boxed:
raise QAPISemError(
self.info,
"event's 'data' can take %s only with 'boxed': true"
% self.arg_type.describe())
2023-03-16 07:13:25 +00:00
self.arg_type.check(schema)
if self.arg_type.has_conditional_members() and not self.boxed:
raise QAPISemError(
self.info,
"conditional event arguments require 'boxed': true")
def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
super().connect_doc(doc)
doc = doc or self.doc
if doc:
if self.arg_type and self.arg_type.is_implicit():
self.arg_type.connect_doc(doc)
def visit(self, visitor: QAPISchemaVisitor) -> None:
super().visit(visitor)
visitor.visit_event(
self.name, self.info, self.ifcond, self.features,
self.arg_type, self.boxed)
class QAPISchema:
def __init__(self, fname: str):
self.fname = fname
qapi/parser: Don't try to handle file errors Fixes: f5d4361cda Fixes: 52a474180a Fixes: 46f49468c6 Remove the try/except block that handles file-opening errors in QAPISchemaParser.__init__() and add one each to QAPISchemaParser._include() and QAPISchema.__init__() respectively. This simultaneously fixes the typing of info.fname (f5d4361cda), A static typing violation in test-qapi (46f49468c6), and a regression of an error message (52a474180a). The short-ish version of what motivates this patch is: - It's hard to write a good error message in the init method, because we need to determine the context of our caller to do so. It's easier to just let the caller write the message. - We don't want to allow QAPISourceInfo(None, None, None) to exist. The typing introduced by commit f5d4361cda types the 'fname' field as (non-optional) str, which was premature until the removal of this construct. - Errors made using such an object are currently incorrect (since 52a474180a) - It's not technically a semantic error if we cannot open the schema. - There are various typing constraints that make mixing these two cases undesirable for a single special case. - test-qapi's code handling an fname of 'None' is now dead, drop it. Additionally, Not all QAPIError objects have an 'info' field (since 46f49468), so deleting this stanza corrects a typing oversight in test-qapi introduced by that commit. Other considerations: - open() is moved to a 'with' block to ensure file pointers are cleaned up deterministically. - Python 3.3 deprecated IOError and made it a synonym for OSError. Avoid the misleading perception these exception handlers are narrower than they really are. The long version: The error message here is incorrect (since commit 52a474180a): > python3 qapi-gen.py 'fake.json' qapi-gen.py: qapi-gen.py: can't read schema file 'fake.json': No such file or directory In pursuing it, we find that QAPISourceInfo has a special accommodation for when there's no filename. Meanwhile, the intent when QAPISourceInfo was typed (f5d4361cda) was non-optional 'str'. This usage was overlooked. To remove this, I'd want to avoid having a "fake" QAPISourceInfo object. I also don't want to explicitly begin accommodating QAPISourceInfo itself being None, because we actually want to eventually prove that this can never happen -- We don't want to confuse "The file isn't open yet" with "This error stems from a definition that wasn't defined in any file". (An earlier series tried to create a dummy info object, but it was tough to prove in review that it worked correctly without creating new regressions. This patch avoids that distraction. We would like to first prove that we never raise QAPISemError for any built-in object before we add "special" info objects. We aren't ready to do that yet.) So, which way out of the labyrinth? Here's one way: Don't try to handle errors at a level with "mixed" semantic contexts; i.e. don't mix inclusion errors (should report a source line where the include was triggered) and command line errors (where we specified a file we couldn't read). Remove the error handling from the initializer of the parser. Pythonic! Now it's the caller's job to figure out what to do about it. Handle the error in QAPISchemaParser._include() instead, where we can write a targeted error message where we are guaranteed to have an 'info' context to report with. The root level error can similarly move to QAPISchema.__init__(), where we know we'll never have an info context to report with, so we use a more abstract error type. Now the error looks sensible again: > python3 qapi-gen.py 'fake.json' qapi-gen.py: can't read schema file 'fake.json': No such file or directory With these error cases separated, QAPISourceInfo can be solidified as never having placeholder arguments that violate our desired types. Clean up test-qapi along similar lines. Signed-off-by: John Snow <jsnow@redhat.com> Message-Id: <20210519183951.3946870-2-jsnow@redhat.com> Reviewed-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
2021-05-19 18:39:37 +00:00
try:
parser = QAPISchemaParser(fname)
except OSError as err:
raise QAPIError(
f"can't read schema file '{fname}': {err.strerror}"
) from err
exprs = check_exprs(parser.exprs)
self.docs = parser.docs
self._entity_list: List[QAPISchemaEntity] = []
self._entity_dict: Dict[str, QAPISchemaDefinition] = {}
self._module_dict: Dict[str, QAPISchemaModule] = OrderedDict()
self._schema_dir = os.path.dirname(fname)
self._make_module(QAPISchemaModule.BUILTIN_MODULE_NAME)
self._make_module(fname)
self._predefining = True
self._def_predefineds()
self._predefining = False
self._def_exprs(exprs)
self.check()
def _def_entity(self, ent: QAPISchemaEntity) -> None:
self._entity_list.append(ent)
def _def_definition(self, defn: QAPISchemaDefinition) -> None:
# Only the predefined types are allowed to not have info
assert defn.info or self._predefining
self._def_entity(defn)
# TODO reject names that differ only in '_' vs. '.' vs. '-',
# because they're liable to clash in generated C.
other_defn = self._entity_dict.get(defn.name)
if other_defn:
if other_defn.info:
where = QAPISourceError(other_defn.info, "previous definition")
raise QAPISemError(
defn.info,
"'%s' is already defined\n%s" % (defn.name, where))
raise QAPISemError(
defn.info, "%s is already defined" % other_defn.describe())
self._entity_dict[defn.name] = defn
def lookup_entity(self,name: str) -> Optional[QAPISchemaEntity]:
return self._entity_dict.get(name)
def lookup_type(self, name: str) -> Optional[QAPISchemaType]:
typ = self.lookup_entity(name)
if isinstance(typ, QAPISchemaType):
return typ
return None
def resolve_type(
self,
name: str,
info: Optional[QAPISourceInfo],
what: Union[None, str, Callable[[QAPISourceInfo], str]],
) -> QAPISchemaType:
typ = self.lookup_type(name)
if not typ:
assert info and what # built-in types must not fail lookup
if callable(what):
what = what(info)
raise QAPISemError(
info, "%s uses unknown type '%s'" % (what, name))
return typ
def _module_name(self, fname: str) -> str:
if QAPISchemaModule.is_system_module(fname):
return fname
return os.path.relpath(fname, self._schema_dir)
def _make_module(self, fname: str) -> QAPISchemaModule:
name = self._module_name(fname)
if name not in self._module_dict:
self._module_dict[name] = QAPISchemaModule(name)
return self._module_dict[name]
def module_by_fname(self, fname: str) -> QAPISchemaModule:
name = self._module_name(fname)
return self._module_dict[name]
def _def_include(self, expr: QAPIExpression) -> None:
include = expr['include']
assert expr.doc is None
self._def_entity(
QAPISchemaInclude(self._make_module(include), expr.info))
def _def_builtin_type(
self, name: str, json_type: str, c_type: str
) -> None:
self._def_definition(QAPISchemaBuiltinType(name, json_type, c_type))
# Instantiating only the arrays that are actually used would
# be nice, but we can't as long as their generated code
# (qapi-builtin-types.[ch]) may be shared by some other
# schema.
self._make_array_type(name, None)
def _def_predefineds(self) -> None:
for t in [('str', 'string', 'char' + POINTER_SUFFIX),
('number', 'number', 'double'),
('int', 'int', 'int64_t'),
('int8', 'int', 'int8_t'),
('int16', 'int', 'int16_t'),
('int32', 'int', 'int32_t'),
('int64', 'int', 'int64_t'),
('uint8', 'int', 'uint8_t'),
('uint16', 'int', 'uint16_t'),
('uint32', 'int', 'uint32_t'),
('uint64', 'int', 'uint64_t'),
('size', 'int', 'uint64_t'),
('bool', 'boolean', 'bool'),
('any', 'value', 'QObject' + POINTER_SUFFIX),
('null', 'null', 'QNull' + POINTER_SUFFIX)]:
self._def_builtin_type(*t)
self.the_empty_object_type = QAPISchemaObjectType(
'q_empty', None, None, None, None, None, [], None)
self._def_definition(self.the_empty_object_type)
qtypes = ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist',
'qbool']
qtype_values = self._make_enum_members(
[{'name': n} for n in qtypes], None)
self._def_definition(QAPISchemaEnumType(
'QType', None, None, None, None, qtype_values, 'QTYPE'))
def _make_features(
self,
features: Optional[List[Dict[str, Any]]],
info: Optional[QAPISourceInfo],
) -> List[QAPISchemaFeature]:
if features is None:
return []
return [QAPISchemaFeature(f['name'], info,
QAPISchemaIfCond(f.get('if')))
for f in features]
def _make_enum_member(
self,
name: str,
ifcond: Optional[Union[str, Dict[str, Any]]],
features: Optional[List[Dict[str, Any]]],
info: Optional[QAPISourceInfo],
) -> QAPISchemaEnumMember:
return QAPISchemaEnumMember(name, info,
QAPISchemaIfCond(ifcond),
self._make_features(features, info))
def _make_enum_members(
self, values: List[Dict[str, Any]], info: Optional[QAPISourceInfo]
) -> List[QAPISchemaEnumMember]:
return [self._make_enum_member(v['name'], v.get('if'),
v.get('features'), info)
for v in values]
def _make_array_type(
self, element_type: str, info: Optional[QAPISourceInfo]
) -> str:
name = element_type + 'List' # reserved by check_defn_name_str()
if not self.lookup_type(name):
self._def_definition(QAPISchemaArrayType(
name, info, element_type))
return name
def _make_implicit_object_type(
self,
name: str,
info: QAPISourceInfo,
ifcond: QAPISchemaIfCond,
role: str,
members: List[QAPISchemaObjectTypeMember],
) -> Optional[str]:
if not members:
return None
# See also QAPISchemaObjectTypeMember.describe()
name = 'q_obj_%s-%s' % (name, role)
typ = self.lookup_entity(name)
if typ:
assert(isinstance(typ, QAPISchemaObjectType))
# The implicit object type has multiple users. This can
# only be a duplicate definition, which will be flagged
# later.
pass
else:
self._def_definition(QAPISchemaObjectType(
name, info, None, ifcond, None, None, members, None))
return name
def _def_enum_type(self, expr: QAPIExpression) -> None:
name = expr['enum']
data = expr['data']
prefix = expr.get('prefix')
ifcond = QAPISchemaIfCond(expr.get('if'))
info = expr.info
features = self._make_features(expr.get('features'), info)
self._def_definition(QAPISchemaEnumType(
name, info, expr.doc, ifcond, features,
self._make_enum_members(data, info), prefix))
def _make_member(
self,
name: str,
typ: Union[List[str], str],
ifcond: QAPISchemaIfCond,
features: Optional[List[Dict[str, Any]]],
info: QAPISourceInfo,
) -> QAPISchemaObjectTypeMember:
optional = False
if name.startswith('*'):
name = name[1:]
optional = True
if isinstance(typ, list):
assert len(typ) == 1
typ = self._make_array_type(typ[0], info)
return QAPISchemaObjectTypeMember(name, info, typ, optional, ifcond,
self._make_features(features, info))
def _make_members(
self,
data: Dict[str, Any],
info: QAPISourceInfo,
) -> List[QAPISchemaObjectTypeMember]:
return [self._make_member(key, value['type'],
QAPISchemaIfCond(value.get('if')),
value.get('features'), info)
for (key, value) in data.items()]
def _def_struct_type(self, expr: QAPIExpression) -> None:
name = expr['struct']
base = expr.get('base')
data = expr['data']
info = expr.info
ifcond = QAPISchemaIfCond(expr.get('if'))
features = self._make_features(expr.get('features'), info)
self._def_definition(QAPISchemaObjectType(
name, info, expr.doc, ifcond, features, base,
self._make_members(data, info),
None))
def _make_variant(
self,
case: str,
typ: str,
ifcond: QAPISchemaIfCond,
info: QAPISourceInfo,
) -> QAPISchemaVariant:
if isinstance(typ, list):
assert len(typ) == 1
typ = self._make_array_type(typ[0], info)
return QAPISchemaVariant(case, info, typ, ifcond)
def _def_union_type(self, expr: QAPIExpression) -> None:
name = expr['union']
base = expr['base']
tag_name = expr['discriminator']
data = expr['data']
assert isinstance(data, dict)
info = expr.info
ifcond = QAPISchemaIfCond(expr.get('if'))
features = self._make_features(expr.get('features'), info)
if isinstance(base, dict):
base = self._make_implicit_object_type(
name, info, ifcond,
'base', self._make_members(base, info))
variants = [
self._make_variant(key, value['type'],
QAPISchemaIfCond(value.get('if')),
info)
for (key, value) in data.items()]
members: List[QAPISchemaObjectTypeMember] = []
self._def_definition(
QAPISchemaObjectType(name, info, expr.doc, ifcond, features,
base, members,
QAPISchemaBranches(
info, variants, tag_name)))
def _def_alternate_type(self, expr: QAPIExpression) -> None:
name = expr['alternate']
data = expr['data']
assert isinstance(data, dict)
ifcond = QAPISchemaIfCond(expr.get('if'))
info = expr.info
features = self._make_features(expr.get('features'), info)
variants = [
self._make_variant(key, value['type'],
QAPISchemaIfCond(value.get('if')),
info)
for (key, value) in data.items()]
tag_member = QAPISchemaObjectTypeMember('type', info, 'QType', False)
self._def_definition(
QAPISchemaAlternateType(
name, info, expr.doc, ifcond, features,
QAPISchemaAlternatives(info, variants, tag_member)))
def _def_command(self, expr: QAPIExpression) -> None:
name = expr['command']
data = expr.get('data')
rets = expr.get('returns')
gen = expr.get('gen', True)
success_response = expr.get('success-response', True)
boxed = expr.get('boxed', False)
allow_oob = expr.get('allow-oob', False)
allow_preconfig = expr.get('allow-preconfig', False)
coroutine = expr.get('coroutine', False)
ifcond = QAPISchemaIfCond(expr.get('if'))
info = expr.info
features = self._make_features(expr.get('features'), info)
if isinstance(data, OrderedDict):
data = self._make_implicit_object_type(
name, info, ifcond,
'arg', self._make_members(data, info))
if isinstance(rets, list):
assert len(rets) == 1
rets = self._make_array_type(rets[0], info)
self._def_definition(
QAPISchemaCommand(name, info, expr.doc, ifcond, features, data,
rets, gen, success_response, boxed, allow_oob,
allow_preconfig, coroutine))
def _def_event(self, expr: QAPIExpression) -> None:
name = expr['event']
data = expr.get('data')
boxed = expr.get('boxed', False)
ifcond = QAPISchemaIfCond(expr.get('if'))
info = expr.info
features = self._make_features(expr.get('features'), info)
if isinstance(data, OrderedDict):
data = self._make_implicit_object_type(
name, info, ifcond,
'arg', self._make_members(data, info))
self._def_definition(QAPISchemaEvent(name, info, expr.doc, ifcond,
features, data, boxed))
def _def_exprs(self, exprs: List[QAPIExpression]) -> None:
for expr in exprs:
if 'enum' in expr:
self._def_enum_type(expr)
elif 'struct' in expr:
self._def_struct_type(expr)
elif 'union' in expr:
self._def_union_type(expr)
elif 'alternate' in expr:
self._def_alternate_type(expr)
elif 'command' in expr:
self._def_command(expr)
elif 'event' in expr:
self._def_event(expr)
elif 'include' in expr:
self._def_include(expr)
else:
assert False
def check(self) -> None:
for ent in self._entity_list:
ent.check(self)
ent.connect_doc()
for ent in self._entity_list:
ent.set_module(self)
for doc in self.docs:
doc.check()
def visit(self, visitor: QAPISchemaVisitor) -> None:
visitor.visit_begin(self)
for mod in self._module_dict.values():
mod.visit(visitor)
visitor.visit_end()