1ecf92e36SJohn Snow""" 2ecf92e36SJohn SnowQAPI domain extension. 3ecf92e36SJohn Snow""" 4ecf92e36SJohn Snow 503947c80SJohn Snow# The best laid plans of mice and men, ... 603947c80SJohn Snow# pylint: disable=too-many-lines 703947c80SJohn Snow 8ecf92e36SJohn Snowfrom __future__ import annotations 9ecf92e36SJohn Snow 107127e14fSJohn Snowimport re 11*25d44f57SJohn Snowimport types 12ecf92e36SJohn Snowfrom typing import ( 13ecf92e36SJohn Snow TYPE_CHECKING, 14e93d29d2SJohn Snow List, 1536ceafadSJohn Snow NamedTuple, 16ecf92e36SJohn Snow Tuple, 17*25d44f57SJohn Snow Type, 181ea66486SJohn Snow cast, 19ecf92e36SJohn Snow) 20ecf92e36SJohn Snow 21dca2f3c4SJohn Snowfrom docutils import nodes 228799c364SJohn Snowfrom docutils.parsers.rst import directives 23dca2f3c4SJohn Snow 24a1fe2cd4SJohn Snowfrom compat import ( 25a1fe2cd4SJohn Snow CompatField, 26a1fe2cd4SJohn Snow CompatGroupedField, 27a1fe2cd4SJohn Snow CompatTypedField, 28a1fe2cd4SJohn Snow KeywordNode, 29707f2bbbSJohn Snow ParserFix, 30707f2bbbSJohn Snow Signature, 31a1fe2cd4SJohn Snow SpaceNode, 32a1fe2cd4SJohn Snow) 337320feebSJohn Snowfrom sphinx import addnodes 341ea66486SJohn Snowfrom sphinx.directives import ObjectDescription 35e93d29d2SJohn Snowfrom sphinx.domains import ( 36e93d29d2SJohn Snow Domain, 37e93d29d2SJohn Snow Index, 38e93d29d2SJohn Snow IndexEntry, 39e93d29d2SJohn Snow ObjType, 40e93d29d2SJohn Snow) 41e93d29d2SJohn Snowfrom sphinx.locale import _, __ 42760b37e1SJohn Snowfrom sphinx.roles import XRefRole 43ecf92e36SJohn Snowfrom sphinx.util import logging 447c7247b2SJohn Snowfrom sphinx.util.docutils import SphinxDirective 451ea66486SJohn Snowfrom sphinx.util.nodes import make_id, make_refnode 46ecf92e36SJohn Snow 47ecf92e36SJohn Snow 48ecf92e36SJohn Snowif TYPE_CHECKING: 498fad3662SJohn Snow from typing import ( 508fad3662SJohn Snow AbstractSet, 518fad3662SJohn Snow Any, 528fad3662SJohn Snow Dict, 538fad3662SJohn Snow Iterable, 548fad3662SJohn Snow Optional, 558fad3662SJohn Snow Union, 568fad3662SJohn Snow ) 578fad3662SJohn Snow 587320feebSJohn Snow from docutils.nodes import Element, Node 59dca2f3c4SJohn Snow 608fad3662SJohn Snow from sphinx.addnodes import desc_signature, pending_xref 61ecf92e36SJohn Snow from sphinx.application import Sphinx 62dca2f3c4SJohn Snow from sphinx.builders import Builder 63dca2f3c4SJohn Snow from sphinx.environment import BuildEnvironment 648799c364SJohn Snow from sphinx.util.typing import OptionSpec 65ecf92e36SJohn Snow 668fad3662SJohn Snow 67ecf92e36SJohn Snowlogger = logging.getLogger(__name__) 68ecf92e36SJohn Snow 69ecf92e36SJohn Snow 70ef137a22SJohn Snowdef _unpack_field( 71ef137a22SJohn Snow field: nodes.Node, 72ef137a22SJohn Snow) -> Tuple[nodes.field_name, nodes.field_body]: 73ef137a22SJohn Snow """ 74ef137a22SJohn Snow docutils helper: unpack a field node in a type-safe manner. 75ef137a22SJohn Snow """ 76ef137a22SJohn Snow assert isinstance(field, nodes.field) 77ef137a22SJohn Snow assert len(field.children) == 2 78ef137a22SJohn Snow assert isinstance(field.children[0], nodes.field_name) 79ef137a22SJohn Snow assert isinstance(field.children[1], nodes.field_body) 80ef137a22SJohn Snow return (field.children[0], field.children[1]) 81ef137a22SJohn Snow 82ef137a22SJohn Snow 8336ceafadSJohn Snowclass ObjectEntry(NamedTuple): 8436ceafadSJohn Snow docname: str 8536ceafadSJohn Snow node_id: str 8636ceafadSJohn Snow objtype: str 8736ceafadSJohn Snow aliased: bool 8836ceafadSJohn Snow 8936ceafadSJohn Snow 90760b37e1SJohn Snowclass QAPIXRefRole(XRefRole): 91760b37e1SJohn Snow 92760b37e1SJohn Snow def process_link( 93760b37e1SJohn Snow self, 94760b37e1SJohn Snow env: BuildEnvironment, 95760b37e1SJohn Snow refnode: Element, 96760b37e1SJohn Snow has_explicit_title: bool, 97760b37e1SJohn Snow title: str, 98760b37e1SJohn Snow target: str, 99760b37e1SJohn Snow ) -> tuple[str, str]: 1007127e14fSJohn Snow refnode["qapi:namespace"] = env.ref_context.get("qapi:namespace") 101760b37e1SJohn Snow refnode["qapi:module"] = env.ref_context.get("qapi:module") 102760b37e1SJohn Snow 103760b37e1SJohn Snow # Cross-references that begin with a tilde adjust the title to 104760b37e1SJohn Snow # only show the reference without a leading module, even if one 105760b37e1SJohn Snow # was provided. This is a Sphinx-standard syntax; give it 106760b37e1SJohn Snow # priority over QAPI-specific type markup below. 107760b37e1SJohn Snow hide_module = False 108760b37e1SJohn Snow if target.startswith("~"): 109760b37e1SJohn Snow hide_module = True 110760b37e1SJohn Snow target = target[1:] 111760b37e1SJohn Snow 112760b37e1SJohn Snow # Type names that end with "?" are considered optional 113760b37e1SJohn Snow # arguments and should be documented as such, but it's not 114760b37e1SJohn Snow # part of the xref itself. 115760b37e1SJohn Snow if target.endswith("?"): 116760b37e1SJohn Snow refnode["qapi:optional"] = True 117760b37e1SJohn Snow target = target[:-1] 118760b37e1SJohn Snow 119760b37e1SJohn Snow # Type names wrapped in brackets denote lists. strip the 120760b37e1SJohn Snow # brackets and remember to add them back later. 121760b37e1SJohn Snow if target.startswith("[") and target.endswith("]"): 122760b37e1SJohn Snow refnode["qapi:array"] = True 123760b37e1SJohn Snow target = target[1:-1] 124760b37e1SJohn Snow 125760b37e1SJohn Snow if has_explicit_title: 126760b37e1SJohn Snow # Don't mess with the title at all if it was explicitly set. 127760b37e1SJohn Snow # Explicit title syntax for references is e.g. 128760b37e1SJohn Snow # :qapi:type:`target <explicit title>` 129760b37e1SJohn Snow # and this explicit title overrides everything else here. 130760b37e1SJohn Snow return title, target 131760b37e1SJohn Snow 132760b37e1SJohn Snow title = target 133760b37e1SJohn Snow if hide_module: 134760b37e1SJohn Snow title = target.split(".")[-1] 135760b37e1SJohn Snow 136760b37e1SJohn Snow return title, target 137760b37e1SJohn Snow 13803947c80SJohn Snow def result_nodes( 13903947c80SJohn Snow self, 14003947c80SJohn Snow document: nodes.document, 14103947c80SJohn Snow env: BuildEnvironment, 14203947c80SJohn Snow node: Element, 14303947c80SJohn Snow is_ref: bool, 14403947c80SJohn Snow ) -> Tuple[List[nodes.Node], List[nodes.system_message]]: 14503947c80SJohn Snow 14603947c80SJohn Snow # node here is the pending_xref node (or whatever nodeclass was 14703947c80SJohn Snow # configured at XRefRole class instantiation time). 14803947c80SJohn Snow results: List[nodes.Node] = [node] 14903947c80SJohn Snow 15003947c80SJohn Snow if node.get("qapi:array"): 15103947c80SJohn Snow results.insert(0, nodes.literal("[", "[")) 15203947c80SJohn Snow results.append(nodes.literal("]", "]")) 15303947c80SJohn Snow 15403947c80SJohn Snow if node.get("qapi:optional"): 15503947c80SJohn Snow results.append(nodes.Text(", ")) 15603947c80SJohn Snow results.append(nodes.emphasis("?", "optional")) 15703947c80SJohn Snow 15803947c80SJohn Snow return results, [] 15903947c80SJohn Snow 160760b37e1SJohn Snow 161707f2bbbSJohn Snowclass QAPIDescription(ParserFix): 1621ea66486SJohn Snow """ 1631ea66486SJohn Snow Generic QAPI description. 1641ea66486SJohn Snow 1651ea66486SJohn Snow This is meant to be an abstract class, not instantiated 1661ea66486SJohn Snow directly. This class handles the abstract details of indexing, the 1671ea66486SJohn Snow TOC, and reference targets for QAPI descriptions. 1681ea66486SJohn Snow """ 1691ea66486SJohn Snow 1701ea66486SJohn Snow def handle_signature(self, sig: str, signode: desc_signature) -> Signature: 1711ea66486SJohn Snow # Do nothing. The return value here is the "name" of the entity 1721ea66486SJohn Snow # being documented; for QAPI, this is the same as the 1731ea66486SJohn Snow # "signature", which is just a name. 1741ea66486SJohn Snow 1751ea66486SJohn Snow # Normally this method must also populate signode with nodes to 1761ea66486SJohn Snow # render the signature; here we do nothing instead - the 1771ea66486SJohn Snow # subclasses will handle this. 1781ea66486SJohn Snow return sig 1791ea66486SJohn Snow 1801ea66486SJohn Snow def get_index_text(self, name: Signature) -> Tuple[str, str]: 1811ea66486SJohn Snow """Return the text for the index entry of the object.""" 1821ea66486SJohn Snow 1831ea66486SJohn Snow # NB: this is used for the global index, not the QAPI index. 1841ea66486SJohn Snow return ("single", f"{name} (QMP {self.objtype})") 1851ea66486SJohn Snow 18674d40b01SJohn Snow def _get_context(self) -> Tuple[str, str]: 18774d40b01SJohn Snow namespace = self.options.get( 18874d40b01SJohn Snow "namespace", self.env.ref_context.get("qapi:namespace", "") 18974d40b01SJohn Snow ) 190e36afc7bSJohn Snow modname = self.options.get( 191e36afc7bSJohn Snow "module", self.env.ref_context.get("qapi:module", "") 192e36afc7bSJohn Snow ) 19374d40b01SJohn Snow 19474d40b01SJohn Snow return namespace, modname 195e36afc7bSJohn Snow 196e36afc7bSJohn Snow def _get_fqn(self, name: Signature) -> str: 19774d40b01SJohn Snow namespace, modname = self._get_context() 198e36afc7bSJohn Snow 199e36afc7bSJohn Snow # If we're documenting a module, don't include the module as 200e36afc7bSJohn Snow # part of the FQN; we ARE the module! 201e36afc7bSJohn Snow if self.objtype == "module": 202e36afc7bSJohn Snow modname = "" 203e36afc7bSJohn Snow 204e36afc7bSJohn Snow if modname: 205e36afc7bSJohn Snow name = f"{modname}.{name}" 20674d40b01SJohn Snow if namespace: 20774d40b01SJohn Snow name = f"{namespace}:{name}" 208e36afc7bSJohn Snow return name 209e36afc7bSJohn Snow 2101ea66486SJohn Snow def add_target_and_index( 2111ea66486SJohn Snow self, name: Signature, sig: str, signode: desc_signature 2121ea66486SJohn Snow ) -> None: 2131ea66486SJohn Snow # name is the return value of handle_signature. 2141ea66486SJohn Snow # sig is the original, raw text argument to handle_signature. 2151ea66486SJohn Snow # For QAPI, these are identical, currently. 2161ea66486SJohn Snow 2171ea66486SJohn Snow assert self.objtype 2181ea66486SJohn Snow 219e36afc7bSJohn Snow if not (fullname := signode.get("fullname", "")): 220e36afc7bSJohn Snow fullname = self._get_fqn(name) 2211ea66486SJohn Snow 2221ea66486SJohn Snow node_id = make_id( 2231ea66486SJohn Snow self.env, self.state.document, self.objtype, fullname 2241ea66486SJohn Snow ) 2251ea66486SJohn Snow signode["ids"].append(node_id) 2261ea66486SJohn Snow 2271ea66486SJohn Snow self.state.document.note_explicit_target(signode) 2281ea66486SJohn Snow domain = cast(QAPIDomain, self.env.get_domain("qapi")) 2291ea66486SJohn Snow domain.note_object(fullname, self.objtype, node_id, location=signode) 2301ea66486SJohn Snow 2311ea66486SJohn Snow if "no-index-entry" not in self.options: 2321ea66486SJohn Snow arity, indextext = self.get_index_text(name) 2331ea66486SJohn Snow assert self.indexnode is not None 2341ea66486SJohn Snow if indextext: 2351ea66486SJohn Snow self.indexnode["entries"].append( 2361ea66486SJohn Snow (arity, indextext, node_id, "", None) 2371ea66486SJohn Snow ) 2381ea66486SJohn Snow 239e36afc7bSJohn Snow @staticmethod 24074d40b01SJohn Snow def split_fqn(name: str) -> Tuple[str, str, str]: 24174d40b01SJohn Snow if ":" in name: 24274d40b01SJohn Snow ns, name = name.split(":") 24374d40b01SJohn Snow else: 24474d40b01SJohn Snow ns = "" 24574d40b01SJohn Snow 246e36afc7bSJohn Snow if "." in name: 247e36afc7bSJohn Snow module, name = name.split(".") 248e36afc7bSJohn Snow else: 249e36afc7bSJohn Snow module = "" 250e36afc7bSJohn Snow 25174d40b01SJohn Snow return (ns, module, name) 252e36afc7bSJohn Snow 2531ea66486SJohn Snow def _object_hierarchy_parts( 2541ea66486SJohn Snow self, sig_node: desc_signature 2551ea66486SJohn Snow ) -> Tuple[str, ...]: 2561ea66486SJohn Snow if "fullname" not in sig_node: 2571ea66486SJohn Snow return () 258e36afc7bSJohn Snow return self.split_fqn(sig_node["fullname"]) 2591ea66486SJohn Snow 2601ea66486SJohn Snow def _toc_entry_name(self, sig_node: desc_signature) -> str: 2611ea66486SJohn Snow # This controls the name in the TOC and on the sidebar. 2621ea66486SJohn Snow 2631ea66486SJohn Snow # This is the return type of _object_hierarchy_parts(). 2641ea66486SJohn Snow toc_parts = cast(Tuple[str, ...], sig_node.get("_toc_parts", ())) 2651ea66486SJohn Snow if not toc_parts: 2661ea66486SJohn Snow return "" 2671ea66486SJohn Snow 2681ea66486SJohn Snow config = self.env.app.config 26974d40b01SJohn Snow namespace, modname, name = toc_parts 270e36afc7bSJohn Snow 2711ea66486SJohn Snow if config.toc_object_entries_show_parents == "domain": 272e36afc7bSJohn Snow ret = name 273e36afc7bSJohn Snow if modname and modname != self.env.ref_context.get( 274e36afc7bSJohn Snow "qapi:module", "" 275e36afc7bSJohn Snow ): 276e36afc7bSJohn Snow ret = f"{modname}.{name}" 27774d40b01SJohn Snow if namespace and namespace != self.env.ref_context.get( 27874d40b01SJohn Snow "qapi:namespace", "" 27974d40b01SJohn Snow ): 28074d40b01SJohn Snow ret = f"{namespace}:{ret}" 281e36afc7bSJohn Snow return ret 2821ea66486SJohn Snow if config.toc_object_entries_show_parents == "hide": 2831ea66486SJohn Snow return name 2841ea66486SJohn Snow if config.toc_object_entries_show_parents == "all": 285e36afc7bSJohn Snow return sig_node.get("fullname", name) 2861ea66486SJohn Snow return "" 2871ea66486SJohn Snow 2881ea66486SJohn Snow 2898799c364SJohn Snowclass QAPIObject(QAPIDescription): 2908799c364SJohn Snow """ 2918799c364SJohn Snow Description of a generic QAPI object. 2928799c364SJohn Snow 2938799c364SJohn Snow It's not used directly, but is instead subclassed by specific directives. 2948799c364SJohn Snow """ 2958799c364SJohn Snow 2968799c364SJohn Snow # Inherit some standard options from Sphinx's ObjectDescription 2978799c364SJohn Snow option_spec: OptionSpec = ( # type:ignore[misc] 2988799c364SJohn Snow ObjectDescription.option_spec.copy() 2998799c364SJohn Snow ) 3008799c364SJohn Snow option_spec.update( 3018799c364SJohn Snow { 3029ca404f0SJohn Snow # Context overrides: 3039ca404f0SJohn Snow "namespace": directives.unchanged, 3049ca404f0SJohn Snow "module": directives.unchanged, 305700d51a4SJohn Snow # These are QAPI originals: 306700d51a4SJohn Snow "since": directives.unchanged, 3076a413302SJohn Snow "ifcond": directives.unchanged, 3081a0c090aSJohn Snow "deprecated": directives.flag, 309d25808c2SJohn Snow "unstable": directives.flag, 3108799c364SJohn Snow } 3118799c364SJohn Snow ) 3128799c364SJohn Snow 3133d9a23f9SJohn Snow doc_field_types = [ 3143d9a23f9SJohn Snow # :feat name: descr 315a1fe2cd4SJohn Snow CompatGroupedField( 3163d9a23f9SJohn Snow "feature", 3173d9a23f9SJohn Snow label=_("Features"), 3183d9a23f9SJohn Snow names=("feat",), 3193d9a23f9SJohn Snow can_collapse=False, 3203d9a23f9SJohn Snow ), 3213d9a23f9SJohn Snow ] 3223d9a23f9SJohn Snow 3238799c364SJohn Snow def get_signature_prefix(self) -> List[nodes.Node]: 3248799c364SJohn Snow """Return a prefix to put before the object name in the signature.""" 3258799c364SJohn Snow assert self.objtype 3268799c364SJohn Snow return [ 3278799c364SJohn Snow KeywordNode("", self.objtype.title()), 3288799c364SJohn Snow SpaceNode(" "), 3298799c364SJohn Snow ] 3308799c364SJohn Snow 3318799c364SJohn Snow def get_signature_suffix(self) -> List[nodes.Node]: 3328799c364SJohn Snow """Return a suffix to put after the object name in the signature.""" 333700d51a4SJohn Snow ret: List[nodes.Node] = [] 334700d51a4SJohn Snow 335700d51a4SJohn Snow if "since" in self.options: 336700d51a4SJohn Snow ret += [ 337700d51a4SJohn Snow SpaceNode(" "), 338700d51a4SJohn Snow addnodes.desc_sig_element( 339700d51a4SJohn Snow "", f"(Since: {self.options['since']})" 340700d51a4SJohn Snow ), 341700d51a4SJohn Snow ] 342700d51a4SJohn Snow 343700d51a4SJohn Snow return ret 3448799c364SJohn Snow 3458799c364SJohn Snow def handle_signature(self, sig: str, signode: desc_signature) -> Signature: 3468799c364SJohn Snow """ 3478799c364SJohn Snow Transform a QAPI definition name into RST nodes. 3488799c364SJohn Snow 3498799c364SJohn Snow This method was originally intended for handling function 3508799c364SJohn Snow signatures. In the QAPI domain, however, we only pass the 3518799c364SJohn Snow definition name as the directive argument and handle everything 3528799c364SJohn Snow else in the content body with field lists. 3538799c364SJohn Snow 3548799c364SJohn Snow As such, the only argument here is "sig", which is just the QAPI 3558799c364SJohn Snow definition name. 3568799c364SJohn Snow """ 35774d40b01SJohn Snow # No module or domain info allowed in the signature! 35874d40b01SJohn Snow assert ":" not in sig 35974d40b01SJohn Snow assert "." not in sig 3608799c364SJohn Snow 36174d40b01SJohn Snow namespace, modname = self._get_context() 362e36afc7bSJohn Snow signode["fullname"] = self._get_fqn(sig) 36374d40b01SJohn Snow signode["namespace"] = namespace 3648799c364SJohn Snow signode["module"] = modname 36574d40b01SJohn Snow 3668799c364SJohn Snow sig_prefix = self.get_signature_prefix() 3678799c364SJohn Snow if sig_prefix: 3688799c364SJohn Snow signode += addnodes.desc_annotation( 3698799c364SJohn Snow str(sig_prefix), "", *sig_prefix 3708799c364SJohn Snow ) 3718799c364SJohn Snow signode += addnodes.desc_name(sig, sig) 3728799c364SJohn Snow signode += self.get_signature_suffix() 3738799c364SJohn Snow 3748799c364SJohn Snow return sig 3758799c364SJohn Snow 3761a0c090aSJohn Snow def _add_infopips(self, contentnode: addnodes.desc_content) -> None: 3771a0c090aSJohn Snow # Add various eye-catches and things that go below the signature 3781a0c090aSJohn Snow # bar, but precede the user-defined content. 3791a0c090aSJohn Snow infopips = nodes.container() 3801a0c090aSJohn Snow infopips.attributes["classes"].append("qapi-infopips") 3811a0c090aSJohn Snow 3826a413302SJohn Snow def _add_pip( 3836a413302SJohn Snow source: str, content: Union[str, List[nodes.Node]], classname: str 3846a413302SJohn Snow ) -> None: 3851a0c090aSJohn Snow node = nodes.container(source) 3866a413302SJohn Snow if isinstance(content, str): 3871a0c090aSJohn Snow node.append(nodes.Text(content)) 3886a413302SJohn Snow else: 3896a413302SJohn Snow node.extend(content) 3901a0c090aSJohn Snow node.attributes["classes"].extend(["qapi-infopip", classname]) 3911a0c090aSJohn Snow infopips.append(node) 3921a0c090aSJohn Snow 3931a0c090aSJohn Snow if "deprecated" in self.options: 3941a0c090aSJohn Snow _add_pip( 3951a0c090aSJohn Snow ":deprecated:", 3961a0c090aSJohn Snow f"This {self.objtype} is deprecated.", 3971a0c090aSJohn Snow "qapi-deprecated", 3981a0c090aSJohn Snow ) 3991a0c090aSJohn Snow 400d25808c2SJohn Snow if "unstable" in self.options: 401d25808c2SJohn Snow _add_pip( 402d25808c2SJohn Snow ":unstable:", 403d25808c2SJohn Snow f"This {self.objtype} is unstable/experimental.", 404d25808c2SJohn Snow "qapi-unstable", 405d25808c2SJohn Snow ) 406d25808c2SJohn Snow 4076a413302SJohn Snow if self.options.get("ifcond", ""): 4086a413302SJohn Snow ifcond = self.options["ifcond"] 4096a413302SJohn Snow _add_pip( 4106a413302SJohn Snow f":ifcond: {ifcond}", 4116a413302SJohn Snow [ 4126a413302SJohn Snow nodes.emphasis("", "Availability"), 4136a413302SJohn Snow nodes.Text(": "), 4146a413302SJohn Snow nodes.literal(ifcond, ifcond), 4156a413302SJohn Snow ], 4166a413302SJohn Snow "qapi-ifcond", 4176a413302SJohn Snow ) 4186a413302SJohn Snow 4191a0c090aSJohn Snow if infopips.children: 4201a0c090aSJohn Snow contentnode.insert(0, infopips) 4211a0c090aSJohn Snow 422ef137a22SJohn Snow def _validate_field(self, field: nodes.field) -> None: 423ef137a22SJohn Snow """Validate field lists in this QAPI Object Description.""" 424ef137a22SJohn Snow name, _ = _unpack_field(field) 425ef137a22SJohn Snow allowed_fields = set(self.env.app.config.qapi_allowed_fields) 426ef137a22SJohn Snow 427ef137a22SJohn Snow field_label = name.astext() 428ef137a22SJohn Snow if field_label in allowed_fields: 429ef137a22SJohn Snow # Explicitly allowed field list name, OK. 430ef137a22SJohn Snow return 431ef137a22SJohn Snow 432ef137a22SJohn Snow try: 433ef137a22SJohn Snow # split into field type and argument (if provided) 434ef137a22SJohn Snow # e.g. `:arg type name: descr` is 435ef137a22SJohn Snow # field_type = "arg", field_arg = "type name". 436ef137a22SJohn Snow field_type, field_arg = field_label.split(None, 1) 437ef137a22SJohn Snow except ValueError: 438ef137a22SJohn Snow # No arguments provided 439ef137a22SJohn Snow field_type = field_label 440ef137a22SJohn Snow field_arg = "" 441ef137a22SJohn Snow 442ef137a22SJohn Snow typemap = self.get_field_type_map() 443ef137a22SJohn Snow if field_type in typemap: 444ef137a22SJohn Snow # This is a special docfield, yet-to-be-processed. Catch 445ef137a22SJohn Snow # correct names, but incorrect arguments. This mismatch WILL 446ef137a22SJohn Snow # cause Sphinx to render this field incorrectly (without a 447ef137a22SJohn Snow # warning), which is never what we want. 448ef137a22SJohn Snow typedesc = typemap[field_type][0] 449ef137a22SJohn Snow if typedesc.has_arg != bool(field_arg): 450ef137a22SJohn Snow msg = f"docfield field list type {field_type!r} " 451ef137a22SJohn Snow if typedesc.has_arg: 452ef137a22SJohn Snow msg += "requires an argument." 453ef137a22SJohn Snow else: 454ef137a22SJohn Snow msg += "takes no arguments." 455ef137a22SJohn Snow logger.warning(msg, location=field) 456ef137a22SJohn Snow else: 457ef137a22SJohn Snow # This is unrecognized entirely. It's valid rST to use 458ef137a22SJohn Snow # arbitrary fields, but let's ensure the documentation 459ef137a22SJohn Snow # writer has done this intentionally. 460ef137a22SJohn Snow valid = ", ".join(sorted(set(typemap) | allowed_fields)) 461ef137a22SJohn Snow msg = ( 462ef137a22SJohn Snow f"Unrecognized field list name {field_label!r}.\n" 463ef137a22SJohn Snow f"Valid fields for qapi:{self.objtype} are: {valid}\n" 464ef137a22SJohn Snow "\n" 465ef137a22SJohn Snow "If this usage is intentional, please add it to " 466ef137a22SJohn Snow "'qapi_allowed_fields' in docs/conf.py." 467ef137a22SJohn Snow ) 468ef137a22SJohn Snow logger.warning(msg, location=field) 469ef137a22SJohn Snow 4701a0c090aSJohn Snow def transform_content(self, content_node: addnodes.desc_content) -> None: 471707f2bbbSJohn Snow # This hook runs after before_content and the nested parse, but 472707f2bbbSJohn Snow # before the DocFieldTransformer is executed. 473707f2bbbSJohn Snow super().transform_content(content_node) 474707f2bbbSJohn Snow 4751a0c090aSJohn Snow self._add_infopips(content_node) 4761a0c090aSJohn Snow 477ef137a22SJohn Snow # Validate field lists. 478ef137a22SJohn Snow for child in content_node: 479ef137a22SJohn Snow if isinstance(child, nodes.field_list): 480ef137a22SJohn Snow for field in child.children: 481ef137a22SJohn Snow assert isinstance(field, nodes.field) 482ef137a22SJohn Snow self._validate_field(field) 483ef137a22SJohn Snow 4848799c364SJohn Snow 4851884492eSJohn Snowclass SpecialTypedField(CompatTypedField): 4861884492eSJohn Snow def make_field(self, *args: Any, **kwargs: Any) -> nodes.field: 4871884492eSJohn Snow ret = super().make_field(*args, **kwargs) 4881884492eSJohn Snow 4891884492eSJohn Snow # Look for the characteristic " -- " text node that Sphinx 4901884492eSJohn Snow # inserts for each TypedField entry ... 4911884492eSJohn Snow for node in ret.traverse(lambda n: str(n) == " -- "): 4921884492eSJohn Snow par = node.parent 4931884492eSJohn Snow if par.children[0].astext() != "q_dummy": 4941884492eSJohn Snow continue 4951884492eSJohn Snow 4961884492eSJohn Snow # If the first node's text is q_dummy, this is a dummy 4971884492eSJohn Snow # field we want to strip down to just its contents. 4981884492eSJohn Snow del par.children[:-1] 4991884492eSJohn Snow 5001884492eSJohn Snow return ret 5011884492eSJohn Snow 5021884492eSJohn Snow 503758bbdcdSJohn Snowclass QAPICommand(QAPIObject): 504758bbdcdSJohn Snow """Description of a QAPI Command.""" 505758bbdcdSJohn Snow 50661837970SJohn Snow doc_field_types = QAPIObject.doc_field_types.copy() 50761837970SJohn Snow doc_field_types.extend( 50861837970SJohn Snow [ 50961837970SJohn Snow # :arg TypeName ArgName: descr 5101884492eSJohn Snow SpecialTypedField( 51161837970SJohn Snow "argument", 51261837970SJohn Snow label=_("Arguments"), 51361837970SJohn Snow names=("arg",), 51403947c80SJohn Snow typerolename="type", 51561837970SJohn Snow can_collapse=False, 51661837970SJohn Snow ), 5179605c204SJohn Snow # :error: descr 518a1fe2cd4SJohn Snow CompatField( 5199605c204SJohn Snow "error", 5209605c204SJohn Snow label=_("Errors"), 5219605c204SJohn Snow names=("error", "errors"), 5229605c204SJohn Snow has_arg=False, 5239605c204SJohn Snow ), 5248b77f8d5SJohn Snow # :return TypeName: descr 525a1fe2cd4SJohn Snow CompatGroupedField( 5268b77f8d5SJohn Snow "returnvalue", 5278b77f8d5SJohn Snow label=_("Return"), 52803947c80SJohn Snow rolename="type", 5298b77f8d5SJohn Snow names=("return",), 5308b77f8d5SJohn Snow can_collapse=True, 5318b77f8d5SJohn Snow ), 53261837970SJohn Snow ] 53361837970SJohn Snow ) 534758bbdcdSJohn Snow 535758bbdcdSJohn Snow 536902c9b0eSJohn Snowclass QAPIEnum(QAPIObject): 537902c9b0eSJohn Snow """Description of a QAPI Enum.""" 538902c9b0eSJohn Snow 539902c9b0eSJohn Snow doc_field_types = QAPIObject.doc_field_types.copy() 540902c9b0eSJohn Snow doc_field_types.extend( 541902c9b0eSJohn Snow [ 542902c9b0eSJohn Snow # :value name: descr 543a1fe2cd4SJohn Snow CompatGroupedField( 544902c9b0eSJohn Snow "value", 545902c9b0eSJohn Snow label=_("Values"), 546902c9b0eSJohn Snow names=("value",), 547902c9b0eSJohn Snow can_collapse=False, 548902c9b0eSJohn Snow ) 549902c9b0eSJohn Snow ] 550902c9b0eSJohn Snow ) 551902c9b0eSJohn Snow 552902c9b0eSJohn Snow 553bac3f131SJohn Snowclass QAPIAlternate(QAPIObject): 554bac3f131SJohn Snow """Description of a QAPI Alternate.""" 555bac3f131SJohn Snow 556bac3f131SJohn Snow doc_field_types = QAPIObject.doc_field_types.copy() 557bac3f131SJohn Snow doc_field_types.extend( 558bac3f131SJohn Snow [ 559bac3f131SJohn Snow # :alt type name: descr 560a1fe2cd4SJohn Snow CompatTypedField( 561bac3f131SJohn Snow "alternative", 562bac3f131SJohn Snow label=_("Alternatives"), 563bac3f131SJohn Snow names=("alt",), 56403947c80SJohn Snow typerolename="type", 565bac3f131SJohn Snow can_collapse=False, 566bac3f131SJohn Snow ), 567bac3f131SJohn Snow ] 568bac3f131SJohn Snow ) 569bac3f131SJohn Snow 570bac3f131SJohn Snow 5716d5f6f69SJohn Snowclass QAPIObjectWithMembers(QAPIObject): 5726d5f6f69SJohn Snow """Base class for Events/Structs/Unions""" 5736d5f6f69SJohn Snow 5746d5f6f69SJohn Snow doc_field_types = QAPIObject.doc_field_types.copy() 5756d5f6f69SJohn Snow doc_field_types.extend( 5766d5f6f69SJohn Snow [ 5776d5f6f69SJohn Snow # :member type name: descr 5781884492eSJohn Snow SpecialTypedField( 5796d5f6f69SJohn Snow "member", 5806d5f6f69SJohn Snow label=_("Members"), 5816d5f6f69SJohn Snow names=("memb",), 58203947c80SJohn Snow typerolename="type", 5836d5f6f69SJohn Snow can_collapse=False, 5846d5f6f69SJohn Snow ), 5856d5f6f69SJohn Snow ] 5866d5f6f69SJohn Snow ) 5876d5f6f69SJohn Snow 5886d5f6f69SJohn Snow 5896d5f6f69SJohn Snowclass QAPIEvent(QAPIObjectWithMembers): 590707f2bbbSJohn Snow # pylint: disable=too-many-ancestors 5916d5f6f69SJohn Snow """Description of a QAPI Event.""" 5926d5f6f69SJohn Snow 5936d5f6f69SJohn Snow 5943fe3349dSJohn Snowclass QAPIJSONObject(QAPIObjectWithMembers): 595707f2bbbSJohn Snow # pylint: disable=too-many-ancestors 5963fe3349dSJohn Snow """Description of a QAPI Object: structs and unions.""" 5973fe3349dSJohn Snow 5983fe3349dSJohn Snow 5997320feebSJohn Snowclass QAPIModule(QAPIDescription): 6007320feebSJohn Snow """ 6017320feebSJohn Snow Directive to mark description of a new module. 6027320feebSJohn Snow 6037320feebSJohn Snow This directive doesn't generate any special formatting, and is just 6047320feebSJohn Snow a pass-through for the content body. Named section titles are 6057320feebSJohn Snow allowed in the content body. 6067320feebSJohn Snow 6077320feebSJohn Snow Use this directive to create entries for the QAPI module in the 6087320feebSJohn Snow global index and the QAPI index; as well as to associate subsequent 6097320feebSJohn Snow definitions with the module they are defined in for purposes of 6107320feebSJohn Snow search and QAPI index organization. 6117320feebSJohn Snow 6127320feebSJohn Snow :arg: The name of the module. 6137320feebSJohn Snow :opt no-index: Don't add cross-reference targets or index entries. 6147320feebSJohn Snow :opt no-typesetting: Don't render the content body (but preserve any 6157320feebSJohn Snow cross-reference target IDs in the squelched output.) 6167320feebSJohn Snow 6177320feebSJohn Snow Example:: 6187320feebSJohn Snow 6197320feebSJohn Snow .. qapi:module:: block-core 6207320feebSJohn Snow :no-index: 6217320feebSJohn Snow :no-typesetting: 6227320feebSJohn Snow 6237320feebSJohn Snow Lorem ipsum, dolor sit amet ... 6247320feebSJohn Snow """ 6257320feebSJohn Snow 6267320feebSJohn Snow def run(self) -> List[Node]: 6277320feebSJohn Snow modname = self.arguments[0].strip() 6287320feebSJohn Snow self.env.ref_context["qapi:module"] = modname 6297320feebSJohn Snow ret = super().run() 6307320feebSJohn Snow 6317320feebSJohn Snow # ObjectDescription always creates a visible signature bar. We 6327320feebSJohn Snow # want module items to be "invisible", however. 6337320feebSJohn Snow 6347320feebSJohn Snow # Extract the content body of the directive: 6357320feebSJohn Snow assert isinstance(ret[-1], addnodes.desc) 6367320feebSJohn Snow desc_node = ret.pop(-1) 6377320feebSJohn Snow assert isinstance(desc_node.children[1], addnodes.desc_content) 6387320feebSJohn Snow ret.extend(desc_node.children[1].children) 6397320feebSJohn Snow 6407320feebSJohn Snow # Re-home node_ids so anchor refs still work: 6417320feebSJohn Snow node_ids: List[str] 6427320feebSJohn Snow if node_ids := [ 6437320feebSJohn Snow node_id 6447320feebSJohn Snow for el in desc_node.children[0].traverse(nodes.Element) 6457320feebSJohn Snow for node_id in cast(List[str], el.get("ids", ())) 6467320feebSJohn Snow ]: 6477320feebSJohn Snow target_node = nodes.target(ids=node_ids) 6487320feebSJohn Snow ret.insert(1, target_node) 6497320feebSJohn Snow 6507320feebSJohn Snow return ret 6517320feebSJohn Snow 6527320feebSJohn Snow 6537c7247b2SJohn Snowclass QAPINamespace(SphinxDirective): 6547c7247b2SJohn Snow has_content = False 6557c7247b2SJohn Snow required_arguments = 1 6567c7247b2SJohn Snow 6577c7247b2SJohn Snow def run(self) -> List[Node]: 6587c7247b2SJohn Snow namespace = self.arguments[0].strip() 6597c7247b2SJohn Snow self.env.ref_context["qapi:namespace"] = namespace 6607c7247b2SJohn Snow 6617c7247b2SJohn Snow return [] 6627c7247b2SJohn Snow 6637c7247b2SJohn Snow 664e93d29d2SJohn Snowclass QAPIIndex(Index): 665e93d29d2SJohn Snow """ 666e93d29d2SJohn Snow Index subclass to provide the QAPI definition index. 667e93d29d2SJohn Snow """ 668e93d29d2SJohn Snow 669e93d29d2SJohn Snow # pylint: disable=too-few-public-methods 670e93d29d2SJohn Snow 671e93d29d2SJohn Snow name = "index" 672e93d29d2SJohn Snow localname = _("QAPI Index") 673e93d29d2SJohn Snow shortname = _("QAPI Index") 674*25d44f57SJohn Snow namespace = "" 675e93d29d2SJohn Snow 676e93d29d2SJohn Snow def generate( 677e93d29d2SJohn Snow self, 678e93d29d2SJohn Snow docnames: Optional[Iterable[str]] = None, 679e93d29d2SJohn Snow ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]: 680e93d29d2SJohn Snow assert isinstance(self.domain, QAPIDomain) 681e93d29d2SJohn Snow content: Dict[str, List[IndexEntry]] = {} 682e93d29d2SJohn Snow collapse = False 683e93d29d2SJohn Snow 684*25d44f57SJohn Snow for objname, obj in self.domain.objects.items(): 685e93d29d2SJohn Snow if docnames and obj.docname not in docnames: 686e93d29d2SJohn Snow continue 687e93d29d2SJohn Snow 688*25d44f57SJohn Snow ns, _mod, name = QAPIDescription.split_fqn(objname) 689*25d44f57SJohn Snow 690*25d44f57SJohn Snow if self.namespace != ns: 691*25d44f57SJohn Snow continue 692e93d29d2SJohn Snow 693e93d29d2SJohn Snow # Add an alphabetical entry: 694*25d44f57SJohn Snow entries = content.setdefault(name[0].upper(), []) 695e93d29d2SJohn Snow entries.append( 696e93d29d2SJohn Snow IndexEntry( 697*25d44f57SJohn Snow name, 0, obj.docname, obj.node_id, obj.objtype, "", "" 698e93d29d2SJohn Snow ) 699e93d29d2SJohn Snow ) 700e93d29d2SJohn Snow 701e93d29d2SJohn Snow # Add a categorical entry: 702e93d29d2SJohn Snow category = obj.objtype.title() + "s" 703e93d29d2SJohn Snow entries = content.setdefault(category, []) 704e93d29d2SJohn Snow entries.append( 705*25d44f57SJohn Snow IndexEntry(name, 0, obj.docname, obj.node_id, "", "", "") 706e93d29d2SJohn Snow ) 707e93d29d2SJohn Snow 708*25d44f57SJohn Snow # Sort entries within each category alphabetically 709*25d44f57SJohn Snow for category in content: 710*25d44f57SJohn Snow content[category] = sorted(content[category]) 711*25d44f57SJohn Snow 712*25d44f57SJohn Snow # Sort the categories themselves; type names first, ABC entries last. 713e93d29d2SJohn Snow sorted_content = sorted( 714e93d29d2SJohn Snow content.items(), 715e93d29d2SJohn Snow key=lambda x: (len(x[0]) == 1, x[0]), 716e93d29d2SJohn Snow ) 717e93d29d2SJohn Snow return sorted_content, collapse 718e93d29d2SJohn Snow 719e93d29d2SJohn Snow 720ecf92e36SJohn Snowclass QAPIDomain(Domain): 721ecf92e36SJohn Snow """QAPI language domain.""" 722ecf92e36SJohn Snow 723ecf92e36SJohn Snow name = "qapi" 724ecf92e36SJohn Snow label = "QAPI" 725ecf92e36SJohn Snow 72636ceafadSJohn Snow # This table associates cross-reference object types (key) with an 72736ceafadSJohn Snow # ObjType instance, which defines the valid cross-reference roles 72836ceafadSJohn Snow # for each object type. 729902c9b0eSJohn Snow # 730902c9b0eSJohn Snow # e.g., the :qapi:type: cross-reference role can refer to enum, 731902c9b0eSJohn Snow # struct, union, or alternate objects; but :qapi:obj: can refer to 732902c9b0eSJohn Snow # anything. Each object also gets its own targeted cross-reference role. 7337320feebSJohn Snow object_types: Dict[str, ObjType] = { 7347320feebSJohn Snow "module": ObjType(_("module"), "mod", "any"), 735758bbdcdSJohn Snow "command": ObjType(_("command"), "cmd", "any"), 7366d5f6f69SJohn Snow "event": ObjType(_("event"), "event", "any"), 737902c9b0eSJohn Snow "enum": ObjType(_("enum"), "enum", "type", "any"), 7383fe3349dSJohn Snow "object": ObjType(_("object"), "obj", "type", "any"), 739bac3f131SJohn Snow "alternate": ObjType(_("alternate"), "alt", "type", "any"), 7407320feebSJohn Snow } 74136ceafadSJohn Snow 7427320feebSJohn Snow # Each of these provides a rST directive, 7437320feebSJohn Snow # e.g. .. qapi:module:: block-core 7447320feebSJohn Snow directives = { 7457c7247b2SJohn Snow "namespace": QAPINamespace, 7467320feebSJohn Snow "module": QAPIModule, 747758bbdcdSJohn Snow "command": QAPICommand, 7486d5f6f69SJohn Snow "event": QAPIEvent, 749902c9b0eSJohn Snow "enum": QAPIEnum, 7503fe3349dSJohn Snow "object": QAPIJSONObject, 751bac3f131SJohn Snow "alternate": QAPIAlternate, 7527320feebSJohn Snow } 753760b37e1SJohn Snow 754760b37e1SJohn Snow # These are all cross-reference roles; e.g. 755760b37e1SJohn Snow # :qapi:cmd:`query-block`. The keys correlate to the names used in 756760b37e1SJohn Snow # the object_types table values above. 757760b37e1SJohn Snow roles = { 7587320feebSJohn Snow "mod": QAPIXRefRole(), 759758bbdcdSJohn Snow "cmd": QAPIXRefRole(), 7606d5f6f69SJohn Snow "event": QAPIXRefRole(), 761902c9b0eSJohn Snow "enum": QAPIXRefRole(), 7623fe3349dSJohn Snow "obj": QAPIXRefRole(), # specifically structs and unions. 763bac3f131SJohn Snow "alt": QAPIXRefRole(), 764902c9b0eSJohn Snow # reference any data type (excludes modules, commands, events) 765902c9b0eSJohn Snow "type": QAPIXRefRole(), 766760b37e1SJohn Snow "any": QAPIXRefRole(), # reference *any* type of QAPI object. 767760b37e1SJohn Snow } 76836ceafadSJohn Snow 76936ceafadSJohn Snow # Moved into the data property at runtime; 77036ceafadSJohn Snow # this is the internal index of reference-able objects. 77136ceafadSJohn Snow initial_data: Dict[str, Dict[str, Tuple[Any]]] = { 77236ceafadSJohn Snow "objects": {}, # fullname -> ObjectEntry 77336ceafadSJohn Snow } 77436ceafadSJohn Snow 775e93d29d2SJohn Snow # Index pages to generate; each entry is an Index class. 776e93d29d2SJohn Snow indices = [ 777e93d29d2SJohn Snow QAPIIndex, 778e93d29d2SJohn Snow ] 779ecf92e36SJohn Snow 78036ceafadSJohn Snow @property 78136ceafadSJohn Snow def objects(self) -> Dict[str, ObjectEntry]: 78236ceafadSJohn Snow ret = self.data.setdefault("objects", {}) 78336ceafadSJohn Snow return ret # type: ignore[no-any-return] 78436ceafadSJohn Snow 785*25d44f57SJohn Snow def setup(self) -> None: 786*25d44f57SJohn Snow namespaces = set(self.env.app.config.qapi_namespaces) 787*25d44f57SJohn Snow for namespace in namespaces: 788*25d44f57SJohn Snow new_index: Type[QAPIIndex] = types.new_class( 789*25d44f57SJohn Snow f"{namespace}Index", bases=(QAPIIndex,) 790*25d44f57SJohn Snow ) 791*25d44f57SJohn Snow new_index.name = f"{namespace.lower()}-index" 792*25d44f57SJohn Snow new_index.localname = _(f"{namespace} Index") 793*25d44f57SJohn Snow new_index.shortname = _(f"{namespace} Index") 794*25d44f57SJohn Snow new_index.namespace = namespace 795*25d44f57SJohn Snow 796*25d44f57SJohn Snow self.indices.append(new_index) 797*25d44f57SJohn Snow 798*25d44f57SJohn Snow super().setup() 799*25d44f57SJohn Snow 80036ceafadSJohn Snow def note_object( 80136ceafadSJohn Snow self, 80236ceafadSJohn Snow name: str, 80336ceafadSJohn Snow objtype: str, 80436ceafadSJohn Snow node_id: str, 80536ceafadSJohn Snow aliased: bool = False, 80636ceafadSJohn Snow location: Any = None, 80736ceafadSJohn Snow ) -> None: 80836ceafadSJohn Snow """Note a QAPI object for cross reference.""" 80936ceafadSJohn Snow if name in self.objects: 81036ceafadSJohn Snow other = self.objects[name] 81136ceafadSJohn Snow if other.aliased and aliased is False: 81236ceafadSJohn Snow # The original definition found. Override it! 81336ceafadSJohn Snow pass 81436ceafadSJohn Snow elif other.aliased is False and aliased: 81536ceafadSJohn Snow # The original definition is already registered. 81636ceafadSJohn Snow return 81736ceafadSJohn Snow else: 81836ceafadSJohn Snow # duplicated 81936ceafadSJohn Snow logger.warning( 82036ceafadSJohn Snow __( 82136ceafadSJohn Snow "duplicate object description of %s, " 82236ceafadSJohn Snow "other instance in %s, use :no-index: for one of them" 82336ceafadSJohn Snow ), 82436ceafadSJohn Snow name, 82536ceafadSJohn Snow other.docname, 82636ceafadSJohn Snow location=location, 82736ceafadSJohn Snow ) 82836ceafadSJohn Snow self.objects[name] = ObjectEntry( 82936ceafadSJohn Snow self.env.docname, node_id, objtype, aliased 83036ceafadSJohn Snow ) 83136ceafadSJohn Snow 83236ceafadSJohn Snow def clear_doc(self, docname: str) -> None: 83336ceafadSJohn Snow for fullname, obj in list(self.objects.items()): 83436ceafadSJohn Snow if obj.docname == docname: 83536ceafadSJohn Snow del self.objects[fullname] 83636ceafadSJohn Snow 837ecf92e36SJohn Snow def merge_domaindata( 838ecf92e36SJohn Snow self, docnames: AbstractSet[str], otherdata: Dict[str, Any] 839ecf92e36SJohn Snow ) -> None: 84036ceafadSJohn Snow for fullname, obj in otherdata["objects"].items(): 84136ceafadSJohn Snow if obj.docname in docnames: 84236ceafadSJohn Snow # Sphinx's own python domain doesn't appear to bother to 84336ceafadSJohn Snow # check for collisions. Assert they don't happen and 84436ceafadSJohn Snow # we'll fix it if/when the case arises. 84536ceafadSJohn Snow assert fullname not in self.objects, ( 84636ceafadSJohn Snow "bug - collision on merge?" 84736ceafadSJohn Snow f" {fullname=} {obj=} {self.objects[fullname]=}" 84836ceafadSJohn Snow ) 84936ceafadSJohn Snow self.objects[fullname] = obj 850ecf92e36SJohn Snow 851dca2f3c4SJohn Snow def find_obj( 8527127e14fSJohn Snow self, namespace: str, modname: str, name: str, typ: Optional[str] 8537127e14fSJohn Snow ) -> List[Tuple[str, ObjectEntry]]: 854dca2f3c4SJohn Snow """ 8557127e14fSJohn Snow Find a QAPI object for "name", maybe using contextual information. 856dca2f3c4SJohn Snow 857dca2f3c4SJohn Snow Returns a list of (name, object entry) tuples. 858dca2f3c4SJohn Snow 8597127e14fSJohn Snow :param namespace: The current namespace context (if any!) under 8607127e14fSJohn Snow which we are searching. 8617127e14fSJohn Snow :param modname: The current module context (if any!) under 8627127e14fSJohn Snow which we are searching. 8637127e14fSJohn Snow :param name: The name of the x-ref to resolve; may or may not 8647127e14fSJohn Snow include leading context. 8657127e14fSJohn Snow :param type: The role name of the x-ref we're resolving, if 8667127e14fSJohn Snow provided. This is absent for "any" role lookups. 867dca2f3c4SJohn Snow """ 868dca2f3c4SJohn Snow if not name: 869ecf92e36SJohn Snow return [] 870ecf92e36SJohn Snow 8717127e14fSJohn Snow # ## 8727127e14fSJohn Snow # what to search for 8737127e14fSJohn Snow # ## 874dca2f3c4SJohn Snow 8757127e14fSJohn Snow parts = list(QAPIDescription.split_fqn(name)) 8767127e14fSJohn Snow explicit = tuple(bool(x) for x in parts) 8777127e14fSJohn Snow 8787127e14fSJohn Snow # Fill in the blanks where possible: 8797127e14fSJohn Snow if namespace and not parts[0]: 8807127e14fSJohn Snow parts[0] = namespace 8817127e14fSJohn Snow if modname and not parts[1]: 8827127e14fSJohn Snow parts[1] = modname 8837127e14fSJohn Snow 8847127e14fSJohn Snow implicit_fqn = "" 8857127e14fSJohn Snow if all(parts): 8867127e14fSJohn Snow implicit_fqn = f"{parts[0]}:{parts[1]}.{parts[2]}" 887dca2f3c4SJohn Snow 888dca2f3c4SJohn Snow if typ is None: 8897127e14fSJohn Snow # :any: lookup, search everything: 890dca2f3c4SJohn Snow objtypes: List[str] = list(self.object_types) 891dca2f3c4SJohn Snow else: 892dca2f3c4SJohn Snow # type is specified and will be a role (e.g. obj, mod, cmd) 893dca2f3c4SJohn Snow # convert this to eligible object types (e.g. command, module) 894dca2f3c4SJohn Snow # using the QAPIDomain.object_types table. 895dca2f3c4SJohn Snow objtypes = self.objtypes_for_role(typ, []) 896dca2f3c4SJohn Snow 8977127e14fSJohn Snow # ## 8987127e14fSJohn Snow # search! 8997127e14fSJohn Snow # ## 9007127e14fSJohn Snow 9017127e14fSJohn Snow def _search(needle: str) -> List[str]: 9027127e14fSJohn Snow if ( 9037127e14fSJohn Snow needle 9047127e14fSJohn Snow and needle in self.objects 9057127e14fSJohn Snow and self.objects[needle].objtype in objtypes 906dca2f3c4SJohn Snow ): 9077127e14fSJohn Snow return [needle] 9087127e14fSJohn Snow return [] 9097127e14fSJohn Snow 9107127e14fSJohn Snow if found := _search(name): 9117127e14fSJohn Snow # Exact match! 9127127e14fSJohn Snow pass 9137127e14fSJohn Snow elif found := _search(implicit_fqn): 9147127e14fSJohn Snow # Exact match using contextual information to fill in the gaps. 9157127e14fSJohn Snow pass 916dca2f3c4SJohn Snow else: 9177127e14fSJohn Snow # No exact hits, perform applicable fuzzy searches. 9187127e14fSJohn Snow searches = [] 9197127e14fSJohn Snow 9207127e14fSJohn Snow esc = tuple(re.escape(s) for s in parts) 9217127e14fSJohn Snow 9227127e14fSJohn Snow # Try searching for ns:*.name or ns:name 9237127e14fSJohn Snow if explicit[0] and not explicit[1]: 9247127e14fSJohn Snow searches.append(f"^{esc[0]}:([^\\.]+\\.)?{esc[2]}$") 9257127e14fSJohn Snow # Try searching for *:module.name or module.name 9267127e14fSJohn Snow if explicit[1] and not explicit[0]: 9277127e14fSJohn Snow searches.append(f"(^|:){esc[1]}\\.{esc[2]}$") 9287127e14fSJohn Snow # Try searching for context-ns:*.name or context-ns:name 9297127e14fSJohn Snow if parts[0] and not (explicit[0] or explicit[1]): 9307127e14fSJohn Snow searches.append(f"^{esc[0]}:([^\\.]+\\.)?{esc[2]}$") 9317127e14fSJohn Snow # Try searching for *:context-mod.name or context-mod.name 9327127e14fSJohn Snow if parts[1] and not (explicit[0] or explicit[1]): 9337127e14fSJohn Snow searches.append(f"(^|:){esc[1]}\\.{esc[2]}$") 9347127e14fSJohn Snow # Try searching for *:name, *.name, or name 9357127e14fSJohn Snow if not (explicit[0] or explicit[1]): 9367127e14fSJohn Snow searches.append(f"(^|:|\\.){esc[2]}$") 9377127e14fSJohn Snow 9387127e14fSJohn Snow for search in searches: 9397127e14fSJohn Snow if found := [ 940dca2f3c4SJohn Snow oname 941dca2f3c4SJohn Snow for oname in self.objects 9427127e14fSJohn Snow if re.search(search, oname) 943dca2f3c4SJohn Snow and self.objects[oname].objtype in objtypes 9447127e14fSJohn Snow ]: 9457127e14fSJohn Snow break 946dca2f3c4SJohn Snow 9477127e14fSJohn Snow matches = [(oname, self.objects[oname]) for oname in found] 948dca2f3c4SJohn Snow if len(matches) > 1: 949dca2f3c4SJohn Snow matches = [m for m in matches if not m[1].aliased] 950dca2f3c4SJohn Snow return matches 951dca2f3c4SJohn Snow 952760b37e1SJohn Snow def resolve_xref( 953760b37e1SJohn Snow self, 954760b37e1SJohn Snow env: BuildEnvironment, 955760b37e1SJohn Snow fromdocname: str, 956760b37e1SJohn Snow builder: Builder, 957760b37e1SJohn Snow typ: str, 958760b37e1SJohn Snow target: str, 959760b37e1SJohn Snow node: pending_xref, 960760b37e1SJohn Snow contnode: Element, 961760b37e1SJohn Snow ) -> nodes.reference | None: 9627127e14fSJohn Snow namespace = node.get("qapi:namespace") 963760b37e1SJohn Snow modname = node.get("qapi:module") 9647127e14fSJohn Snow matches = self.find_obj(namespace, modname, target, typ) 965760b37e1SJohn Snow 966760b37e1SJohn Snow if not matches: 967d48a8f8dSJohn Snow # Normally, we could pass warn_dangling=True to QAPIXRefRole(), 968d48a8f8dSJohn Snow # but that will trigger on references to these built-in types, 969d48a8f8dSJohn Snow # which we'd like to ignore instead. 970d48a8f8dSJohn Snow 971d48a8f8dSJohn Snow # Take care of that warning here instead, so long as the 972d48a8f8dSJohn Snow # reference isn't to one of our built-in core types. 973d48a8f8dSJohn Snow if target not in ( 974d48a8f8dSJohn Snow "string", 975d48a8f8dSJohn Snow "number", 976d48a8f8dSJohn Snow "int", 977d48a8f8dSJohn Snow "boolean", 978d48a8f8dSJohn Snow "null", 979d48a8f8dSJohn Snow "value", 980d48a8f8dSJohn Snow "q_empty", 981d48a8f8dSJohn Snow ): 982d48a8f8dSJohn Snow logger.warning( 983d48a8f8dSJohn Snow __("qapi:%s reference target not found: %r"), 984d48a8f8dSJohn Snow typ, 985d48a8f8dSJohn Snow target, 986d48a8f8dSJohn Snow type="ref", 987d48a8f8dSJohn Snow subtype="qapi", 988d48a8f8dSJohn Snow location=node, 989d48a8f8dSJohn Snow ) 990760b37e1SJohn Snow return None 991760b37e1SJohn Snow 992760b37e1SJohn Snow if len(matches) > 1: 993760b37e1SJohn Snow logger.warning( 994760b37e1SJohn Snow __("more than one target found for cross-reference %r: %s"), 995760b37e1SJohn Snow target, 996760b37e1SJohn Snow ", ".join(match[0] for match in matches), 997760b37e1SJohn Snow type="ref", 998760b37e1SJohn Snow subtype="qapi", 999760b37e1SJohn Snow location=node, 1000760b37e1SJohn Snow ) 1001760b37e1SJohn Snow 1002760b37e1SJohn Snow name, obj = matches[0] 1003760b37e1SJohn Snow return make_refnode( 1004760b37e1SJohn Snow builder, fromdocname, obj.docname, obj.node_id, contnode, name 1005760b37e1SJohn Snow ) 1006760b37e1SJohn Snow 1007dca2f3c4SJohn Snow def resolve_any_xref( 1008dca2f3c4SJohn Snow self, 1009dca2f3c4SJohn Snow env: BuildEnvironment, 1010dca2f3c4SJohn Snow fromdocname: str, 1011dca2f3c4SJohn Snow builder: Builder, 1012dca2f3c4SJohn Snow target: str, 1013dca2f3c4SJohn Snow node: pending_xref, 1014dca2f3c4SJohn Snow contnode: Element, 1015dca2f3c4SJohn Snow ) -> List[Tuple[str, nodes.reference]]: 1016dca2f3c4SJohn Snow results: List[Tuple[str, nodes.reference]] = [] 10177127e14fSJohn Snow matches = self.find_obj( 10187127e14fSJohn Snow node.get("qapi:namespace"), node.get("qapi:module"), target, None 10197127e14fSJohn Snow ) 1020dca2f3c4SJohn Snow for name, obj in matches: 1021dca2f3c4SJohn Snow rolename = self.role_for_objtype(obj.objtype) 1022dca2f3c4SJohn Snow assert rolename is not None 1023dca2f3c4SJohn Snow role = f"qapi:{rolename}" 1024dca2f3c4SJohn Snow refnode = make_refnode( 1025dca2f3c4SJohn Snow builder, fromdocname, obj.docname, obj.node_id, contnode, name 1026dca2f3c4SJohn Snow ) 1027dca2f3c4SJohn Snow results.append((role, refnode)) 1028dca2f3c4SJohn Snow return results 1029dca2f3c4SJohn Snow 1030ecf92e36SJohn Snow 1031ecf92e36SJohn Snowdef setup(app: Sphinx) -> Dict[str, Any]: 1032ecf92e36SJohn Snow app.setup_extension("sphinx.directives") 1033ef137a22SJohn Snow app.add_config_value( 1034ef137a22SJohn Snow "qapi_allowed_fields", 1035ef137a22SJohn Snow set(), 1036ef137a22SJohn Snow "env", # Setting impacts parsing phase 1037ef137a22SJohn Snow types=set, 1038ef137a22SJohn Snow ) 1039*25d44f57SJohn Snow app.add_config_value( 1040*25d44f57SJohn Snow "qapi_namespaces", 1041*25d44f57SJohn Snow set(), 1042*25d44f57SJohn Snow "env", 1043*25d44f57SJohn Snow types=set, 1044*25d44f57SJohn Snow ) 1045ecf92e36SJohn Snow app.add_domain(QAPIDomain) 1046ecf92e36SJohn Snow 1047ecf92e36SJohn Snow return { 1048ecf92e36SJohn Snow "version": "1.0", 1049ecf92e36SJohn Snow "env_version": 1, 1050ecf92e36SJohn Snow "parallel_read_safe": True, 1051ecf92e36SJohn Snow "parallel_write_safe": True, 1052ecf92e36SJohn Snow } 1053