All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Marc-André Lureau" <marcandre.lureau@redhat.com>
To: qemu-devel@nongnu.org
Cc: eblake@redhat.com, armbru@redhat.com,
	"Marc-André Lureau" <marcandre.lureau@redhat.com>
Subject: [Qemu-devel] [PATCH v5 13/17] qapi: add qapi2texi script
Date: Thu, 17 Nov 2016 19:55:00 +0400	[thread overview]
Message-ID: <20161117155504.21843-14-marcandre.lureau@redhat.com> (raw)
In-Reply-To: <20161117155504.21843-1-marcandre.lureau@redhat.com>

As the name suggests, the qapi2texi script converts JSON QAPI
description into a texi file suitable for different target
formats (info/man/txt/pdf/html...).

It parses the following kind of blocks:

Free-form:

  ##
  # = Section
  # == Subsection
  #
  # Some text foo with *emphasis*
  # 1. with a list
  # 2. like that
  #
  # And some code:
  # | $ echo foo
  # | -> do this
  # | <- get that
  #
  ##

Symbol:

  ##
  # @symbol:
  #
  # Symbol body ditto ergo sum. Foo bar
  # baz ding.
  #
  # @arg: foo
  # @arg: #optional foo
  #
  # Returns: returns bla bla
  #          Or bla blah
  #
  # Since: version
  # Notes: notes, comments can have
  #        - itemized list
  #        - like this
  #
  # Example:
  #
  # -> { "execute": "quit" }
  # <- { "return": {} }
  #
  ##

That's roughly following the following EBNF grammar:

api_comment = "##\n" comment "##\n"
comment = freeform_comment | symbol_comment
freeform_comment = { "# " text "\n" | "#\n" }
symbol_comment = "# @" name ":\n" { member | meta | freeform_comment }
member = "# @" name ':' [ text ] freeform_comment
meta = "# " ( "Returns:", "Since:", "Note:", "Notes:", "Example:", "Examples:" ) [ text ] freeform_comment
text = free-text markdown-like, "#optional" for members

