|  | """ | 
|  | QAPI domain extension. | 
|  | """ | 
|  |  | 
|  | # The best laid plans of mice and men, ... | 
|  | # pylint: disable=too-many-lines | 
|  |  | 
|  | from __future__ import annotations | 
|  |  | 
|  | import re | 
|  | import types | 
|  | from typing import ( | 
|  | TYPE_CHECKING, | 
|  | List, | 
|  | NamedTuple, | 
|  | Tuple, | 
|  | Type, | 
|  | cast, | 
|  | ) | 
|  |  | 
|  | from docutils import nodes | 
|  | from docutils.parsers.rst import directives | 
|  | from sphinx import addnodes | 
|  | from sphinx.directives import ObjectDescription | 
|  | from sphinx.domains import ( | 
|  | Domain, | 
|  | Index, | 
|  | IndexEntry, | 
|  | ObjType, | 
|  | ) | 
|  | from sphinx.locale import _, __ | 
|  | from sphinx.roles import XRefRole | 
|  | from sphinx.util import logging | 
|  | from sphinx.util.docutils import SphinxDirective | 
|  | from sphinx.util.nodes import make_id, make_refnode | 
|  |  | 
|  | from compat import ( | 
|  | CompatField, | 
|  | CompatGroupedField, | 
|  | CompatTypedField, | 
|  | KeywordNode, | 
|  | ParserFix, | 
|  | Signature, | 
|  | SpaceNode, | 
|  | ) | 
|  |  | 
|  |  | 
|  | if TYPE_CHECKING: | 
|  | from typing import ( | 
|  | AbstractSet, | 
|  | Any, | 
|  | Dict, | 
|  | Iterable, | 
|  | Optional, | 
|  | Union, | 
|  | ) | 
|  |  | 
|  | from docutils.nodes import Element, Node | 
|  | from sphinx.addnodes import desc_signature, pending_xref | 
|  | from sphinx.application import Sphinx | 
|  | from sphinx.builders import Builder | 
|  | from sphinx.environment import BuildEnvironment | 
|  | from sphinx.util.typing import OptionSpec | 
|  |  | 
|  |  | 
|  | logger = logging.getLogger(__name__) | 
|  |  | 
|  |  | 
|  | def _unpack_field( | 
|  | field: nodes.Node, | 
|  | ) -> Tuple[nodes.field_name, nodes.field_body]: | 
|  | """ | 
|  | docutils helper: unpack a field node in a type-safe manner. | 
|  | """ | 
|  | assert isinstance(field, nodes.field) | 
|  | assert len(field.children) == 2 | 
|  | assert isinstance(field.children[0], nodes.field_name) | 
|  | assert isinstance(field.children[1], nodes.field_body) | 
|  | return (field.children[0], field.children[1]) | 
|  |  | 
|  |  | 
|  | class ObjectEntry(NamedTuple): | 
|  | docname: str | 
|  | node_id: str | 
|  | objtype: str | 
|  | aliased: bool | 
|  |  | 
|  |  | 
|  | class QAPIXRefRole(XRefRole): | 
|  |  | 
|  | def process_link( | 
|  | self, | 
|  | env: BuildEnvironment, | 
|  | refnode: Element, | 
|  | has_explicit_title: bool, | 
|  | title: str, | 
|  | target: str, | 
|  | ) -> tuple[str, str]: | 
|  | refnode["qapi:namespace"] = env.ref_context.get("qapi:namespace") | 
|  | refnode["qapi:module"] = env.ref_context.get("qapi:module") | 
|  |  | 
|  | # Cross-references that begin with a tilde adjust the title to | 
|  | # only show the reference without a leading module, even if one | 
|  | # was provided. This is a Sphinx-standard syntax; give it | 
|  | # priority over QAPI-specific type markup below. | 
|  | hide_module = False | 
|  | if target.startswith("~"): | 
|  | hide_module = True | 
|  | target = target[1:] | 
|  |  | 
|  | # Type names that end with "?" are considered optional | 
|  | # arguments and should be documented as such, but it's not | 
|  | # part of the xref itself. | 
|  | if target.endswith("?"): | 
|  | refnode["qapi:optional"] = True | 
|  | target = target[:-1] | 
|  |  | 
|  | # Type names wrapped in brackets denote lists. strip the | 
|  | # brackets and remember to add them back later. | 
|  | if target.startswith("[") and target.endswith("]"): | 
|  | refnode["qapi:array"] = True | 
|  | target = target[1:-1] | 
|  |  | 
|  | if has_explicit_title: | 
|  | # Don't mess with the title at all if it was explicitly set. | 
|  | # Explicit title syntax for references is e.g. | 
|  | # :qapi:type:`target <explicit title>` | 
|  | # and this explicit title overrides everything else here. | 
|  | return title, target | 
|  |  | 
|  | title = target | 
|  | if hide_module: | 
|  | title = target.split(".")[-1] | 
|  |  | 
|  | return title, target | 
|  |  | 
|  | def result_nodes( | 
|  | self, | 
|  | document: nodes.document, | 
|  | env: BuildEnvironment, | 
|  | node: Element, | 
|  | is_ref: bool, | 
|  | ) -> Tuple[List[nodes.Node], List[nodes.system_message]]: | 
|  |  | 
|  | # node here is the pending_xref node (or whatever nodeclass was | 
|  | # configured at XRefRole class instantiation time). | 
|  | results: List[nodes.Node] = [node] | 
|  |  | 
|  | if node.get("qapi:array"): | 
|  | results.insert(0, nodes.literal("[", "[")) | 
|  | results.append(nodes.literal("]", "]")) | 
|  |  | 
|  | if node.get("qapi:optional"): | 
|  | results.append(nodes.Text(", ")) | 
|  | results.append(nodes.emphasis("?", "optional")) | 
|  |  | 
|  | return results, [] | 
|  |  | 
|  |  | 
|  | class QAPIDescription(ParserFix): | 
|  | """ | 
|  | Generic QAPI description. | 
|  |  | 
|  | This is meant to be an abstract class, not instantiated | 
|  | directly. This class handles the abstract details of indexing, the | 
|  | TOC, and reference targets for QAPI descriptions. | 
|  | """ | 
|  |  | 
|  | def handle_signature(self, sig: str, signode: desc_signature) -> Signature: | 
|  | # pylint: disable=unused-argument | 
|  |  | 
|  | # Do nothing. The return value here is the "name" of the entity | 
|  | # being documented; for QAPI, this is the same as the | 
|  | # "signature", which is just a name. | 
|  |  | 
|  | # Normally this method must also populate signode with nodes to | 
|  | # render the signature; here we do nothing instead - the | 
|  | # subclasses will handle this. | 
|  | return sig | 
|  |  | 
|  | def get_index_text(self, name: Signature) -> Tuple[str, str]: | 
|  | """Return the text for the index entry of the object.""" | 
|  |  | 
|  | # NB: this is used for the global index, not the QAPI index. | 
|  | return ("single", f"{name} (QMP {self.objtype})") | 
|  |  | 
|  | def _get_context(self) -> Tuple[str, str]: | 
|  | namespace = self.options.get( | 
|  | "namespace", self.env.ref_context.get("qapi:namespace", "") | 
|  | ) | 
|  | modname = self.options.get( | 
|  | "module", self.env.ref_context.get("qapi:module", "") | 
|  | ) | 
|  |  | 
|  | return namespace, modname | 
|  |  | 
|  | def _get_fqn(self, name: Signature) -> str: | 
|  | namespace, modname = self._get_context() | 
|  |  | 
|  | # If we're documenting a module, don't include the module as | 
|  | # part of the FQN; we ARE the module! | 
|  | if self.objtype == "module": | 
|  | modname = "" | 
|  |  | 
|  | if modname: | 
|  | name = f"{modname}.{name}" | 
|  | if namespace: | 
|  | name = f"{namespace}:{name}" | 
|  | return name | 
|  |  | 
|  | def add_target_and_index( | 
|  | self, name: Signature, sig: str, signode: desc_signature | 
|  | ) -> None: | 
|  | # pylint: disable=unused-argument | 
|  |  | 
|  | # name is the return value of handle_signature. | 
|  | # sig is the original, raw text argument to handle_signature. | 
|  | # For QAPI, these are identical, currently. | 
|  |  | 
|  | assert self.objtype | 
|  |  | 
|  | if not (fullname := signode.get("fullname", "")): | 
|  | fullname = self._get_fqn(name) | 
|  |  | 
|  | node_id = make_id( | 
|  | self.env, self.state.document, self.objtype, fullname | 
|  | ) | 
|  | signode["ids"].append(node_id) | 
|  |  | 
|  | self.state.document.note_explicit_target(signode) | 
|  | domain = cast(QAPIDomain, self.env.get_domain("qapi")) | 
|  | domain.note_object(fullname, self.objtype, node_id, location=signode) | 
|  |  | 
|  | if "no-index-entry" not in self.options: | 
|  | arity, indextext = self.get_index_text(name) | 
|  | assert self.indexnode is not None | 
|  | if indextext: | 
|  | self.indexnode["entries"].append( | 
|  | (arity, indextext, node_id, "", None) | 
|  | ) | 
|  |  | 
|  | @staticmethod | 
|  | def split_fqn(name: str) -> Tuple[str, str, str]: | 
|  | if ":" in name: | 
|  | ns, name = name.split(":") | 
|  | else: | 
|  | ns = "" | 
|  |  | 
|  | if "." in name: | 
|  | module, name = name.split(".") | 
|  | else: | 
|  | module = "" | 
|  |  | 
|  | return (ns, module, name) | 
|  |  | 
|  | def _object_hierarchy_parts( | 
|  | self, sig_node: desc_signature | 
|  | ) -> Tuple[str, ...]: | 
|  | if "fullname" not in sig_node: | 
|  | return () | 
|  | return self.split_fqn(sig_node["fullname"]) | 
|  |  | 
|  | def _toc_entry_name(self, sig_node: desc_signature) -> str: | 
|  | # This controls the name in the TOC and on the sidebar. | 
|  |  | 
|  | # This is the return type of _object_hierarchy_parts(). | 
|  | toc_parts = cast(Tuple[str, ...], sig_node.get("_toc_parts", ())) | 
|  | if not toc_parts: | 
|  | return "" | 
|  |  | 
|  | config = self.env.app.config | 
|  | namespace, modname, name = toc_parts | 
|  |  | 
|  | if config.toc_object_entries_show_parents == "domain": | 
|  | ret = name | 
|  | if modname and modname != self.env.ref_context.get( | 
|  | "qapi:module", "" | 
|  | ): | 
|  | ret = f"{modname}.{name}" | 
|  | if namespace and namespace != self.env.ref_context.get( | 
|  | "qapi:namespace", "" | 
|  | ): | 
|  | ret = f"{namespace}:{ret}" | 
|  | return ret | 
|  | if config.toc_object_entries_show_parents == "hide": | 
|  | return name | 
|  | if config.toc_object_entries_show_parents == "all": | 
|  | return sig_node.get("fullname", name) | 
|  | return "" | 
|  |  | 
|  |  | 
|  | class QAPIObject(QAPIDescription): | 
|  | """ | 
|  | Description of a generic QAPI object. | 
|  |  | 
|  | It's not used directly, but is instead subclassed by specific directives. | 
|  | """ | 
|  |  | 
|  | # Inherit some standard options from Sphinx's ObjectDescription | 
|  | option_spec: OptionSpec = (  # type:ignore[misc] | 
|  | ObjectDescription.option_spec.copy() | 
|  | ) | 
|  | option_spec.update( | 
|  | { | 
|  | # Context overrides: | 
|  | "namespace": directives.unchanged, | 
|  | "module": directives.unchanged, | 
|  | # These are QAPI originals: | 
|  | "since": directives.unchanged, | 
|  | "ifcond": directives.unchanged, | 
|  | "deprecated": directives.flag, | 
|  | "unstable": directives.flag, | 
|  | } | 
|  | ) | 
|  |  | 
|  | doc_field_types = [ | 
|  | # :feat name: descr | 
|  | CompatGroupedField( | 
|  | "feature", | 
|  | label=_("Features"), | 
|  | names=("feat",), | 
|  | can_collapse=False, | 
|  | ), | 
|  | ] | 
|  |  | 
|  | def get_signature_prefix(self) -> List[nodes.Node]: | 
|  | """Return a prefix to put before the object name in the signature.""" | 
|  | assert self.objtype | 
|  | return [ | 
|  | KeywordNode("", self.objtype.title()), | 
|  | SpaceNode(" "), | 
|  | ] | 
|  |  | 
|  | def get_signature_suffix(self) -> List[nodes.Node]: | 
|  | """Return a suffix to put after the object name in the signature.""" | 
|  | ret: List[nodes.Node] = [] | 
|  |  | 
|  | if "since" in self.options: | 
|  | ret += [ | 
|  | SpaceNode(" "), | 
|  | addnodes.desc_sig_element( | 
|  | "", f"(Since: {self.options['since']})" | 
|  | ), | 
|  | ] | 
|  |  | 
|  | return ret | 
|  |  | 
|  | def handle_signature(self, sig: str, signode: desc_signature) -> Signature: | 
|  | """ | 
|  | Transform a QAPI definition name into RST nodes. | 
|  |  | 
|  | This method was originally intended for handling function | 
|  | signatures. In the QAPI domain, however, we only pass the | 
|  | definition name as the directive argument and handle everything | 
|  | else in the content body with field lists. | 
|  |  | 
|  | As such, the only argument here is "sig", which is just the QAPI | 
|  | definition name. | 
|  | """ | 
|  | # No module or domain info allowed in the signature! | 
|  | assert ":" not in sig | 
|  | assert "." not in sig | 
|  |  | 
|  | namespace, modname = self._get_context() | 
|  | signode["fullname"] = self._get_fqn(sig) | 
|  | signode["namespace"] = namespace | 
|  | signode["module"] = modname | 
|  |  | 
|  | sig_prefix = self.get_signature_prefix() | 
|  | if sig_prefix: | 
|  | signode += addnodes.desc_annotation( | 
|  | str(sig_prefix), "", *sig_prefix | 
|  | ) | 
|  | signode += addnodes.desc_name(sig, sig) | 
|  | signode += self.get_signature_suffix() | 
|  |  | 
|  | return sig | 
|  |  | 
|  | def _add_infopips(self, contentnode: addnodes.desc_content) -> None: | 
|  | # Add various eye-catches and things that go below the signature | 
|  | # bar, but precede the user-defined content. | 
|  | infopips = nodes.container() | 
|  | infopips.attributes["classes"].append("qapi-infopips") | 
|  |  | 
|  | def _add_pip( | 
|  | source: str, content: Union[str, List[nodes.Node]], classname: str | 
|  | ) -> None: | 
|  | node = nodes.container(source) | 
|  | if isinstance(content, str): | 
|  | node.append(nodes.Text(content)) | 
|  | else: | 
|  | node.extend(content) | 
|  | node.attributes["classes"].extend(["qapi-infopip", classname]) | 
|  | infopips.append(node) | 
|  |  | 
|  | if "deprecated" in self.options: | 
|  | _add_pip( | 
|  | ":deprecated:", | 
|  | f"This {self.objtype} is deprecated.", | 
|  | "qapi-deprecated", | 
|  | ) | 
|  |  | 
|  | if "unstable" in self.options: | 
|  | _add_pip( | 
|  | ":unstable:", | 
|  | f"This {self.objtype} is unstable/experimental.", | 
|  | "qapi-unstable", | 
|  | ) | 
|  |  | 
|  | if self.options.get("ifcond", ""): | 
|  | ifcond = self.options["ifcond"] | 
|  | _add_pip( | 
|  | f":ifcond: {ifcond}", | 
|  | [ | 
|  | nodes.emphasis("", "Availability"), | 
|  | nodes.Text(": "), | 
|  | nodes.literal(ifcond, ifcond), | 
|  | ], | 
|  | "qapi-ifcond", | 
|  | ) | 
|  |  | 
|  | if infopips.children: | 
|  | contentnode.insert(0, infopips) | 
|  |  | 
|  | def _validate_field(self, field: nodes.field) -> None: | 
|  | """Validate field lists in this QAPI Object Description.""" | 
|  | name, _ = _unpack_field(field) | 
|  | allowed_fields = set(self.env.app.config.qapi_allowed_fields) | 
|  |  | 
|  | field_label = name.astext() | 
|  | if field_label in allowed_fields: | 
|  | # Explicitly allowed field list name, OK. | 
|  | return | 
|  |  | 
|  | try: | 
|  | # split into field type and argument (if provided) | 
|  | # e.g. `:arg type name: descr` is | 
|  | # field_type = "arg", field_arg = "type name". | 
|  | field_type, field_arg = field_label.split(None, 1) | 
|  | except ValueError: | 
|  | # No arguments provided | 
|  | field_type = field_label | 
|  | field_arg = "" | 
|  |  | 
|  | typemap = self.get_field_type_map() | 
|  | if field_type in typemap: | 
|  | # This is a special docfield, yet-to-be-processed. Catch | 
|  | # correct names, but incorrect arguments. This mismatch WILL | 
|  | # cause Sphinx to render this field incorrectly (without a | 
|  | # warning), which is never what we want. | 
|  | typedesc = typemap[field_type][0] | 
|  | if typedesc.has_arg != bool(field_arg): | 
|  | msg = f"docfield field list type {field_type!r} " | 
|  | if typedesc.has_arg: | 
|  | msg += "requires an argument." | 
|  | else: | 
|  | msg += "takes no arguments." | 
|  | logger.warning(msg, location=field) | 
|  | else: | 
|  | # This is unrecognized entirely. It's valid rST to use | 
|  | # arbitrary fields, but let's ensure the documentation | 
|  | # writer has done this intentionally. | 
|  | valid = ", ".join(sorted(set(typemap) | allowed_fields)) | 
|  | msg = ( | 
|  | f"Unrecognized field list name {field_label!r}.\n" | 
|  | f"Valid fields for qapi:{self.objtype} are: {valid}\n" | 
|  | "\n" | 
|  | "If this usage is intentional, please add it to " | 
|  | "'qapi_allowed_fields' in docs/conf.py." | 
|  | ) | 
|  | logger.warning(msg, location=field) | 
|  |  | 
|  | def transform_content(self, content_node: addnodes.desc_content) -> None: | 
|  | # This hook runs after before_content and the nested parse, but | 
|  | # before the DocFieldTransformer is executed. | 
|  | super().transform_content(content_node) | 
|  |  | 
|  | self._add_infopips(content_node) | 
|  |  | 
|  | # Validate field lists. | 
|  | for child in content_node: | 
|  | if isinstance(child, nodes.field_list): | 
|  | for field in child.children: | 
|  | assert isinstance(field, nodes.field) | 
|  | self._validate_field(field) | 
|  |  | 
|  |  | 
|  | class SpecialTypedField(CompatTypedField): | 
|  | def make_field(self, *args: Any, **kwargs: Any) -> nodes.field: | 
|  | ret = super().make_field(*args, **kwargs) | 
|  |  | 
|  | # Look for the characteristic " -- " text node that Sphinx | 
|  | # inserts for each TypedField entry ... | 
|  | for node in ret.traverse(lambda n: str(n) == " -- "): | 
|  | par = node.parent | 
|  | if par.children[0].astext() != "q_dummy": | 
|  | continue | 
|  |  | 
|  | # If the first node's text is q_dummy, this is a dummy | 
|  | # field we want to strip down to just its contents. | 
|  | del par.children[:-1] | 
|  |  | 
|  | return ret | 
|  |  | 
|  |  | 
|  | class QAPICommand(QAPIObject): | 
|  | """Description of a QAPI Command.""" | 
|  |  | 
|  | doc_field_types = QAPIObject.doc_field_types.copy() | 
|  | doc_field_types.extend( | 
|  | [ | 
|  | # :arg TypeName ArgName: descr | 
|  | SpecialTypedField( | 
|  | "argument", | 
|  | label=_("Arguments"), | 
|  | names=("arg",), | 
|  | typerolename="type", | 
|  | can_collapse=False, | 
|  | ), | 
|  | # :error: descr | 
|  | CompatField( | 
|  | "error", | 
|  | label=_("Errors"), | 
|  | names=("error", "errors"), | 
|  | has_arg=False, | 
|  | ), | 
|  | # :return TypeName: descr | 
|  | CompatGroupedField( | 
|  | "returnvalue", | 
|  | label=_("Return"), | 
|  | rolename="type", | 
|  | names=("return",), | 
|  | can_collapse=True, | 
|  | ), | 
|  | ] | 
|  | ) | 
|  |  | 
|  |  | 
|  | class QAPIEnum(QAPIObject): | 
|  | """Description of a QAPI Enum.""" | 
|  |  | 
|  | doc_field_types = QAPIObject.doc_field_types.copy() | 
|  | doc_field_types.extend( | 
|  | [ | 
|  | # :value name: descr | 
|  | CompatGroupedField( | 
|  | "value", | 
|  | label=_("Values"), | 
|  | names=("value",), | 
|  | can_collapse=False, | 
|  | ) | 
|  | ] | 
|  | ) | 
|  |  | 
|  |  | 
|  | class QAPIAlternate(QAPIObject): | 
|  | """Description of a QAPI Alternate.""" | 
|  |  | 
|  | doc_field_types = QAPIObject.doc_field_types.copy() | 
|  | doc_field_types.extend( | 
|  | [ | 
|  | # :alt type name: descr | 
|  | CompatTypedField( | 
|  | "alternative", | 
|  | label=_("Alternatives"), | 
|  | names=("alt",), | 
|  | typerolename="type", | 
|  | can_collapse=False, | 
|  | ), | 
|  | ] | 
|  | ) | 
|  |  | 
|  |  | 
|  | class QAPIObjectWithMembers(QAPIObject): | 
|  | """Base class for Events/Structs/Unions""" | 
|  |  | 
|  | doc_field_types = QAPIObject.doc_field_types.copy() | 
|  | doc_field_types.extend( | 
|  | [ | 
|  | # :member type name: descr | 
|  | SpecialTypedField( | 
|  | "member", | 
|  | label=_("Members"), | 
|  | names=("memb",), | 
|  | typerolename="type", | 
|  | can_collapse=False, | 
|  | ), | 
|  | ] | 
|  | ) | 
|  |  | 
|  |  | 
|  | class QAPIEvent(QAPIObjectWithMembers): | 
|  | # pylint: disable=too-many-ancestors | 
|  | """Description of a QAPI Event.""" | 
|  |  | 
|  |  | 
|  | class QAPIJSONObject(QAPIObjectWithMembers): | 
|  | # pylint: disable=too-many-ancestors | 
|  | """Description of a QAPI Object: structs and unions.""" | 
|  |  | 
|  |  | 
|  | class QAPIModule(QAPIDescription): | 
|  | """ | 
|  | Directive to mark description of a new module. | 
|  |  | 
|  | This directive doesn't generate any special formatting, and is just | 
|  | a pass-through for the content body. Named section titles are | 
|  | allowed in the content body. | 
|  |  | 
|  | Use this directive to create entries for the QAPI module in the | 
|  | global index and the QAPI index; as well as to associate subsequent | 
|  | definitions with the module they are defined in for purposes of | 
|  | search and QAPI index organization. | 
|  |  | 
|  | :arg: The name of the module. | 
|  | :opt no-index: Don't add cross-reference targets or index entries. | 
|  | :opt no-typesetting: Don't render the content body (but preserve any | 
|  | cross-reference target IDs in the squelched output.) | 
|  |  | 
|  | Example:: | 
|  |  | 
|  | .. qapi:module:: block-core | 
|  | :no-index: | 
|  | :no-typesetting: | 
|  |  | 
|  | Lorem ipsum, dolor sit amet ... | 
|  | """ | 
|  |  | 
|  | def run(self) -> List[Node]: | 
|  | modname = self.arguments[0].strip() | 
|  | self.env.ref_context["qapi:module"] = modname | 
|  | ret = super().run() | 
|  |  | 
|  | # ObjectDescription always creates a visible signature bar. We | 
|  | # want module items to be "invisible", however. | 
|  |  | 
|  | # Extract the content body of the directive: | 
|  | assert isinstance(ret[-1], addnodes.desc) | 
|  | desc_node = ret.pop(-1) | 
|  | assert isinstance(desc_node.children[1], addnodes.desc_content) | 
|  | ret.extend(desc_node.children[1].children) | 
|  |  | 
|  | # Re-home node_ids so anchor refs still work: | 
|  | node_ids: List[str] | 
|  | if node_ids := [ | 
|  | node_id | 
|  | for el in desc_node.children[0].traverse(nodes.Element) | 
|  | for node_id in cast(List[str], el.get("ids", ())) | 
|  | ]: | 
|  | target_node = nodes.target(ids=node_ids) | 
|  | ret.insert(1, target_node) | 
|  |  | 
|  | return ret | 
|  |  | 
|  |  | 
|  | class QAPINamespace(SphinxDirective): | 
|  | has_content = False | 
|  | required_arguments = 1 | 
|  |  | 
|  | def run(self) -> List[Node]: | 
|  | namespace = self.arguments[0].strip() | 
|  | self.env.ref_context["qapi:namespace"] = namespace | 
|  |  | 
|  | return [] | 
|  |  | 
|  |  | 
|  | class QAPIIndex(Index): | 
|  | """ | 
|  | Index subclass to provide the QAPI definition index. | 
|  | """ | 
|  |  | 
|  | # pylint: disable=too-few-public-methods | 
|  |  | 
|  | name = "index" | 
|  | localname = _("QAPI Index") | 
|  | shortname = _("QAPI Index") | 
|  | namespace = "" | 
|  |  | 
|  | def generate( | 
|  | self, | 
|  | docnames: Optional[Iterable[str]] = None, | 
|  | ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]: | 
|  | assert isinstance(self.domain, QAPIDomain) | 
|  | content: Dict[str, List[IndexEntry]] = {} | 
|  | collapse = False | 
|  |  | 
|  | for objname, obj in self.domain.objects.items(): | 
|  | if docnames and obj.docname not in docnames: | 
|  | continue | 
|  |  | 
|  | ns, _mod, name = QAPIDescription.split_fqn(objname) | 
|  |  | 
|  | if self.namespace != ns: | 
|  | continue | 
|  |  | 
|  | # Add an alphabetical entry: | 
|  | entries = content.setdefault(name[0].upper(), []) | 
|  | entries.append( | 
|  | IndexEntry( | 
|  | name, 0, obj.docname, obj.node_id, obj.objtype, "", "" | 
|  | ) | 
|  | ) | 
|  |  | 
|  | # Add a categorical entry: | 
|  | category = obj.objtype.title() + "s" | 
|  | entries = content.setdefault(category, []) | 
|  | entries.append( | 
|  | IndexEntry(name, 0, obj.docname, obj.node_id, "", "", "") | 
|  | ) | 
|  |  | 
|  | # Sort entries within each category alphabetically | 
|  | for category in content: | 
|  | content[category] = sorted(content[category]) | 
|  |  | 
|  | # Sort the categories themselves; type names first, ABC entries last. | 
|  | sorted_content = sorted( | 
|  | content.items(), | 
|  | key=lambda x: (len(x[0]) == 1, x[0]), | 
|  | ) | 
|  | return sorted_content, collapse | 
|  |  | 
|  |  | 
|  | class QAPIDomain(Domain): | 
|  | """QAPI language domain.""" | 
|  |  | 
|  | name = "qapi" | 
|  | label = "QAPI" | 
|  |  | 
|  | # This table associates cross-reference object types (key) with an | 
|  | # ObjType instance, which defines the valid cross-reference roles | 
|  | # for each object type. | 
|  | # | 
|  | # e.g., the :qapi:type: cross-reference role can refer to enum, | 
|  | # struct, union, or alternate objects; but :qapi:obj: can refer to | 
|  | # anything. Each object also gets its own targeted cross-reference role. | 
|  | object_types: Dict[str, ObjType] = { | 
|  | "module": ObjType(_("module"), "mod", "any"), | 
|  | "command": ObjType(_("command"), "cmd", "any"), | 
|  | "event": ObjType(_("event"), "event", "any"), | 
|  | "enum": ObjType(_("enum"), "enum", "type", "any"), | 
|  | "object": ObjType(_("object"), "obj", "type", "any"), | 
|  | "alternate": ObjType(_("alternate"), "alt", "type", "any"), | 
|  | } | 
|  |  | 
|  | # Each of these provides a rST directive, | 
|  | # e.g. .. qapi:module:: block-core | 
|  | directives = { | 
|  | "namespace": QAPINamespace, | 
|  | "module": QAPIModule, | 
|  | "command": QAPICommand, | 
|  | "event": QAPIEvent, | 
|  | "enum": QAPIEnum, | 
|  | "object": QAPIJSONObject, | 
|  | "alternate": QAPIAlternate, | 
|  | } | 
|  |  | 
|  | # These are all cross-reference roles; e.g. | 
|  | # :qapi:cmd:`query-block`. The keys correlate to the names used in | 
|  | # the object_types table values above. | 
|  | roles = { | 
|  | "mod": QAPIXRefRole(), | 
|  | "cmd": QAPIXRefRole(), | 
|  | "event": QAPIXRefRole(), | 
|  | "enum": QAPIXRefRole(), | 
|  | "obj": QAPIXRefRole(),  # specifically structs and unions. | 
|  | "alt": QAPIXRefRole(), | 
|  | # reference any data type (excludes modules, commands, events) | 
|  | "type": QAPIXRefRole(), | 
|  | "any": QAPIXRefRole(),  # reference *any* type of QAPI object. | 
|  | } | 
|  |  | 
|  | # Moved into the data property at runtime; | 
|  | # this is the internal index of reference-able objects. | 
|  | initial_data: Dict[str, Dict[str, Tuple[Any]]] = { | 
|  | "objects": {},  # fullname -> ObjectEntry | 
|  | } | 
|  |  | 
|  | # Index pages to generate; each entry is an Index class. | 
|  | indices = [ | 
|  | QAPIIndex, | 
|  | ] | 
|  |  | 
|  | @property | 
|  | def objects(self) -> Dict[str, ObjectEntry]: | 
|  | ret = self.data.setdefault("objects", {}) | 
|  | return ret  # type: ignore[no-any-return] | 
|  |  | 
|  | def setup(self) -> None: | 
|  | namespaces = set(self.env.app.config.qapi_namespaces) | 
|  | for namespace in namespaces: | 
|  | new_index: Type[QAPIIndex] = types.new_class( | 
|  | f"{namespace}Index", bases=(QAPIIndex,) | 
|  | ) | 
|  | new_index.name = f"{namespace.lower()}-index" | 
|  | new_index.localname = _(f"{namespace} Index") | 
|  | new_index.shortname = _(f"{namespace} Index") | 
|  | new_index.namespace = namespace | 
|  |  | 
|  | self.indices.append(new_index) | 
|  |  | 
|  | super().setup() | 
|  |  | 
|  | def note_object( | 
|  | self, | 
|  | name: str, | 
|  | objtype: str, | 
|  | node_id: str, | 
|  | aliased: bool = False, | 
|  | location: Any = None, | 
|  | ) -> None: | 
|  | """Note a QAPI object for cross reference.""" | 
|  | if name in self.objects: | 
|  | other = self.objects[name] | 
|  | if other.aliased and aliased is False: | 
|  | # The original definition found. Override it! | 
|  | pass | 
|  | elif other.aliased is False and aliased: | 
|  | # The original definition is already registered. | 
|  | return | 
|  | else: | 
|  | # duplicated | 
|  | logger.warning( | 
|  | __( | 
|  | "duplicate object description of %s, " | 
|  | "other instance in %s, use :no-index: for one of them" | 
|  | ), | 
|  | name, | 
|  | other.docname, | 
|  | location=location, | 
|  | ) | 
|  | self.objects[name] = ObjectEntry( | 
|  | self.env.docname, node_id, objtype, aliased | 
|  | ) | 
|  |  | 
|  | def clear_doc(self, docname: str) -> None: | 
|  | for fullname, obj in list(self.objects.items()): | 
|  | if obj.docname == docname: | 
|  | del self.objects[fullname] | 
|  |  | 
|  | def merge_domaindata( | 
|  | self, docnames: AbstractSet[str], otherdata: Dict[str, Any] | 
|  | ) -> None: | 
|  | for fullname, obj in otherdata["objects"].items(): | 
|  | if obj.docname in docnames: | 
|  | # Sphinx's own python domain doesn't appear to bother to | 
|  | # check for collisions. Assert they don't happen and | 
|  | # we'll fix it if/when the case arises. | 
|  | assert fullname not in self.objects, ( | 
|  | "bug - collision on merge?" | 
|  | f" {fullname=} {obj=} {self.objects[fullname]=}" | 
|  | ) | 
|  | self.objects[fullname] = obj | 
|  |  | 
|  | def find_obj( | 
|  | self, namespace: str, modname: str, name: str, typ: Optional[str] | 
|  | ) -> List[Tuple[str, ObjectEntry]]: | 
|  | """ | 
|  | Find a QAPI object for "name", maybe using contextual information. | 
|  |  | 
|  | Returns a list of (name, object entry) tuples. | 
|  |  | 
|  | :param namespace: The current namespace context (if any!) under | 
|  | which we are searching. | 
|  | :param modname: The current module context (if any!) under | 
|  | which we are searching. | 
|  | :param name: The name of the x-ref to resolve; may or may not | 
|  | include leading context. | 
|  | :param type: The role name of the x-ref we're resolving, if | 
|  | provided. This is absent for "any" role lookups. | 
|  | """ | 
|  | if not name: | 
|  | return [] | 
|  |  | 
|  | # ## | 
|  | # what to search for | 
|  | # ## | 
|  |  | 
|  | parts = list(QAPIDescription.split_fqn(name)) | 
|  | explicit = tuple(bool(x) for x in parts) | 
|  |  | 
|  | # Fill in the blanks where possible: | 
|  | if namespace and not parts[0]: | 
|  | parts[0] = namespace | 
|  | if modname and not parts[1]: | 
|  | parts[1] = modname | 
|  |  | 
|  | implicit_fqn = "" | 
|  | if all(parts): | 
|  | implicit_fqn = f"{parts[0]}:{parts[1]}.{parts[2]}" | 
|  |  | 
|  | if typ is None: | 
|  | # :any: lookup, search everything: | 
|  | objtypes: List[str] = list(self.object_types) | 
|  | else: | 
|  | # type is specified and will be a role (e.g. obj, mod, cmd) | 
|  | # convert this to eligible object types (e.g. command, module) | 
|  | # using the QAPIDomain.object_types table. | 
|  | objtypes = self.objtypes_for_role(typ, []) | 
|  |  | 
|  | # ## | 
|  | # search! | 
|  | # ## | 
|  |  | 
|  | def _search(needle: str) -> List[str]: | 
|  | if ( | 
|  | needle | 
|  | and needle in self.objects | 
|  | and self.objects[needle].objtype in objtypes | 
|  | ): | 
|  | return [needle] | 
|  | return [] | 
|  |  | 
|  | if found := _search(name): | 
|  | # Exact match! | 
|  | pass | 
|  | elif found := _search(implicit_fqn): | 
|  | # Exact match using contextual information to fill in the gaps. | 
|  | pass | 
|  | else: | 
|  | # No exact hits, perform applicable fuzzy searches. | 
|  | searches = [] | 
|  |  | 
|  | esc = tuple(re.escape(s) for s in parts) | 
|  |  | 
|  | # Try searching for ns:*.name or ns:name | 
|  | if explicit[0] and not explicit[1]: | 
|  | searches.append(f"^{esc[0]}:([^\\.]+\\.)?{esc[2]}$") | 
|  | # Try searching for *:module.name or module.name | 
|  | if explicit[1] and not explicit[0]: | 
|  | searches.append(f"(^|:){esc[1]}\\.{esc[2]}$") | 
|  | # Try searching for context-ns:*.name or context-ns:name | 
|  | if parts[0] and not (explicit[0] or explicit[1]): | 
|  | searches.append(f"^{esc[0]}:([^\\.]+\\.)?{esc[2]}$") | 
|  | # Try searching for *:context-mod.name or context-mod.name | 
|  | if parts[1] and not (explicit[0] or explicit[1]): | 
|  | searches.append(f"(^|:){esc[1]}\\.{esc[2]}$") | 
|  | # Try searching for *:name, *.name, or name | 
|  | if not (explicit[0] or explicit[1]): | 
|  | searches.append(f"(^|:|\\.){esc[2]}$") | 
|  |  | 
|  | for search in searches: | 
|  | if found := [ | 
|  | oname | 
|  | for oname in self.objects | 
|  | if re.search(search, oname) | 
|  | and self.objects[oname].objtype in objtypes | 
|  | ]: | 
|  | break | 
|  |  | 
|  | matches = [(oname, self.objects[oname]) for oname in found] | 
|  | if len(matches) > 1: | 
|  | matches = [m for m in matches if not m[1].aliased] | 
|  | return matches | 
|  |  | 
|  | def resolve_xref( | 
|  | self, | 
|  | env: BuildEnvironment, | 
|  | fromdocname: str, | 
|  | builder: Builder, | 
|  | typ: str, | 
|  | target: str, | 
|  | node: pending_xref, | 
|  | contnode: Element, | 
|  | ) -> nodes.reference | None: | 
|  | namespace = node.get("qapi:namespace") | 
|  | modname = node.get("qapi:module") | 
|  | matches = self.find_obj(namespace, modname, target, typ) | 
|  |  | 
|  | if not matches: | 
|  | # Normally, we could pass warn_dangling=True to QAPIXRefRole(), | 
|  | # but that will trigger on references to these built-in types, | 
|  | # which we'd like to ignore instead. | 
|  |  | 
|  | # Take care of that warning here instead, so long as the | 
|  | # reference isn't to one of our built-in core types. | 
|  | if target not in ( | 
|  | "string", | 
|  | "number", | 
|  | "int", | 
|  | "boolean", | 
|  | "null", | 
|  | "value", | 
|  | "q_empty", | 
|  | ): | 
|  | logger.warning( | 
|  | __("qapi:%s reference target not found: %r"), | 
|  | typ, | 
|  | target, | 
|  | type="ref", | 
|  | subtype="qapi", | 
|  | location=node, | 
|  | ) | 
|  | return None | 
|  |  | 
|  | if len(matches) > 1: | 
|  | logger.warning( | 
|  | __("more than one target found for cross-reference %r: %s"), | 
|  | target, | 
|  | ", ".join(match[0] for match in matches), | 
|  | type="ref", | 
|  | subtype="qapi", | 
|  | location=node, | 
|  | ) | 
|  |  | 
|  | name, obj = matches[0] | 
|  | return make_refnode( | 
|  | builder, fromdocname, obj.docname, obj.node_id, contnode, name | 
|  | ) | 
|  |  | 
|  | def resolve_any_xref( | 
|  | self, | 
|  | env: BuildEnvironment, | 
|  | fromdocname: str, | 
|  | builder: Builder, | 
|  | target: str, | 
|  | node: pending_xref, | 
|  | contnode: Element, | 
|  | ) -> List[Tuple[str, nodes.reference]]: | 
|  | results: List[Tuple[str, nodes.reference]] = [] | 
|  | matches = self.find_obj( | 
|  | node.get("qapi:namespace"), node.get("qapi:module"), target, None | 
|  | ) | 
|  | for name, obj in matches: | 
|  | rolename = self.role_for_objtype(obj.objtype) | 
|  | assert rolename is not None | 
|  | role = f"qapi:{rolename}" | 
|  | refnode = make_refnode( | 
|  | builder, fromdocname, obj.docname, obj.node_id, contnode, name | 
|  | ) | 
|  | results.append((role, refnode)) | 
|  | return results | 
|  |  | 
|  |  | 
|  | def setup(app: Sphinx) -> Dict[str, Any]: | 
|  | app.setup_extension("sphinx.directives") | 
|  | app.add_config_value( | 
|  | "qapi_allowed_fields", | 
|  | set(), | 
|  | "env",  # Setting impacts parsing phase | 
|  | types=set, | 
|  | ) | 
|  | app.add_config_value( | 
|  | "qapi_namespaces", | 
|  | set(), | 
|  | "env", | 
|  | types=set, | 
|  | ) | 
|  | app.add_domain(QAPIDomain) | 
|  |  | 
|  | return { | 
|  | "version": "1.0", | 
|  | "env_version": 1, | 
|  | "parallel_read_safe": True, | 
|  | "parallel_write_safe": True, | 
|  | } |