xref: /qemu/docs/sphinx/qapidoc_legacy.py (revision 84307cd6027c4602913177ff09aeefa4743b7234)
1# coding=utf-8
2# type: ignore
3#
4# QEMU qapidoc QAPI file parsing extension
5#
6# Copyright (c) 2020 Linaro
7#
8# This work is licensed under the terms of the GNU GPLv2 or later.
9# See the COPYING file in the top-level directory.
10
11"""
12qapidoc is a Sphinx extension that implements the qapi-doc directive
13
14The purpose of this extension is to read the documentation comments
15in QAPI schema files, and insert them all into the current document.
16
17It implements one new rST directive, "qapi-doc::".
18Each qapi-doc:: directive takes one argument, which is the
19pathname of the schema file to process, relative to the source tree.
20
21The docs/conf.py file must set the qapidoc_srctree config value to
22the root of the QEMU source tree.
23
24The Sphinx documentation on writing extensions is at:
25https://www.sphinx-doc.org/en/master/development/index.html
26"""
27
28import re
29import textwrap
30
31from docutils import nodes
32from docutils.statemachine import ViewList
33from qapi.error import QAPISemError
34from qapi.gen import QAPISchemaVisitor
35from qapi.parser import QAPIDoc
36
37
38def dedent(text: str) -> str:
39    # Adjust indentation to make description text parse as paragraph.
40
41    lines = text.splitlines(True)
42    if re.match(r"\s+", lines[0]):
43        # First line is indented; description started on the line after
44        # the name. dedent the whole block.
45        return textwrap.dedent(text)
46
47    # Descr started on same line. Dedent line 2+.
48    return lines[0] + textwrap.dedent("".join(lines[1:]))
49
50
51class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
52    """A QAPI schema visitor which generates docutils/Sphinx nodes
53
54    This class builds up a tree of docutils/Sphinx nodes corresponding
55    to documentation for the various QAPI objects. To use it, first
56    create a QAPISchemaGenRSTVisitor object, and call its
57    visit_begin() method.  Then you can call one of the two methods
58    'freeform' (to add documentation for a freeform documentation
59    chunk) or 'symbol' (to add documentation for a QAPI symbol). These
60    will cause the visitor to build up the tree of document
61    nodes. Once you've added all the documentation via 'freeform' and
62    'symbol' method calls, you can call 'get_document_nodes' to get
63    the final list of document nodes (in a form suitable for returning
64    from a Sphinx directive's 'run' method).
65    """
66    def __init__(self, sphinx_directive):
67        self._cur_doc = None
68        self._sphinx_directive = sphinx_directive
69        self._top_node = nodes.section()
70        self._active_headings = [self._top_node]
71
72    def _make_dlitem(self, term, defn):
73        """Return a dlitem node with the specified term and definition.
74
75        term should be a list of Text and literal nodes.
76        defn should be one of:
77        - a string, which will be handed to _parse_text_into_node
78        - a list of Text and literal nodes, which will be put into
79          a paragraph node
80        """
81        dlitem = nodes.definition_list_item()
82        dlterm = nodes.term('', '', *term)
83        dlitem += dlterm
84        if defn:
85            dldef = nodes.definition()
86            if isinstance(defn, list):
87                dldef += nodes.paragraph('', '', *defn)
88            else:
89                self._parse_text_into_node(defn, dldef)
90            dlitem += dldef
91        return dlitem
92
93    def _make_section(self, title):
94        """Return a section node with optional title"""
95        section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
96        if title:
97            section += nodes.title(title, title)
98        return section
99
100    def _nodes_for_ifcond(self, ifcond, with_if=True):
101        """Return list of Text, literal nodes for the ifcond
102
103        Return a list which gives text like ' (If: condition)'.
104        If with_if is False, we don't return the "(If: " and ")".
105        """
106
107        doc = ifcond.docgen()
108        if not doc:
109            return []
110        doc = nodes.literal('', doc)
111        if not with_if:
112            return [doc]
113
114        nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
115        nodelist.append(doc)
116        nodelist.append(nodes.Text(')'))
117        return nodelist
118
119    def _nodes_for_one_member(self, member):
120        """Return list of Text, literal nodes for this member
121
122        Return a list of doctree nodes which give text like
123        'name: type (optional) (If: ...)' suitable for use as the
124        'term' part of a definition list item.
125        """
126        term = [nodes.literal('', member.name)]
127        if member.type.doc_type():
128            term.append(nodes.Text(': '))
129            term.append(nodes.literal('', member.type.doc_type()))
130        if member.optional:
131            term.append(nodes.Text(' (optional)'))
132        if member.ifcond.is_present():
133            term.extend(self._nodes_for_ifcond(member.ifcond))
134        return term
135
136    def _nodes_for_variant_when(self, branches, variant):
137        """Return list of Text, literal nodes for variant 'when' clause
138
139        Return a list of doctree nodes which give text like
140        'when tagname is variant (If: ...)' suitable for use in
141        the 'branches' part of a definition list.
142        """
143        term = [nodes.Text(' when '),
144                nodes.literal('', branches.tag_member.name),
145                nodes.Text(' is '),
146                nodes.literal('', '"%s"' % variant.name)]
147        if variant.ifcond.is_present():
148            term.extend(self._nodes_for_ifcond(variant.ifcond))
149        return term
150
151    def _nodes_for_members(self, doc, what, base=None, branches=None):
152        """Return list of doctree nodes for the table of members"""
153        dlnode = nodes.definition_list()
154        for section in doc.args.values():
155            term = self._nodes_for_one_member(section.member)
156            # TODO drop fallbacks when undocumented members are outlawed
157            if section.text:
158                defn = dedent(section.text)
159            else:
160                defn = [nodes.Text('Not documented')]
161
162            dlnode += self._make_dlitem(term, defn)
163
164        if base:
165            dlnode += self._make_dlitem([nodes.Text('The members of '),
166                                         nodes.literal('', base.doc_type())],
167                                        None)
168
169        if branches:
170            for v in branches.variants:
171                if v.type.name == 'q_empty':
172                    continue
173                assert not v.type.is_implicit()
174                term = [nodes.Text('The members of '),
175                        nodes.literal('', v.type.doc_type())]
176                term.extend(self._nodes_for_variant_when(branches, v))
177                dlnode += self._make_dlitem(term, None)
178
179        if not dlnode.children:
180            return []
181
182        section = self._make_section(what)
183        section += dlnode
184        return [section]
185
186    def _nodes_for_enum_values(self, doc):
187        """Return list of doctree nodes for the table of enum values"""
188        seen_item = False
189        dlnode = nodes.definition_list()
190        for section in doc.args.values():
191            termtext = [nodes.literal('', section.member.name)]
192            if section.member.ifcond.is_present():
193                termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
194            # TODO drop fallbacks when undocumented members are outlawed
195            if section.text:
196                defn = dedent(section.text)
197            else:
198                defn = [nodes.Text('Not documented')]
199
200            dlnode += self._make_dlitem(termtext, defn)
201            seen_item = True
202
203        if not seen_item:
204            return []
205
206        section = self._make_section('Values')
207        section += dlnode
208        return [section]
209
210    def _nodes_for_arguments(self, doc, arg_type):
211        """Return list of doctree nodes for the arguments section"""
212        if arg_type and not arg_type.is_implicit():
213            assert not doc.args
214            section = self._make_section('Arguments')
215            dlnode = nodes.definition_list()
216            dlnode += self._make_dlitem(
217                [nodes.Text('The members of '),
218                 nodes.literal('', arg_type.name)],
219                None)
220            section += dlnode
221            return [section]
222
223        return self._nodes_for_members(doc, 'Arguments')
224
225    def _nodes_for_features(self, doc):
226        """Return list of doctree nodes for the table of features"""
227        seen_item = False
228        dlnode = nodes.definition_list()
229        for section in doc.features.values():
230            dlnode += self._make_dlitem(
231                [nodes.literal('', section.member.name)], dedent(section.text))
232            seen_item = True
233
234        if not seen_item:
235            return []
236
237        section = self._make_section('Features')
238        section += dlnode
239        return [section]
240
241    def _nodes_for_sections(self, doc):
242        """Return list of doctree nodes for additional sections"""
243        nodelist = []
244        for section in doc.sections:
245            if section.kind == QAPIDoc.Kind.TODO:
246                # Hide TODO: sections
247                continue
248
249            if section.kind == QAPIDoc.Kind.PLAIN:
250                # Sphinx cannot handle sectionless titles;
251                # Instead, just append the results to the prior section.
252                container = nodes.container()
253                self._parse_text_into_node(section.text, container)
254                nodelist += container.children
255                continue
256
257            snode = self._make_section(section.kind.name.title())
258            self._parse_text_into_node(dedent(section.text), snode)
259            nodelist.append(snode)
260        return nodelist
261
262    def _nodes_for_if_section(self, ifcond):
263        """Return list of doctree nodes for the "If" section"""
264        nodelist = []
265        if ifcond.is_present():
266            snode = self._make_section('If')
267            snode += nodes.paragraph(
268                '', '', *self._nodes_for_ifcond(ifcond, with_if=False)
269            )
270            nodelist.append(snode)
271        return nodelist
272
273    def _add_doc(self, typ, sections):
274        """Add documentation for a command/object/enum...
275
276        We assume we're documenting the thing defined in self._cur_doc.
277        typ is the type of thing being added ("Command", "Object", etc)
278
279        sections is a list of nodes for sections to add to the definition.
280        """
281
282        doc = self._cur_doc
283        snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
284        snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
285                                       nodes.Text(' (' + typ + ')')])
286        self._parse_text_into_node(doc.body.text, snode)
287        for s in sections:
288            if s is not None:
289                snode += s
290        self._add_node_to_current_heading(snode)
291
292    def visit_enum_type(self, name, info, ifcond, features, members, prefix):
293        doc = self._cur_doc
294        self._add_doc('Enum',
295                      self._nodes_for_enum_values(doc)
296                      + self._nodes_for_features(doc)
297                      + self._nodes_for_sections(doc)
298                      + self._nodes_for_if_section(ifcond))
299
300    def visit_object_type(self, name, info, ifcond, features,
301                          base, members, branches):
302        doc = self._cur_doc
303        if base and base.is_implicit():
304            base = None
305        self._add_doc('Object',
306                      self._nodes_for_members(doc, 'Members', base, branches)
307                      + self._nodes_for_features(doc)
308                      + self._nodes_for_sections(doc)
309                      + self._nodes_for_if_section(ifcond))
310
311    def visit_alternate_type(self, name, info, ifcond, features,
312                             alternatives):
313        doc = self._cur_doc
314        self._add_doc('Alternate',
315                      self._nodes_for_members(doc, 'Members')
316                      + self._nodes_for_features(doc)
317                      + self._nodes_for_sections(doc)
318                      + self._nodes_for_if_section(ifcond))
319
320    def visit_command(self, name, info, ifcond, features, arg_type,
321                      ret_type, gen, success_response, boxed, allow_oob,
322                      allow_preconfig, coroutine):
323        doc = self._cur_doc
324        self._add_doc('Command',
325                      self._nodes_for_arguments(doc, arg_type)
326                      + self._nodes_for_features(doc)
327                      + self._nodes_for_sections(doc)
328                      + self._nodes_for_if_section(ifcond))
329
330    def visit_event(self, name, info, ifcond, features, arg_type, boxed):
331        doc = self._cur_doc
332        self._add_doc('Event',
333                      self._nodes_for_arguments(doc, arg_type)
334                      + self._nodes_for_features(doc)
335                      + self._nodes_for_sections(doc)
336                      + self._nodes_for_if_section(ifcond))
337
338    def symbol(self, doc, entity):
339        """Add documentation for one symbol to the document tree
340
341        This is the main entry point which causes us to add documentation
342        nodes for a symbol (which could be a 'command', 'object', 'event',
343        etc). We do this by calling 'visit' on the schema entity, which
344        will then call back into one of our visit_* methods, depending
345        on what kind of thing this symbol is.
346        """
347        self._cur_doc = doc
348        entity.visit(self)
349        self._cur_doc = None
350
351    def _start_new_heading(self, heading, level):
352        """Start a new heading at the specified heading level
353
354        Create a new section whose title is 'heading' and which is placed
355        in the docutils node tree as a child of the most recent level-1
356        heading. Subsequent document sections (commands, freeform doc chunks,
357        etc) will be placed as children of this new heading section.
358        """
359        if len(self._active_headings) < level:
360            raise QAPISemError(self._cur_doc.info,
361                               'Level %d subheading found outside a '
362                               'level %d heading'
363                               % (level, level - 1))
364        snode = self._make_section(heading)
365        self._active_headings[level - 1] += snode
366        self._active_headings = self._active_headings[:level]
367        self._active_headings.append(snode)
368        return snode
369
370    def _add_node_to_current_heading(self, node):
371        """Add the node to whatever the current active heading is"""
372        self._active_headings[-1] += node
373
374    def freeform(self, doc):
375        """Add a piece of 'freeform' documentation to the document tree
376
377        A 'freeform' document chunk doesn't relate to any particular
378        symbol (for instance, it could be an introduction).
379
380        If the freeform document starts with a line of the form
381        '= Heading text', this is a section or subsection heading, with
382        the heading level indicated by the number of '=' signs.
383        """
384
385        # QAPIDoc documentation says free-form documentation blocks
386        # must have only a body section, nothing else.
387        assert not doc.sections
388        assert not doc.args
389        assert not doc.features
390        self._cur_doc = doc
391
392        text = doc.body.text
393        if re.match(r'=+ ', text):
394            # Section/subsection heading (if present, will always be
395            # the first line of the block)
396            (heading, _, text) = text.partition('\n')
397            (leader, _, heading) = heading.partition(' ')
398            node = self._start_new_heading(heading, len(leader))
399            if text == '':
400                return
401        else:
402            node = nodes.container()
403
404        self._parse_text_into_node(text, node)
405        self._cur_doc = None
406
407    def _parse_text_into_node(self, doctext, node):
408        """Parse a chunk of QAPI-doc-format text into the node
409
410        The doc comment can contain most inline rST markup, including
411        bulleted and enumerated lists.
412        As an extra permitted piece of markup, @var will be turned
413        into ``var``.
414        """
415
416        # Handle the "@var means ``var`` case
417        doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
418
419        rstlist = ViewList()
420        for line in doctext.splitlines():
421            # The reported line number will always be that of the start line
422            # of the doc comment, rather than the actual location of the error.
423            # Being more precise would require overhaul of the QAPIDoc class
424            # to track lines more exactly within all the sub-parts of the doc
425            # comment, as well as counting lines here.
426            rstlist.append(line, self._cur_doc.info.fname,
427                           self._cur_doc.info.line)
428        # Append a blank line -- in some cases rST syntax errors get
429        # attributed to the line after one with actual text, and if there
430        # isn't anything in the ViewList corresponding to that then Sphinx
431        # 1.6's AutodocReporter will then misidentify the source/line location
432        # in the error message (usually attributing it to the top-level
433        # .rst file rather than the offending .json file). The extra blank
434        # line won't affect the rendered output.
435        rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
436        self._sphinx_directive.do_parse(rstlist, node)
437
438    def get_document_node(self):
439        """Return the root docutils node which makes up the document"""
440        return self._top_node
441