Thanks to the following json expressions, the documentation is enhanced
with extra information about the type of arguments and return value
expected.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 tests/Makefile.include                       |  17 ++
 scripts/qapi.py                              | 215 ++++++++++++++++-
 scripts/qapi2texi.py                         | 331 +++++++++++++++++++++++++++
 docs/qapi-code-gen.txt                       |  54 ++++-
 tests/qapi-schema/doc-bad-args.err           |   1 +
 tests/qapi-schema/doc-bad-args.exit          |   1 +
 tests/qapi-schema/doc-bad-args.json          |   6 +
 tests/qapi-schema/doc-bad-args.out           |   0
 tests/qapi-schema/doc-bad-symbol.err         |   1 +
 tests/qapi-schema/doc-bad-symbol.exit        |   1 +
 tests/qapi-schema/doc-bad-symbol.json        |   4 +
 tests/qapi-schema/doc-bad-symbol.out         |   0
 tests/qapi-schema/doc-duplicated-arg.err     |   1 +
 tests/qapi-schema/doc-duplicated-arg.exit    |   1 +
 tests/qapi-schema/doc-duplicated-arg.json    |   5 +
 tests/qapi-schema/doc-duplicated-arg.out     |   0
 tests/qapi-schema/doc-duplicated-return.err  |   1 +
 tests/qapi-schema/doc-duplicated-return.exit |   1 +
 tests/qapi-schema/doc-duplicated-return.json |   6 +
 tests/qapi-schema/doc-duplicated-return.out  |   0
 tests/qapi-schema/doc-duplicated-since.err   |   1 +
 tests/qapi-schema/doc-duplicated-since.exit  |   1 +
 tests/qapi-schema/doc-duplicated-since.json  |   6 +
 tests/qapi-schema/doc-duplicated-since.out   |   0
 tests/qapi-schema/doc-empty-arg.err          |   1 +
 tests/qapi-schema/doc-empty-arg.exit         |   1 +
 tests/qapi-schema/doc-empty-arg.json         |   4 +
 tests/qapi-schema/doc-empty-arg.out          |   0
 tests/qapi-schema/doc-empty-section.err      |   1 +
 tests/qapi-schema/doc-empty-section.exit     |   1 +
 tests/qapi-schema/doc-empty-section.json     |   6 +
 tests/qapi-schema/doc-empty-section.out      |   0
 tests/qapi-schema/doc-empty-symbol.err       |   1 +
 tests/qapi-schema/doc-empty-symbol.exit      |   1 +
 tests/qapi-schema/doc-empty-symbol.json      |   3 +
 tests/qapi-schema/doc-empty-symbol.out       |   0
 tests/qapi-schema/doc-invalid-end.err        |   1 +
 tests/qapi-schema/doc-invalid-end.exit       |   1 +
 tests/qapi-schema/doc-invalid-end.json       |   3 +
 tests/qapi-schema/doc-invalid-end.out        |   0
 tests/qapi-schema/doc-invalid-end2.err       |   1 +
 tests/qapi-schema/doc-invalid-end2.exit      |   1 +
 tests/qapi-schema/doc-invalid-end2.json      |   3 +
 tests/qapi-schema/doc-invalid-end2.out       |   0
 tests/qapi-schema/doc-invalid-return.err     |   1 +
 tests/qapi-schema/doc-invalid-return.exit    |   1 +
 tests/qapi-schema/doc-invalid-return.json    |   5 +
 tests/qapi-schema/doc-invalid-return.out     |   0
 tests/qapi-schema/doc-invalid-section.err    |   1 +
 tests/qapi-schema/doc-invalid-section.exit   |   1 +
 tests/qapi-schema/doc-invalid-section.json   |   4 +
 tests/qapi-schema/doc-invalid-section.out    |   0
 tests/qapi-schema/doc-invalid-start.err      |   1 +
 tests/qapi-schema/doc-invalid-start.exit     |   1 +
 tests/qapi-schema/doc-invalid-start.json     |   3 +
 tests/qapi-schema/doc-invalid-start.out      |   0
 tests/qapi-schema/doc-missing-expr.err       |   1 +
 tests/qapi-schema/doc-missing-expr.exit      |   1 +
 tests/qapi-schema/doc-missing-expr.json      |   3 +
 tests/qapi-schema/doc-missing-expr.out       |   0
 tests/qapi-schema/doc-missing-space.err      |   1 +
 tests/qapi-schema/doc-missing-space.exit     |   1 +
 tests/qapi-schema/doc-missing-space.json     |   4 +
 tests/qapi-schema/doc-missing-space.out      |   0
 tests/qapi-schema/qapi-schema-test.json      |  58 +++++
 tests/qapi-schema/qapi-schema-test.out       |  49 ++++
 tests/qapi-schema/test-qapi.py               |  12 +
 67 files changed, 817 insertions(+), 14 deletions(-)
 create mode 100755 scripts/qapi2texi.py
 create mode 100644 tests/qapi-schema/doc-bad-args.err
 create mode 100644 tests/qapi-schema/doc-bad-args.exit
 create mode 100644 tests/qapi-schema/doc-bad-args.json
 create mode 100644 tests/qapi-schema/doc-bad-args.out
 create mode 100644 tests/qapi-schema/doc-bad-symbol.err
 create mode 100644 tests/qapi-schema/doc-bad-symbol.exit
 create mode 100644 tests/qapi-schema/doc-bad-symbol.json
 create mode 100644 tests/qapi-schema/doc-bad-symbol.out
 create mode 100644 tests/qapi-schema/doc-duplicated-arg.err
 create mode 100644 tests/qapi-schema/doc-duplicated-arg.exit
 create mode 100644 tests/qapi-schema/doc-duplicated-arg.json
 create mode 100644 tests/qapi-schema/doc-duplicated-arg.out
 create mode 100644 tests/qapi-schema/doc-duplicated-return.err
 create mode 100644 tests/qapi-schema/doc-duplicated-return.exit
 create mode 100644 tests/qapi-schema/doc-duplicated-return.json
 create mode 100644 tests/qapi-schema/doc-duplicated-return.out
 create mode 100644 tests/qapi-schema/doc-duplicated-since.err
 create mode 100644 tests/qapi-schema/doc-duplicated-since.exit
 create mode 100644 tests/qapi-schema/doc-duplicated-since.json
 create mode 100644 tests/qapi-schema/doc-duplicated-since.out
 create mode 100644 tests/qapi-schema/doc-empty-arg.err
 create mode 100644 tests/qapi-schema/doc-empty-arg.exit
 create mode 100644 tests/qapi-schema/doc-empty-arg.json
 create mode 100644 tests/qapi-schema/doc-empty-arg.out
 create mode 100644 tests/qapi-schema/doc-empty-section.err
 create mode 100644 tests/qapi-schema/doc-empty-section.exit
 create mode 100644 tests/qapi-schema/doc-empty-section.json
 create mode 100644 tests/qapi-schema/doc-empty-section.out
 create mode 100644 tests/qapi-schema/doc-empty-symbol.err
 create mode 100644 tests/qapi-schema/doc-empty-symbol.exit
 create mode 100644 tests/qapi-schema/doc-empty-symbol.json
 create mode 100644 tests/qapi-schema/doc-empty-symbol.out
 create mode 100644 tests/qapi-schema/doc-invalid-end.err
 create mode 100644 tests/qapi-schema/doc-invalid-end.exit
 create mode 100644 tests/qapi-schema/doc-invalid-end.json
 create mode 100644 tests/qapi-schema/doc-invalid-end.out
 create mode 100644 tests/qapi-schema/doc-invalid-end2.err
 create mode 100644 tests/qapi-schema/doc-invalid-end2.exit
 create mode 100644 tests/qapi-schema/doc-invalid-end2.json
 create mode 100644 tests/qapi-schema/doc-invalid-end2.out
 create mode 100644 tests/qapi-schema/doc-invalid-return.err
 create mode 100644 tests/qapi-schema/doc-invalid-return.exit
 create mode 100644 tests/qapi-schema/doc-invalid-return.json
 create mode 100644 tests/qapi-schema/doc-invalid-return.out
 create mode 100644 tests/qapi-schema/doc-invalid-section.err
 create mode 100644 tests/qapi-schema/doc-invalid-section.exit
 create mode 100644 tests/qapi-schema/doc-invalid-section.json
 create mode 100644 tests/qapi-schema/doc-invalid-section.out
 create mode 100644 tests/qapi-schema/doc-invalid-start.err
 create mode 100644 tests/qapi-schema/doc-invalid-start.exit
 create mode 100644 tests/qapi-schema/doc-invalid-start.json
 create mode 100644 tests/qapi-schema/doc-invalid-start.out
 create mode 100644 tests/qapi-schema/doc-missing-expr.err
 create mode 100644 tests/qapi-schema/doc-missing-expr.exit
 create mode 100644 tests/qapi-schema/doc-missing-expr.json
 create mode 100644 tests/qapi-schema/doc-missing-expr.out
 create mode 100644 tests/qapi-schema/doc-missing-space.err
 create mode 100644 tests/qapi-schema/doc-missing-space.exit
 create mode 100644 tests/qapi-schema/doc-missing-space.json
 create mode 100644 tests/qapi-schema/doc-missing-space.out

diff --git a/tests/Makefile.include b/tests/Makefile.include
index e98d3b6..f16764c 100644
--- a/tests/Makefile.include
+++ b/tests/Makefile.include
@@ -350,6 +350,21 @@ qapi-schema += base-cycle-direct.json
 qapi-schema += base-cycle-indirect.json
 qapi-schema += command-int.json
 qapi-schema += comments.json
+qapi-schema += doc-bad-args.json
+qapi-schema += doc-bad-symbol.json
+qapi-schema += doc-duplicated-arg.json
+qapi-schema += doc-duplicated-return.json
+qapi-schema += doc-duplicated-since.json
+qapi-schema += doc-empty-arg.json
+qapi-schema += doc-empty-section.json
+qapi-schema += doc-empty-symbol.json
+qapi-schema += doc-invalid-end.json
+qapi-schema += doc-invalid-end2.json
+qapi-schema += doc-invalid-return.json
+qapi-schema += doc-invalid-section.json
+qapi-schema += doc-invalid-start.json
+qapi-schema += doc-missing-expr.json
+qapi-schema += doc-missing-space.json
 qapi-schema += double-data.json
 qapi-schema += double-type.json
 qapi-schema += duplicate-key.json
@@ -443,6 +458,8 @@ qapi-schema += union-optional-branch.json
 qapi-schema += union-unknown.json
 qapi-schema += unknown-escape.json
 qapi-schema += unknown-expr-key.json
+
+
 check-qapi-schema-y := $(addprefix tests/qapi-schema/, $(qapi-schema))
 
 GENERATED_HEADERS += tests/test-qapi-types.h tests/test-qapi-visit.h \
diff --git a/scripts/qapi.py b/scripts/qapi.py
index 4d1b0e4..1b456b4 100644
--- a/scripts/qapi.py
+++ b/scripts/qapi.py
@@ -122,6 +122,109 @@ class QAPILineError(Exception):
             "%s:%d: %s" % (self.info['file'], self.info['line'], self.msg)
 
 
