xref: /openbmc/qemu/docs/sphinx/qapi_domain.py (revision dca2f3c47137b2bc276f443b3c269215a1ef9166)
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 docutils import nodes
20
21from sphinx.addnodes import pending_xref
22from sphinx.domains import (
23    Domain,
24    Index,
25    IndexEntry,
26    ObjType,
27)
28from sphinx.locale import _, __
29from sphinx.util import logging
30from sphinx.util.nodes import make_refnode
31
32
33if TYPE_CHECKING:
34    from docutils.nodes import Element
35
36    from sphinx.application import Sphinx
37    from sphinx.builders import Builder
38    from sphinx.environment import BuildEnvironment
39
40logger = logging.getLogger(__name__)
41
42
43class ObjectEntry(NamedTuple):
44    docname: str
45    node_id: str
46    objtype: str
47    aliased: bool
48
49
50class QAPIIndex(Index):
51    """
52    Index subclass to provide the QAPI definition index.
53    """
54
55    # pylint: disable=too-few-public-methods
56
57    name = "index"
58    localname = _("QAPI Index")
59    shortname = _("QAPI Index")
60
61    def generate(
62        self,
63        docnames: Optional[Iterable[str]] = None,
64    ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
65        assert isinstance(self.domain, QAPIDomain)
66        content: Dict[str, List[IndexEntry]] = {}
67        collapse = False
68
69        # list of all object (name, ObjectEntry) pairs, sorted by name
70        # (ignoring the module)
71        objects = sorted(
72            self.domain.objects.items(),
73            key=lambda x: x[0].split(".")[-1].lower(),
74        )
75
76        for objname, obj in objects:
77            if docnames and obj.docname not in docnames:
78                continue
79
80            # Strip the module name out:
81            objname = objname.split(".")[-1]
82
83            # Add an alphabetical entry:
84            entries = content.setdefault(objname[0].upper(), [])
85            entries.append(
86                IndexEntry(
87                    objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
88                )
89            )
90
91            # Add a categorical entry:
92            category = obj.objtype.title() + "s"
93            entries = content.setdefault(category, [])
94            entries.append(
95                IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
96            )
97
98        # alphabetically sort categories; type names first, ABC entries last.
99        sorted_content = sorted(
100            content.items(),
101            key=lambda x: (len(x[0]) == 1, x[0]),
102        )
103        return sorted_content, collapse
104
105
106class QAPIDomain(Domain):
107    """QAPI language domain."""
108
109    name = "qapi"
110    label = "QAPI"
111
112    # This table associates cross-reference object types (key) with an
113    # ObjType instance, which defines the valid cross-reference roles
114    # for each object type.
115
116    # Actual table entries for module, command, event, etc will come in
117    # forthcoming commits.
118    object_types: Dict[str, ObjType] = {}
119
120    directives = {}
121    roles = {}
122
123    # Moved into the data property at runtime;
124    # this is the internal index of reference-able objects.
125    initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
126        "objects": {},  # fullname -> ObjectEntry
127    }
128
129    # Index pages to generate; each entry is an Index class.
130    indices = [
131        QAPIIndex,
132    ]
133
134    @property
135    def objects(self) -> Dict[str, ObjectEntry]:
136        ret = self.data.setdefault("objects", {})
137        return ret  # type: ignore[no-any-return]
138
139    def note_object(
140        self,
141        name: str,
142        objtype: str,
143        node_id: str,
144        aliased: bool = False,
145        location: Any = None,
146    ) -> None:
147        """Note a QAPI object for cross reference."""
148        if name in self.objects:
149            other = self.objects[name]
150            if other.aliased and aliased is False:
151                # The original definition found. Override it!
152                pass
153            elif other.aliased is False and aliased:
154                # The original definition is already registered.
155                return
156            else:
157                # duplicated
158                logger.warning(
159                    __(
160                        "duplicate object description of %s, "
161                        "other instance in %s, use :no-index: for one of them"
162                    ),
163                    name,
164                    other.docname,
165                    location=location,
166                )
167        self.objects[name] = ObjectEntry(
168            self.env.docname, node_id, objtype, aliased
169        )
170
171    def clear_doc(self, docname: str) -> None:
172        for fullname, obj in list(self.objects.items()):
173            if obj.docname == docname:
174                del self.objects[fullname]
175
176    def merge_domaindata(
177        self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
178    ) -> None:
179        for fullname, obj in otherdata["objects"].items():
180            if obj.docname in docnames:
181                # Sphinx's own python domain doesn't appear to bother to
182                # check for collisions. Assert they don't happen and
183                # we'll fix it if/when the case arises.
184                assert fullname not in self.objects, (
185                    "bug - collision on merge?"
186                    f" {fullname=} {obj=} {self.objects[fullname]=}"
187                )
188                self.objects[fullname] = obj
189
190    def find_obj(
191        self, modname: str, name: str, typ: Optional[str]
192    ) -> list[tuple[str, ObjectEntry]]:
193        """
194        Find a QAPI object for "name", perhaps using the given module.
195
196        Returns a list of (name, object entry) tuples.
197
198        :param modname: The current module context (if any!)
199                        under which we are searching.
200        :param name: The name of the x-ref to resolve;
201                     may or may not include a leading module.
202        :param type: The role name of the x-ref we're resolving, if provided.
203                     (This is absent for "any" lookups.)
204        """
205        if not name:
206            return []
207
208        names: list[str] = []
209        matches: list[tuple[str, ObjectEntry]] = []
210
211        fullname = name
212        if "." in fullname:
213            # We're searching for a fully qualified reference;
214            # ignore the contextual module.
215            pass
216        elif modname:
217            # We're searching for something from somewhere;
218            # try searching the current module first.
219            # e.g. :qapi:cmd:`query-block` or `query-block` is being searched.
220            fullname = f"{modname}.{name}"
221
222        if typ is None:
223            # type isn't specified, this is a generic xref.
224            # search *all* qapi-specific object types.
225            objtypes: List[str] = list(self.object_types)
226        else:
227            # type is specified and will be a role (e.g. obj, mod, cmd)
228            # convert this to eligible object types (e.g. command, module)
229            # using the QAPIDomain.object_types table.
230            objtypes = self.objtypes_for_role(typ, [])
231
232        if name in self.objects and self.objects[name].objtype in objtypes:
233            names = [name]
234        elif (
235            fullname in self.objects
236            and self.objects[fullname].objtype in objtypes
237        ):
238            names = [fullname]
239        else:
240            # exact match wasn't found; e.g. we are searching for
241            # `query-block` from a different (or no) module.
242            searchname = "." + name
243            names = [
244                oname
245                for oname in self.objects
246                if oname.endswith(searchname)
247                and self.objects[oname].objtype in objtypes
248            ]
249
250        matches = [(oname, self.objects[oname]) for oname in names]
251        if len(matches) > 1:
252            matches = [m for m in matches if not m[1].aliased]
253        return matches
254
255    def resolve_any_xref(
256        self,
257        env: BuildEnvironment,
258        fromdocname: str,
259        builder: Builder,
260        target: str,
261        node: pending_xref,
262        contnode: Element,
263    ) -> List[Tuple[str, nodes.reference]]:
264        results: List[Tuple[str, nodes.reference]] = []
265        matches = self.find_obj(node.get("qapi:module"), target, None)
266        for name, obj in matches:
267            rolename = self.role_for_objtype(obj.objtype)
268            assert rolename is not None
269            role = f"qapi:{rolename}"
270            refnode = make_refnode(
271                builder, fromdocname, obj.docname, obj.node_id, contnode, name
272            )
273            results.append((role, refnode))
274        return results
275
276
277def setup(app: Sphinx) -> Dict[str, Any]:
278    app.setup_extension("sphinx.directives")
279    app.add_domain(QAPIDomain)
280
281    return {
282        "version": "1.0",
283        "env_version": 1,
284        "parallel_read_safe": True,
285        "parallel_write_safe": True,
286    }
287