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