QAPI patches patches for 2024-02-26

-----BEGIN PGP SIGNATURE-----
 
 iQJGBAABCAAwFiEENUvIs9frKmtoZ05fOHC0AOuRhlMFAmXcXWUSHGFybWJydUBy
 ZWRoYXQuY29tAAoJEDhwtADrkYZTMqQP/3aZ8Jm48Bh4sG5JxJ01aM3iX//cuA8X
 0xoKBfDWkwKvUDE+sv+2qH1WxcNGS4wb/lLaT6Jf0S8EzzelDx5zYGwXXsmBljoZ
 Kh8BI7Oe1SQwI5Z36q/efG9isQ6nqGyxR9xu1BTbJCSOZXzxpOBljbSyXOtKc5C/
 HM4TK82geLLgm04gMkoPmFdu3BXDfPYnGJ3UNLE9hTPoW7XL4EGFAOOWuDcrSF5n
 OdHjfK9TK9PcxsJGqVUqLHwfRQYLMBni6OkurdFOVdLM1v4C707NuryjaGQc1WEI
 xKwsJDKR+zG7vGu4y594HN/Ivoaqci8MMTbDgVmHZ3LaI3RUfSplGTDSYLjCp8jz
 XDx82+hhmb/2ZMmE0tarUQyv0dimrZSEh6cWWHMvp63edKTywoB/eIDR9lBteTZe
 wRvkSKmN6oKJI8cNiiXZqw5y2JPvhNag4Xdr8kHKwHgxVWP+SneInLCC+T2SMgio
 EeC+S4CVTdjPvEC96dOGrsqKn+gl/h74PK5ZdTaD1B6XCuIalsRn6REujqW6Ew6n
 rj7Iec/noejeOsflzBWRKT91t2Zck/MRLhX9nYqybBxyxUFvFS7M6ok/iq4oEtZR
 lJooF6iiq8xtEzoLselfGFAZTUxhwLdUfXPVDx7p5HDpJci88xv6zmav9eE84JbH
 mBD55GEH17ka
 =81Zq
 -----END PGP SIGNATURE-----

Merge tag 'pull-qapi-2024-02-26' of https://repo.or.cz/qemu/armbru into staging

QAPI patches patches for 2024-02-26

# -----BEGIN PGP SIGNATURE-----
#
# iQJGBAABCAAwFiEENUvIs9frKmtoZ05fOHC0AOuRhlMFAmXcXWUSHGFybWJydUBy
# ZWRoYXQuY29tAAoJEDhwtADrkYZTMqQP/3aZ8Jm48Bh4sG5JxJ01aM3iX//cuA8X
# 0xoKBfDWkwKvUDE+sv+2qH1WxcNGS4wb/lLaT6Jf0S8EzzelDx5zYGwXXsmBljoZ
# Kh8BI7Oe1SQwI5Z36q/efG9isQ6nqGyxR9xu1BTbJCSOZXzxpOBljbSyXOtKc5C/
# HM4TK82geLLgm04gMkoPmFdu3BXDfPYnGJ3UNLE9hTPoW7XL4EGFAOOWuDcrSF5n
# OdHjfK9TK9PcxsJGqVUqLHwfRQYLMBni6OkurdFOVdLM1v4C707NuryjaGQc1WEI
# xKwsJDKR+zG7vGu4y594HN/Ivoaqci8MMTbDgVmHZ3LaI3RUfSplGTDSYLjCp8jz
# XDx82+hhmb/2ZMmE0tarUQyv0dimrZSEh6cWWHMvp63edKTywoB/eIDR9lBteTZe
# wRvkSKmN6oKJI8cNiiXZqw5y2JPvhNag4Xdr8kHKwHgxVWP+SneInLCC+T2SMgio
# EeC+S4CVTdjPvEC96dOGrsqKn+gl/h74PK5ZdTaD1B6XCuIalsRn6REujqW6Ew6n
# rj7Iec/noejeOsflzBWRKT91t2Zck/MRLhX9nYqybBxyxUFvFS7M6ok/iq4oEtZR
# lJooF6iiq8xtEzoLselfGFAZTUxhwLdUfXPVDx7p5HDpJci88xv6zmav9eE84JbH
# mBD55GEH17ka
# =81Zq
# -----END PGP SIGNATURE-----
# gpg: Signature made Mon 26 Feb 2024 09:44:05 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