+class QAPIDoc(object):
+    class Section(object):
+        def __init__(self, name=""):
+            # optional section name (argument/member or section name)
+            self.name = name
+            # the list of strings for this section
+            self.content = []
+
+        def append(self, line):
+            self.content.append(line)
+
+        def __repr__(self):
+            return "\n".join(self.content).strip()
+
+    class ArgSection(Section):
+        pass
+
+    def __init__(self, parser):
+        self.parser = parser
+        self.symbol = None
+        self.body = QAPIDoc.Section()
+        # a dict {'arg': ArgSection, ...}
+        self.args = OrderedDict()
+        # a list of Section
+        self.meta = []
+        # the current section
+        self.section = self.body
+        # associated expression and info (set by expression parser)
+        self.expr = None
+        self.info = None
+
+    def has_meta(self, name):
+        """Returns True if the doc has a meta section 'name'"""
+        for i in self.meta:
+            if i.name == name:
+                return True
+        return False
+
+    def append(self, line):
+        """Adds a # comment line, to be parsed and added to current section"""
+        line = line[1:]
+        if not line:
+            self._append_freeform(line)
+            return
+
+        if line[0] != ' ':
+            raise QAPISchemaError(self.parser, "missing space after #")
+        line = line[1:]
+
+        if self.symbol:
+            self._append_symbol_line(line)
+        elif (not self.body.content and
+              line.startswith("@") and line.endswith(":")):
+            self.symbol = line[1:-1]
+            if not self.symbol:
+                raise QAPISchemaError(self.parser, "Invalid symbol")
+        else:
+            self._append_freeform(line)
+
+    def _append_symbol_line(self, line):
+        name = line.split(' ', 1)[0]
+
+        if name.startswith("@") and name.endswith(":"):
+            line = line[len(name)+1:]
+            self._start_args_section(name[1:-1])
+        elif name in ("Returns:", "Since:",
+                      # those are often singular or plural
+                      "Note:", "Notes:",
+                      "Example:", "Examples:"):
+            line = line[len(name)+1:]
+            self._start_meta_section(name[:-1])
+
+        self._append_freeform(line)
+
+    def _start_args_section(self, name):
+        if not name:
+            raise QAPISchemaError(self.parser, "Invalid argument name")
+        if name in self.args:
+            raise QAPISchemaError(self.parser, "'%s' arg duplicated" % name)
+        self.section = QAPIDoc.ArgSection(name)
+        self.args[name] = self.section
+
+    def _start_meta_section(self, name):
+        if name in ("Returns", "Since") and self.has_meta(name):
+            raise QAPISchemaError(self.parser,
+                                  "Duplicated '%s' section" % name)
+        self.section = QAPIDoc.Section(name)
+        self.meta.append(self.section)
+
+    def _append_freeform(self, line):
+        in_arg = isinstance(self.section, QAPIDoc.ArgSection)
+        if in_arg and self.section.content and not self.section.content[-1] \
+           and line and not line[0].isspace():
+            # an empty line followed by a non-indented
+            # comment ends the argument section
+            self.section = self.body
+            self._append_freeform(line)
+            return
+        if in_arg or not self.section.name.startswith("Example"):
+            line = line.strip()
+        self.section.append(line)
+
+
 class QAPISchemaParser(object):
 
     def __init__(self, fp, previously_included=[], incl_info=None):
@@ -137,11 +240,18 @@ class QAPISchemaParser(object):
         self.line = 1
         self.line_pos = 0
         self.exprs = []
+        self.docs = []
         self.accept()
 
         while self.tok is not None:
             info = {'file': fname, 'line': self.line,
                     'parent': self.incl_info}
+            if self.tok == '#' and self.val.startswith('##'):
+                doc = self.get_doc()
+                doc.info = info
+                self.docs.append(doc)
+                continue
+
             expr = self.get_expr(False)
             if isinstance(expr, dict) and "include" in expr:
                 if len(expr) != 1:
@@ -160,6 +270,7 @@ class QAPISchemaParser(object):
                         raise QAPILineError(info, "Inclusion loop for %s"
                                             % include)
                     inf = inf['parent']
+
                 # skip multiple include of the same file
                 if incl_abs_fname in previously_included:
                     continue
@@ -171,12 +282,38 @@ class QAPISchemaParser(object):
                 exprs_include = QAPISchemaParser(fobj, previously_included,
                                                  info)
                 self.exprs.extend(exprs_include.exprs)
+                self.docs.extend(exprs_include.docs)
             else:
                 expr_elem = {'expr': expr,
                              'info': info}
+                if self.docs and not self.docs[-1].expr:
+                    self.docs[-1].expr = expr
+                    expr_elem['doc'] = self.docs[-1]
+
                 self.exprs.append(expr_elem)
 
-    def accept(self):
+    def get_doc(self):
+        if self.val != '##':
+            raise QAPISchemaError(self, "Junk after '##' at start of "
+                                  "documentation comment")
+
+        doc = QAPIDoc(self)
+        self.accept(False)
+        while self.tok == '#':
+            if self.val.startswith('##'):
+                # End of doc comment
+                if self.val != '##':
+                    raise QAPISchemaError(self, "Junk after '##' at end of "
+                                          "documentation comment")
+                self.accept()
+                return doc
+            else:
+                doc.append(self.val)
+            self.accept(False)
+
+        raise QAPISchemaError(self, "Documentation comment must end with '##'")
+
+    def accept(self, skip_comment=True):
         while True:
             self.tok = self.src[self.cursor]
             self.pos = self.cursor
@@ -184,7 +321,13 @@ class QAPISchemaParser(object):
             self.val = None
 
             if self.tok == '#':
+                if self.src[self.cursor] == '#':
+                    # Start of doc comment
+                    skip_comment = False
                 self.cursor = self.src.find('\n', self.cursor)
+                if not skip_comment:
+                    self.val = self.src[self.pos:self.cursor]
+                    return
             elif self.tok in "{}:,[]":
                 return
             elif self.tok == "'":
@@ -713,7 +856,7 @@ def check_keys(expr_elem, meta, required, optional=[]):
                                 % (key, meta, name))
 
 
-def check_exprs(exprs):
+def check_exprs(exprs, strict_doc):
     global all_names
 
     # Learn the types and check for valid expression keys
@@ -722,6 +865,11 @@ def check_exprs(exprs):
     for expr_elem in exprs:
         expr = expr_elem['expr']
         info = expr_elem['info']
