xref: /openbmc/qemu/docs/sphinx/qapi_domain.py (revision faeacf858bd9529cab10a13ff9d2137c8f2ae17c)
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 QAPICommand(QAPIObject):
437    """Description of a QAPI Command."""
438
439    doc_field_types = QAPIObject.doc_field_types.copy()
440    doc_field_types.extend(
441        [
442            # :arg TypeName ArgName: descr
443            CompatTypedField(
444                "argument",
445                label=_("Arguments"),
446                names=("arg",),
447                typerolename="type",
448                can_collapse=False,
449            ),
450            # :error: descr
451            CompatField(
452                "error",
453                label=_("Errors"),
454                names=("error", "errors"),
455                has_arg=False,
456            ),
457            # :return TypeName: descr
458            CompatGroupedField(
459                "returnvalue",
460                label=_("Return"),
461                rolename="type",
462                names=("return",),
463                can_collapse=True,
464            ),
465        ]
466    )
467
468
469class QAPIEnum(QAPIObject):
470    """Description of a QAPI Enum."""
471
472    doc_field_types = QAPIObject.doc_field_types.copy()
473    doc_field_types.extend(
474        [
475            # :value name: descr
476            CompatGroupedField(
477                "value",
478                label=_("Values"),
479                names=("value",),
480                can_collapse=False,
481            )
482        ]
483    )
484
485
486class QAPIAlternate(QAPIObject):
487    """Description of a QAPI Alternate."""
488
489    doc_field_types = QAPIObject.doc_field_types.copy()
490    doc_field_types.extend(
491        [
492            # :alt type name: descr
493            CompatTypedField(
494                "alternative",
495                label=_("Alternatives"),
496                names=("alt",),
497                typerolename="type",
498                can_collapse=False,
499            ),
500        ]
501    )
502
503
504class QAPIObjectWithMembers(QAPIObject):
505    """Base class for Events/Structs/Unions"""
506
507    doc_field_types = QAPIObject.doc_field_types.copy()
508    doc_field_types.extend(
509        [
510            # :member type name: descr
511            CompatTypedField(
512                "member",
513                label=_("Members"),
514                names=("memb",),
515                typerolename="type",
516                can_collapse=False,
517            ),
518        ]
519    )
520
521
522class QAPIEvent(QAPIObjectWithMembers):
523    # pylint: disable=too-many-ancestors
524    """Description of a QAPI Event."""
525
526
527class QAPIJSONObject(QAPIObjectWithMembers):
528    # pylint: disable=too-many-ancestors
529    """Description of a QAPI Object: structs and unions."""
530
531
532class QAPIModule(QAPIDescription):
533    """
534    Directive to mark description of a new module.
535
536    This directive doesn't generate any special formatting, and is just
537    a pass-through for the content body. Named section titles are
538    allowed in the content body.
539
540    Use this directive to create entries for the QAPI module in the
541    global index and the QAPI index; as well as to associate subsequent
542    definitions with the module they are defined in for purposes of
543    search and QAPI index organization.
544
545    :arg: The name of the module.
546    :opt no-index: Don't add cross-reference targets or index entries.
547    :opt no-typesetting: Don't render the content body (but preserve any
548       cross-reference target IDs in the squelched output.)
549
550    Example::
551
552       .. qapi:module:: block-core
553          :no-index:
554          :no-typesetting:
555
556          Lorem ipsum, dolor sit amet ...
557    """
558
559    def run(self) -> List[Node]:
560        modname = self.arguments[0].strip()
561        self.env.ref_context["qapi:module"] = modname
562        ret = super().run()
563
564        # ObjectDescription always creates a visible signature bar. We
565        # want module items to be "invisible", however.
566
567        # Extract the content body of the directive:
568        assert isinstance(ret[-1], addnodes.desc)
569        desc_node = ret.pop(-1)
570        assert isinstance(desc_node.children[1], addnodes.desc_content)
571        ret.extend(desc_node.children[1].children)
572
573        # Re-home node_ids so anchor refs still work:
574        node_ids: List[str]
575        if node_ids := [
576            node_id
577            for el in desc_node.children[0].traverse(nodes.Element)
578            for node_id in cast(List[str], el.get("ids", ()))
579        ]:
580            target_node = nodes.target(ids=node_ids)
581            ret.insert(1, target_node)
582
583        return ret
584
585
586class QAPIIndex(Index):
587    """
588    Index subclass to provide the QAPI definition index.
589    """
590
591    # pylint: disable=too-few-public-methods
592
593    name = "index"
594    localname = _("QAPI Index")
595    shortname = _("QAPI Index")
596
597    def generate(
598        self,
599        docnames: Optional[Iterable[str]] = None,
600    ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
601        assert isinstance(self.domain, QAPIDomain)
602        content: Dict[str, List[IndexEntry]] = {}
603        collapse = False
604
605        # list of all object (name, ObjectEntry) pairs, sorted by name
606        # (ignoring the module)
607        objects = sorted(
608            self.domain.objects.items(),
609            key=lambda x: x[0].split(".")[-1].lower(),
610        )
611
612        for objname, obj in objects:
613            if docnames and obj.docname not in docnames:
614                continue
615
616            # Strip the module name out:
617            objname = objname.split(".")[-1]
618
619            # Add an alphabetical entry:
620            entries = content.setdefault(objname[0].upper(), [])
621            entries.append(
622                IndexEntry(
623                    objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
624                )
625            )
626
627            # Add a categorical entry:
628            category = obj.objtype.title() + "s"
629            entries = content.setdefault(category, [])
630            entries.append(
631                IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
632            )
633
634        # alphabetically sort categories; type names first, ABC entries last.
635        sorted_content = sorted(
636            content.items(),
637            key=lambda x: (len(x[0]) == 1, x[0]),
638        )
639        return sorted_content, collapse
640
641
642class QAPIDomain(Domain):
643    """QAPI language domain."""
644
645    name = "qapi"
646    label = "QAPI"
647
648    # This table associates cross-reference object types (key) with an
649    # ObjType instance, which defines the valid cross-reference roles
650    # for each object type.
651    #
652    # e.g., the :qapi:type: cross-reference role can refer to enum,
653    # struct, union, or alternate objects; but :qapi:obj: can refer to
654    # anything. Each object also gets its own targeted cross-reference role.
655    object_types: Dict[str, ObjType] = {
656        "module": ObjType(_("module"), "mod", "any"),
657        "command": ObjType(_("command"), "cmd", "any"),
658        "event": ObjType(_("event"), "event", "any"),
659        "enum": ObjType(_("enum"), "enum", "type", "any"),
660        "object": ObjType(_("object"), "obj", "type", "any"),
661        "alternate": ObjType(_("alternate"), "alt", "type", "any"),
662    }
663
664    # Each of these provides a rST directive,
665    # e.g. .. qapi:module:: block-core
666    directives = {
667        "module": QAPIModule,
668        "command": QAPICommand,
669        "event": QAPIEvent,
670        "enum": QAPIEnum,
671        "object": QAPIJSONObject,
672        "alternate": QAPIAlternate,
673    }
674
675    # These are all cross-reference roles; e.g.
676    # :qapi:cmd:`query-block`. The keys correlate to the names used in
677    # the object_types table values above.
678    roles = {
679        "mod": QAPIXRefRole(),
680        "cmd": QAPIXRefRole(),
681        "event": QAPIXRefRole(),
682        "enum": QAPIXRefRole(),
683        "obj": QAPIXRefRole(),  # specifically structs and unions.
684        "alt": QAPIXRefRole(),
685        # reference any data type (excludes modules, commands, events)
686        "type": QAPIXRefRole(),
687        "any": QAPIXRefRole(),  # reference *any* type of QAPI object.
688    }
689
690    # Moved into the data property at runtime;
691    # this is the internal index of reference-able objects.
692    initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
693        "objects": {},  # fullname -> ObjectEntry
694    }
695
696    # Index pages to generate; each entry is an Index class.
697    indices = [
698        QAPIIndex,
699    ]
700
701    @property
702    def objects(self) -> Dict[str, ObjectEntry]:
703        ret = self.data.setdefault("objects", {})
704        return ret  # type: ignore[no-any-return]
705
706    def note_object(
707        self,
708        name: str,
709        objtype: str,
710        node_id: str,
711        aliased: bool = False,
712        location: Any = None,
713    ) -> None:
714        """Note a QAPI object for cross reference."""
715        if name in self.objects:
716            other = self.objects[name]
717            if other.aliased and aliased is False:
718                # The original definition found. Override it!
719                pass
720            elif other.aliased is False and aliased:
721                # The original definition is already registered.
722                return
723            else:
724                # duplicated
725                logger.warning(
726                    __(
727                        "duplicate object description of %s, "
728                        "other instance in %s, use :no-index: for one of them"
729                    ),
730                    name,
731                    other.docname,
732                    location=location,
733                )
734        self.objects[name] = ObjectEntry(
735            self.env.docname, node_id, objtype, aliased
736        )
737
738    def clear_doc(self, docname: str) -> None:
739        for fullname, obj in list(self.objects.items()):
740            if obj.docname == docname:
741                del self.objects[fullname]
742
743    def merge_domaindata(
744        self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
745    ) -> None:
746        for fullname, obj in otherdata["objects"].items():
747            if obj.docname in docnames:
748                # Sphinx's own python domain doesn't appear to bother to
749                # check for collisions. Assert they don't happen and
750                # we'll fix it if/when the case arises.
751                assert fullname not in self.objects, (
752                    "bug - collision on merge?"
753                    f" {fullname=} {obj=} {self.objects[fullname]=}"
754                )
755                self.objects[fullname] = obj
756
757    def find_obj(
758        self, modname: str, name: str, typ: Optional[str]
759    ) -> list[tuple[str, ObjectEntry]]:
760        """
761        Find a QAPI object for "name", perhaps using the given module.
762
763        Returns a list of (name, object entry) tuples.
764
765        :param modname: The current module context (if any!)
766                        under which we are searching.
767        :param name: The name of the x-ref to resolve;
768                     may or may not include a leading module.
769        :param type: The role name of the x-ref we're resolving, if provided.
770                     (This is absent for "any" lookups.)
771        """
772        if not name:
773            return []
774
775        names: list[str] = []
776        matches: list[tuple[str, ObjectEntry]] = []
777
778        fullname = name
779        if "." in fullname:
780            # We're searching for a fully qualified reference;
781            # ignore the contextual module.
782            pass
783        elif modname:
784            # We're searching for something from somewhere;
785            # try searching the current module first.
786            # e.g. :qapi:cmd:`query-block` or `query-block` is being searched.
787            fullname = f"{modname}.{name}"
788
789        if typ is None:
790            # type isn't specified, this is a generic xref.
791            # search *all* qapi-specific object types.
792            objtypes: List[str] = list(self.object_types)
793        else:
794            # type is specified and will be a role (e.g. obj, mod, cmd)
795            # convert this to eligible object types (e.g. command, module)
796            # using the QAPIDomain.object_types table.
797            objtypes = self.objtypes_for_role(typ, [])
798
799        if name in self.objects and self.objects[name].objtype in objtypes:
800            names = [name]
801        elif (
802            fullname in self.objects
803            and self.objects[fullname].objtype in objtypes
804        ):
805            names = [fullname]
806        else:
807            # exact match wasn't found; e.g. we are searching for
808            # `query-block` from a different (or no) module.
809            searchname = "." + name
810            names = [
811                oname
812                for oname in self.objects
813                if oname.endswith(searchname)
814                and self.objects[oname].objtype in objtypes
815            ]
816
817        matches = [(oname, self.objects[oname]) for oname in names]
818        if len(matches) > 1:
819            matches = [m for m in matches if not m[1].aliased]
820        return matches
821
822    def resolve_xref(
823        self,
824        env: BuildEnvironment,
825        fromdocname: str,
826        builder: Builder,
827        typ: str,
828        target: str,
829        node: pending_xref,
830        contnode: Element,
831    ) -> nodes.reference | None:
832        modname = node.get("qapi:module")
833        matches = self.find_obj(modname, target, typ)
834
835        if not matches:
836            # Normally, we could pass warn_dangling=True to QAPIXRefRole(),
837            # but that will trigger on references to these built-in types,
838            # which we'd like to ignore instead.
839
840            # Take care of that warning here instead, so long as the
841            # reference isn't to one of our built-in core types.
842            if target not in (
843                "string",
844                "number",
845                "int",
846                "boolean",
847                "null",
848                "value",
849                "q_empty",
850            ):
851                logger.warning(
852                    __("qapi:%s reference target not found: %r"),
853                    typ,
854                    target,
855                    type="ref",
856                    subtype="qapi",
857                    location=node,
858                )
859            return None
860
861        if len(matches) > 1:
862            logger.warning(
863                __("more than one target found for cross-reference %r: %s"),
864                target,
865                ", ".join(match[0] for match in matches),
866                type="ref",
867                subtype="qapi",
868                location=node,
869            )
870
871        name, obj = matches[0]
872        return make_refnode(
873            builder, fromdocname, obj.docname, obj.node_id, contnode, name
874        )
875
876    def resolve_any_xref(
877        self,
878        env: BuildEnvironment,
879        fromdocname: str,
880        builder: Builder,
881        target: str,
882        node: pending_xref,
883        contnode: Element,
884    ) -> List[Tuple[str, nodes.reference]]:
885        results: List[Tuple[str, nodes.reference]] = []
886        matches = self.find_obj(node.get("qapi:module"), target, None)
887        for name, obj in matches:
888            rolename = self.role_for_objtype(obj.objtype)
889            assert rolename is not None
890            role = f"qapi:{rolename}"
891            refnode = make_refnode(
892                builder, fromdocname, obj.docname, obj.node_id, contnode, name
893            )
894            results.append((role, refnode))
895        return results
896
897
898def setup(app: Sphinx) -> Dict[str, Any]:
899    app.setup_extension("sphinx.directives")
900    app.add_config_value(
901        "qapi_allowed_fields",
902        set(),
903        "env",  # Setting impacts parsing phase
904        types=set,
905    )
906    app.add_domain(QAPIDomain)
907
908    return {
909        "version": "1.0",
910        "env_version": 1,
911        "parallel_read_safe": True,
912        "parallel_write_safe": True,
913    }
914