瀏覽代碼

qapi: add qapi2texi script

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 description:

  ##
  # @symbol:
  #
  # Symbol body ditto ergo sum. Foo bar
  # baz ding.
  #
  # @param1: the frob to frobnicate
  # @param2: #optional how hard to frobnicate
  #
  # Returns: the frobnicated frob.
  #          If frob isn't frobnicatable, GenericError.
  #
  # 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 | tag_section | freeform_comment }
member = "# @" name ':' [ text ] "\n" freeform_comment
tag_section = "# " ( "Returns:", "Since:", "Note:", "Notes:", "Example:", "Examples:" ) [ text ]  "\n" freeform_comment
text = free text with markup

Note that the grammar is ambiguous: a line "# @foo:\n" can be parsed
both as freeform_comment and as symbol_comment.  The actual parser
recognizes symbol_comment.

See docs/qapi-code-gen.txt for more details.

Deficiencies and limitations:
- the generated QMP documentation includes internal types
- union type support is lacking
- type information is lacking in generated documentation
- doc comment error message positions are imprecise, they point
  to the beginning of the comment.
- a few minor issues, all marked TODO/FIXME in the code

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Message-Id: <20170113144135.5150-16-marcandre.lureau@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[test-qapi.py tweaked to avoid trailing empty lines in .out]
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Marc-André Lureau 8 年之前
父節點
當前提交
3313b6124b
共有 100 個文件被更改,包括 956 次插入68 次删除
  1. 147 33
      docs/qapi-code-gen.txt
  2. 249 2
      scripts/qapi.py
  3. 271 0
      scripts/qapi2texi.py
  4. 20 0
      tests/Makefile.include
  5. 1 1
      tests/qapi-schema/alternate-any.err
  6. 4 0
      tests/qapi-schema/alternate-any.json
  7. 1 1
      tests/qapi-schema/alternate-array.err
  8. 7 0
      tests/qapi-schema/alternate-array.json
  9. 1 1
      tests/qapi-schema/alternate-base.err
  10. 7 0
      tests/qapi-schema/alternate-base.json
  11. 1 1
      tests/qapi-schema/alternate-clash.err
  12. 4 0
      tests/qapi-schema/alternate-clash.json
  13. 1 1
      tests/qapi-schema/alternate-conflict-dict.err
  14. 10 0
      tests/qapi-schema/alternate-conflict-dict.json
  15. 1 1
      tests/qapi-schema/alternate-conflict-string.err
  16. 7 0
      tests/qapi-schema/alternate-conflict-string.json
  17. 1 1
      tests/qapi-schema/alternate-empty.err
  18. 4 0
      tests/qapi-schema/alternate-empty.json
  19. 1 1
      tests/qapi-schema/alternate-nested.err
  20. 7 0
      tests/qapi-schema/alternate-nested.json
  21. 1 1
      tests/qapi-schema/alternate-unknown.err
  22. 4 0
      tests/qapi-schema/alternate-unknown.json
  23. 1 1
      tests/qapi-schema/args-alternate.err
  24. 8 0
      tests/qapi-schema/args-alternate.json
  25. 1 1
      tests/qapi-schema/args-any.err
  26. 4 0
      tests/qapi-schema/args-any.json
  27. 1 1
      tests/qapi-schema/args-array-empty.err
  28. 4 0
      tests/qapi-schema/args-array-empty.json
  29. 1 1
      tests/qapi-schema/args-array-unknown.err
  30. 4 0
      tests/qapi-schema/args-array-unknown.json
  31. 1 1
      tests/qapi-schema/args-bad-boxed.err
  32. 4 0
      tests/qapi-schema/args-bad-boxed.json
  33. 1 1
      tests/qapi-schema/args-boxed-anon.err
  34. 4 0
      tests/qapi-schema/args-boxed-anon.json
  35. 1 1
      tests/qapi-schema/args-boxed-empty.err
  36. 8 0
      tests/qapi-schema/args-boxed-empty.json
  37. 1 1
      tests/qapi-schema/args-boxed-string.err
  38. 4 0
      tests/qapi-schema/args-boxed-string.json
  39. 1 1
      tests/qapi-schema/args-int.err
  40. 4 0
      tests/qapi-schema/args-int.json
  41. 1 1
      tests/qapi-schema/args-invalid.err
  42. 3 0
      tests/qapi-schema/args-invalid.json
  43. 1 1
      tests/qapi-schema/args-member-array-bad.err
  44. 4 0
      tests/qapi-schema/args-member-array-bad.json
  45. 1 1
      tests/qapi-schema/args-member-case.err
  46. 4 0
      tests/qapi-schema/args-member-case.json
  47. 1 1
      tests/qapi-schema/args-member-unknown.err
  48. 4 0
      tests/qapi-schema/args-member-unknown.json
  49. 1 1
      tests/qapi-schema/args-name-clash.err
  50. 4 0
      tests/qapi-schema/args-name-clash.json
  51. 1 1
      tests/qapi-schema/args-union.err
  52. 7 0
      tests/qapi-schema/args-union.json
  53. 1 1
      tests/qapi-schema/args-unknown.err
  54. 4 0
      tests/qapi-schema/args-unknown.json
  55. 1 1
      tests/qapi-schema/bad-base.err
  56. 7 0
      tests/qapi-schema/bad-base.json
  57. 1 1
      tests/qapi-schema/bad-data.err
  58. 4 0
      tests/qapi-schema/bad-data.json
  59. 1 1
      tests/qapi-schema/bad-ident.err
  60. 4 0
      tests/qapi-schema/bad-ident.json
  61. 1 1
      tests/qapi-schema/bad-type-bool.err
  62. 4 0
      tests/qapi-schema/bad-type-bool.json
  63. 1 1
      tests/qapi-schema/bad-type-dict.err
  64. 4 0
      tests/qapi-schema/bad-type-dict.json
  65. 1 1
      tests/qapi-schema/base-cycle-direct.err
  66. 4 0
      tests/qapi-schema/base-cycle-direct.json
  67. 1 1
      tests/qapi-schema/base-cycle-indirect.err
  68. 7 0
      tests/qapi-schema/base-cycle-indirect.json
  69. 1 1
      tests/qapi-schema/command-int.err
  70. 4 0
      tests/qapi-schema/command-int.json
  71. 4 0
      tests/qapi-schema/comments.json
  72. 1 0
      tests/qapi-schema/comments.out
  73. 1 0
      tests/qapi-schema/doc-bad-args.err
  74. 1 0
      tests/qapi-schema/doc-bad-args.exit
  75. 8 0
      tests/qapi-schema/doc-bad-args.json
  76. 0 0
      tests/qapi-schema/doc-bad-args.out
  77. 1 0
      tests/qapi-schema/doc-bad-symbol.err
  78. 1 0
      tests/qapi-schema/doc-bad-symbol.exit
  79. 6 0
      tests/qapi-schema/doc-bad-symbol.json
  80. 0 0
      tests/qapi-schema/doc-bad-symbol.out
  81. 1 0
      tests/qapi-schema/doc-duplicated-arg.err
  82. 1 0
      tests/qapi-schema/doc-duplicated-arg.exit
  83. 7 0
      tests/qapi-schema/doc-duplicated-arg.json
  84. 0 0
      tests/qapi-schema/doc-duplicated-arg.out
  85. 1 0
      tests/qapi-schema/doc-duplicated-return.err
  86. 1 0
      tests/qapi-schema/doc-duplicated-return.exit
  87. 8 0
      tests/qapi-schema/doc-duplicated-return.json
  88. 0 0
      tests/qapi-schema/doc-duplicated-return.out
  89. 1 0
      tests/qapi-schema/doc-duplicated-since.err
  90. 1 0
      tests/qapi-schema/doc-duplicated-since.exit
  91. 8 0
      tests/qapi-schema/doc-duplicated-since.json
  92. 0 0
      tests/qapi-schema/doc-duplicated-since.out
  93. 1 0
      tests/qapi-schema/doc-empty-arg.err
  94. 1 0
      tests/qapi-schema/doc-empty-arg.exit
  95. 6 0
      tests/qapi-schema/doc-empty-arg.json
  96. 0 0
      tests/qapi-schema/doc-empty-arg.out
  97. 1 0
      tests/qapi-schema/doc-empty-section.err
  98. 1 0
      tests/qapi-schema/doc-empty-section.exit
  99. 8 0
      tests/qapi-schema/doc-empty-section.json
  100. 0 0
      tests/qapi-schema/doc-empty-section.out