+
+        if strict_doc and 'doc' not in expr_elem:
+            raise QAPILineError(info,
+                                "Expression missing documentation comment")
+
         if 'enum' in expr:
             check_keys(expr_elem, 'enum', ['data'], ['prefix'])
             add_enum(expr['enum'], info, expr['data'])
@@ -780,6 +928,63 @@ def check_exprs(exprs):
     return exprs
 
 
+def check_simple_doc(doc):
+    if doc.symbol:
+        raise QAPILineError(doc.info,
+                            "'%s' documention is not followed by the definition"
+                            % doc.symbol)
+
+    body = str(doc.body)
+    if re.search(r'@\S+:', body, re.MULTILINE):
+        raise QAPILineError(doc.info,
+                            "Document body cannot contain @NAME: sections")
+
+
+def check_expr_doc(doc, expr, info):
+    for i in ('enum', 'union', 'alternate', 'struct', 'command', 'event'):
+        if i in expr:
+            meta = i
+            break
+
+    name = expr[meta]
+    if doc.symbol != name:
+        raise QAPILineError(info, "Definition of '%s' follows documentation"
+                            " for '%s'" % (name, doc.symbol))
+    if doc.has_meta('Returns') and 'command' not in expr:
+        raise QAPILineError(info, "Invalid return documentation")
+
+    doc_args = set(doc.args.keys())
+    if meta == 'union':
+        data = expr.get('base', [])
+    else:
+        data = expr.get('data', [])
+    if isinstance(data, dict):
+        data = data.keys()
+    args = set([name.strip('*') for name in data])
+    if meta == 'alternate' or \
+       (meta == 'union' and not expr.get('discriminator')):
+        args.add('type')
+    if not doc_args.issubset(args):
+        raise QAPILineError(info, "Members documentation is not a subset of"
+                            " API %r > %r" % (list(doc_args), list(args)))
+
+
+def check_docs(docs):
+    for doc in docs:
+        for section in doc.args.values() + doc.meta:
+            content = str(section)
+            if not content or content.isspace():
+                raise QAPILineError(doc.info,
+                                    "Empty doc section '%s'" % section.name)
+
+        if not doc.expr:
+            check_simple_doc(doc)
+        else:
+            check_expr_doc(doc, doc.expr, doc.info)
+
+    return docs
+
+
 #
 # Schema compiler frontend
 #
@@ -1249,9 +1454,11 @@ class QAPISchemaEvent(QAPISchemaEntity):
 
 
 class QAPISchema(object):
-    def __init__(self, fname):
+    def __init__(self, fname, strict_doc=False):
         try:
-            self.exprs = check_exprs(QAPISchemaParser(open(fname, "r")).exprs)
+            parser = QAPISchemaParser(open(fname, "r"))
+            self.exprs = check_exprs(parser.exprs, strict_doc)
+            self.docs = check_docs(parser.docs)
             self._entity_dict = {}
             self._predefining = True
             self._def_predefineds()
