QAPI patches for 2019-10-29

-----BEGIN PGP SIGNATURE-----
 
 iQJGBAABCAAwFiEENUvIs9frKmtoZ05fOHC0AOuRhlMFAl233vgSHGFybWJydUBy
 ZWRoYXQuY29tAAoJEDhwtADrkYZTV4gP/02KEDNINA7n4Rm/56PpCDJiPB0xaro8
 zxd/GB6FoB84xHzDFLyzKCi1mgVgAnFYI2VJr0M7ILFfow+pJY4PfqkM4472HTUw
 devJgBlzBFTbuI6jnATKMfKnEo5baGItWJfuTpJE2d/EJo0A92JOtuFBnYqRiaLJ
 3UcQatTmwMTJRdzZewpQ6fdq387VAM9D+XVvCROq4kjjOUoSSRz9RwHJSTxEYISc
 jYDKJEUeVHMRfpht9x43PnI5f0TEkkGS+qKDKZSLauOepkgsvrYp/1byYfWRukiL
 Ioe38w7DTv66Sd4bp/+5iPXR2Z8TWX5IBaMwvEOiJlAGGQAQVhoUEMX3pftoCjZz
 ReMUrf7fYMUfJ93El7eDb/8cVipIq7oLtRruMzqENsUYAQTUjRmx7VL01Vukrlcu
 4xRWQ0Sk7zlQxOp6Zb8KS+qe0DKaVFJn9XR4vIrJ0VChqr2UTWUMSxuCUcebynaJ
 0JJ1ZNP92QaJM45g5U+zeNbWEYEm0LtsvUVy/vjaEi7hR0AkA4cckfTxAiRP/6kV
 mhENFKh1yhrJu/XWeBmMia9s2jRMmbZtI1ABiMR4K15MSCmX607pfC6ty9AZ1G+w
 2dFoBjwzqIYGmX82TXHeCr1ImrhGKl30SI+ccpNwqufAev7HcvOIxno4h2VWuBV0
 ZJ/F4oqoIJWo
 =c3NL
 -----END PGP SIGNATURE-----

Merge remote-tracking branch 'remotes/armbru/tags/pull-qapi-2019-10-29' into staging

QAPI patches for 2019-10-29

# gpg: Signature made Tue 29 Oct 2019 06:40:56 GMT
# gpg:                using RSA key 354BC8B3D7EB2A6B68674E5F3870B400EB918653
# gpg:                issuer "armbru@redhat.com"
# gpg: Good signature from "Markus Armbruster <armbru@redhat.com>" [full]
# gpg:                 aka "Markus Armbruster <armbru@pond.sub.org>" [full]
# Primary key fingerprint: 354B C8B3 D7EB 2A6B 6867  4E5F 3870 B400 EB91 8653

* remotes/armbru/tags/pull-qapi-2019-10-29:
  qapi: Check feature documentation against the schema
  qapi: Polish reporting of bogus member documentation
  qapi: Lift features into QAPISchemaEntity
  qapi: Fold normalize_enum() into check_enum()
  qapi: Fold normalize_features() into check_features()
  qapi: Fold normalize_if() into check_if()
  qapi: Eliminate .check_doc() overrides
  qapi: Simplify ._make_implicit_object_type()
  qapi: Fix doc comment checking for commands and events
  qapi: Clean up doc comment checking for implicit union base
  qapi: Fix enum doc comment checking
  qapi: Split .connect_doc(), .check_doc() off .check()
  qapi: De-duplicate entity documentation generation code
  qapi: Implement boxed event argument documentation
  qemu-doc: Belatedly document QMP command deprecation
  tests/qapi-schema: Fix feature documentation testing
  tests/qapi-schema: Cover alternate documentation comments
  tests/qapi-schema: Demonstrate command and event doc comment bugs
  tests/qapi-schema: Demonstrate feature and enum doc comment bugs

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
Peter Maydell 2019-10-29 20:06:08 +00:00
commit 16884391c7
29 changed files with 270 additions and 111 deletions

View File

@ -723,8 +723,6 @@
# Trigger generation of broadcast RARP frames to update network switches.
# This can be useful when network bonds fail-over the active slave.
#
# @params: AnnounceParameters giving timing and repetition count of announce
#
# Example:
#
# -> { "execute": "announce-self",