+ 147 - 33
docs/qapi-code-gen.txt

@@ -44,40 +44,154 @@ Input must be ASCII (although QMP supports full Unicode strings, the
 QAPI parser does not).  At present, there is no place where a QAPI
 schema requires the use of JSON numbers or null.
 
+
+=== Comments ===
+
 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
-x.y.z)' comment.  For example:
-
-    ##
-    # @BlockStats:
-    #
-    # Statistics of a virtual block device or a block backing device.
-    #
-    # @device: #optional If the stats are for a virtual block device, the name
-    #          corresponding to the virtual block device.
-    #
-    # @stats:  A @BlockDeviceStats for the device.
-    #
-    # @parent: #optional This describes the file block device if it has one.
-    #
-    # @backing: #optional This describes the backing block device if it has one.
-    #           (Since 2.0)
-    #
-    # Since: 0.14.0
-    ##
-    { 'struct': 'BlockStats',
-      'data': {'*device': 'str', 'stats': 'BlockDeviceStats',
-               '*parent': 'BlockStats',
-               '*backing': 'BlockStats'} }
+newline is ignored.
+
+A multi-line comment that starts and ends with a '##' line is a
+documentation comment.  These are parsed by the documentation
+generator, which recognizes certain markup detailed below.
+
+
+==== Documentation markup ====
+
+Comment text starting with '=' is a section title:
+
+    # = Section title
+
+Double the '=' for a subsection title:
+
+    # == Subection title
+
+'|' denotes examples:
+
+    # | Text of the example, may span
+    # | multiple lines
+
+'*' starts an itemized list:
+
+    # * First item, may span
+    #   multiple lines
+    # * Second item
+
+You can also use '-' instead of '*'.
+
+A decimal number followed by '.' starts a numbered list:
+
+    # 1. First item, may span
+    #    multiple lines
+    # 2. Second item
+
+The actual number doesn't matter.  You could even use '*' instead of
+'2.' for the second item.
+
+Lists can't be nested.  Blank lines are currently not supported within
+lists.
+
+Additional whitespace between the initial '#' and the comment text is
+permitted.
+
+*foo* and _foo_ are for strong and emphasis styles respectively (they
+do not work over multiple lines). @foo is used to reference a name in
+the schema.
+
+Example:
+
+##
+# = 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
+#
+##
+
+
+==== Expression documentation ====
+
+Each expression that isn't an include directive must be preceded by a
+documentation block.  Such blocks are called expression documentation
+blocks.
+
+The documentation block consists of a first line naming the
+expression, an optional overview, a description of each argument (for
+commands and events) or member (for structs, unions and alternates),
+and optional tagged sections.
+
+FIXME: the parser accepts these things in almost any order.
+
+Optional arguments / 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.
+
+A tagged section starts with one of the following words:
+"Note:"/"Notes:", "Since:", "Example"/"Examples", "Returns:", "TODO:".
+The section ends with the start of a new section.
+
+A 'Since: x.y.z' tagged section lists the release that introduced the
+expression.
+
+For example:
+
+##
+# @BlockStats:
+#
+# Statistics of a virtual block device or a block backing device.
+#
+# @device: #optional If the stats are for a virtual block device, the name
+#          corresponding to the virtual block device.
+#
+# @node-name: #optional The node name of the device. (since 2.3)
+#
+# ... more members ...
+#
+# Since: 0.14.0
+##
+{ 'struct': 'BlockStats',
+  'data': {'*device': 'str', '*node-name': 'str',
+           ... more members ... } }
+
+##
+# @query-blockstats:
+#
+# Query the @BlockStats for all virtual block devices.
+#
+# @query-nodes: #optional If true, the command will query all the
+#               block nodes ... explain, explain ...  (since 2.3)
+#
+# Returns: A list of @BlockStats for each virtual block devices.
+#
+# Since: 0.14.0
+#
+# Example:
+#
+# -> { "execute": "query-blockstats" }
+# <- {
+#      ... lots of output ...
+#    }
+#
+##
+{ 'command': 'query-blockstats',
+  'data': { '*query-nodes': 'bool' },
+  'returns': ['BlockStats'] }
+
+==== Free-form documentation ====
+
+A documentation block that isn't an expression documentation block is
+a free-form documentation block.  These may be used to provide
+additional text and structuring content.
+
+
+=== Schema overview ===
 
 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