diff --git a/scripts/qapi2texi.py b/scripts/qapi2texi.py
new file mode 100755
index 0000000..0cec43a
--- /dev/null
+++ b/scripts/qapi2texi.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python
+# QAPI texi generator
+#
+# This work is licensed under the terms of the GNU LGPL, version 2+.
+# See the COPYING file in the top-level directory.
+"""This script produces the documentation of a qapi schema in texinfo format"""
+import re
+import sys
+
+import qapi
+
+COMMAND_FMT = """
+@deftypefn {type} {{{ret}}} {name} @
+{{{args}}}
+
+{body}
+
+@end deftypefn
+
+""".format
+
+ENUM_FMT = """
+@deftp Enum {name}
+
+{body}
+
+@end deftp
+
+""".format
+
+STRUCT_FMT = """
+@deftp {{{type}}} {name} @
+{{{attrs}}}
+
+{body}
+
+@end deftp
+
+""".format
+
+EXAMPLE_FMT = """@example
+{code}
+@end example
+""".format
+
+
+def subst_strong(doc):
+    """Replaces *foo* by @strong{foo}"""
+    return re.sub(r'\*([^_\n]+)\*', r'@emph{\1}', doc)
+
+
+def subst_emph(doc):
+    """Replaces _foo_ by @emph{foo}"""
+    return re.sub(r'\s_([^_\n]+)_\s', r' @emph{\1} ', doc)
+
+
+def subst_vars(doc):
+    """Replaces @var by @code{var}"""
+    return re.sub(r'@([\w-]+)', r'@code{\1}', doc)
+
+
+def subst_braces(doc):
+    """Replaces {} with @{ @}"""
+    return doc.replace("{", "@{").replace("}", "@}")
+
+
+def texi_example(doc):
+    """Format @example"""
+    doc = subst_braces(doc).strip('\n')
+    return EXAMPLE_FMT(code=doc)
+
+
+def texi_comment(doc):
+    """
+    Format a comment
+
+    Lines starting with:
+    - |: generates an @example
+    - =: generates @section
+    - ==: generates @subsection
+    - 1. or 1): generates an @enumerate @item
+    - o/*/-: generates an @itemize list
+    """
+    lines = []
+    doc = subst_braces(doc)
+    doc = subst_vars(doc)
+    doc = subst_emph(doc)
+    doc = subst_strong(doc)
+    inlist = ""
+    lastempty = False
+    for line in doc.split('\n'):
+        empty = line == ""
+
+        if line.startswith("| "):
+            line = EXAMPLE_FMT(code=line[2:])
+        elif line.startswith("= "):
+            line = "@section " + line[2:]
+        elif line.startswith("== "):
+            line = "@subsection " + line[3:]
+        elif re.match("^([0-9]*[.)]) ", line):
+            if not inlist:
+                lines.append("@enumerate")
+                inlist = "enumerate"
+            line = line[line.find(" ")+1:]
+            lines.append("@item")
+        elif re.match("^[o*-] ", line):
+            if not inlist:
+                lines.append("@itemize %s" % {'o': "@bullet",
+                                              '*': "@minus",
+                                              '-': ""}[line[0]])
+                inlist = "itemize"
+            lines.append("@item")
+            line = line[2:]
+        elif lastempty and inlist:
+            lines.append("@end %s\n" % inlist)
+            inlist = ""
+
+        lastempty = empty
+        lines.append(line)
+
+    if inlist:
+        lines.append("@end %s\n" % inlist)
+    return "\n".join(lines)
+
+
+def texi_args(expr, key="data"):
+    """
+    Format the functions/structure/events.. arguments/members
+    """
+    if key not in expr:
+        return ""
+
+    args = expr[key]
+    arg_list = []
+    if isinstance(args, str):
+        arg_list.append(args)
+    else:
+        for name, typ in args.iteritems():
+            # optional arg
+            if name.startswith("*"):
+                name = name[1:]
+                arg_list.append("['%s': @var{%s}]" % (name, typ))
+            # regular arg
+            else:
+                arg_list.append("'%s': @var{%s}" % (name, typ))
+
+    return ", ".join(arg_list)
+
+
+def texi_body(doc):
+    """
+    Format the body of a symbol documentation:
+    - a table of arguments
+    - followed by "Returns/Notes/Since/Example" sections
+    """
+    def _section_order(section):
+        return {"Returns": 0,
+                "Note": 1,
+                "Notes": 1,
+                "Since": 2,
+                "Example": 3,
+                "Examples": 3}[section]
+
+    body = "@table @asis\n"
+    for arg, section in doc.args.iteritems():
+        desc = str(section)
+        opt = ''
+        if desc.startswith("#optional"):
+            desc = desc[10:]
+            opt = ' *'
+        elif desc.endswith("#optional"):
+            desc = desc[:-10]
+            opt = ' *'
+        body += "@item @code{'%s'}%s\n%s\n" % (arg, opt, texi_comment(desc))
+    body += "@end table\n"
+    body += texi_comment(str(doc.body))
+
+    meta = sorted(doc.meta, key=lambda i: _section_order(i.name))
+    for section in meta:
+        name, doc = (section.name, str(section))
+        func = texi_comment
+        if name.startswith("Example"):
+            func = texi_example
+
+        body += "\n@quotation %s\n%s\n@end quotation" % \
+                (name, func(doc))
+    return body
+
+
+def texi_alternate(expr, doc):
+    """
+    Format an alternate to texi
+    """
+    args = texi_args(expr)
+    body = texi_body(doc)
+    return STRUCT_FMT(type="Alternate",
+                      name=doc.symbol,
+                      attrs="[ " + args + " ]",
+                      body=body)
+
+
+def texi_union(expr, doc):
+    """
+    Format an union to texi
+    """
+    attrs = "@{ " + texi_args(expr, "base") + " @}"
+    discriminator = expr.get("discriminator")
+    if not discriminator:
+        union = "Flat Union"
+        discriminator = "type"
+    else:
+        union = "Union"
+    attrs += " + '%s' = [ " % discriminator
+    attrs += texi_args(expr, "data") + " ]"
+    body = texi_body(doc)
+
+    return STRUCT_FMT(type=union,
+                      name=doc.symbol,
+                      attrs=attrs,
+                      body=body)
+
+
+def texi_enum(expr, doc):
+    """
+    Format an enum to texi
+    """
+    for i in expr['data']:
+        if i not in doc.args:
+            doc.args[i] = ''
+    body = texi_body(doc)
+    return ENUM_FMT(name=doc.symbol,
+                    body=body)
+
+
+def texi_struct(expr, doc):
+    """
+    Format a struct to texi
+    """
+    args = texi_args(expr)
+    body = texi_body(doc)
+    attrs = "@{ " + args + " @}"
+    base = expr.get("base")
+    if base:
+        attrs += " + %s" % base
+    return STRUCT_FMT(type="Struct",
+                      name=doc.symbol,
+                      attrs=attrs,
+                      body=body)
+
+
+def texi_command(expr, doc):
+    """
+    Format a command to texi
+    """
+    args = texi_args(expr)
+    ret = expr["returns"] if "returns" in expr else ""
+    body = texi_body(doc)
+    return COMMAND_FMT(type="Command",
+                       name=doc.symbol,
+                       ret=ret,
+                       args="(" + args + ")",
+                       body=body)
+
+
+def texi_event(expr, doc):
+    """
+    Format an event to texi
+    """
+    args = texi_args(expr)
+    body = texi_body(doc)
+    return COMMAND_FMT(type="Event",
+                       name=doc.symbol,
+                       ret="",
+                       args="(" + args + ")",
+                       body=body)
+
+
+def texi_expr(expr, doc):
+    """
+    Format an expr to texi
+    """
+    (kind, _) = expr.items()[0]
+
+    fmt = {"command": texi_command,
+           "struct": texi_struct,
+           "enum": texi_enum,
+           "union": texi_union,
+           "alternate": texi_alternate,
+           "event": texi_event}
+    try:
+        fmt = fmt[kind]
+    except KeyError:
+        raise ValueError("Unknown expression kind '%s'" % kind)
+
+    return fmt(expr, doc)
+
+
+def texi(docs):
+    """
+    Convert QAPI schema expressions to texi documentation
+    """
+    res = []
+    for doc in docs:
+        expr = doc.expr
+        if not expr:
+            res.append(texi_body(doc))
+            continue
+        try:
+            doc = texi_expr(expr, doc)
+            res.append(doc)
+        except:
+            print >>sys.stderr, "error at @%s" % doc.info
+            raise
+
+    return '\n'.join(res)
+
+
+def main(argv):
+    """
+    Takes schema argument, prints result to stdout
+    """
+    if len(argv) != 2:
+        print >>sys.stderr, "%s: need exactly 1 argument: SCHEMA" % argv[0]
+        sys.exit(1)
+
+    schema = qapi.QAPISchema(argv[1], strict_doc=True)
+    print texi(schema.docs)
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/docs/qapi-code-gen.txt b/docs/qapi-code-gen.txt
index 2841c51..8bc963e 100644
--- a/docs/qapi-code-gen.txt
+++ b/docs/qapi-code-gen.txt
@@ -45,16 +45,13 @@ QAPI parser does not).  At present, there is no place where a QAPI
 schema requires the use of JSON numbers or null.
 
 Comments are allowed; anything between an unquoted # and the following
-newline is ignored.  Although there is not yet a documentation
-generator, a form of stylized comments has developed for consistently
-documenting details about an expression and when it was added to the
-schema.  The documentation is delimited between two lines of ##, then
-the first line names the expression, an optional overview is provided,
-then individual documentation about each member of 'data' is provided,
-and finally, a 'Since: x.y.z' tag lists the release that introduced
-the expression.  Optional members are tagged with the phrase
-'#optional', often with their default value; and extensions added
-after the expression was first released are also given a '(since
+newline is ignored.  The documentation is delimited between two lines
+of ##, then the first line names the expression, an optional overview
+is provided, then individual documentation about each member of 'data'
+is provided, and finally, a 'Since: x.y.z' tag lists the release that
+introduced the expression.  Optional members are tagged with the
+phrase '#optional', often with their default value; and extensions
+added after the expression was first released are also given a '(since
 x.y.z)' comment.  For example:
 
     ##