View File

@ -149,6 +149,18 @@ QEMU 4.1 has three options, please migrate to one of these three:
@section QEMU Machine Protocol (QMP) commands
@subsection change (since 2.5.0)
Use ``blockdev-change-medium'' or ``change-vnc-password'' instead.
@subsection migrate_set_downtime and migrate_set_speed (since 2.8.0)
Use ``migrate-set-parameters'' instead.
@subsection migrate-set-cache-size and query-migrate-cache-size (since 2.11.0)
Use ``migrate-set-parameters'' and ``query-migrate-parameters'' instead.
@subsection query-block result field dirty-bitmaps[i].status (since 4.0)
The ``status'' field of the ``BlockDirtyInfo'' structure, returned by

View File

@ -12,7 +12,7 @@ from qapi.gen import QAPIGenDoc, QAPISchemaVisitor
MSG_FMT = """
@deftypefn {type} {{}} {name}
{body}
{body}{members}{features}{sections}
@end deftypefn
""".format
@ -20,7 +20,7 @@ MSG_FMT = """
TYPE_FMT = """
@deftp {{{type}}} {name}
{body}
{body}{members}{features}{sections}
@end deftp
""".format
@ -149,7 +149,8 @@ def texi_member(member, desc, suffix):
suffix, desc, texi_if(member.ifcond, prefix='@*'))
def texi_members(doc, what, base, variants, member_func):
def texi_members(doc, what, base=None, variants=None,
member_func=texi_member):
"""Format the table of members"""
items = ''
for section in doc.args.values():
@ -182,6 +183,14 @@ def texi_members(doc, what, base, variants, member_func):
return '\n@b{%s:}\n@table @asis\n%s@end table\n' % (what, items)
def texi_arguments(doc, boxed_arg_type):
if boxed_arg_type:
assert not doc.args
return ('\n@b{Arguments:} the members of @code{%s}\n'
% boxed_arg_type.name)
return texi_members(doc, 'Arguments')
def texi_features(doc):
"""Format the table of features"""
items = ''
@ -208,12 +217,22 @@ def texi_sections(doc, ifcond):
return body
def texi_entity(doc, what, ifcond, base=None, variants=None,
member_func=texi_member):
return (texi_body(doc)
+ texi_members(doc, what, base, variants, member_func)
+ texi_features(doc)
+ texi_sections(doc, ifcond))
def texi_type(typ, doc, ifcond, members):
return TYPE_FMT(type=typ,
name=doc.symbol,
body=texi_body(doc),
members=members,
features=texi_features(doc),
sections=texi_sections(doc, ifcond))
def texi_msg(typ, doc, ifcond, members):
return MSG_FMT(type=typ,
name=doc.symbol,
body=texi_body(doc),
members=members,
features=texi_features(doc),
sections=texi_sections(doc, ifcond))
class QAPISchemaGenDocVisitor(QAPISchemaVisitor):
@ -227,48 +246,36 @@ class QAPISchemaGenDocVisitor(QAPISchemaVisitor):
def visit_enum_type(self, name, info, ifcond, members, prefix):
doc = self.cur_doc
self._gen.add(TYPE_FMT(type='Enum',
name=doc.symbol,
body=texi_entity(doc, 'Values', ifcond,
member_func=texi_enum_value)))
self._gen.add(texi_type('Enum', doc, ifcond,
texi_members(doc, 'Values',
member_func=texi_enum_value)))
def visit_object_type(self, name, info, ifcond, base, members, variants,
features):
doc = self.cur_doc
if base and base.is_implicit():
base = None
self._gen.add(TYPE_FMT(type='Object',
name=doc.symbol,
body=texi_entity(doc, 'Members', ifcond,
base, variants)))
self._gen.add(texi_type('Object', doc, ifcond,
texi_members(doc, 'Members', base, variants)))
def visit_alternate_type(self, name, info, ifcond, variants):
doc = self.cur_doc
self._gen.add(TYPE_FMT(type='Alternate',
name=doc.symbol,
body=texi_entity(doc, 'Members', ifcond)))
self._gen.add(texi_type('Alternate', doc, ifcond,
texi_members(doc, 'Members')))
def visit_command(self, name, info, ifcond, arg_type, ret_type, gen,
success_response, boxed, allow_oob, allow_preconfig,
features):
doc = self.cur_doc
if boxed:
body = texi_body(doc)
body += ('\n@b{Arguments:} the members of @code{%s}\n'
% arg_type.name)
body += texi_features(doc)
body += texi_sections(doc, ifcond)
else:
body = texi_entity(doc, 'Arguments', ifcond)
self._gen.add(MSG_FMT(type='Command',
name=doc.symbol,
body=body))
self._gen.add(texi_msg('Command', doc, ifcond,
texi_arguments(doc,
arg_type if boxed else None)))
def visit_event(self, name, info, ifcond, arg_type, boxed):
doc = self.cur_doc
self._gen.add(MSG_FMT(type='Event',
name=doc.symbol,
body=texi_entity(doc, 'Arguments', ifcond)))
self._gen.add(texi_msg('Event', doc, ifcond,
texi_arguments(doc,
arg_type if boxed else None)))
def symbol(self, doc, entity):
if self._gen._body:

View File

@ -95,12 +95,6 @@ def check_flags(expr, info):
info, "flag '%s' may only use true value" % key)
def normalize_if(expr):
ifcond = expr.get('if')
if isinstance(ifcond, str):
expr['if'] = [ifcond]
def check_if(expr, info, source):
def check_if_str(ifcond, info):
@ -126,6 +120,7 @@ def check_if(expr, info, source):
check_if_str(elt, info)
else:
check_if_str(ifcond, info)
expr['if'] = [ifcond]
def normalize_members(members):
@ -175,21 +170,16 @@ def check_type(value, info, source,
raise QAPISemError(info, "%s uses reserved name" % key_source)
check_keys(arg, info, key_source, ['type'], ['if'])
check_if(arg, info, key_source)
normalize_if(arg)
check_type(arg['type'], info, key_source, allow_array=True)
def normalize_features(features):
if isinstance(features, list):
features[:] = [f if isinstance(f, dict) else {'name': f}
for f in features]
def check_features(features, info):
if features is None:
return
if not isinstance(features, list):
raise QAPISemError(info, "'features' must be an array")
features[:] = [f if isinstance(f, dict) else {'name': f}
for f in features]
for f in features:
source = "'features' member"
assert isinstance(f, dict)
@ -198,13 +188,6 @@ def check_features(features, info):
source = "%s '%s'" % (source, f['name'])
check_name_str(f['name'], info, source)
check_if(f, info, source)
normalize_if(f)
def normalize_enum(expr):
if isinstance(expr['data'], list):
expr['data'] = [m if isinstance(m, dict) else {'name': m}
for m in expr['data']]
def check_enum(expr, info):
@ -219,6 +202,8 @@ def check_enum(expr, info):
permit_upper = name in info.pragma.name_case_whitelist
members[:] = [m if isinstance(m, dict) else {'name': m}
for m in members]
for member in members:
source = "'data' member"
check_keys(member, info, source, ['name'], ['if'])
@ -227,7 +212,6 @@ def check_enum(expr, info):
check_name_str(member['name'], info, source,
enum_member=True, permit_upper=permit_upper)
check_if(member, info, source)
normalize_if(member)
def check_struct(expr, info):
@ -259,7 +243,6 @@ def check_union(expr, info):
check_name_str(key, info, source)
check_keys(value, info, source, ['type'], ['if'])
check_if(value, info, source)
normalize_if(value)
check_type(value['type'], info, source, allow_array=not base)
@ -273,7 +256,6 @@ def check_alternate(expr, info):
check_name_str(key, info, source)
check_keys(value, info, source, ['type'], ['if'])
check_if(value, info, source)
normalize_if(value)
check_type(value['type'], info, source)
@ -339,7 +321,6 @@ def check_exprs(exprs):
if meta == 'enum':
check_keys(expr, info, meta,
['enum', 'data'], ['if', 'prefix'])
normalize_enum(expr)
check_enum(expr, info)
elif meta == 'union':
check_keys(expr, info, meta,
@ -357,7 +338,6 @@ def check_exprs(exprs):
check_keys(expr, info, meta,
['struct', 'data'], ['base', 'if', 'features'])
normalize_members(expr['data'])
normalize_features(expr.get('features'))
check_struct(expr, info)
elif meta == 'command':
check_keys(expr, info, meta,
@ -366,7 +346,6 @@ def check_exprs(exprs):
'gen', 'success-response', 'allow-oob',
'allow-preconfig'])
normalize_members(expr.get('data'))
normalize_features(expr.get('features'))
check_command(expr, info)
elif meta == 'event':
check_keys(expr, info, meta,
@ -376,7 +355,6 @@ def check_exprs(exprs):
else:
assert False, 'unexpected meta type'
normalize_if(expr)
check_if(expr, info, meta)
check_flags(expr, info)

View File

@ -555,16 +555,31 @@ class QAPIDoc(object):
self.args[member.name] = QAPIDoc.ArgSection(member.name)
self.args[member.name].connect(member)
def connect_feature(self, feature):
if feature.name not in self.features:
raise QAPISemError(feature.info,
"feature '%s' lacks documentation"
% feature.name)
self.features[feature.name] = QAPIDoc.ArgSection(feature.name)
self.features[feature.name].connect(feature)
def check_expr(self, expr):
if self.has_section('Returns') and 'command' not in expr:
raise QAPISemError(self.info,
"'Returns:' is only valid for commands")
def check(self):
bogus = [name for name, section in self.args.items()
if not section.member]
if bogus:
raise QAPISemError(
self.info,
"the following documented members are not in "
"the declaration: %s" % ", ".join(bogus))
def check_args_section(args, info, what):
bogus = [name for name, section in args.items()
if not section.member]
if bogus:
raise QAPISemError(
self.info,
"documented member%s '%s' %s not exist"
% ("s" if len(bogus) > 1 else "",
"', '".join(bogus),
"do" if len(bogus) > 1 else "does"))
check_args_section(self.args, self.info, 'members')
check_args_section(self.features, self.info, 'features')

View File

@ -27,8 +27,11 @@ from qapi.parser import QAPISchemaParser
class QAPISchemaEntity(object):
meta = None
def __init__(self, name, info, doc, ifcond=None):
def __init__(self, name, info, doc, ifcond=None, features=None):
assert name is None or isinstance(name, str)
for f in features or []:
assert isinstance(f, QAPISchemaFeature)
f.set_defined_in(name)
self.name = name
self._module = None
# For explicitly defined entities, info points to the (explicit)
@ -39,6 +42,7 @@ class QAPISchemaEntity(object):
self.info = info
self.doc = doc
self._ifcond = ifcond or []
self.features = features or []
self._checked = False
def c_name(self):
@ -49,8 +53,21 @@ class QAPISchemaEntity(object):
if self.info:
self._module = os.path.relpath(self.info.fname,
os.path.dirname(schema.fname))
seen = {}
for f in self.features:
f.check_clash(self.info, seen)
if self.doc:
self.doc.connect_feature(f)
self._checked = True
def connect_doc(self, doc=None):
pass
def check_doc(self):
if self.doc:
self.doc.check()
@property
def ifcond(self):
assert self._checked
@ -217,8 +234,12 @@ class QAPISchemaEnumType(QAPISchemaType):
seen = {}
for m in self.members:
m.check_clash(self.info, seen)
if self.doc:
self.doc.connect_member(m)
def connect_doc(self, doc=None):
doc = doc or self.doc
if doc:
for m in self.members:
doc.connect_member(m)
def is_implicit(self):
# See QAPISchema._make_implicit_enum_type() and ._def_predefineds()
@ -296,7 +317,7 @@ class QAPISchemaObjectType(QAPISchemaType):
# struct has local_members, optional base, and no variants
# flat union has base, variants, and no local_members
# simple union has local_members, variants, and no base
QAPISchemaType.__init__(self, name, info, doc, ifcond)
QAPISchemaType.__init__(self, name, info, doc, ifcond, features)
self.meta = 'union' if variants else 'struct'
assert base is None or isinstance(base, str)
for m in local_members:
@ -305,15 +326,11 @@ class QAPISchemaObjectType(QAPISchemaType):
if variants is not None:
assert isinstance(variants, QAPISchemaObjectTypeVariants)
variants.set_defined_in(name)
for f in features:
assert isinstance(f, QAPISchemaFeature)
f.set_defined_in(name)
self._base_name = base
self.base = None
self.local_members = local_members
self.variants = variants
self.members = None
self.features = features
def check(self, schema):
# This calls another type T's .check() exactly when the C
@ -345,22 +362,12 @@ class QAPISchemaObjectType(QAPISchemaType):
for m in self.local_members:
m.check(schema)
m.check_clash(self.info, seen)
if self.doc:
self.doc.connect_member(m)
members = seen.values()
if self.variants:
self.variants.check(schema, seen)
self.variants.check_clash(self.info, seen)
# Features are in a name space separate from members
seen = {}
for f in self.features:
f.check_clash(self.info, seen)
if self.doc:
self.doc.check()
self.members = members # mark completed
# Check that the members of this type do not cause duplicate JSON members,
@ -372,6 +379,14 @@ class QAPISchemaObjectType(QAPISchemaType):
for m in self.members:
m.check_clash(info, seen)
def connect_doc(self, doc=None):
doc = doc or self.doc
if doc:
if self.base and self.base.is_implicit():
self.base.connect_doc(doc)
for m in self.local_members:
doc.connect_member(m)
@property
def ifcond(self):
assert self._checked
@ -639,10 +654,12 @@ class QAPISchemaAlternateType(QAPISchemaType):
"%s can't be distinguished from '%s'"
% (v.describe(self.info), types_seen[qt]))
types_seen[qt] = v.name
if self.doc:
self.doc.connect_member(v)
if self.doc:
self.doc.check()
def connect_doc(self, doc=None):
doc = doc or self.doc
if doc:
for v in self.variants.variants:
doc.connect_member(v)
def c_type(self):
return c_name(self.name) + pointer_suffix
@ -662,12 +679,9 @@ class QAPISchemaCommand(QAPISchemaEntity):
def __init__(self, name, info, doc, ifcond, arg_type, ret_type,
gen, success_response, boxed, allow_oob, allow_preconfig,
features):
QAPISchemaEntity.__init__(self, name, info, doc, ifcond)
QAPISchemaEntity.__init__(self, name, info, doc, ifcond, features)
assert not arg_type or isinstance(arg_type, str)
assert not ret_type or isinstance(ret_type, str)
for f in features:
assert isinstance(f, QAPISchemaFeature)
f.set_defined_in(name)
self._arg_type_name = arg_type
self.arg_type = None
self._ret_type_name = ret_type
@ -677,7 +691,6 @@ class QAPISchemaCommand(QAPISchemaEntity):
self.boxed = boxed
self.allow_oob = allow_oob
self.allow_preconfig = allow_preconfig
self.features = features
def check(self, schema):
QAPISchemaEntity.check(self, schema)
@ -707,10 +720,11 @@ class QAPISchemaCommand(QAPISchemaEntity):
"command's 'returns' cannot take %s"
% self.ret_type.describe())
# Features are in a name space separate from members
seen = {}
for f in self.features:
f.check_clash(self.info, seen)
def connect_doc(self, doc=None):
doc = doc or self.doc
if doc:
if self.arg_type and self.arg_type.is_implicit():
self.arg_type.connect_doc(doc)
def visit(self, visitor):
QAPISchemaEntity.visit(self, visitor)
@ -748,6 +762,12 @@ class QAPISchemaEvent(QAPISchemaEntity):
"event's 'data' can take %s only with 'boxed': true"
% self.arg_type.describe())
def connect_doc(self, doc=None):
doc = doc or self.doc
if doc:
if self.arg_type and self.arg_type.is_implicit():
self.arg_type.connect_doc(doc)
def visit(self, visitor):
QAPISchemaEntity.visit(self, visitor)
visitor.visit_event(self.name, self.info, self.ifcond,
@ -873,8 +893,7 @@ class QAPISchema(object):
self._def_entity(QAPISchemaArrayType(name, info, element_type))
return name
def _make_implicit_object_type(self, name, info, doc, ifcond,
role, members):
def _make_implicit_object_type(self, name, info, ifcond, role, members):
if not members:
return None
# See also QAPISchemaObjectTypeMember.describe()
@ -892,7 +911,7 @@ class QAPISchema(object):
# TODO kill simple unions or implement the disjunction
assert (ifcond or []) == typ._ifcond # pylint: disable=protected-access
else:
self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond,
self._def_entity(QAPISchemaObjectType(name, info, None, ifcond,
None, members, None, []))
return name
@ -939,7 +958,7 @@ class QAPISchema(object):
assert len(typ) == 1
typ = self._make_array_type(typ[0], info)
typ = self._make_implicit_object_type(
typ, info, None, self.lookup_type(typ),
typ, info, self.lookup_type(typ),
'wrapper', [self._make_member('data', typ, None, info)])
return QAPISchemaObjectTypeVariant(case, info, typ, ifcond)
@ -952,7 +971,7 @@ class QAPISchema(object):
tag_member = None
if isinstance(base, dict):
base = self._make_implicit_object_type(
name, info, doc, ifcond,
name, info, ifcond,
'base', self._make_members(base, info))
if tag_name:
variants = [self._make_variant(key, value['type'],
@ -999,7 +1018,7 @@ class QAPISchema(object):
features = expr.get('features', [])
if isinstance(data, OrderedDict):
data = self._make_implicit_object_type(
name, info, doc, ifcond, 'arg', self._make_members(data, info))
name, info, ifcond, 'arg', self._make_members(data, info))
if isinstance(rets, list):
assert len(rets) == 1
rets = self._make_array_type(rets[0], info)
@ -1015,7 +1034,7 @@ class QAPISchema(object):
ifcond = expr.get('if')
if isinstance(data, OrderedDict):
data = self._make_implicit_object_type(
name, info, doc, ifcond, 'arg', self._make_members(data, info))
name, info, ifcond, 'arg', self._make_members(data, info))
self._def_entity(QAPISchemaEvent(name, info, doc, ifcond, data, boxed))
def _def_exprs(self, exprs):
@ -1043,6 +1062,8 @@ class QAPISchema(object):
def check(self):
for ent in self._entity_list:
ent.check(self)
ent.connect_doc()
ent.check_doc()
def visit(self, visitor):
visitor.visit_begin(self)

View File

@ -341,7 +341,11 @@ qapi-schema += base-cycle-indirect.json
qapi-schema += command-int.json
qapi-schema += comments.json
qapi-schema += doc-bad-alternate-member.json
qapi-schema += doc-bad-boxed-command-arg.json
qapi-schema += doc-bad-command-arg.json
qapi-schema += doc-bad-enum-member.json
qapi-schema += doc-bad-event-arg.json
qapi-schema += doc-bad-feature.json
qapi-schema += doc-bad-section.json
qapi-schema += doc-bad-symbol.json
qapi-schema += doc-bad-union-member.json
@ -365,6 +369,7 @@ qapi-schema += doc-missing-expr.json
qapi-schema += doc-missing-space.json
qapi-schema += doc-missing.json
qapi-schema += doc-no-symbol.json
qapi-schema += doc-undoc-feature.json
qapi-schema += double-type.json
qapi-schema += duplicate-key.json
qapi-schema += empty.json

View File

@ -1 +1 @@
doc-bad-alternate-member.json:3: the following documented members are not in the declaration: aa, bb
doc-bad-alternate-member.json:3: documented members 'aa', 'bb' do not exist

View File

@ -0,0 +1 @@
doc-bad-boxed-command-arg.json:9: documented member 'a' does not exist

View File

@ -0,0 +1,14 @@
# Boxed arguments are not to be documented with the command
##
# @Args:
# @a: an argument
##
{ 'struct': 'Args', 'data': { 'a': 'int' } }
##
# @cmd-boxed:
# @a: bogus
##
{ 'command': 'cmd-boxed', 'boxed': true,
'data': 'Args' }

View File

@ -1 +1 @@
doc-bad-command-arg.json:3: the following documented members are not in the declaration: b
doc-bad-command-arg.json:3: documented member 'b' does not exist

View File

@ -0,0 +1 @@
doc-bad-enum-member.json:3: documented member 'a' does not exist

View File

@ -0,0 +1,8 @@
# Members listed in the doc comment must exist in the actual schema
##
# @Foo:
# @a: a
# @b: b
##
{ 'enum': 'Foo', 'data': [ 'b' ] }

View File

@ -0,0 +1 @@
doc-bad-event-arg.json:3: documented member 'a' does not exist

View File

@ -0,0 +1,7 @@
# Arguments listed in the doc comment must exist in the actual schema
##
# @FOO:
# @a: a
##
{ 'event': 'FOO' }

View File

View File

@ -0,0 +1 @@
doc-bad-feature.json:3: documented member 'a' does not exist

View File

@ -0,0 +1,9 @@
# Features listed in the doc comment must exist in the actual schema
##
# @foo:
#
# Features:
# @a: a
##
{ 'command': 'foo' }

View File

View File

@ -1 +1 @@
doc-bad-union-member.json:3: the following documented members are not in the declaration: a, b
doc-bad-union-member.json:3: documented members 'a', 'b' do not exist

View File

@ -98,6 +98,14 @@
{ 'union': 'SugaredUnion',
'data': { 'one': 'Variant1', 'two': { 'type': 'Variant2', 'if': 'IFTWO' } } }
##
# @Alternate:
# @i: an integer
# @b is undocumented
##
{ 'alternate': 'Alternate',
'data': { 'i': 'int', 'b': 'bool' } }
##
# == Another subsection
##
@ -149,3 +157,9 @@
{ 'command': 'cmd-boxed', 'boxed': true,
'data': 'Object',
'features': [ 'cmd-feat1', 'cmd-feat2' ] }
##
# @EVT-BOXED:
##
{ 'event': 'EVT-BOXED', 'boxed': true,
'data': 'Object' }

View File

@ -42,6 +42,10 @@ object SugaredUnion
case one: q_obj_Variant1-wrapper
case two: q_obj_Variant2-wrapper
if ['IFTWO']
alternate Alternate
tag type
case i: int
case b: bool
object q_obj_cmd-arg
member arg1: int optional=False
member arg2: str optional=True
@ -54,6 +58,8 @@ command cmd-boxed Object -> None
gen=True success_response=True boxed=True oob=False preconfig=False
feature cmd-feat1
feature cmd-feat2
event EVT-BOXED Object
boxed=True
doc freeform
body=
= Section
@ -120,6 +126,8 @@ A paragraph
Another paragraph (but no @var: line)
arg=var1
feature=variant1-feat
a feature
doc symbol=Variant2
body=
@ -131,6 +139,14 @@ doc symbol=SugaredUnion
arg=type
doc symbol=Alternate
body=
arg=i
an integer
@b is undocumented
arg=b
doc freeform
body=
== Another subsection
@ -144,6 +160,10 @@ the second
argument
arg=arg3
feature=cmd-feat1
a feature
feature=cmd-feat2
another feature
section=Note
@arg3 is undocumented
section=Returns
@ -166,7 +186,14 @@ Duis aute irure dolor
doc symbol=cmd-boxed
body=
If you're bored enough to read this, go see a video of boxed cats
feature=cmd-feat1
a feature
feature=cmd-feat2
another feature
section=Example
-> in
<- out
doc symbol=EVT-BOXED
body=

View File

@ -170,6 +170,23 @@ One of @t{"one"}, @t{"two"}
@end deftp
@deftp {Alternate} Alternate
@b{Members:}
@table @asis
@item @code{i: int}
an integer
@code{b} is undocumented
@item @code{b: boolean}
Not documented
@end table
@end deftp
@subsection Another subsection
@ -258,3 +275,13 @@ another feature
@end deftypefn
@deftypefn Event {} EVT-BOXED
@b{Arguments:} the members of @code{Object}
@end deftypefn

View File

@ -0,0 +1,2 @@
doc-undoc-feature.json: In command 'foo':
doc-undoc-feature.json:9: feature 'undoc' lacks documentation

View File

@ -0,0 +1,9 @@
# Doc comment must cover all features
##
# @foo:
#
# Features:
# @doc: documented feature
##
{ 'command': 'foo', 'features': ['undoc', 'doc'] }

View File

View File

@ -117,6 +117,8 @@ def test_frontend(fname):
print(' body=\n%s' % doc.body.text)
for arg, section in doc.args.items():
print(' arg=%s\n%s' % (arg, section.text))
for feat, section in doc.features.items():
print(' feature=%s\n%s' % (feat, section.text))
for section in doc.sections:
print(' section=%s\n%s' % (section.name, section.text))