* tag 'pull-qapi-2024-02-26' of https://repo.or.cz/qemu/armbru:
  qapi: Divorce QAPIDoc from QAPIParseError
  qapi: Reject multiple and empty feature descriptions
  qapi: Rewrite doc comment parser
  qapi: Merge adjacent untagged sections
  qapi: Call QAPIDoc.check() always
  qapi: Recognize section tags and 'Features:' only after blank line
  qapi: Require descriptions and tagged sections to be indented
  qapi: Reject section heading in the middle of a doc comment
  qapi: Rename QAPIDoc.Section.name to .tag
  qapi: Improve error message for empty doc sections
  qapi: Improve error position for bogus invalid "Returns" section
  qapi: Improve error position for bogus argument descriptions
  sphinx/qapidoc: Drop code to generate doc for simple union branch
  tests/qapi-schema: Cover 'Features:' not followed by descriptions
  tests/qapi-schema: Cover duplicate 'Features:' line
  tests/qapi-schema: Fix test 'QAPI rST doc'
  qapi: Misc cleanups to migrate QAPIs

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
Peter Maydell 2024-02-26 11:22:32 +00:00
commit 03d496a992
58 changed files with 2624 additions and 2630 deletions

View File

@ -973,7 +973,7 @@ commands and events), member (for structs and unions), branch (for
alternates), or value (for enums), a description of each feature (if
any), and finally optional tagged sections.
Descriptions start with '\@name:'. The description text should be
Descriptions start with '\@name:'. The description text must be
indented like this::
# @name: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
@ -986,19 +986,20 @@ indented like this::
Extensions added after the definition was first released carry a
"(since x.y.z)" comment.
The feature descriptions must be preceded by a line "Features:", like
this::
The feature descriptions must be preceded by a blank line and then a
line "Features:", like this::
#
# Features:
#
# @feature: Description text
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 tagged section begins with a paragraph that starts with one of the
following words: "Note:"/"Notes:", "Since:", "Example:"/"Examples:",
"Returns:", "TODO:". It ends with the start of a new section.
The second and subsequent lines of sections other than
"Example"/"Examples" should be indented like this::
The second and subsequent lines of tagged sections must be indented
like this::
# Note: Ut enim ad minim veniam, quis nostrud exercitation ullamco
# laboris nisi ut aliquip ex ea commodo consequat.
@ -1053,7 +1054,6 @@ For example::
# <- {
# ... lots of output ...
# }
#
##
{ 'command': 'query-blockstats',
'data': { '*query-nodes': 'bool' },
@ -1087,8 +1087,10 @@ need to line up with each other, like this::
# or cache associativity unknown)
# (since 5.0)
Section tags are case-sensitive and end with a colon. Good example::
Section tags are case-sensitive and end with a colon. They are only
recognized after a blank line. Good example::
#
# Since: 7.1
Bad examples (all ordinary paragraphs)::

View File

@ -180,13 +180,9 @@ class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
if variants:
for v in variants.variants:
if v.type.is_implicit():
assert not v.type.base and not v.type.variants
for m in v.type.local_members:
term = self._nodes_for_one_member(m)
term.extend(self._nodes_for_variant_when(variants, v))
dlnode += self._make_dlitem(term, None)
else:
if v.type.name == 'q_empty':
continue
assert not v.type.is_implicit()
term = [nodes.Text('The members of '),
nodes.literal('', v.type.doc_type())]
term.extend(self._nodes_for_variant_when(variants, v))
@ -243,8 +239,8 @@ class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
seen_item = False
dlnode = nodes.definition_list()
for section in doc.features.values():
dlnode += self._make_dlitem([nodes.literal('', section.name)],
section.text)
dlnode += self._make_dlitem(
[nodes.literal('', section.member.name)], section.text)
seen_item = True
if not seen_item:
@ -262,11 +258,11 @@ class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
"""Return list of doctree nodes for additional sections"""
nodelist = []
for section in doc.sections:
if section.name and section.name == 'TODO':
if section.tag and section.tag == 'TODO':
# Hide TODO: sections
continue
snode = self._make_section(section.name)
if section.name and section.name.startswith('Example'):
snode = self._make_section(section.tag)
if section.tag and section.tag.startswith('Example'):
snode += self._nodes_for_example(section.text)
else:
self._parse_text_into_node(section.text, snode)

View File

@ -1728,6 +1728,7 @@
#
# -> { "execute": "migrate", "arguments": { "uri": "tcp:0:4446" } }
# <- { "return": {} }
#
# -> { "execute": "migrate",
# "arguments": {
# "channels": [ { "channel-type": "main",
@ -1796,19 +1797,19 @@
#
# 3. The uri format is the same as for -incoming
#
# 5. For now, number of migration streams is restricted to one,
# 4. For now, number of migration streams is restricted to one,
# i.e number of items in 'channels' list is just 1.
#
# 4. The 'uri' and 'channels' arguments are mutually exclusive;
# 5. The 'uri' and 'channels' arguments are mutually exclusive;
# exactly one of the two should be present.
#
# Example:
#
# -> { "execute": "migrate-incoming",
# "arguments": { "uri": "tcp::4446" } }
# "arguments": { "uri": "tcp:0:4446" } }
# <- { "return": {} }
#
# -> { "execute": "migrate",
# -> { "execute": "migrate-incoming",
# "arguments": {
# "channels": [ { "channel-type": "main",
# "addr": { "transport": "socket",
@ -1817,7 +1818,7 @@
# "port": "1050" } } ] } }
# <- { "return": {} }
#
# -> { "execute": "migrate",
# -> { "execute": "migrate-incoming",
# "arguments": {
# "channels": [ { "channel-type": "main",
# "addr": { "transport": "exec",
@ -1825,7 +1826,7 @@
# "/some/sock" ] } } ] } }
# <- { "return": {} }
#
# -> { "execute": "migrate",
# -> { "execute": "migrate-incoming",
# "arguments": {
# "channels": [ { "channel-type": "main",
# "addr": { "transport": "rdma",

View File

@ -108,8 +108,6 @@
#
# @status: the virtual machine @RunState
#
# Features:
#
# Since: 0.14
#
##

View File

@ -134,7 +134,7 @@ class QAPISchemaParser:
info = self.info
if self.tok == '#':
self.reject_expr_doc(cur_doc)
for cur_doc in self.get_doc(info):
cur_doc = self.get_doc()
self.docs.append(cur_doc)
continue
@ -414,39 +414,177 @@ class QAPISchemaParser:
self, "expected '{', '[', string, or boolean")
return expr
def get_doc(self, info: QAPISourceInfo) -> List['QAPIDoc']:
if self.val != '##':
def get_doc_line(self) -> Optional[str]:
if self.tok != '#':
raise QAPIParseError(
self, "junk after '##' at start of documentation comment")
docs = []
cur_doc = QAPIDoc(self, info)
self.accept(False)
while self.tok == '#':
self, "documentation comment must end with '##'")
assert isinstance(self.val, str)
if self.val.startswith('##'):
# End of doc comment
if self.val != '##':
raise QAPIParseError(
self, "junk after '##' at end of documentation comment")
return None
if self.val == '#':
return ''
if self.val[1] != ' ':
raise QAPIParseError(self, "missing space after #")
return self.val[2:].rstrip()
@staticmethod
def _match_at_name_colon(string: str) -> Optional[Match[str]]:
return re.match(r'@([^:]*): *', string)
def get_doc_indented(self, doc: 'QAPIDoc') -> Optional[str]:
self.accept(False)
line = self.get_doc_line()
while line == '':
doc.append_line(line)
self.accept(False)
line = self.get_doc_line()
if line is None:
return line
indent = must_match(r'\s*', line).end()
if not indent:
return line
doc.append_line(line[indent:])
prev_line_blank = False
while True:
self.accept(False)
line = self.get_doc_line()
if line is None:
return line
if self._match_at_name_colon(line):
return line
cur_indent = must_match(r'\s*', line).end()
if line != '' and cur_indent < indent:
if prev_line_blank:
return line
raise QAPIParseError(
self,
"junk after '##' at end of documentation comment")
cur_doc.end_comment()
docs.append(cur_doc)
self.accept()
return docs
if self.val.startswith('# ='):
if cur_doc.symbol:
"unexpected de-indent (expected at least %d spaces)" %
indent)
doc.append_line(line[indent:])
prev_line_blank = True
def get_doc_paragraph(self, doc: 'QAPIDoc') -> Optional[str]:
while True:
self.accept(False)
line = self.get_doc_line()
if line is None:
return line
if line == '':
return line
doc.append_line(line)
def get_doc(self) -> 'QAPIDoc':
if self.val != '##':
raise QAPIParseError(
self, "junk after '##' at start of documentation comment")
info = self.info
self.accept(False)
line = self.get_doc_line()
if line is not None and line.startswith('@'):
# Definition documentation
if not line.endswith(':'):
raise QAPIParseError(self, "line should end with ':'")
# Invalid names are not checked here, but the name
# provided *must* match the following definition,
# which *is* validated in expr.py.
symbol = line[1:-1]
if not symbol:
raise QAPIParseError(self, "name required after '@'")
doc = QAPIDoc(info, symbol)
self.accept(False)
line = self.get_doc_line()
no_more_args = False
while line is not None:
# Blank lines
while line == '':
self.accept(False)
line = self.get_doc_line()
if line is None:
break
# Non-blank line, first of a section
if line == 'Features:':
if doc.features:
raise QAPIParseError(
self, "duplicated 'Features:' line")
self.accept(False)
line = self.get_doc_line()
while line == '':
self.accept(False)
line = self.get_doc_line()
while (line is not None
and (match := self._match_at_name_colon(line))):
doc.new_feature(self.info, match.group(1))
text = line[match.end():]
if text:
doc.append_line(text)
line = self.get_doc_indented(doc)
if not doc.features:
raise QAPIParseError(
self, 'feature descriptions expected')
no_more_args = True
elif match := self._match_at_name_colon(line):
# description
if no_more_args:
raise QAPIParseError(
self,
"description of '@%s:' follows a section"
% match.group(1))
while (line is not None
and (match := self._match_at_name_colon(line))):
doc.new_argument(self.info, match.group(1))
text = line[match.end():]
if text:
doc.append_line(text)
line = self.get_doc_indented(doc)
no_more_args = True
elif match := re.match(
r'(Returns|Since|Notes?|Examples?|TODO): *',
line):
# tagged section
doc.new_tagged_section(self.info, match.group(1))
text = line[match.end():]
if text:
doc.append_line(text)
line = self.get_doc_indented(doc)
no_more_args = True
elif line.startswith('='):
raise QAPIParseError(
self,
"unexpected '=' markup in definition documentation")
if cur_doc.body.text:
cur_doc.end_comment()
docs.append(cur_doc)
cur_doc = QAPIDoc(self, info)
cur_doc.append(self.val)
else:
# tag-less paragraph
doc.ensure_untagged_section(self.info)
doc.append_line(line)
line = self.get_doc_paragraph(doc)
else:
# Free-form documentation
doc = QAPIDoc(info)
doc.ensure_untagged_section(self.info)
first = True
while line is not None:
if match := self._match_at_name_colon(line):
raise QAPIParseError(
self,
"'@%s:' not allowed in free-form documentation"
% match.group(1))
if line.startswith('='):
if not first:
raise QAPIParseError(
self,
"'=' heading must come first in a comment block")
doc.append_line(line)
self.accept(False)
line = self.get_doc_line()
first = False
raise QAPIParseError(self, "documentation comment must end with '##'")
self.accept(False)
doc.end()
return doc
class QAPIDoc:
@ -469,275 +607,88 @@ class QAPIDoc:
"""
class Section:
# pylint: disable=too-few-public-methods
def __init__(self, parser: QAPISchemaParser,
name: Optional[str] = None):
# parser, for error messages about indentation
self._parser = parser
# optional section name (argument/member or section name)
self.name = name
# section text without section name
def __init__(self, info: QAPISourceInfo,
tag: Optional[str] = None):
# section source info, i.e. where it begins
self.info = info
# section tag, if any ('Returns', '@name', ...)
self.tag = tag
# section text without tag
self.text = ''
# indentation to strip (None means indeterminate)
self._indent = None if self.name else 0
def append(self, line: str) -> None:
line = line.rstrip()
if line:
indent = must_match(r'\s*', line).end()
if self._indent is None:
# indeterminate indentation
if self.text != '':
# non-blank, non-first line determines indentation
self._indent = indent
elif indent < self._indent:
raise QAPIParseError(
self._parser,
"unexpected de-indent (expected at least %d spaces)" %
self._indent)
line = line[self._indent:]
def append_line(self, line: str) -> None:
self.text += line + '\n'
class ArgSection(Section):
def __init__(self, parser: QAPISchemaParser,
name: str):
super().__init__(parser, name)
def __init__(self, info: QAPISourceInfo, tag: str):
super().__init__(info, tag)
self.member: Optional['QAPISchemaMember'] = None
def connect(self, member: 'QAPISchemaMember') -> None:
self.member = member
class NullSection(Section):
"""
Immutable dummy section for use at the end of a doc block.
"""
# pylint: disable=too-few-public-methods
def append(self, line: str) -> None:
assert False, "Text appended after end_comment() called."
def __init__(self, parser: QAPISchemaParser, info: QAPISourceInfo):
# 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
def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
# info points to the doc comment block's first line
self.info = info
self.symbol: Optional[str] = None
self.body = QAPIDoc.Section(parser)
# dicts mapping parameter/feature names to their ArgSection
self.args: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
self.features: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
# definition doc's symbol, None for free-form doc
self.symbol: Optional[str] = symbol
# the sections in textual order
self.all_sections: List[QAPIDoc.Section] = [QAPIDoc.Section(info)]
# the body section
self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
# dicts mapping parameter/feature names to their description
self.args: Dict[str, QAPIDoc.ArgSection] = {}
self.features: Dict[str, QAPIDoc.ArgSection] = {}
# sections other than .body, .args, .features
self.sections: List[QAPIDoc.Section] = []
# the current section
self._section = self.body
self._append_line = self._append_body_line
def has_section(self, name: str) -> bool:
"""Return True if we have a section with this name."""
for i in self.sections:
if i.name == name:
return True
return False
def end(self) -> None:
for section in self.all_sections:
section.text = section.text.strip('\n')
if section.tag is not None and section.text == '':
raise QAPISemError(
section.info, "text required after '%s:'" % section.tag)
def append(self, line: str) -> None:
"""
Parse a comment line and add it to the documentation.
The way that the line is dealt with depends on which part of
the documentation we're parsing right now:
* The body section: ._append_line is ._append_body_line
* An argument section: ._append_line is ._append_args_line
* A features section: ._append_line is ._append_features_line
* An additional section: ._append_line is ._append_various_line
"""
line = line[1:]
if not line:
self._append_freeform(line)
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
if self.all_sections and not self.all_sections[-1].tag:
# extend current section
self.all_sections[-1].text += '\n'
return
# start new section
section = self.Section(info)
self.sections.append(section)
self.all_sections.append(section)
if line[0] != ' ':
raise QAPIParseError(self._parser, "missing space after #")
line = line[1:]
self._append_line(line)
def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> None:
if tag in ('Returns', 'Since'):
for section in self.all_sections:
if isinstance(section, self.ArgSection):
continue
if section.tag == tag:
raise QAPISemError(
info, "duplicated '%s' section" % tag)
section = self.Section(info, tag)
self.sections.append(section)
self.all_sections.append(section)
def end_comment(self) -> None:
self._switch_section(QAPIDoc.NullSection(self._parser))
@staticmethod
def _match_at_name_colon(string: str) -> Optional[Match[str]]:
return re.match(r'@([^:]*): *', string)
@staticmethod
def _match_section_tag(string: str) -> Optional[Match[str]]:
return re.match(r'(Returns|Since|Notes?|Examples?|TODO): *', string)
def _append_body_line(self, line: str) -> None:
"""
Process a line of documentation text in the body section.
If this a symbol line and it is the section's first line, this
is a definition documentation block for that symbol.
If it's a definition documentation block, another symbol line
begins the argument section for the argument named by it, and
a section tag begins an additional section. Start that
section and append the line to it.
Else, append the line to the current section.
"""
# FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
# recognized, and get silently treated as ordinary text
if not self.symbol and not self.body.text and line.startswith('@'):
if not line.endswith(':'):
raise QAPIParseError(self._parser, "line should end with ':'")
self.symbol = line[1:-1]
# Invalid names are not checked here, but the name provided MUST
# match the following definition, which *is* validated in expr.py.
if not self.symbol:
raise QAPIParseError(
self._parser, "name required after '@'")
elif self.symbol:
# This is a definition documentation block
if self._match_at_name_colon(line):
self._append_line = self._append_args_line
self._append_args_line(line)
elif line == 'Features:':
self._append_line = self._append_features_line
elif self._match_section_tag(line):
self._append_line = self._append_various_line
self._append_various_line(line)
else:
self._append_freeform(line)
else:
# This is a free-form documentation block
self._append_freeform(line)
def _append_args_line(self, line: str) -> None:
"""
Process a line of documentation text in an argument section.
A symbol line begins the next argument section, a section tag
section or a non-indented line after a blank line begins an
additional section. Start that section and append the line to
it.
Else, append the line to the current section.
"""
match = self._match_at_name_colon(line)
if match:
line = line[match.end():]
self._start_args_section(match.group(1))
elif self._match_section_tag(line):
self._append_line = self._append_various_line
self._append_various_line(line)
return
elif (self._section.text.endswith('\n\n')
and line and not line[0].isspace()):
if line == 'Features:':
self._append_line = self._append_features_line
else:
self._start_section()
self._append_line = self._append_various_line
self._append_various_line(line)
return
self._append_freeform(line)
def _append_features_line(self, line: str) -> None:
match = self._match_at_name_colon(line)
if match:
line = line[match.end():]
self._start_features_section(match.group(1))
elif self._match_section_tag(line):
self._append_line = self._append_various_line
self._append_various_line(line)
return
elif (self._section.text.endswith('\n\n')
and line and not line[0].isspace()):
self._start_section()
self._append_line = self._append_various_line
self._append_various_line(line)
return
self._append_freeform(line)
def _append_various_line(self, line: str) -> None:
"""
Process a line of documentation text in an additional section.
A symbol line is an error.
A section tag begins an additional section. Start that
section and append the line to it.
Else, append the line to the current section.
"""
match = self._match_at_name_colon(line)
if match:
raise QAPIParseError(self._parser,
"description of '@%s:' follows a section"
% match.group(1))
match = self._match_section_tag(line)
if match:
line = line[match.end():]
self._start_section(match.group(1))
self._append_freeform(line)
def _start_symbol_section(
self,
symbols_dict: Dict[str, 'QAPIDoc.ArgSection'],
name: str) -> None:
# FIXME invalid names other than the empty string aren't flagged
def _new_description(self, info: QAPISourceInfo, name: str,
desc: Dict[str, ArgSection]) -> None:
if not name:
raise QAPIParseError(self._parser, "invalid parameter name")
if name in symbols_dict:
raise QAPIParseError(self._parser,
"'%s' parameter name duplicated" % name)
assert not self.sections
new_section = QAPIDoc.ArgSection(self._parser, name)
self._switch_section(new_section)
symbols_dict[name] = new_section
raise QAPISemError(info, "invalid parameter name")
if name in desc:
raise QAPISemError(info, "'%s' parameter name duplicated" % name)
section = self.ArgSection(info, '@' + name)
self.all_sections.append(section)
desc[name] = section
def _start_args_section(self, name: str) -> None:
self._start_symbol_section(self.args, name)
def new_argument(self, info: QAPISourceInfo, name: str) -> None:
self._new_description(info, name, self.args)
def _start_features_section(self, name: str) -> None:
self._start_symbol_section(self.features, name)
def new_feature(self, info: QAPISourceInfo, name: str) -> None:
self._new_description(info, name, self.features)
def _start_section(self, name: Optional[str] = None) -> None:
if name in ('Returns', 'Since') and self.has_section(name):
raise QAPIParseError(self._parser,
"duplicated '%s' section" % name)
new_section = QAPIDoc.Section(self._parser, name)
self._switch_section(new_section)
self.sections.append(new_section)
def _switch_section(self, new_section: 'QAPIDoc.Section') -> None:
text = self._section.text = self._section.text.strip('\n')
# Only the 'body' section is allowed to have an empty body.
# All other sections, including anonymous ones, must have text.
if self._section != self.body and not text:
# We do not create anonymous sections unless there is
# something to put in them; this is a parser bug.
assert self._section.name
raise QAPIParseError(
self._parser,
"empty doc section '%s'" % self._section.name)
self._section = new_section
def _append_freeform(self, line: str) -> None:
match = re.match(r'(@\S+:)', line)
if match:
raise QAPIParseError(self._parser,
"'%s' not allowed in free-form documentation"
% match.group(1))
self._section.append(line)
def append_line(self, line: str) -> None:
self.all_sections[-1].append_line(line)
def connect_member(self, member: 'QAPISchemaMember') -> None:
if member.name not in self.args:
@ -745,8 +696,8 @@ class QAPIDoc:
raise QAPISemError(member.info,
"%s '%s' lacks documentation"
% (member.role, member.name))
self.args[member.name] = QAPIDoc.ArgSection(self._parser,
member.name)
self.args[member.name] = QAPIDoc.ArgSection(
self.info, '@' + member.name)
self.args[member.name].connect(member)
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
@ -757,8 +708,12 @@ class QAPIDoc:
self.features[feature.name].connect(feature)
def check_expr(self, expr: QAPIExpression) -> None:
if self.has_section('Returns') and 'command' not in expr:
raise QAPISemError(self.info,
if 'command' not in expr:
sec = next((sec for sec in self.sections
if sec.tag == 'Returns'),
None)
if sec:
raise QAPISemError(sec.info,
"'Returns:' is only valid for commands")
def check(self) -> None:
@ -770,7 +725,7 @@ class QAPIDoc:
if not section.member]
if bogus:
raise QAPISemError(
self.info,
args[bogus[0]].info,
"documented %s%s '%s' %s not exist" % (
what,
"s" if len(bogus) > 1 else "",

View File

@ -95,10 +95,6 @@ class QAPISchemaEntity:
for f in self.features:
doc.connect_feature(f)
def check_doc(self):
if self.doc:
self.doc.check()
def _set_module(self, schema, info):
assert self._checked
fname = info.fname if info else QAPISchemaModule.BUILTIN_MODULE_NAME
@ -1223,9 +1219,10 @@ class QAPISchema:
for ent in self._entity_list:
ent.check(self)
ent.connect_doc()
ent.check_doc()
for ent in self._entity_list:
ent.set_module(self)
for doc in self.docs:
doc.check()
def visit(self, visitor):
visitor.visit_begin(self)

View File

@ -1 +1 @@
doc-bad-alternate-member.json:3: documented members 'aa', 'bb' do not exist
doc-bad-alternate-member.json:7: documented members 'aa', 'bb' do not exist

View File

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

View File

@ -1 +1 @@
doc-bad-command-arg.json:3: documented member 'b' does not exist
doc-bad-command-arg.json:6: documented member 'b' does not exist

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
doc-bad-union-member.json:3: documented members 'a', 'b' do not exist
doc-bad-union-member.json:5: documented members 'a', 'b' do not exist

View File

@ -0,0 +1 @@
doc-duplicate-features.json:9:1: duplicated 'Features:' line

View File

@ -0,0 +1,11 @@
# Duplicate 'Features:' line
##
# @foo:
#
# Features:
# @feat: mumble
#
# Features:
##
{ 'command': 'foo', 'features': ['feat'] }

View File

@ -1 +1 @@
doc-duplicated-arg.json:6:1: 'a' parameter name duplicated
doc-duplicated-arg.json:6: 'a' parameter name duplicated

View File

@ -1 +1 @@
doc-duplicated-return.json:7:1: duplicated 'Returns' section
doc-duplicated-return.json:8: duplicated 'Returns' section

View File

@ -4,5 +4,6 @@
# @foo:
#
# Returns: 0
#
# Returns: 1
##

View File

@ -1 +1 @@
doc-duplicated-since.json:7:1: duplicated 'Since' section
doc-duplicated-since.json:8: duplicated 'Since' section

View File

@ -4,5 +4,6 @@
# @foo:
#
# Since: 0
#
# Since: 1
##

View File

@ -1 +1 @@
doc-empty-arg.json:5:1: invalid parameter name
doc-empty-arg.json:5: invalid parameter name

View File

@ -0,0 +1 @@
doc-empty-features.json:8:1: feature descriptions expected

View File

@ -0,0 +1,10 @@
# 'Features:' line not followed by feature descriptions
##
# @foo:
#
# Features:
#
# not a description
##
{ 'command': 'foo' }

View File

View File

@ -1 +1 @@
doc-empty-section.json:7:1: empty doc section 'Note'
doc-empty-section.json:6: text required after 'Note:'

View File

@ -9,7 +9,9 @@
##
# = Section
#
##
##
# == Subsection
#
# *with emphasis*
@ -152,22 +154,29 @@
# Features:
# @cmd-feat1: a feature
# @cmd-feat2: another feature
#
# Note: @arg3 is undocumented
#
# Returns: @Object
#
# TODO: frobnicate
#
# Notes:
#
# - Lorem ipsum dolor sit amet
# - Ut enim ad minim veniam
#
# Duis aute irure dolor
#
# Example:
#
# -> in
# <- out
#
# Examples:
# - *verbatim*
# - {braces}
#
# Since: 2.10
##
{ 'command': 'cmd',
@ -178,9 +187,11 @@
##
# @cmd-boxed:
# If you're bored enough to read this, go see a video of boxed cats
#
# Features:
# @cmd-feat1: a feature
# @cmd-feat2: another feature
#
# Example:
#
# -> in

View File

@ -44,7 +44,7 @@ Values
~~~~~~
"one" (**If: **"IFONE")
The _one_ {and only}
The _one_ {and only}, description on the same line
"two"
Not documented
@ -76,7 +76,7 @@ Members
~~~~~~~
"base1": "Enum"
the first member
description starts on a new line, minimally indented
If
@ -90,7 +90,9 @@ If
A paragraph
Another paragraph (but no "var": line)
Another paragraph
"var1" is undocumented
Members
@ -141,7 +143,8 @@ Members
~~~~~~~
"i": "int"
an integer "b" is undocumented
description starts on the same line remainder indented the same "b"
is undocumented
"b": "boolean"
Not documented
@ -172,10 +175,10 @@ Arguments
~~~~~~~~~
"arg1": "int"
the first argument
description starts on a new line, indented
"arg2": "string" (optional)
the second argument
description starts on the same line remainder indented differently
"arg3": "boolean"
Not documented
@ -203,12 +206,6 @@ Returns
"Object"
TODO
~~~~
frobnicate
Notes
~~~~~

View File

@ -1 +1 @@
doc-invalid-return.json:3: 'Returns:' is only valid for commands
doc-invalid-return.json:6: 'Returns:' is only valid for commands

View File

@ -2,6 +2,7 @@
##
# @FOO:
#
# Returns: blah
##
{ 'event': 'FOO' }

View File

@ -0,0 +1 @@
doc-non-first-section.json:5:1: '=' heading must come first in a comment block

View File

@ -0,0 +1,6 @@
# = section must be first line
##
#
# = Not first
##

View File

@ -66,10 +66,12 @@ schemas = [
'doc-bad-union-member.json',
'doc-before-include.json',
'doc-before-pragma.json',
'doc-duplicate-features.json',
'doc-duplicated-arg.json',
'doc-duplicated-return.json',
'doc-duplicated-since.json',
'doc-empty-arg.json',
'doc-empty-features.json',
'doc-empty-section.json',
'doc-empty-symbol.json',
'doc-good.json',
@ -273,15 +275,17 @@ if build_docs
output: ['doc-good.txt.nocr'],
input: qapi_doc_out[0],
build_by_default: true,
command: [remove_cr, '@INPUT@'],
capture: true)
command: [remove_cr],
capture: true,
feed: true)
qapi_doc_ref_nocr = custom_target('QAPI rST doc reference newline-sanitized',
output: ['doc-good.ref.nocr'],
input: files('doc-good.txt'),
build_by_default: true,
command: [remove_cr, '@INPUT@'],
capture: true)
command: [remove_cr],
capture: true,
feed: true)
test('QAPI rST doc', diff, args: ['-u', qapi_doc_ref_nocr[0], qapi_doc_out_nocr[0]],
suite: ['qapi-schema', 'qapi-doc'])

View File

@ -130,7 +130,7 @@ def test_frontend(fname):
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))
print(' section=%s\n%s' % (section.tag, section.text))
def open_test_result(dir_name, file_name, update):