xref: /openbmc/qemu/docs/sphinx/qapi_domain.py (revision e93d29d27e93a25e0bd59d44299fc15486c62246)
1"""
2QAPI domain extension.
3"""
4
5from __future__ import annotations
6
7from typing import (
8    TYPE_CHECKING,
9    AbstractSet,
10    Any,
11    Dict,
12    Iterable,
13    List,
14    NamedTuple,
15    Optional,
16    Tuple,
17)
18
19from sphinx.domains import (
20    Domain,
21    Index,
22    IndexEntry,
23    ObjType,
24)
25from sphinx.locale import _, __
26from sphinx.util import logging
27
28
29if TYPE_CHECKING:
30    from sphinx.application import Sphinx
31
32logger = logging.getLogger(__name__)
33
34
35class ObjectEntry(NamedTuple):
36    docname: str
37    node_id: str
38    objtype: str
39    aliased: bool
40
41
42class QAPIIndex(Index):
43    """
44    Index subclass to provide the QAPI definition index.
45    """
46
47    # pylint: disable=too-few-public-methods
48
49    name = "index"
50    localname = _("QAPI Index")
51    shortname = _("QAPI Index")
52
53    def generate(
54        self,
55        docnames: Optional[Iterable[str]] = None,
56    ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
57        assert isinstance(self.domain, QAPIDomain)
58        content: Dict[str, List[IndexEntry]] = {}
59        collapse = False
60
61        # list of all object (name, ObjectEntry) pairs, sorted by name
62        # (ignoring the module)
63        objects = sorted(
64            self.domain.objects.items(),
65            key=lambda x: x[0].split(".")[-1].lower(),
66        )
67
68        for objname, obj in objects:
69            if docnames and obj.docname not in docnames:
70                continue
71
72            # Strip the module name out:
73            objname = objname.split(".")[-1]
74
75            # Add an alphabetical entry:
76            entries = content.setdefault(objname[0].upper(), [])
77            entries.append(
78                IndexEntry(
79                    objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
80                )
81            )
82
83            # Add a categorical entry:
84            category = obj.objtype.title() + "s"
85            entries = content.setdefault(category, [])
86            entries.append(
87                IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
88            )
89
90        # alphabetically sort categories; type names first, ABC entries last.
91        sorted_content = sorted(
92            content.items(),
93            key=lambda x: (len(x[0]) == 1, x[0]),
94        )
95        return sorted_content, collapse
96
97
98class QAPIDomain(Domain):
99    """QAPI language domain."""
100
101    name = "qapi"
102    label = "QAPI"
103
104    # This table associates cross-reference object types (key) with an
105    # ObjType instance, which defines the valid cross-reference roles
106    # for each object type.
107
108    # Actual table entries for module, command, event, etc will come in
109    # forthcoming commits.
110    object_types: Dict[str, ObjType] = {}
111
112    directives = {}
113    roles = {}
114
115    # Moved into the data property at runtime;
116    # this is the internal index of reference-able objects.
117    initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
118        "objects": {},  # fullname -> ObjectEntry
119    }
120
121    # Index pages to generate; each entry is an Index class.
122    indices = [
123        QAPIIndex,
124    ]
125
126    @property
127    def objects(self) -> Dict[str, ObjectEntry]:
128        ret = self.data.setdefault("objects", {})
129        return ret  # type: ignore[no-any-return]
130
131    def note_object(
132        self,
133        name: str,
134        objtype: str,
135        node_id: str,
136        aliased: bool = False,
137        location: Any = None,
138    ) -> None:
139        """Note a QAPI object for cross reference."""
140        if name in self.objects:
141            other = self.objects[name]
142            if other.aliased and aliased is False:
143                # The original definition found. Override it!
144                pass
145            elif other.aliased is False and aliased:
146                # The original definition is already registered.
147                return
148            else:
149                # duplicated
150                logger.warning(
151                    __(
152                        "duplicate object description of %s, "
153                        "other instance in %s, use :no-index: for one of them"
154                    ),
155                    name,
156                    other.docname,
157                    location=location,
158                )
159        self.objects[name] = ObjectEntry(
160            self.env.docname, node_id, objtype, aliased
161        )
162
163    def clear_doc(self, docname: str) -> None:
164        for fullname, obj in list(self.objects.items()):
165            if obj.docname == docname:
166                del self.objects[fullname]
167
168    def merge_domaindata(
169        self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
170    ) -> None:
171        for fullname, obj in otherdata["objects"].items():
172            if obj.docname in docnames:
173                # Sphinx's own python domain doesn't appear to bother to
174                # check for collisions. Assert they don't happen and
175                # we'll fix it if/when the case arises.
176                assert fullname not in self.objects, (
177                    "bug - collision on merge?"
178                    f" {fullname=} {obj=} {self.objects[fullname]=}"
179                )
180                self.objects[fullname] = obj
181
182    def resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any:
183        # pylint: disable=unused-argument
184        return []
185
186
187def setup(app: Sphinx) -> Dict[str, Any]:
188    app.setup_extension("sphinx.directives")
189    app.add_domain(QAPIDomain)
190
191    return {
192        "version": "1.0",
193        "env_version": 1,
194        "parallel_read_safe": True,
195        "parallel_write_safe": True,
196    }
197