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