mirror of https://github.com/xemu-project/xemu.git
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:
commit
03d496a992
|
@ -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)::
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -108,8 +108,6 @@
|
|||
#
|
||||
# @status: the virtual machine @RunState
|
||||
#
|
||||
# Features:
|
||||
#
|
||||
# Since: 0.14
|
||||
#
|
||||
##
|
||||
|
|
|
@ -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 "",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
doc-bad-feature.json:3: documented feature 'a' does not exist
|
||||
doc-bad-feature.json:7: documented feature 'a' does not exist
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
doc-duplicate-features.json:9:1: duplicated 'Features:' line
|
|
@ -0,0 +1,11 @@
|
|||
# Duplicate 'Features:' line
|
||||
|
||||
##
|
||||
# @foo:
|
||||
#
|
||||
# Features:
|
||||
# @feat: mumble
|
||||
#
|
||||
# Features:
|
||||
##
|
||||
{ 'command': 'foo', 'features': ['feat'] }
|
|
@ -1 +1 @@
|
|||
doc-duplicated-arg.json:6:1: 'a' parameter name duplicated
|
||||
doc-duplicated-arg.json:6: 'a' parameter name duplicated
|
||||
|
|
|
@ -1 +1 @@
|
|||
doc-duplicated-return.json:7:1: duplicated 'Returns' section
|
||||
doc-duplicated-return.json:8: duplicated 'Returns' section
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
# @foo:
|
||||
#
|
||||
# Returns: 0
|
||||
#
|
||||
# Returns: 1
|
||||
##
|
||||
|
|
|
@ -1 +1 @@
|
|||
doc-duplicated-since.json:7:1: duplicated 'Since' section
|
||||
doc-duplicated-since.json:8: duplicated 'Since' section
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
# @foo:
|
||||
#
|
||||
# Since: 0
|
||||
#
|
||||
# Since: 1
|
||||
##
|
||||
|
|
|
@ -1 +1 @@
|
|||
doc-empty-arg.json:5:1: invalid parameter name
|
||||
doc-empty-arg.json:5: invalid parameter name
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
doc-empty-features.json:8:1: feature descriptions expected
|
|
@ -0,0 +1,10 @@
|
|||
# 'Features:' line not followed by feature descriptions
|
||||
|
||||
##
|
||||
# @foo:
|
||||
#
|
||||
# Features:
|
||||
#
|
||||
# not a description
|
||||
##
|
||||
{ 'command': 'foo' }
|
|
@ -1 +1 @@
|
|||
doc-empty-section.json:7:1: empty doc section 'Note'
|
||||
doc-empty-section.json:6: text required after 'Note:'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
~~~~~
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
doc-invalid-return.json:3: 'Returns:' is only valid for commands
|
||||
doc-invalid-return.json:6: 'Returns:' is only valid for commands
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
##
|
||||
# @FOO:
|
||||
#
|
||||
# Returns: blah
|
||||
##
|
||||
{ 'event': 'FOO' }
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
doc-non-first-section.json:5:1: '=' heading must come first in a comment block
|
|
@ -0,0 +1,6 @@
|
|||
# = section must be first line
|
||||
|
||||
##
|
||||
#
|
||||
# = Not first
|
||||
##
|
|
@ -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'])
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue