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>
This commit is contained in:
Marc-André Lureau 2017-01-13 15:41:29 +01:00 committed by Markus Armbruster
parent 231aaf3a82
commit 3313b6124b
276 changed files with 1925 additions and 127 deletions

View File

@ -44,18 +44,103 @@ Input must be ASCII (although QMP supports full Unicode strings, the
QAPI parser does not). At present, there is no place where a QAPI QAPI parser does not). At present, there is no place where a QAPI
schema requires the use of JSON numbers or null. schema requires the use of JSON numbers or null.
=== Comments ===
Comments are allowed; anything between an unquoted # and the following Comments are allowed; anything between an unquoted # and the following
newline is ignored. Although there is not yet a documentation newline is ignored.
generator, a form of stylized comments has developed for consistently
documenting details about an expression and when it was added to the A multi-line comment that starts and ends with a '##' line is a
schema. The documentation is delimited between two lines of ##, then documentation comment. These are parsed by the documentation
the first line names the expression, an optional overview is provided, generator, which recognizes certain markup detailed below.
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 ==== Documentation markup ====
'#optional', often with their default value; and extensions added
after the expression was first released are also given a '(since Comment text starting with '=' is a section title:
x.y.z)' comment. For example:
# = 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: # @BlockStats:
@ -65,19 +150,48 @@ x.y.z)' comment. For example:
# @device: #optional If the stats are for a virtual block device, the name # @device: #optional If the stats are for a virtual block device, the name
# corresponding to the virtual block device. # corresponding to the virtual block device.
# #
# @stats: A @BlockDeviceStats for the device. # @node-name: #optional The node name of the device. (since 2.3)
# #
# @parent: #optional This describes the file block device if it has one. # ... more members ...
#
# @backing: #optional This describes the backing block device if it has one.
# (Since 2.0)
# #
# Since: 0.14.0 # Since: 0.14.0
## ##
{ 'struct': 'BlockStats', { 'struct': 'BlockStats',
'data': {'*device': 'str', 'stats': 'BlockDeviceStats', 'data': {'*device': 'str', '*node-name': 'str',
'*parent': 'BlockStats', ... more members ... } }
'*backing': 'BlockStats'} }
##
# @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 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 that will use those types. Forward references are allowed: the parser

View File

@ -125,6 +125,122 @@ def __init__(self, info, msg):
info['parent'], 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): class QAPISchemaParser(object):
def __init__(self, fp, previously_included=[], incl_info=None): 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 = 1
self.line_pos = 0 self.line_pos = 0
self.exprs = [] self.exprs = []
self.docs = []
self.accept() self.accept()
while self.tok is not None: while self.tok is not None:
info = {'file': fname, 'line': self.line, info = {'file': fname, 'line': self.line,
'parent': self.incl_info} 'parent': self.incl_info}
if self.tok == '#':
doc = self.get_doc(info)
self.docs.append(doc)
continue
expr = self.get_expr(False) expr = self.get_expr(False)
if isinstance(expr, dict) and "include" in expr: if isinstance(expr, dict) and "include" in expr:
if len(expr) != 1: if len(expr) != 1:
@ -162,6 +284,7 @@ def __init__(self, fp, previously_included=[], incl_info=None):
raise QAPISemError(info, "Inclusion loop for %s" raise QAPISemError(info, "Inclusion loop for %s"
% include) % include)
inf = inf['parent'] inf = inf['parent']
# skip multiple include of the same file # skip multiple include of the same file
if incl_abs_fname in previously_included: if incl_abs_fname in previously_included:
continue continue
@ -172,12 +295,19 @@ def __init__(self, fp, previously_included=[], incl_info=None):
exprs_include = QAPISchemaParser(fobj, previously_included, exprs_include = QAPISchemaParser(fobj, previously_included,
info) info)
self.exprs.extend(exprs_include.exprs) self.exprs.extend(exprs_include.exprs)
self.docs.extend(exprs_include.docs)
else: else:
expr_elem = {'expr': expr, expr_elem = {'expr': expr,
'info': info} '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) self.exprs.append(expr_elem)
def accept(self): def accept(self, skip_comment=True):
while True: while True:
self.tok = self.src[self.cursor] self.tok = self.src[self.cursor]
self.pos = self.cursor self.pos = self.cursor
@ -185,7 +315,13 @@ def accept(self):
self.val = None self.val = None
if self.tok == '#': if self.tok == '#':
if self.src[self.cursor] == '#':
# Start of doc comment
skip_comment = False
self.cursor = self.src.find('\n', self.cursor) 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 "{}:,[]": elif self.tok in "{}:,[]":
return return
elif self.tok == "'": elif self.tok == "'":
@ -319,6 +455,28 @@ def get_expr(self, nested):
raise QAPIParseError(self, 'Expected "{", "[" or string') raise QAPIParseError(self, 'Expected "{", "[" or string')
return expr 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 # Semantic analysis of schema expressions
# TODO fold into QAPISchema # TODO fold into QAPISchema
@ -703,6 +861,11 @@ def check_exprs(exprs):
for expr_elem in exprs: for expr_elem in exprs:
expr = expr_elem['expr'] expr = expr_elem['expr']
info = expr_elem['info'] info = expr_elem['info']
if 'doc' not in expr_elem:
raise QAPISemError(info,
"Expression missing documentation comment")
if 'enum' in expr: if 'enum' in expr:
check_keys(expr_elem, 'enum', ['data'], ['prefix']) check_keys(expr_elem, 'enum', ['data'], ['prefix'])
add_enum(expr['enum'], info, expr['data']) add_enum(expr['enum'], info, expr['data'])
@ -761,6 +924,88 @@ def check_exprs(exprs):
return 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 # Schema compiler frontend
# #
@ -1229,7 +1474,9 @@ def visit(self, visitor):
class QAPISchema(object): class QAPISchema(object):
def __init__(self, fname): def __init__(self, fname):
try: 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._entity_dict = {}
self._predefining = True self._predefining = True
self._def_predefineds() self._def_predefineds()

271
scripts/qapi2texi.py Executable file
View File

@ -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)

View File

@ -352,6 +352,24 @@ qapi-schema += base-cycle-direct.json
qapi-schema += base-cycle-indirect.json qapi-schema += base-cycle-indirect.json
qapi-schema += command-int.json qapi-schema += command-int.json
qapi-schema += comments.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-data.json
qapi-schema += double-type.json qapi-schema += double-type.json
qapi-schema += duplicate-key.json qapi-schema += duplicate-key.json
@ -445,6 +463,8 @@ qapi-schema += union-optional-branch.json
qapi-schema += union-unknown.json qapi-schema += union-unknown.json
qapi-schema += unknown-escape.json qapi-schema += unknown-escape.json
qapi-schema += unknown-expr-key.json qapi-schema += unknown-expr-key.json
check-qapi-schema-y := $(addprefix tests/qapi-schema/, $(qapi-schema)) check-qapi-schema-y := $(addprefix tests/qapi-schema/, $(qapi-schema))
GENERATED_HEADERS += tests/test-qapi-types.h tests/test-qapi-visit.h \ GENERATED_HEADERS += tests/test-qapi-types.h tests/test-qapi-visit.h \

View File

@ -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'

View File

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

View File

@ -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

View File

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

View File

@ -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'

View File

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

View File

@ -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)

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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

View File

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

View File

@ -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'

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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'

View File

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

View File

@ -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)

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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'

View File

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

View File

@ -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

View File

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

View File

@ -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'

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
1

View File

@ -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'} }

View File

View File

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

View File

@ -0,0 +1 @@
1

View File

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

View File

View File

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

View File

@ -0,0 +1 @@
1

View File

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

View File

View File

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

View File

@ -0,0 +1 @@
1

View File

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

View File

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

View File

@ -0,0 +1 @@
1

View File

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

View File

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

View File

@ -0,0 +1 @@
1

View File

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

View File

View File

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

View File

@ -0,0 +1 @@
1

View File

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

View File

Some files were not shown because too many files have changed in this diff Show More