xref: /openbmc/qemu/docs/sphinx/qapi_domain.py (revision 2beb051191b526608e0f269559962f4d2f618850)
1"""
2QAPI domain extension.
3"""
4
5# The best laid plans of mice and men, ...
6# pylint: disable=too-many-lines
7
8from __future__ import annotations
9
10from typing import (
11    TYPE_CHECKING,
12    AbstractSet,
13    Any,
14    Dict,
15    Iterable,
16    List,
17    NamedTuple,
18    Optional,
19    Tuple,
20    Union,
21    cast,
22)
23
24from docutils import nodes
25from docutils.parsers.rst import directives
26
27from compat import (
28    CompatField,
29    CompatGroupedField,
30    CompatTypedField,
31    KeywordNode,
32    ParserFix,
33    Signature,
34    SpaceNode,
35)
36from sphinx import addnodes
37from sphinx.addnodes import desc_signature, pending_xref
38from sphinx.directives import ObjectDescription
39from sphinx.domains import (
40    Domain,
41    Index,
42    IndexEntry,
43    ObjType,
44)
45from sphinx.locale import _, __
46from sphinx.roles import XRefRole
47from sphinx.util import logging
48from sphinx.util.nodes import make_id, make_refnode
49
50
51if TYPE_CHECKING:
52    from docutils.nodes import Element, Node
53
54    from sphinx.application import Sphinx
55    from sphinx.builders import Builder
56    from sphinx.environment import BuildEnvironment
57    from sphinx.util.typing import OptionSpec
58
59logger = logging.getLogger(__name__)
60
61
62def _unpack_field(
63    field: nodes.Node,
64) -> Tuple[nodes.field_name, nodes.field_body]:
65    """
66    docutils helper: unpack a field node in a type-safe manner.
67    """
68    assert isinstance(field, nodes.field)
69    assert len(field.children) == 2
70    assert isinstance(field.children[0], nodes.field_name)
71    assert isinstance(field.children[1], nodes.field_body)
72    return (field.children[0], field.children[1])
73
74
75class ObjectEntry(NamedTuple):
76    docname: str
77    node_id: str
78    objtype: str
79    aliased: bool
80
81
82class QAPIXRefRole(XRefRole):
83
84    def process_link(
85        self,
86        env: BuildEnvironment,
87        refnode: Element,
88        has_explicit_title: bool,
89        title: str,
90        target: str,
91    ) -> tuple[str, str]:
92        refnode["qapi:module"] = env.ref_context.get("qapi:module")
93
94        # Cross-references that begin with a tilde adjust the title to
95        # only show the reference without a leading module, even if one
96        # was provided. This is a Sphinx-standard syntax; give it
97        # priority over QAPI-specific type markup below.
98        hide_module = False
99        if target.startswith("~"):
100            hide_module = True
101            target = target[1:]
102
103        # Type names that end with "?" are considered optional
104        # arguments and should be documented as such, but it's not
105        # part of the xref itself.
106        if target.endswith("?"):
107            refnode["qapi:optional"] = True
108            target = target[:-1]
109
110        # Type names wrapped in brackets denote lists. strip the
111        # brackets and remember to add them back later.
112        if target.startswith("[") and target.endswith("]"):
113            refnode["qapi:array"] = True
114            target = target[1:-1]
115
116        if has_explicit_title:
117            # Don't mess with the title at all if it was explicitly set.
118            # Explicit title syntax for references is e.g.
119            # :qapi:type:`target <explicit title>`
120            # and this explicit title overrides everything else here.
121            return title, target
122
123        title = target
124        if hide_module:
125            title = target.split(".")[-1]
126
127        return title, target
128
129    def result_nodes(
130        self,
131        document: nodes.document,
132        env: BuildEnvironment,
133        node: Element,
134        is_ref: bool,
135    ) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
136
137        # node here is the pending_xref node (or whatever nodeclass was
138        # configured at XRefRole class instantiation time).
139        results: List[nodes.Node] = [node]
140
141        if node.get("qapi:array"):
142            results.insert(0, nodes.literal("[", "["))
143            results.append(nodes.literal("]", "]"))
144
145        if node.get("qapi:optional"):
146            results.append(nodes.Text(", "))
147            results.append(nodes.emphasis("?", "optional"))
148
149        return results, []
150
151
152class QAPIDescription(ParserFix):
153    """
154    Generic QAPI description.
155
156    This is meant to be an abstract class, not instantiated
157    directly. This class handles the abstract details of indexing, the
158    TOC, and reference targets for QAPI descriptions.
159    """
160
161    def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
162        # Do nothing. The return value here is the "name" of the entity
163        # being documented; for QAPI, this is the same as the
164        # "signature", which is just a name.
165
166        # Normally this method must also populate signode with nodes to
167        # render the signature; here we do nothing instead - the
168        # subclasses will handle this.
169        return sig
170
171    def get_index_text(self, name: Signature) -> Tuple[str, str]:
172        """Return the text for the index entry of the object."""
173
174        # NB: this is used for the global index, not the QAPI index.
175        return ("single", f"{name} (QMP {self.objtype})")
176
177    def add_target_and_index(
178        self, name: Signature, sig: str, signode: desc_signature
179    ) -> None:
180        # name is the return value of handle_signature.
181        # sig is the original, raw text argument to handle_signature.
182        # For QAPI, these are identical, currently.
183
184        assert self.objtype
185
186        # If we're documenting a module, don't include the module as
187        # part of the FQN.
188        modname = ""
189        if self.objtype != "module":
190            modname = self.options.get(
191                "module", self.env.ref_context.get("qapi:module")
192            )
193        fullname = (modname + "." if modname else "") + name
194
195        node_id = make_id(
196            self.env, self.state.document, self.objtype, fullname
197        )
198        signode["ids"].append(node_id)
199
200        self.state.document.note_explicit_target(signode)
201        domain = cast(QAPIDomain, self.env.get_domain("qapi"))
202        domain.note_object(fullname, self.objtype, node_id, location=signode)
203
204        if "no-index-entry" not in self.options:
205            arity, indextext = self.get_index_text(name)
206            assert self.indexnode is not None
207            if indextext:
208                self.indexnode["entries"].append(
209                    (arity, indextext, node_id, "", None)
210                )
211
212    def _object_hierarchy_parts(
213        self, sig_node: desc_signature
214    ) -> Tuple[str, ...]:
215        if "fullname" not in sig_node:
216            return ()
217        modname = sig_node.get("module")
218        fullname = sig_node["fullname"]
219
220        if modname:
221            return (modname, *fullname.split("."))
222
223        return tuple(fullname.split("."))
224
225    def _toc_entry_name(self, sig_node: desc_signature) -> str:
226        # This controls the name in the TOC and on the sidebar.
227
228        # This is the return type of _object_hierarchy_parts().
229        toc_parts = cast(Tuple[str, ...], sig_node.get("_toc_parts", ()))
230        if not toc_parts:
231            return ""
232
233        config = self.env.app.config
234        *parents, name = toc_parts
235        if config.toc_object_entries_show_parents == "domain":
236            return sig_node.get("fullname", name)
237        if config.toc_object_entries_show_parents == "hide":
238            return name
239        if config.toc_object_entries_show_parents == "all":
240            return ".".join(parents + [name])
241        return ""
242
243
244class QAPIObject(QAPIDescription):
245    """
246    Description of a generic QAPI object.
247
248    It's not used directly, but is instead subclassed by specific directives.
249    """
250
251    # Inherit some standard options from Sphinx's ObjectDescription
252    option_spec: OptionSpec = (  # type:ignore[misc]
253        ObjectDescription.option_spec.copy()
254    )
255    option_spec.update(
256        {
257            # Borrowed from the Python domain:
258            "module": directives.unchanged,  # Override contextual module name
259            # These are QAPI originals:
260            "since": directives.unchanged,
261            "ifcond": directives.unchanged,
262            "deprecated": directives.flag,
263            "unstable": directives.flag,
264        }
265    )
266
267    doc_field_types = [
268        # :feat name: descr
269        CompatGroupedField(
270            "feature",
271            label=_("Features"),
272            names=("feat",),
273            can_collapse=False,
274        ),
275    ]
276
277    def get_signature_prefix(self) -> List[nodes.Node]:
278        """Return a prefix to put before the object name in the signature."""
279        assert self.objtype
280        return [
281            KeywordNode("", self.objtype.title()),
282            SpaceNode(" "),
283        ]
284
285    def get_signature_suffix(self) -> List[nodes.Node]:
286        """Return a suffix to put after the object name in the signature."""
287        ret: List[nodes.Node] = []
288
289        if "since" in self.options:
290            ret += [
291                SpaceNode(" "),
292                addnodes.desc_sig_element(
293                    "", f"(Since: {self.options['since']})"
294                ),
295            ]
296
297        return ret
298
299    def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
300        """
301        Transform a QAPI definition name into RST nodes.
302
303        This method was originally intended for handling function
304        signatures. In the QAPI domain, however, we only pass the
305        definition name as the directive argument and handle everything
306        else in the content body with field lists.
307
308        As such, the only argument here is "sig", which is just the QAPI
309        definition name.
310        """
311        modname = self.options.get(
312            "module", self.env.ref_context.get("qapi:module")
313        )
314
315        signode["fullname"] = sig
316        signode["module"] = modname
317        sig_prefix = self.get_signature_prefix()
318        if sig_prefix:
319            signode += addnodes.desc_annotation(
320                str(sig_prefix), "", *sig_prefix
321            )
322        signode += addnodes.desc_name(sig, sig)
323        signode += self.get_signature_suffix()
324
325        return sig
326
327    def _add_infopips(self, contentnode: addnodes.desc_content) -> None:
328        # Add various eye-catches and things that go below the signature
329        # bar, but precede the user-defined content.
330        infopips = nodes.container()
331        infopips.attributes["classes"].append("qapi-infopips")
332
333        def _add_pip(
334            source: str, content: Union[str, List[nodes.Node]], classname: str
335        ) -> None:
336            node = nodes.container(source)
337            if isinstance(content, str):
338                node.append(nodes.Text(content))
339            else:
340                node.extend(content)
341            node.attributes["classes"].extend(["qapi-infopip", classname])
342            infopips.append(node)
343
344        if "deprecated" in self.options:
345            _add_pip(
346                ":deprecated:",
347                f"This {self.objtype} is deprecated.",
348                "qapi-deprecated",
349            )
350
351        if "unstable" in self.options:
352            _add_pip(
353                ":unstable:",
354                f"This {self.objtype} is unstable/experimental.",
355                "qapi-unstable",
356            )
357
358        if self.options.get("ifcond", ""):
359            ifcond = self.options["ifcond"]
360            _add_pip(
361                f":ifcond: {ifcond}",
362                [
363                    nodes.emphasis("", "Availability"),
364                    nodes.Text(": "),
365                    nodes.literal(ifcond, ifcond),
366                ],
367                "qapi-ifcond",
368            )
369
370        if infopips.children:
371            contentnode.insert(0, infopips)
372
373    def _validate_field(self, field: nodes.field) -> None:
374        """Validate field lists in this QAPI Object Description."""
375        name, _ = _unpack_field(field)
376        allowed_fields = set(self.env.app.config.qapi_allowed_fields)
377
378        field_label = name.astext()
379        if field_label in allowed_fields:
380            # Explicitly allowed field list name, OK.
381            return
382
383        try:
384            # split into field type and argument (if provided)
385            # e.g. `:arg type name: descr` is
386            # field_type = "arg", field_arg = "type name".
387            field_type, field_arg = field_label.split(None, 1)
388        except ValueError:
389            # No arguments provided
390            field_type = field_label
391            field_arg = ""
392
393        typemap = self.get_field_type_map()
394        if field_type in typemap:
395            # This is a special docfield, yet-to-be-processed. Catch
396            # correct names, but incorrect arguments. This mismatch WILL
397            # cause Sphinx to render this field incorrectly (without a
398            # warning), which is never what we want.
399            typedesc = typemap[field_type][0]
400            if typedesc.has_arg != bool(field_arg):
401                msg = f"docfield field list type {field_type!r} "
402                if typedesc.has_arg:
403                    msg += "requires an argument."
404                else:
405                    msg += "takes no arguments."
406                logger.warning(msg, location=field)
407        else:
408            # This is unrecognized entirely. It's valid rST to use
409            # arbitrary fields, but let's ensure the documentation
410            # writer has done this intentionally.
411            valid = ", ".join(sorted(set(typemap) | allowed_fields))
412            msg = (
413                f"Unrecognized field list name {field_label!r}.\n"
414                f"Valid fields for qapi:{self.objtype} are: {valid}\n"
415                "\n"
416                "If this usage is intentional, please add it to "
417                "'qapi_allowed_fields' in docs/conf.py."
418            )
419            logger.warning(msg, location=field)
420
421    def transform_content(self, content_node: addnodes.desc_content) -> None:
422        # This hook runs after before_content and the nested parse, but
423        # before the DocFieldTransformer is executed.
424        super().transform_content(content_node)
425
426        self._add_infopips(content_node)
427
428        # Validate field lists.
429        for child in content_node:
430            if isinstance(child, nodes.field_list):
431                for field in child.children:
432                    assert isinstance(field, nodes.field)
433                    self._validate_field(field)
434
435
436class SpecialTypedField(CompatTypedField):
437    def make_field(self, *args: Any, **kwargs: Any) -> nodes.field:
438        ret = super().make_field(*args, **kwargs)
439
440        # Look for the characteristic " -- " text node that Sphinx
441        # inserts for each TypedField entry ...
442        for node in ret.traverse(lambda n: str(n) == " -- "):
443            par = node.parent
444            if par.children[0].astext() != "q_dummy":
445                continue
446
447            # If the first node's text is q_dummy, this is a dummy
448            # field we want to strip down to just its contents.
449            del par.children[:-1]
450
451        return ret
452
453
454class QAPICommand(QAPIObject):
455    """Description of a QAPI Command."""
456
457    doc_field_types = QAPIObject.doc_field_types.copy()
458    doc_field_types.extend(
459        [
460            # :arg TypeName ArgName: descr
461            SpecialTypedField(
462                "argument",
463                label=_("Arguments"),
464                names=("arg",),
465                typerolename="type",
466                can_collapse=False,
467            ),
468            # :error: descr
469            CompatField(
470                "error",
471                label=_("Errors"),
472                names=("error", "errors"),
473                has_arg=False,
474            ),
475            # :return TypeName: descr
476            CompatGroupedField(
477                "returnvalue",
478                label=_("Return"),
479                rolename="type",
480                names=("return",),
481                can_collapse=True,
482            ),
483        ]
484    )
485
486
487class QAPIEnum(QAPIObject):
488    """Description of a QAPI Enum."""
489
490    doc_field_types = QAPIObject.doc_field_types.copy()
491    doc_field_types.extend(
492        [
493            # :value name: descr
494            CompatGroupedField(
495                "value",
496                label=_("Values"),
497                names=("value",),
498                can_collapse=False,
499            )
500        ]
501    )
502
503
504class QAPIAlternate(QAPIObject):
505    """Description of a QAPI Alternate."""
506
507    doc_field_types = QAPIObject.doc_field_types.copy()
508    doc_field_types.extend(
509        [
510            # :alt type name: descr
511            CompatTypedField(
512                "alternative",
513                label=_("Alternatives"),
514                names=("alt",),
515                typerolename="type",
516                can_collapse=False,
517            ),
518        ]
519    )
520
521
522class QAPIObjectWithMembers(QAPIObject):
523    """Base class for Events/Structs/Unions"""
524
525    doc_field_types = QAPIObject.doc_field_types.copy()
526    doc_field_types.extend(
527        [
528            # :member type name: descr
529            SpecialTypedField(
530                "member",
531                label=_("Members"),
532                names=("memb",),
533                typerolename="type",
534                can_collapse=False,
535            ),
536        ]
537    )
538
539
540class QAPIEvent(QAPIObjectWithMembers):
541    # pylint: disable=too-many-ancestors
542    """Description of a QAPI Event."""
543
544
545class QAPIJSONObject(QAPIObjectWithMembers):
546    # pylint: disable=too-many-ancestors
547    """Description of a QAPI Object: structs and unions."""
548
549
550class QAPIModule(QAPIDescription):
551    """
552    Directive to mark description of a new module.
553
554    This directive doesn't generate any special formatting, and is just
555    a pass-through for the content body. Named section titles are
556    allowed in the content body.
557
558    Use this directive to create entries for the QAPI module in the
559    global index and the QAPI index; as well as to associate subsequent
560    definitions with the module they are defined in for purposes of
561    search and QAPI index organization.
562
563    :arg: The name of the module.
564    :opt no-index: Don't add cross-reference targets or index entries.
565    :opt no-typesetting: Don't render the content body (but preserve any
566       cross-reference target IDs in the squelched output.)
567
568    Example::
569
570       .. qapi:module:: block-core
571          :no-index:
572          :no-typesetting:
573
574          Lorem ipsum, dolor sit amet ...
575    """
576
577    def run(self) -> List[Node]:
578        modname = self.arguments[0].strip()
579        self.env.ref_context["qapi:module"] = modname
580        ret = super().run()
581
582        # ObjectDescription always creates a visible signature bar. We
583        # want module items to be "invisible", however.
584
585        # Extract the content body of the directive:
586        assert isinstance(ret[-1], addnodes.desc)
587        desc_node = ret.pop(-1)
588        assert isinstance(desc_node.children[1], addnodes.desc_content)
589        ret.extend(desc_node.children[1].children)
590
591        # Re-home node_ids so anchor refs still work:
592        node_ids: List[str]
593        if node_ids := [
594            node_id
595            for el in desc_node.children[0].traverse(nodes.Element)
596            for node_id in cast(List[str], el.get("ids", ()))
597        ]:
598            target_node = nodes.target(ids=node_ids)
599            ret.insert(1, target_node)
600
601        return ret
602
603
604class QAPIIndex(Index):
605    """
606    Index subclass to provide the QAPI definition index.
607    """
608
609    # pylint: disable=too-few-public-methods
610
611    name = "index"
612    localname = _("QAPI Index")
613    shortname = _("QAPI Index")
614
615    def generate(
616        self,
617        docnames: Optional[Iterable[str]] = None,
618    ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
619        assert isinstance(self.domain, QAPIDomain)
620        content: Dict[str, List[IndexEntry]] = {}
621        collapse = False
622
623        # list of all object (name, ObjectEntry) pairs, sorted by name
624        # (ignoring the module)
625        objects = sorted(
626            self.domain.objects.items(),
627            key=lambda x: x[0].split(".")[-1].lower(),
628        )
629
630        for objname, obj in objects:
631            if docnames and obj.docname not in docnames:
632                continue
633
634            # Strip the module name out:
635            objname = objname.split(".")[-1]
636
637            # Add an alphabetical entry:
638            entries = content.setdefault(objname[0].upper(), [])
639            entries.append(
640                IndexEntry(
641                    objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
642                )
643            )
644
645            # Add a categorical entry:
646            category = obj.objtype.title() + "s"
647            entries = content.setdefault(category, [])
648            entries.append(
649                IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
650            )
651
652        # alphabetically sort categories; type names first, ABC entries last.
653        sorted_content = sorted(
654            content.items(),
655            key=lambda x: (len(x[0]) == 1, x[0]),
656        )
657        return sorted_content, collapse
658
659
660class QAPIDomain(Domain):
661    """QAPI language domain."""
662
663    name = "qapi"
664    label = "QAPI"
665
666    # This table associates cross-reference object types (key) with an
667    # ObjType instance, which defines the valid cross-reference roles
668    # for each object type.
669    #
670    # e.g., the :qapi:type: cross-reference role can refer to enum,
671    # struct, union, or alternate objects; but :qapi:obj: can refer to
672    # anything. Each object also gets its own targeted cross-reference role.
673    object_types: Dict[str, ObjType] = {
674        "module": ObjType(_("module"), "mod", "any"),
675        "command": ObjType(_("command"), "cmd", "any"),
676        "event": ObjType(_("event"), "event", "any"),
677        "enum": ObjType(_("enum"), "enum", "type", "any"),
678        "object": ObjType(_("object"), "obj", "type", "any"),
679        "alternate": ObjType(_("alternate"), "alt", "type", "any"),
680    }
681
682    # Each of these provides a rST directive,
683    # e.g. .. qapi:module:: block-core
684    directives = {
685        "module": QAPIModule,
686        "command": QAPICommand,
687        "event": QAPIEvent,
688        "enum": QAPIEnum,
689        "object": QAPIJSONObject,
690        "alternate": QAPIAlternate,
691    }
692
693    # These are all cross-reference roles; e.g.
694    # :qapi:cmd:`query-block`. The keys correlate to the names used in
695    # the object_types table values above.
696    roles = {
697        "mod": QAPIXRefRole(),
698        "cmd": QAPIXRefRole(),
699        "event": QAPIXRefRole(),
700        "enum": QAPIXRefRole(),
701        "obj": QAPIXRefRole(),  # specifically structs and unions.
702        "alt": QAPIXRefRole(),
703        # reference any data type (excludes modules, commands, events)
704        "type": QAPIXRefRole(),
705        "any": QAPIXRefRole(),  # reference *any* type of QAPI object.
706    }
707
708    # Moved into the data property at runtime;
709    # this is the internal index of reference-able objects.
710    initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
711        "objects": {},  # fullname -> ObjectEntry
712    }
713
714    # Index pages to generate; each entry is an Index class.
715    indices = [
716        QAPIIndex,
717    ]
718
719    @property
720    def objects(self) -> Dict[str, ObjectEntry]:
721        ret = self.data.setdefault("objects", {})
722        return ret  # type: ignore[no-any-return]
723
724    def note_object(
725        self,
726        name: str,
727        objtype: str,
728        node_id: str,
729        aliased: bool = False,
730        location: Any = None,
731    ) -> None:
732        """Note a QAPI object for cross reference."""
733        if name in self.objects:
734            other = self.objects[name]
735            if other.aliased and aliased is False:
736                # The original definition found. Override it!
737                pass
738            elif other.aliased is False and aliased:
739                # The original definition is already registered.
740                return
741            else:
742                # duplicated
743                logger.warning(
744                    __(
745                        "duplicate object description of %s, "
746                        "other instance in %s, use :no-index: for one of them"
747                    ),
748                    name,
749                    other.docname,
750                    location=location,
751                )
752        self.objects[name] = ObjectEntry(
753            self.env.docname, node_id, objtype, aliased
754        )
755
756    def clear_doc(self, docname: str) -> None:
757        for fullname, obj in list(self.objects.items()):
758            if obj.docname == docname:
759                del self.objects[fullname]
760
761    def merge_domaindata(
762        self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
763    ) -> None:
764        for fullname, obj in otherdata["objects"].items():
765            if obj.docname in docnames:
766                # Sphinx's own python domain doesn't appear to bother to
767                # check for collisions. Assert they don't happen and
768                # we'll fix it if/when the case arises.
769                assert fullname not in self.objects, (
770                    "bug - collision on merge?"
771                    f" {fullname=} {obj=} {self.objects[fullname]=}"
772                )
773                self.objects[fullname] = obj
774
775    def find_obj(
776        self, modname: str, name: str, typ: Optional[str]
777    ) -> list[tuple[str, ObjectEntry]]:
778        """
779        Find a QAPI object for "name", perhaps using the given module.
780
781        Returns a list of (name, object entry) tuples.
782
783        :param modname: The current module context (if any!)
784                        under which we are searching.
785        :param name: The name of the x-ref to resolve;
786                     may or may not include a leading module.
787        :param type: The role name of the x-ref we're resolving, if provided.
788                     (This is absent for "any" lookups.)
789        """
790        if not name:
791            return []
792
793        names: list[str] = []
794        matches: list[tuple[str, ObjectEntry]] = []
795
796        fullname = name
797        if "." in fullname:
798            # We're searching for a fully qualified reference;
799            # ignore the contextual module.
800            pass
801        elif modname:
802            # We're searching for something from somewhere;
803            # try searching the current module first.
804            # e.g. :qapi:cmd:`query-block` or `query-block` is being searched.
805            fullname = f"{modname}.{name}"
806
807        if typ is None:
808            # type isn't specified, this is a generic xref.
809            # search *all* qapi-specific object types.
810            objtypes: List[str] = list(self.object_types)
811        else:
812            # type is specified and will be a role (e.g. obj, mod, cmd)
813            # convert this to eligible object types (e.g. command, module)
814            # using the QAPIDomain.object_types table.
815            objtypes = self.objtypes_for_role(typ, [])
816
817        if name in self.objects and self.objects[name].objtype in objtypes:
818            names = [name]
819        elif (
820            fullname in self.objects
821            and self.objects[fullname].objtype in objtypes
822        ):
823            names = [fullname]
824        else:
825            # exact match wasn't found; e.g. we are searching for
826            # `query-block` from a different (or no) module.
827            searchname = "." + name
828            names = [
829                oname
830                for oname in self.objects
831                if oname.endswith(searchname)
832                and self.objects[oname].objtype in objtypes
833            ]
834
835        matches = [(oname, self.objects[oname]) for oname in names]
836        if len(matches) > 1:
837            matches = [m for m in matches if not m[1].aliased]
838        return matches
839
840    def resolve_xref(
841        self,
842        env: BuildEnvironment,
843        fromdocname: str,
844        builder: Builder,
845        typ: str,
846        target: str,
847        node: pending_xref,
848        contnode: Element,
849    ) -> nodes.reference | None:
850        modname = node.get("qapi:module")
851        matches = self.find_obj(modname, target, typ)
852
853        if not matches:
854            # Normally, we could pass warn_dangling=True to QAPIXRefRole(),
855            # but that will trigger on references to these built-in types,
856            # which we'd like to ignore instead.
857
858            # Take care of that warning here instead, so long as the
859            # reference isn't to one of our built-in core types.
860            if target not in (
861                "string",
862                "number",
863                "int",
864                "boolean",
865                "null",
866                "value",
867                "q_empty",
868            ):
869                logger.warning(
870                    __("qapi:%s reference target not found: %r"),
871                    typ,
872                    target,
873                    type="ref",
874                    subtype="qapi",
875                    location=node,
876                )
877            return None
878
879        if len(matches) > 1:
880            logger.warning(
881                __("more than one target found for cross-reference %r: %s"),
882                target,
883                ", ".join(match[0] for match in matches),
884                type="ref",
885                subtype="qapi",
886                location=node,
887            )
888
889        name, obj = matches[0]
890        return make_refnode(
891            builder, fromdocname, obj.docname, obj.node_id, contnode, name
892        )
893
894    def resolve_any_xref(
895        self,
896        env: BuildEnvironment,
897        fromdocname: str,
898        builder: Builder,
899        target: str,
900        node: pending_xref,
901        contnode: Element,
902    ) -> List[Tuple[str, nodes.reference]]:
903        results: List[Tuple[str, nodes.reference]] = []
904        matches = self.find_obj(node.get("qapi:module"), target, None)
905        for name, obj in matches:
906            rolename = self.role_for_objtype(obj.objtype)
907            assert rolename is not None
908            role = f"qapi:{rolename}"
909            refnode = make_refnode(
910                builder, fromdocname, obj.docname, obj.node_id, contnode, name
911            )
912            results.append((role, refnode))
913        return results
914
915
916def setup(app: Sphinx) -> Dict[str, Any]:
917    app.setup_extension("sphinx.directives")
918    app.add_config_value(
919        "qapi_allowed_fields",
920        set(),
921        "env",  # Setting impacts parsing phase
922        types=set,
923    )
924    app.add_domain(QAPIDomain)
925
926    return {
927        "version": "1.0",
928        "env_version": 1,
929        "parallel_read_safe": True,
930        "parallel_write_safe": True,
931    }
932