+ 249 - 2
scripts/qapi.py

@@ -125,6 +125,122 @@ def __init__(self, info, msg):
                            info['parent'], msg)
 
 
+class QAPIDoc(object):
+    class Section(object):
+        def __init__(self, name=None):
+            # optional section name (argument/member or section name)
+            self.name = name
+            # the list of lines 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, info):
+        # self.parser is used to report errors with QAPIParseError.  The
+        # resulting error position depends on the state of the parser.
+        # It happens to be the beginning of the comment.  More or less
+        # servicable, but action at a distance.
+        self.parser = parser
+        self.info = info
+        self.symbol = None
+        self.body = QAPIDoc.Section()
+        # dict mapping parameter name to ArgSection
+        self.args = OrderedDict()
+        # a list of Section
+        self.sections = []
+        # the current section
+        self.section = self.body
+        # associated expression (to be set by expression parser)
+        self.expr = None
+
+    def has_section(self, name):
+        """Return True if we have a section with this name."""
+        for i in self.sections:
+            if i.name == name:
+                return True
+        return False
+
+    def append(self, line):
+        """Parse a comment line and add it to the documentation."""
+        line = line[1:]
+        if not line:
+            self._append_freeform(line)
+            return
+
+        if line[0] != ' ':
+            raise QAPIParseError(self.parser, "Missing space after #")
+        line = line[1:]
+
+        # FIXME not nice: things like '#  @foo:' and '# @foo: ' aren't
+        # recognized, and get silently treated as ordinary text
+        if self.symbol:
+            self._append_symbol_line(line)
+        elif not self.body.content and line.startswith("@"):
+            if not line.endswith(":"):
+                raise QAPIParseError(self.parser, "Line should end with :")
+            self.symbol = line[1:-1]
+            # FIXME invalid names other than the empty string aren't flagged
+            if not self.symbol:
+                raise QAPIParseError(self.parser, "Invalid name")
+        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:",
+                      "TODO:"):
+            line = line[len(name)+1:]
+            self._start_section(name[:-1])
+
+        self._append_freeform(line)
+
+    def _start_args_section(self, name):
+        # FIXME invalid names other than the empty string aren't flagged
+        if not name:
+            raise QAPIParseError(self.parser, "Invalid parameter name")
+        if name in self.args:
+            raise QAPIParseError(self.parser,
+                                 "'%s' parameter name duplicated" % name)
+        if self.sections:
+            raise QAPIParseError(self.parser,
+                                 "'@%s:' can't follow '%s' section"
+                                 % (name, self.sections[0].name))
+        self.section = QAPIDoc.ArgSection(name)
+        self.args[name] = self.section
+
+    def _start_section(self, name=""):
+        if name in ("Returns", "Since") and self.has_section(name):
+            raise QAPIParseError(self.parser,
+                                 "Duplicated '%s' section" % name)
+        self.section = QAPIDoc.Section(name)
+        self.sections.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()):
+            self._start_section()
+        if (in_arg or not self.section.name
+                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):
@@ -140,11 +256,17 @@ def __init__(self, fp, previously_included=[], incl_info=None):
         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 == '#':
+                doc = self.get_doc(info)
+                self.docs.append(doc)
+                continue
+
             expr = self.get_expr(False)
             if isinstance(expr, dict) and "include" in expr:
                 if len(expr) != 1:
@@ -162,6 +284,7 @@ def __init__(self, fp, previously_included=[], incl_info=None):
                         raise QAPISemError(info, "Inclusion loop for %s"
                                            % include)
                     inf = inf['parent']
+
                 # skip multiple include of the same file
                 if incl_abs_fname in previously_included:
                     continue
@@ -172,12 +295,19 @@ def __init__(self, fp, previously_included=[], incl_info=None):
                 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 self.docs[-1].info['file'] == fname
+                        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 accept(self, skip_comment=True):
         while True:
             self.tok = self.src[self.cursor]
             self.pos = self.cursor
@@ -185,7 +315,13 @@ def accept(self):
             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 == "'":
@@ -319,6 +455,28 @@ def get_expr(self, nested):
             raise QAPIParseError(self, 'Expected "{", "[" or string')
         return expr
 
+    def get_doc(self, info):
+        if self.val != '##':
+            raise QAPIParseError(self, "Junk after '##' at start of "
+                                 "documentation comment")
+
+        doc = QAPIDoc(self, info)
+        self.accept(False)
+        while self.tok == '#':
+            if self.val.startswith('##'):
+                # End of doc comment
+                if self.val != '##':
+                    raise QAPIParseError(self, "Junk after '##' at end of "
+                                         "documentation comment")
+                self.accept()
+                return doc
+            else:
+                doc.append(self.val)
+            self.accept(False)
+
+        raise QAPIParseError(self, "Documentation comment must end with '##'")
+
+
 #
 # Semantic analysis of schema expressions
 # TODO fold into QAPISchema
@@ -703,6 +861,11 @@ def check_exprs(exprs):
     for expr_elem in exprs:
         expr = expr_elem['expr']
         info = expr_elem['info']
+
+        if 'doc' not in expr_elem:
+            raise QAPISemError(info,
+                               "Expression missing documentation comment")
+
         if 'enum' in expr:
             check_keys(expr_elem, 'enum', ['data'], ['prefix'])
             add_enum(expr['enum'], info, expr['data'])
@@ -761,6 +924,88 @@ def check_exprs(exprs):
     return exprs
 
 
