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