xref: /openbmc/qemu/docs/sphinx/qapi_domain.py (revision 9beccc2df03026dc2979f0f28b8ff952e356164e)
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