+def check_freeform_doc(doc):
+    if doc.symbol:
+        raise QAPISemError(doc.info,
+                           "Documention for '%s' is not followed"
+                           " by the definition" % doc.symbol)
+
+    body = str(doc.body)
+    if re.search(r'@\S+:', body, re.MULTILINE):
+        raise QAPISemError(doc.info,
+                           "Free-form documentation block must not contain"
+                           " @NAME: sections")
+
+
+def check_definition_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 QAPISemError(info, "Definition of '%s' follows documentation"
+                           " for '%s'" % (name, doc.symbol))
+    if doc.has_section('Returns') and 'command' not in expr:
+        raise QAPISemError(info, "'Returns:' is only valid for commands")
+
+    if meta == 'union':
+        args = expr.get('base', [])
+    else:
+        args = expr.get('data', [])
+    if isinstance(args, str):
+        return
+    if isinstance(args, dict):
+        args = args.keys()
+    assert isinstance(args, list)
+
+    if (meta == 'alternate'
+            or (meta == 'union' and not expr.get('discriminator'))):
+        args.append('type')
+
+    for arg in args:
+        if arg[0] == '*':
+            opt = True
+            desc = doc.args.get(arg[1:])
+        else:
+            opt = False
+            desc = doc.args.get(arg)
+        if not desc:
+            continue
+        desc_opt = "#optional" in str(desc)
+        if desc_opt and not opt:
+            raise QAPISemError(info, "Description has #optional, "
+                               "but the declaration doesn't")
+        if not desc_opt and opt:
+            # silently fix the doc
+            # TODO either fix the schema and make this an error,
+            # or drop #optional entirely
+            desc.append("#optional")
+
+    doc_args = set(doc.args.keys())
+    args = set([name.strip('*') for name in args])
+    if not doc_args.issubset(args):
+        raise QAPISemError(info, "The following documented members are not in "
+                           "the declaration: %s" % ", ".join(doc_args - args))
+
+
+def check_docs(docs):
+    for doc in docs:
+        for section in doc.args.values() + doc.sections:
+            content = str(section)
+            if not content or content.isspace():
+                raise QAPISemError(doc.info,
+                                   "Empty doc section '%s'" % section.name)
+
+        if not doc.expr:
+            check_freeform_doc(doc)
+        else:
+            check_definition_doc(doc, doc.expr, doc.info)
+
+    return docs
+
+
 #
 # Schema compiler frontend
 #
@@ -1229,7 +1474,9 @@ def visit(self, visitor):
 class QAPISchema(object):
     def __init__(self, fname):
         try:
-            self.exprs = check_exprs(QAPISchemaParser(open(fname, "r")).exprs)
+            parser = QAPISchemaParser(open(fname, "r"))
+            self.exprs = check_exprs(parser.exprs)
+            self.docs = check_docs(parser.docs)
             self._entity_dict = {}
             self._predefining = True
             self._def_predefineds()

+ 271 - 0
scripts/qapi2texi.py