@@ -73,12 +70,49 @@ x.y.z)' comment.  For example:
     #           (Since 2.0)
     #
     # Since: 0.14.0
+    #
+    # Notes: You can also make a list:
+    #        - with items
+    #        - like this
+    #
+    # Example:
+    #
+    # -> { "execute": ... }
+    # <- { "return": ... }
+    #
     ##
     { 'struct': 'BlockStats',
       'data': {'*device': 'str', 'stats': 'BlockDeviceStats',
                '*parent': 'BlockStats',
                '*backing': 'BlockStats'} }
 
+It's also possible to create documentation sections, such as:
+
+    ##
+    # = Section
+    # == Subsection
+    #
+    # Some text foo with *strong* and _emphasis_
+    # 1. with a list
+    # 2. like that
+    #
+    # And some code:
+    # | $ echo foo
+    # | -> do this
+    # | <- get that
+    #
+    ##
+
+Text *foo* and _foo_ are for "strong" and "emphasis" styles (they do
+not work over multiple lines). @foo is used to reference a symbol.
+
+Lines starting with the following characters and a space:
+- | are examples
+- = are top section
+- == are subsection
+- X. or X) are enumerations (X is any number)
+- o/*/- are itemized list
+
 The schema sets up a series of types, as well as commands and events
 that will use those types.  Forward references are allowed: the parser
 scans in two passes, where the first pass learns all type names, and
diff --git a/tests/qapi-schema/doc-bad-args.err b/tests/qapi-schema/doc-bad-args.err
new file mode 100644
index 0000000..a55e003
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-args.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-bad-args.json:1: Members documentation is not a subset of API ['a', 'b'] > ['a']
diff --git a/tests/qapi-schema/doc-bad-args.exit b/tests/qapi-schema/doc-bad-args.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-args.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-bad-args.json b/tests/qapi-schema/doc-bad-args.json
new file mode 100644
index 0000000..26992ea
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-args.json
@@ -0,0 +1,6 @@
+##
+# @foo:
+# @a: a
+# @b: b
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }
diff --git a/tests/qapi-schema/doc-bad-args.out b/tests/qapi-schema/doc-bad-args.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-bad-symbol.err b/tests/qapi-schema/doc-bad-symbol.err
new file mode 100644
index 0000000..9c969d1
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-symbol.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-bad-symbol.json:1: Definition of 'foo' follows documentation for 'food'
diff --git a/tests/qapi-schema/doc-bad-symbol.exit b/tests/qapi-schema/doc-bad-symbol.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-symbol.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-bad-symbol.json b/tests/qapi-schema/doc-bad-symbol.json
new file mode 100644
index 0000000..7255152
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-symbol.json
@@ -0,0 +1,4 @@
+##
+# @food:
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }
diff --git a/tests/qapi-schema/doc-bad-symbol.out b/tests/qapi-schema/doc-bad-symbol.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-duplicated-arg.err b/tests/qapi-schema/doc-duplicated-arg.err
new file mode 100644
index 0000000..88a272b
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-arg.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-arg.json:4:1: 'a' arg duplicated
diff --git a/tests/qapi-schema/doc-duplicated-arg.exit b/tests/qapi-schema/doc-duplicated-arg.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-arg.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-duplicated-arg.json b/tests/qapi-schema/doc-duplicated-arg.json
new file mode 100644
index 0000000..f7804c2
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-arg.json
@@ -0,0 +1,5 @@
+##
+# @foo:
+# @a:
+# @a:
+##
diff --git a/tests/qapi-schema/doc-duplicated-arg.out b/tests/qapi-schema/doc-duplicated-arg.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-duplicated-return.err b/tests/qapi-schema/doc-duplicated-return.err
new file mode 100644
index 0000000..1b02880
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-return.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-return.json:5:1: Duplicated 'Returns' section
diff --git a/tests/qapi-schema/doc-duplicated-return.exit b/tests/qapi-schema/doc-duplicated-return.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-return.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-duplicated-return.json b/tests/qapi-schema/doc-duplicated-return.json
new file mode 100644
index 0000000..de7234b
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-return.json
@@ -0,0 +1,6 @@
+##
+# @foo:
+#
+# Returns: 0
+# Returns: 1
+##
diff --git a/tests/qapi-schema/doc-duplicated-return.out b/tests/qapi-schema/doc-duplicated-return.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-duplicated-since.err b/tests/qapi-schema/doc-duplicated-since.err
new file mode 100644
index 0000000..72efb2a
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-since.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-since.json:5:1: Duplicated 'Since' section
diff --git a/tests/qapi-schema/doc-duplicated-since.exit b/tests/qapi-schema/doc-duplicated-since.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-since.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-duplicated-since.json b/tests/qapi-schema/doc-duplicated-since.json
new file mode 100644
index 0000000..da261a1
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-since.json
@@ -0,0 +1,6 @@
+##
+# @foo:
+#
+# Since: 0
+# Since: 1
+##
diff --git a/tests/qapi-schema/doc-duplicated-since.out b/tests/qapi-schema/doc-duplicated-since.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-empty-arg.err b/tests/qapi-schema/doc-empty-arg.err
new file mode 100644
index 0000000..0647eed
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-arg.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-empty-arg.json:3:1: Invalid argument name
diff --git a/tests/qapi-schema/doc-empty-arg.exit b/tests/qapi-schema/doc-empty-arg.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-arg.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-empty-arg.json b/tests/qapi-schema/doc-empty-arg.json
new file mode 100644
index 0000000..74f526c
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-arg.json
@@ -0,0 +1,4 @@
+##
+# @foo:
+# @:
+##
diff --git a/tests/qapi-schema/doc-empty-arg.out b/tests/qapi-schema/doc-empty-arg.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-empty-section.err b/tests/qapi-schema/doc-empty-section.err
new file mode 100644
index 0000000..c940332
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-section.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-empty-section.json:1: Empty doc section 'Note'
diff --git a/tests/qapi-schema/doc-empty-section.exit b/tests/qapi-schema/doc-empty-section.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-section.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-empty-section.json b/tests/qapi-schema/doc-empty-section.json
new file mode 100644
index 0000000..ed3867d
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-section.json
@@ -0,0 +1,6 @@
+##
+# @foo:
+#
+# Note:
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }
diff --git a/tests/qapi-schema/doc-empty-section.out b/tests/qapi-schema/doc-empty-section.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-empty-symbol.err b/tests/qapi-schema/doc-empty-symbol.err
new file mode 100644
index 0000000..955dc3c
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-symbol.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-empty-symbol.json:2:1: Invalid symbol
diff --git a/tests/qapi-schema/doc-empty-symbol.exit b/tests/qapi-schema/doc-empty-symbol.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-symbol.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-empty-symbol.json b/tests/qapi-schema/doc-empty-symbol.json
new file mode 100644
index 0000000..8a2d662
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-symbol.json
@@ -0,0 +1,3 @@
+##
+# @:
+##
diff --git a/tests/qapi-schema/doc-empty-symbol.out b/tests/qapi-schema/doc-empty-symbol.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-end.err b/tests/qapi-schema/doc-invalid-end.err
new file mode 100644
index 0000000..5ed8911
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-end.json:3:2: Documentation comment must end with '##'
diff --git a/tests/qapi-schema/doc-invalid-end.exit b/tests/qapi-schema/doc-invalid-end.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-end.json b/tests/qapi-schema/doc-invalid-end.json
new file mode 100644
index 0000000..7452718
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end.json
@@ -0,0 +1,3 @@
+##
+# An invalid comment
+#
diff --git a/tests/qapi-schema/doc-invalid-end.out b/tests/qapi-schema/doc-invalid-end.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-end2.err b/tests/qapi-schema/doc-invalid-end2.err
new file mode 100644
index 0000000..acd23bb
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end2.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-end2.json:3:1: Junk after '##' at end of documentation comment
diff --git a/tests/qapi-schema/doc-invalid-end2.exit b/tests/qapi-schema/doc-invalid-end2.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end2.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-end2.json b/tests/qapi-schema/doc-invalid-end2.json
new file mode 100644
index 0000000..12e669e
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end2.json
@@ -0,0 +1,3 @@
+##
+#
+## invalid
diff --git a/tests/qapi-schema/doc-invalid-end2.out b/tests/qapi-schema/doc-invalid-end2.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-return.err b/tests/qapi-schema/doc-invalid-return.err
new file mode 100644
index 0000000..c3f2691
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-return.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-return.json:1: Invalid return documentation
diff --git a/tests/qapi-schema/doc-invalid-return.exit b/tests/qapi-schema/doc-invalid-return.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-return.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-return.json b/tests/qapi-schema/doc-invalid-return.json
new file mode 100644
index 0000000..6568b6d
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-return.json
@@ -0,0 +1,5 @@
+##
+# @foo:
+# Returns: blah
+##
+{ 'event': 'foo' }
diff --git a/tests/qapi-schema/doc-invalid-return.out b/tests/qapi-schema/doc-invalid-return.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-section.err b/tests/qapi-schema/doc-invalid-section.err
new file mode 100644
index 0000000..f4c12aa
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-section.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-section.json:1: Document body cannot contain @NAME: sections
diff --git a/tests/qapi-schema/doc-invalid-section.exit b/tests/qapi-schema/doc-invalid-section.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-section.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-section.json b/tests/qapi-schema/doc-invalid-section.json
new file mode 100644
index 0000000..9afa2f1
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-section.json
@@ -0,0 +1,4 @@
+##
+# freeform
+# @note: foo
+##
diff --git a/tests/qapi-schema/doc-invalid-section.out b/tests/qapi-schema/doc-invalid-section.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-start.err b/tests/qapi-schema/doc-invalid-start.err
new file mode 100644
index 0000000..194c8d7
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-start.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-start.json:1:1: Junk after '##' at start of documentation comment
diff --git a/tests/qapi-schema/doc-invalid-start.exit b/tests/qapi-schema/doc-invalid-start.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-start.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-start.json b/tests/qapi-schema/doc-invalid-start.json
new file mode 100644
index 0000000..f85adfd
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-start.json
@@ -0,0 +1,3 @@
+## invalid
+#
+##
diff --git a/tests/qapi-schema/doc-invalid-start.out b/tests/qapi-schema/doc-invalid-start.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-missing-expr.err b/tests/qapi-schema/doc-missing-expr.err
new file mode 100644
index 0000000..e4ed135
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-expr.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-missing-expr.json:1: 'bar' documention is not followed by the definition
diff --git a/tests/qapi-schema/doc-missing-expr.exit b/tests/qapi-schema/doc-missing-expr.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-expr.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-missing-expr.json b/tests/qapi-schema/doc-missing-expr.json
new file mode 100644
index 0000000..a0be2e1
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-expr.json
@@ -0,0 +1,3 @@
+##
+# @bar:
+##
diff --git a/tests/qapi-schema/doc-missing-expr.out b/tests/qapi-schema/doc-missing-expr.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-missing-space.err b/tests/qapi-schema/doc-missing-space.err
new file mode 100644
index 0000000..37056ce
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-space.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-missing-space.json:3:1: missing space after #
diff --git a/tests/qapi-schema/doc-missing-space.exit b/tests/qapi-schema/doc-missing-space.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-space.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-missing-space.json b/tests/qapi-schema/doc-missing-space.json
new file mode 100644
index 0000000..39303de
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-space.json
@@ -0,0 +1,4 @@
+##
+# missing space:
+#wef
+##
diff --git a/tests/qapi-schema/doc-missing-space.out b/tests/qapi-schema/doc-missing-space.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/qapi-schema-test.json b/tests/qapi-schema/qapi-schema-test.json
index 1719463..d921ec4 100644
--- a/tests/qapi-schema/qapi-schema-test.json
+++ b/tests/qapi-schema/qapi-schema-test.json
@@ -3,6 +3,43 @@
 # This file is a stress test of supported qapi constructs that must
 # parse and compile correctly.
 
+##
+# = Section
+# == subsection
+#
+# Some text foo with *strong* and _emphasis_
+# 1. with a list
+# 2. like that @foo
+#
+# And some code:
+# | $ echo foo
+# | -> do this
+# | <- get that
+#
+# Note: is not a meta
+##
+
+##
+# @TestStruct:
+# @integer: foo
+#           blah
+#
+#           bao
+#
+# @boolean: bar
+# @string: baz
+#
+# body with @var
+#
+# Example:
+#
+# -> { "execute": ... }
+# <- { "return": ... }
+#
+# Since: 2.3
+# Note: a note
+#
+##
 { 'struct': 'TestStruct',
   'data': { 'integer': 'int', 'boolean': 'bool', 'string': 'str' } }
 
@@ -123,6 +160,27 @@
   'data': {'ud1a': 'UserDefOne', '*ud1b': 'UserDefOne'},
   'returns': 'UserDefTwo' }
 
+##
+# Another comment
+##
+
+##
+# @guest-get-time:
+#
+# @a: an integer
+# @b: #optional integer
+#
+# @guest-get-time body
+#
+# Returns: returns something
+#
+# Example:
+#
+# -> { "execute": "guest-get-time", ... }
+# <- { "return": "42" }
+#
+##
+
 # Returning a non-dictionary requires a name from the whitelist
 { 'command': 'guest-get-time', 'data': {'a': 'int', '*b': 'int' },
   'returns': 'int' }
diff --git a/tests/qapi-schema/qapi-schema-test.out b/tests/qapi-schema/qapi-schema-test.out
index 9d99c4e..fde9e06 100644
--- a/tests/qapi-schema/qapi-schema-test.out
+++ b/tests/qapi-schema/qapi-schema-test.out
@@ -232,3 +232,52 @@ command user_def_cmd1 q_obj_user_def_cmd1-arg -> None
    gen=True success_response=True boxed=False
 command user_def_cmd2 q_obj_user_def_cmd2-arg -> UserDefTwo
    gen=True success_response=True boxed=False
+doc freeform
+    body=
+= Section
+== subsection
+
+Some text foo with *strong* and _emphasis_
+1. with a list
+2. like that @foo
+
+And some code:
+| $ echo foo
+| -> do this
+| <- get that
+
+Note: is not a meta
+doc symbol=TestStruct expr=('struct', 'TestStruct')
+    arg=integer
+foo
+blah
+
+bao
+    arg=boolean
+bar
+    arg=string
+baz
+    meta=Example
+-> { "execute": ... }
+<- { "return": ... }
+    meta=Since
+2.3
+    meta=Note
+a note
+    body=
+body with @var
+doc freeform
+    body=
+Another comment
+doc symbol=guest-get-time expr=('command', 'guest-get-time')
+    arg=a
+an integer
+    arg=b
+#optional integer
+    meta=Returns
+returns something
+    meta=Example
+-> { "execute": "guest-get-time", ... }
+<- { "return": "42" }
+    body=
+@guest-get-time body
diff --git a/tests/qapi-schema/test-qapi.py b/tests/qapi-schema/test-qapi.py
index ef74e2c..22da014 100644
--- a/tests/qapi-schema/test-qapi.py
+++ b/tests/qapi-schema/test-qapi.py
@@ -55,3 +55,15 @@ class QAPISchemaTestVisitor(QAPISchemaVisitor):
 
 schema = QAPISchema(sys.argv[1])
 schema.visit(QAPISchemaTestVisitor())
+
+for doc in schema.docs:
+    if doc.symbol:
+        print 'doc symbol=%s expr=%s' % \
+            (doc.symbol, doc.expr.items()[0])
+    else:
+        print 'doc freeform'
+    for arg, section in doc.args.iteritems():
+        print '    arg=%s\n%s' % (arg, section)
+    for section in doc.meta:
+        print '    meta=%s\n%s' % (section.name, section)
+    print '    body=\n%s' % doc.body
-- 
2.10.0

  parent reply	other threads:[~2016-11-17 15:56 UTC|newest]

Thread overview: 48+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2016-11-17 15:54 [Qemu-devel] [PATCH v5 00/17] qapi doc generation (whole version, squashed) Marc-André Lureau
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 01/17] qapi: improve device_add schema Marc-André Lureau
2016-11-17 17:38   ` Markus Armbruster
2016-11-17 19:49   ` Eric Blake
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 02/17] qga/schema: fix double-return in doc Marc-André Lureau
2016-11-17 17:38   ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 03/17] qga/schema: improve guest-set-vcpus Returns: section Marc-André Lureau
2016-11-17 17:39   ` Markus Armbruster
2016-11-18  8:49     ` Marc-André Lureau
2016-11-18 14:07       ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 04/17] qapi: fix schema symbol sections Marc-André Lureau
2016-11-17 17:39   ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 05/17] qapi: fix missing symbol @prefix Marc-André Lureau
2016-11-17 17:40   ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 06/17] qapi: fix various symbols mismatch in documentation Marc-André Lureau
2016-11-17 17:40   ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 07/17] qapi: use one symbol per line Marc-André Lureau
2016-11-17 17:40   ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 08/17] qapi: add missing colon-ending for section name Marc-André Lureau
2016-11-17 17:41   ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 09/17] qapi: add some sections in docs Marc-André Lureau
2016-11-17 17:43   ` Markus Armbruster
2016-11-30 15:38   ` Markus Armbruster
2016-11-30 16:07     ` Marc-André Lureau
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 10/17] qapi: improve TransactionAction doc Marc-André Lureau
2016-11-17 18:03   ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 11/17] docs: add master qapi texi files Marc-André Lureau
2016-11-18  9:09   ` Markus Armbruster
2016-11-17 15:54 ` [Qemu-devel] [PATCH v5 12/17] qapi: rename QAPIExprError/QAPILineError Marc-André Lureau
2016-11-18 10:17   ` Markus Armbruster
2016-11-18 10:31     ` Marc-André Lureau
2016-11-18 14:13       ` Markus Armbruster
2016-11-17 15:55 ` Marc-André Lureau [this message]
2016-11-30 16:06   ` [Qemu-devel] [PATCH v5 13/17] qapi: add qapi2texi script Markus Armbruster
2016-12-05 17:35     ` Marc-André Lureau
2016-12-06 11:50       ` Markus Armbruster
2016-12-06 13:07         ` Marc-André Lureau
2016-12-07 16:05           ` Markus Armbruster
2016-11-17 15:55 ` [Qemu-devel] [PATCH v5 14/17] texi2pod: learn quotation, deftp and deftypefn Marc-André Lureau
2016-11-17 15:55 ` [Qemu-devel] [PATCH v5 15/17] (SQUASHED) move doc to schema Marc-André Lureau
2016-11-17 15:55 ` [Qemu-devel] [PATCH v5 16/17] docs: add qemu logo Marc-André Lureau
2016-11-18 12:52   ` Markus Armbruster
2016-11-21 10:50     ` Marc-André Lureau
2016-11-21 12:07       ` Markus Armbruster
2016-11-17 15:55 ` [Qemu-devel] [PATCH v5 17/17] build-sys: add qapi doc generation targets Marc-André Lureau
2016-11-18 12:31   ` Markus Armbruster
2016-11-21 12:30     ` Marc-André Lureau
2016-12-05 16:53 ` [Qemu-devel] [PATCH v5 00/17] qapi doc generation (whole version, squashed) Markus Armbruster

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20161117155504.21843-14-marcandre.lureau@redhat.com \
    --to=marcandre.lureau@redhat.com \
    --cc=armbru@redhat.com \
    --cc=eblake@redhat.com \
    --cc=qemu-devel@nongnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.