@@ -0,0 +1,271 @@
+#!/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} {{}} {name}
+
+{body}
+
+@end deftypefn
+
+""".format
+
+ENUM_FMT = """
+@deftp Enum {name}
+
+{body}
+
+@end deftp
+
+""".format
+
+STRUCT_FMT = """
+@deftp {{{type}}} {name}
+
+{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'\b_([^_\n]+)_\b', 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"""
+    # TODO: Neglects to escape @ characters.
+    # We should probably escape them in subst_braces(), and rename the
+    # function to subst_special() or subs_texi_special().  If we do that, we
+    # need to delay it until after subst_vars() in texi_format().
+    doc = subst_braces(doc).strip('\n')
+    return EXAMPLE_FMT(code=doc)
+
+
+def texi_format(doc):
+    """
+    Format documentation
+
+    Lines starting with:
+    - |: generates an @example
+    - =: generates @section
+    - ==: generates @subsection
+    - 1. or 1): generates an @enumerate @item
+    - */-: 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 == ""
+
+        # FIXME: Doing this in a single if / elif chain is
+        # problematic.  For instance, a line without markup terminates
+        # a list if it follows a blank line (reaches the final elif),
+        # but a line with some *other* markup, such as a = title
+        # doesn't.
+        #
+        # Make sure to update section "Documentation markup" in
+        # docs/qapi-code-gen.txt when fixing this.
+        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(r'^([0-9]*\.) ', line):
+            if not inlist:
+                lines.append("@enumerate")
+                inlist = "enumerate"
+            line = line[line.find(" ")+1:]
+            lines.append("@item")
+        elif re.match(r'^[*-] ', line):
+            if not inlist:
+                lines.append("@itemize %s" % {'*': "@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_body(doc):
+    """
+    Format the body of a symbol documentation:
+    - main body
+    - table of arguments
+    - followed by "Returns/Notes/Since/Example" sections
+    """
+    body = texi_format(str(doc.body)) + "\n"
+    if doc.args:
+        body += "@table @asis\n"
+        for arg, section in doc.args.iteritems():
+            desc = str(section)
+            opt = ''
+            if "#optional" in desc:
+                desc = desc.replace("#optional", "")
+                opt = ' (optional)'
+            body += "@item @code{'%s'}%s\n%s\n" % (arg, opt,
+                                                   texi_format(desc))
+        body += "@end table\n"
+
+    for section in doc.sections:
+        name, doc = (section.name, str(section))
+        func = texi_format
+        if name.startswith("Example"):
+            func = texi_example
+
+        if name:
+            # FIXME the indentation produced by @quotation in .txt and
+            # .html output is confusing
+            body += "\n@quotation %s\n%s\n@end quotation" % \
+                    (name, func(doc))
+        else:
+            body += func(doc)
+
+    return body
+
+
+def texi_alternate(expr, doc):
+    """Format an alternate to texi"""
+    body = texi_body(doc)
+    return STRUCT_FMT(type="Alternate",
+                      name=doc.symbol,
+                      body=body)
+
+
+def texi_union(expr, doc):
+    """Format a union to texi"""
+    discriminator = expr.get("discriminator")
+    if discriminator:
+        union = "Flat Union"
+    else:
+        union = "Simple Union"
+
+    body = texi_body(doc)
+    return STRUCT_FMT(type=union,
+                      name=doc.symbol,
+                      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"""
+    body = texi_body(doc)
+    return STRUCT_FMT(type="Struct",
+                      name=doc.symbol,
+                      body=body)
+
+
+def texi_command(expr, doc):
+    """Format a command to texi"""
+    body = texi_body(doc)
+    return COMMAND_FMT(type="Command",
+                       name=doc.symbol,
+                       body=body)
+
+
+def texi_event(expr, doc):
+    """Format an event to texi"""
+    body = texi_body(doc)
+    return COMMAND_FMT(type="Event",
+                       name=doc.symbol,
+                       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}[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])
+    print texi(schema.docs)
+
+
+if __name__ == "__main__":
+    main(sys.argv)

+ 20 - 0
tests/Makefile.include

@@ -352,6 +352,24 @@ 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-interleaved-section.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-colon.json
+qapi-schema += doc-missing-expr.json
+qapi-schema += doc-missing-space.json
+qapi-schema += doc-optional.json
 qapi-schema += double-data.json
 qapi-schema += double-type.json
 qapi-schema += duplicate-key.json
@@ -445,6 +463,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 \

+ 1 - 1
tests/qapi-schema/alternate-any.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-any.json:2: Alternate 'Alt' member 'one' cannot use type 'any'
+tests/qapi-schema/alternate-any.json:6: Alternate 'Alt' member 'one' cannot use type 'any'

+ 4 - 0
tests/qapi-schema/alternate-any.json

@@ -1,4 +1,8 @@
 # we do not allow the 'any' type as an alternate branch
+
+##
+# @Alt:
+##
 { 'alternate': 'Alt',
   'data': { 'one': 'any',
             'two': 'int' } }

+ 1 - 1
tests/qapi-schema/alternate-array.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-array.json:5: Member 'two' of alternate 'Alt' cannot be an array
+tests/qapi-schema/alternate-array.json:12: Member 'two' of alternate 'Alt' cannot be an array

+ 7 - 0
tests/qapi-schema/alternate-array.json

@@ -1,7 +1,14 @@
 # we do not allow array branches in alternates
+
+##
+# @One:
+##
 # TODO: should we support this?
 { 'struct': 'One',
   'data': { 'name': 'str' } }
+##
+# @Alt:
+##
 { 'alternate': 'Alt',
   'data': { 'one': 'One',
             'two': [ 'int' ] } }

+ 1 - 1
tests/qapi-schema/alternate-base.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-base.json:4: Unknown key 'base' in alternate 'Alt'
+tests/qapi-schema/alternate-base.json:11: Unknown key 'base' in alternate 'Alt'

+ 7 - 0
tests/qapi-schema/alternate-base.json

@@ -1,6 +1,13 @@
 # we reject alternate with base type
+
+##
+# @Base:
+##
 { 'struct': 'Base',
   'data': { 'string': 'str' } }
+##
+# @Alt:
+##
 { 'alternate': 'Alt',
   'base': 'Base',
   'data': { 'number': 'int' } }

+ 1 - 1
tests/qapi-schema/alternate-clash.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-clash.json:7: 'a_b' (branch of Alt1) collides with 'a-b' (branch of Alt1)
+tests/qapi-schema/alternate-clash.json:11: 'a_b' (branch of Alt1) collides with 'a-b' (branch of Alt1)

+ 4 - 0
tests/qapi-schema/alternate-clash.json

@@ -4,5 +4,9 @@
 # TODO: In the future, if alternates are simplified to not generate
 # the implicit Alt1Kind enum, we would still have a collision with the
 # resulting C union trying to have two members named 'a_b'.
+
+##
+# @Alt1:
+##
 { 'alternate': 'Alt1',
   'data': { 'a-b': 'str', 'a_b': 'int' } }

+ 1 - 1
tests/qapi-schema/alternate-conflict-dict.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-conflict-dict.json:6: Alternate 'Alt' member 'two' can't be distinguished from member 'one'
+tests/qapi-schema/alternate-conflict-dict.json:16: Alternate 'Alt' member 'two' can't be distinguished from member 'one'

+ 10 - 0
tests/qapi-schema/alternate-conflict-dict.json

@@ -1,8 +1,18 @@
 # we reject alternates with multiple object branches
+
+##
+# @One:
+##
 { 'struct': 'One',
   'data': { 'name': 'str' } }
+##
+# @Two:
+##
 { 'struct': 'Two',
   'data': { 'value': 'int' } }
+##
+# @Alt:
+##
 { 'alternate': 'Alt',
   'data': { 'one': 'One',
             'two': 'Two' } }

+ 1 - 1
tests/qapi-schema/alternate-conflict-string.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-conflict-string.json:4: Alternate 'Alt' member 'two' can't be distinguished from member 'one'
+tests/qapi-schema/alternate-conflict-string.json:11: Alternate 'Alt' member 'two' can't be distinguished from member 'one'

+ 7 - 0
tests/qapi-schema/alternate-conflict-string.json

@@ -1,6 +1,13 @@
 # we reject alternates with multiple string-like branches
+
+##
+# @Enum:
+##
 { 'enum': 'Enum',
   'data': [ 'hello', 'world' ] }
+##
+# @Alt:
+##
 { 'alternate': 'Alt',
   'data': { 'one': 'str',
             'two': 'Enum' } }

+ 1 - 1
tests/qapi-schema/alternate-empty.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-empty.json:2: Alternate 'Alt' should have at least two branches in 'data'
+tests/qapi-schema/alternate-empty.json:6: Alternate 'Alt' should have at least two branches in 'data'

+ 4 - 0
tests/qapi-schema/alternate-empty.json

@@ -1,2 +1,6 @@
 # alternates must list at least two types to be useful
+
+##
+# @Alt:
+##
 { 'alternate': 'Alt', 'data': { 'i': 'int' } }

+ 1 - 1
tests/qapi-schema/alternate-nested.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-nested.json:4: Member 'nested' of alternate 'Alt2' cannot use alternate type 'Alt1'
+tests/qapi-schema/alternate-nested.json:11: Member 'nested' of alternate 'Alt2' cannot use alternate type 'Alt1'

+ 7 - 0
tests/qapi-schema/alternate-nested.json

@@ -1,5 +1,12 @@
 # we reject a nested alternate branch
+
+##
+# @Alt1:
+##
 { 'alternate': 'Alt1',
   'data': { 'name': 'str', 'value': 'int' } }
+##
+# @Alt2:
+##
 { 'alternate': 'Alt2',
   'data': { 'nested': 'Alt1', 'b': 'bool' } }

+ 1 - 1
tests/qapi-schema/alternate-unknown.err

@@ -1 +1 @@
-tests/qapi-schema/alternate-unknown.json:2: Member 'unknown' of alternate 'Alt' uses unknown type 'MissingType'
+tests/qapi-schema/alternate-unknown.json:6: Member 'unknown' of alternate 'Alt' uses unknown type 'MissingType'

+ 4 - 0
tests/qapi-schema/alternate-unknown.json

@@ -1,3 +1,7 @@
 # we reject an alternate with unknown type in branch
+
+##
+# @Alt:
+##
 { 'alternate': 'Alt',
   'data': { 'unknown': 'MissingType', 'i': 'int' } }

+ 1 - 1
tests/qapi-schema/args-alternate.err

@@ -1 +1 @@
-tests/qapi-schema/args-alternate.json:3: 'data' for command 'oops' cannot use alternate type 'Alt'
+tests/qapi-schema/args-alternate.json:11: 'data' for command 'oops' cannot use alternate type 'Alt'

+ 8 - 0
tests/qapi-schema/args-alternate.json

@@ -1,3 +1,11 @@
 # we do not allow alternate arguments
+
+##
+# @Alt:
+##
 { 'alternate': 'Alt', 'data': { 'case1': 'int', 'case2': 'str' } }
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': 'Alt' }

+ 1 - 1
tests/qapi-schema/args-any.err

@@ -1 +1 @@
-tests/qapi-schema/args-any.json:2: 'data' for command 'oops' cannot use built-in type 'any'
+tests/qapi-schema/args-any.json:6: 'data' for command 'oops' cannot use built-in type 'any'

+ 4 - 0
tests/qapi-schema/args-any.json

@@ -1,2 +1,6 @@
 # we do not allow an 'any' argument
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': 'any' }

+ 1 - 1
tests/qapi-schema/args-array-empty.err

@@ -1 +1 @@
-tests/qapi-schema/args-array-empty.json:2: Member 'empty' of 'data' for command 'oops': array type must contain single type name
+tests/qapi-schema/args-array-empty.json:6: Member 'empty' of 'data' for command 'oops': array type must contain single type name

+ 4 - 0
tests/qapi-schema/args-array-empty.json

@@ -1,2 +1,6 @@
 # we reject an array for data if it does not contain a known type
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': { 'empty': [ ] } }

+ 1 - 1
tests/qapi-schema/args-array-unknown.err

@@ -1 +1 @@
-tests/qapi-schema/args-array-unknown.json:2: Member 'array' of 'data' for command 'oops' uses unknown type 'NoSuchType'
+tests/qapi-schema/args-array-unknown.json:6: Member 'array' of 'data' for command 'oops' uses unknown type 'NoSuchType'

+ 4 - 0
tests/qapi-schema/args-array-unknown.json

@@ -1,2 +1,6 @@
 # we reject an array for data if it does not contain a known type
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': { 'array': [ 'NoSuchType' ] } }

+ 1 - 1
tests/qapi-schema/args-bad-boxed.err

@@ -1 +1 @@
-tests/qapi-schema/args-bad-boxed.json:2: 'boxed' of command 'foo' should only use true value
+tests/qapi-schema/args-bad-boxed.json:6: 'boxed' of command 'foo' should only use true value

+ 4 - 0
tests/qapi-schema/args-bad-boxed.json

@@ -1,2 +1,6 @@
 # 'boxed' should only appear with value true
+
+##
+# @foo:
+##
 { 'command': 'foo', 'boxed': false }

+ 1 - 1
tests/qapi-schema/args-boxed-anon.err

@@ -1 +1 @@
-tests/qapi-schema/args-boxed-anon.json:2: 'data' for command 'foo' should be a type name
+tests/qapi-schema/args-boxed-anon.json:6: 'data' for command 'foo' should be a type name

+ 4 - 0
tests/qapi-schema/args-boxed-anon.json

@@ -1,2 +1,6 @@
 # 'boxed' can only be used with named types
+
+##
+# @foo:
+##
 { 'command': 'foo', 'boxed': true, 'data': { 'string': 'str' } }

+ 1 - 1
tests/qapi-schema/args-boxed-empty.err

@@ -1 +1 @@
-tests/qapi-schema/args-boxed-empty.json:3: Cannot use 'boxed' with empty type
+tests/qapi-schema/args-boxed-empty.json:11: Cannot use 'boxed' with empty type

+ 8 - 0
tests/qapi-schema/args-boxed-empty.json

@@ -1,3 +1,11 @@
 # 'boxed' requires a non-empty type
+
+##
+# @Empty:
+##
 { 'struct': 'Empty', 'data': {} }
+
+##
+# @foo:
+##
 { 'command': 'foo', 'boxed': true, 'data': 'Empty' }

+ 1 - 1
tests/qapi-schema/args-boxed-string.err

@@ -1 +1 @@
-tests/qapi-schema/args-boxed-string.json:2: 'data' for command 'foo' cannot use built-in type 'str'
+tests/qapi-schema/args-boxed-string.json:6: 'data' for command 'foo' cannot use built-in type 'str'

+ 4 - 0
tests/qapi-schema/args-boxed-string.json

@@ -1,2 +1,6 @@
 # 'boxed' requires a complex (not built-in) type
+
+##
+# @foo:
+##
 { 'command': 'foo', 'boxed': true, 'data': 'str' }

+ 1 - 1
tests/qapi-schema/args-int.err

@@ -1 +1 @@
-tests/qapi-schema/args-int.json:2: 'data' for command 'oops' cannot use built-in type 'int'
+tests/qapi-schema/args-int.json:6: 'data' for command 'oops' cannot use built-in type 'int'

+ 4 - 0
tests/qapi-schema/args-int.json

@@ -1,2 +1,6 @@
 # we reject commands where data is not an array or complex type
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': 'int' }

+ 1 - 1
tests/qapi-schema/args-invalid.err

@@ -1 +1 @@
-tests/qapi-schema/args-invalid.json:1: 'data' for command 'foo' should be a dictionary or type name
+tests/qapi-schema/args-invalid.json:4: 'data' for command 'foo' should be a dictionary or type name

+ 3 - 0
tests/qapi-schema/args-invalid.json

@@ -1,2 +1,5 @@
+##
+# @foo:
+##
 { 'command': 'foo',
   'data': false }

+ 1 - 1
tests/qapi-schema/args-member-array-bad.err

@@ -1 +1 @@
-tests/qapi-schema/args-member-array-bad.json:2: Member 'member' of 'data' for command 'oops': array type must contain single type name
+tests/qapi-schema/args-member-array-bad.json:6: Member 'member' of 'data' for command 'oops': array type must contain single type name

+ 4 - 0
tests/qapi-schema/args-member-array-bad.json

@@ -1,2 +1,6 @@
 # we reject data if it does not contain a valid array type
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': { 'member': [ { 'nested': 'str' } ] } }

+ 1 - 1
tests/qapi-schema/args-member-case.err

@@ -1 +1 @@
-tests/qapi-schema/args-member-case.json:2: 'Arg' (parameter of no-way-this-will-get-whitelisted) should not use uppercase
+tests/qapi-schema/args-member-case.json:6: 'Arg' (parameter of no-way-this-will-get-whitelisted) should not use uppercase

+ 4 - 0
tests/qapi-schema/args-member-case.json

@@ -1,2 +1,6 @@
 # Member names should be 'lower-case' unless the struct/command is whitelisted
+
+##
+# @no-way-this-will-get-whitelisted:
+##
 { 'command': 'no-way-this-will-get-whitelisted', 'data': { 'Arg': 'int' } }

+ 1 - 1
tests/qapi-schema/args-member-unknown.err

@@ -1 +1 @@
-tests/qapi-schema/args-member-unknown.json:2: Member 'member' of 'data' for command 'oops' uses unknown type 'NoSuchType'
+tests/qapi-schema/args-member-unknown.json:6: Member 'member' of 'data' for command 'oops' uses unknown type 'NoSuchType'

+ 4 - 0
tests/qapi-schema/args-member-unknown.json

@@ -1,2 +1,6 @@
 # we reject data if it does not contain a known type
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': { 'member': 'NoSuchType' } }

+ 1 - 1
tests/qapi-schema/args-name-clash.err

@@ -1 +1 @@
-tests/qapi-schema/args-name-clash.json:4: 'a_b' (parameter of oops) collides with 'a-b' (parameter of oops)
+tests/qapi-schema/args-name-clash.json:8: 'a_b' (parameter of oops) collides with 'a-b' (parameter of oops)

+ 4 - 0
tests/qapi-schema/args-name-clash.json

@@ -1,4 +1,8 @@
 # C member name collision
 # Reject members that clash when mapped to C names (we would have two 'a_b'
 # members).
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': { 'a-b': 'str', 'a_b': 'str' } }

+ 1 - 1
tests/qapi-schema/args-union.err

@@ -1 +1 @@
-tests/qapi-schema/args-union.json:3: 'data' for command 'oops' cannot use union type 'Uni'
+tests/qapi-schema/args-union.json:10: 'data' for command 'oops' cannot use union type 'Uni'

+ 7 - 0
tests/qapi-schema/args-union.json

@@ -1,3 +1,10 @@
 # use of union arguments requires 'boxed':true
+
+##
+# @Uni:
+##
 { 'union': 'Uni', 'data': { 'case1': 'int', 'case2': 'str' } }
+##
+# oops:
+##
 { 'command': 'oops', 'data': 'Uni' }

+ 1 - 1
tests/qapi-schema/args-unknown.err

@@ -1 +1 @@
-tests/qapi-schema/args-unknown.json:2: 'data' for command 'oops' uses unknown type 'NoSuchType'
+tests/qapi-schema/args-unknown.json:6: 'data' for command 'oops' uses unknown type 'NoSuchType'

+ 4 - 0
tests/qapi-schema/args-unknown.json

@@ -1,2 +1,6 @@
 # we reject data if it does not contain a known type
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': 'NoSuchType' }

+ 1 - 1
tests/qapi-schema/bad-base.err

@@ -1 +1 @@
-tests/qapi-schema/bad-base.json:3: 'base' for struct 'MyType' cannot use union type 'Union'
+tests/qapi-schema/bad-base.json:10: 'base' for struct 'MyType' cannot use union type 'Union'

+ 7 - 0
tests/qapi-schema/bad-base.json

@@ -1,3 +1,10 @@
 # we reject a base that is not a struct
+
+##
+# @Union:
+##
 { 'union': 'Union', 'data': { 'a': 'int', 'b': 'str' } }
+##
+# @MyType:
+##
 { 'struct': 'MyType', 'base': 'Union', 'data': { 'c': 'int' } }

+ 1 - 1
tests/qapi-schema/bad-data.err

@@ -1 +1 @@
-tests/qapi-schema/bad-data.json:2: 'data' for command 'oops' cannot be an array
+tests/qapi-schema/bad-data.json:6: 'data' for command 'oops' cannot be an array

+ 4 - 0
tests/qapi-schema/bad-data.json

@@ -1,2 +1,6 @@
 # we ensure 'data' is a dictionary for all but enums
+
+##
+# @oops:
+##
 { 'command': 'oops', 'data': [ ] }

+ 1 - 1
tests/qapi-schema/bad-ident.err

@@ -1 +1 @@
-tests/qapi-schema/bad-ident.json:2: 'struct' does not allow optional name '*oops'
+tests/qapi-schema/bad-ident.json:6: 'struct' does not allow optional name '*oops'

+ 4 - 0
tests/qapi-schema/bad-ident.json

@@ -1,2 +1,6 @@
 # we reject creating a type name with bad name
+
+##
+# @*oops:
+##
 { 'struct': '*oops', 'data': { 'i': 'int' } }

+ 1 - 1
tests/qapi-schema/bad-type-bool.err

@@ -1 +1 @@
-tests/qapi-schema/bad-type-bool.json:2: 'struct' key must have a string value
+tests/qapi-schema/bad-type-bool.json:6: 'struct' key must have a string value

+ 4 - 0
tests/qapi-schema/bad-type-bool.json

@@ -1,2 +1,6 @@
 # we reject an expression with a metatype that is not a string
+
+##
+# @true:
+##
 { 'struct': true, 'data': { } }

+ 1 - 1
tests/qapi-schema/bad-type-dict.err

@@ -1 +1 @@
-tests/qapi-schema/bad-type-dict.json:2: 'command' key must have a string value
+tests/qapi-schema/bad-type-dict.json:6: 'command' key must have a string value

+ 4 - 0
tests/qapi-schema/bad-type-dict.json

@@ -1,2 +1,6 @@
 # we reject an expression with a metatype that is not a string
+
+##
+# @foo:
+##
 { 'command': { } }

+ 1 - 1
tests/qapi-schema/base-cycle-direct.err

@@ -1 +1 @@
-tests/qapi-schema/base-cycle-direct.json:2: Object Loopy contains itself
+tests/qapi-schema/base-cycle-direct.json:6: Object Loopy contains itself

+ 4 - 0
tests/qapi-schema/base-cycle-direct.json

@@ -1,2 +1,6 @@
 # we reject a loop in base classes
+
+##
+# @Loopy:
+##
 { 'struct': 'Loopy', 'base': 'Loopy', 'data': {} }

+ 1 - 1
tests/qapi-schema/base-cycle-indirect.err

@@ -1 +1 @@
-tests/qapi-schema/base-cycle-indirect.json:2: Object Base1 contains itself
+tests/qapi-schema/base-cycle-indirect.json:6: Object Base1 contains itself

+ 7 - 0
tests/qapi-schema/base-cycle-indirect.json

@@ -1,3 +1,10 @@
 # we reject a loop in base classes
+
+##
+# @Base1:
+##
 { 'struct': 'Base1', 'base': 'Base2', 'data': {} }
+##
+# @Base2:
+##
 { 'struct': 'Base2', 'base': 'Base1', 'data': {} }

+ 1 - 1
tests/qapi-schema/command-int.err

@@ -1 +1 @@
-tests/qapi-schema/command-int.json:2: built-in 'int' is already defined
+tests/qapi-schema/command-int.json:6: built-in 'int' is already defined

+ 4 - 0
tests/qapi-schema/command-int.json

@@ -1,2 +1,6 @@
 # we reject collisions between commands and types
+
+##
+# @int:
+##
 { 'command': 'int', 'data': { 'character': 'str' } }

+ 4 - 0
tests/qapi-schema/comments.json

@@ -1,4 +1,8 @@
 # Unindented comment
+
+##
+# @Status:
+##
 { 'enum': 'Status',             # Comment to the right of code
   # Indented comment
   'data': [ 'good', 'bad', 'ugly' ] }

+ 1 - 0
tests/qapi-schema/comments.out

@@ -2,3 +2,4 @@ enum QType ['none', 'qnull', 'qint', 'qstring', 'qdict', 'qlist', 'qfloat', 'qbo
     prefix QTYPE
 enum Status ['good', 'bad', 'ugly']
 object q_empty
+doc symbol=Status expr=('enum', 'Status')

+ 1 - 0
tests/qapi-schema/doc-bad-args.err

@@ -0,0 +1 @@
+tests/qapi-schema/doc-bad-args.json:3: The following documented members are not in the declaration: b

+ 1 - 0
tests/qapi-schema/doc-bad-args.exit

@@ -0,0 +1 @@
+1

+ 8 - 0
tests/qapi-schema/doc-bad-args.json

@@ -0,0 +1,8 @@
+# Arguments listed in the doc comment must exist in the actual schema
+
+##
+# @foo:
+# @a: a
+# @b: b
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }

+ 0 - 0
tests/qapi-schema/doc-bad-args.out


+ 1 - 0
tests/qapi-schema/doc-bad-symbol.err

@@ -0,0 +1 @@
+tests/qapi-schema/doc-bad-symbol.json:3: Definition of 'foo' follows documentation for 'food'

+ 1 - 0
tests/qapi-schema/doc-bad-symbol.exit

@@ -0,0 +1 @@
+1

+ 6 - 0
tests/qapi-schema/doc-bad-symbol.json

@@ -0,0 +1,6 @@
+# Documentation symbol mismatch with expression
+
+##
+# @food:
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }

+ 0 - 0
tests/qapi-schema/doc-bad-symbol.out


+ 1 - 0
tests/qapi-schema/doc-duplicated-arg.err

@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-arg.json:6:1: 'a' parameter name duplicated

+ 1 - 0
tests/qapi-schema/doc-duplicated-arg.exit

@@ -0,0 +1 @@
+1

+ 7 - 0
tests/qapi-schema/doc-duplicated-arg.json

@@ -0,0 +1,7 @@
+# Do not allow duplicated argument
+
+##
+# @foo:
+# @a:
+# @a:
+##

+ 0 - 0
tests/qapi-schema/doc-duplicated-arg.out


+ 1 - 0
tests/qapi-schema/doc-duplicated-return.err

@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-return.json:7:1: Duplicated 'Returns' section

+ 1 - 0
tests/qapi-schema/doc-duplicated-return.exit

@@ -0,0 +1 @@
+1

+ 8 - 0
tests/qapi-schema/doc-duplicated-return.json

@@ -0,0 +1,8 @@
+# Do not allow duplicated Returns section
+
+##
+# @foo:
+#
+# Returns: 0
+# Returns: 1
+##

+ 0 - 0
tests/qapi-schema/doc-duplicated-return.out


+ 1 - 0
tests/qapi-schema/doc-duplicated-since.err

@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-since.json:7:1: Duplicated 'Since' section

+ 1 - 0
tests/qapi-schema/doc-duplicated-since.exit

@@ -0,0 +1 @@
+1

+ 8 - 0
tests/qapi-schema/doc-duplicated-since.json

@@ -0,0 +1,8 @@
+# Do not allow duplicated Since section
+
+##
+# @foo:
+#
+# Since: 0
+# Since: 1
+##

+ 0 - 0
tests/qapi-schema/doc-duplicated-since.out


+ 1 - 0
tests/qapi-schema/doc-empty-arg.err

@@ -0,0 +1 @@
+tests/qapi-schema/doc-empty-arg.json:5:1: Invalid parameter name

+ 1 - 0
tests/qapi-schema/doc-empty-arg.exit

@@ -0,0 +1 @@
+1

+ 6 - 0
tests/qapi-schema/doc-empty-arg.json

@@ -0,0 +1,6 @@
+# An invalid empty argument name
+
+##
+# @foo:
+# @:
+##

+ 0 - 0
tests/qapi-schema/doc-empty-arg.out


+ 1 - 0
tests/qapi-schema/doc-empty-section.err

@@ -0,0 +1 @@
+tests/qapi-schema/doc-empty-section.json:3: Empty doc section 'Note'

+ 1 - 0
tests/qapi-schema/doc-empty-section.exit

@@ -0,0 +1 @@
+1

+ 8 - 0
tests/qapi-schema/doc-empty-section.json

@@ -0,0 +1,8 @@
+# Tagged-section must not be empty
+
+##
+# @foo:
+#
+# Note:
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }

+ 0 - 0
tests/qapi-schema/doc-empty-section.out


部分文件因文件數量過多而無法顯示