All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 00/14] qapi: enforce section ordering
@ 2026-09-28 22:36 John Snow
  2026-09-28 22:36 ` [PATCH 01/14] qapi: rename "plain" sections to "details" John Snow
                   ` (13 more replies)
  0 siblings, 14 replies; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

howdy,

this series begins enforcing a strict section ordering in the QAPI
parser. This allows us to change how we store sections internally in
QAPIDoc so that we always have well-defined points in any
documentation block where we may insert auto-generated documentation
sections when applicable.

This series is effectively RFC quality; I am aware that we will need
more unit tests for the new negative cases at a minimum.

This is the last series messing around with the QAPIDoc format before
I move on to directly working on the inliner again.

John Snow (14):
  qapi: rename "plain" sections to "details"
  docs/qapi-code-gen: expand each section's syntax into a mini-section
  qapi: re-order QAPI doc block sections
  qapi/parser: enforce doc block section ordering
  qapi: move 'details' sections to the bottom of doc blocks
  qapi/parser: enforce 'details' sections' relative position
  qapi: re-order 'since' sections to always be last
  qapi/parser: enforce strict positioning for "Since:" section
  qapi/parser: remove _insert_after_intro() helper
  qapi/parser: add _append() method
  qapi/parser: privatize "all_sections" field as "_sections"
  qapi/parser: add _active_section private member
  qapi/parser: add new _sections mapping
  qapi/parser: convert single-section properties to getter properties

 docs/devel/qapi-code-gen.rst    | 117 ++++++--
 docs/interop/firmware.json      |  54 ++--
 docs/interop/vhost-user.json    |  29 +-
 docs/sphinx/qapidoc.py          |  15 +-
 qapi/accelerator.json           |  16 +-
 qapi/acpi.json                  |  36 +--
 qapi/block-core.json            | 476 ++++++++++++++++----------------
 qapi/block-export.json          |  38 +--
 qapi/block.json                 | 114 ++++----
 qapi/char.json                  |  36 +--
 qapi/compat.json                |  14 +-
 qapi/control.json               |  12 +-
 qapi/crypto.json                |   4 +-
 qapi/dump.json                  |  16 +-
 qapi/introspect.json            |  14 +-
 qapi/job.json                   |  26 +-
 qapi/machine-s390x.json         |  28 +-
 qapi/machine.json               | 324 +++++++++++-----------
 qapi/migration.json             | 144 +++++-----
 qapi/misc-arm.json              |   4 +-
 qapi/misc-i386.json             |  58 ++--
 qapi/misc.json                  |  84 +++---
 qapi/net.json                   |  64 ++---
 qapi/pci.json                   |   4 +-
 qapi/qdev.json                  |  24 +-
 qapi/qom.json                   |  56 ++--
 qapi/replay.json                |  16 +-
 qapi/rocker.json                |  16 +-
 qapi/run-state.json             |  64 ++---
 qapi/stats.json                 |   6 +-
 qapi/tpm.json                   |  12 +-
 qapi/trace.json                 |   8 +-
 qapi/transaction.json           |  34 +--
 qapi/ui.json                    |  90 +++---
 qapi/vfio.json                  |   4 +-
 qapi/virtio.json                |  46 +--
 qga/qapi-schema.json            |  80 +++---
 scripts/qapi/parser.py          | 232 ++++++++--------
 tests/qapi-schema/doc-good.json |  16 +-
 tests/qapi-schema/doc-good.out  |  34 +--
 tests/qapi-schema/doc-good.txt  |  22 +-
 tests/qapi-schema/test-qapi.py  |   2 +-
 42 files changed, 1274 insertions(+), 1215 deletions(-)

-- 
2.55.0




^ permalink raw reply	[flat|nested] 35+ messages in thread

* [PATCH 01/14] qapi: rename "plain" sections to "details"
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-29 11:49   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 02/14] docs/qapi-code-gen: expand each section's syntax into a mini-section John Snow
                   ` (12 subsequent siblings)
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

With this change, we no longer have "plaintext" sections at all: they
are all now either "Intro" or "Details". This patch does not enforce
where those details sections must occur; that change occurs in a later
patch.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/sphinx/qapidoc.py         |  2 +-
 scripts/qapi/parser.py         | 18 +++++++++++-------
 tests/qapi-schema/doc-good.out | 18 +++++++++---------
 3 files changed, 21 insertions(+), 17 deletions(-)

diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 54a32e45f7e..ae57056a393 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -383,7 +383,7 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
         for section in sections:
             section.text = self.reformat_arobase(section.text)
 
-            if section.kind.name in ("PLAIN", "INTRO"):
+            if section.kind.name in ("INTRO", "DETAILS"):
                 self.visit_plaintext(section)
             elif section.kind == QAPIDoc.Kind.MEMBER:
                 assert isinstance(section, QAPIDoc.ArgSection)
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index dcc04b2663e..fe284a3f866 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -674,7 +674,7 @@ class QAPIDoc:
     """
 
     class Kind(enum.Enum):
-        PLAIN = 0
+        DETAILS = 0
         MEMBER = 1
         FEATURE = 2
         RETURNS = 3
@@ -747,12 +747,16 @@ def has_features(self) -> bool:
     def end(self) -> None:
         for section in self.all_sections:
             section.text = section.text.strip('\n')
-            if not (section.kind.name in ("INTRO", "PLAIN") or section.text):
+            if (
+                    section.kind not in (
+                        QAPIDoc.Kind.INTRO, QAPIDoc.Kind.DETAILS
+                    ) and section.text == ''
+            ):
                 raise QAPISemError(
                     section.info, "text required after '%s:'" % section.kind)
 
     def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
-        kind = QAPIDoc.Kind.PLAIN
+        kind = QAPIDoc.Kind.DETAILS
 
         if self.all_sections and self.all_sections[-1].kind == kind:
             # extend current section
@@ -837,8 +841,8 @@ def _insert_after_intro(
         """
         Insert a section immediately after the intro section.
 
-        While we convert PLAIN sections to INTRO sections, all
-        contiguous INTRO/PLAIN sections at the start of a QAPIDoc
+        While we convert DETAILS sections to INTRO sections, all
+        contiguous INTRO/DETAILS sections at the start of a QAPIDoc
         section list are treated as "the intro".
 
         Once INTRO conversion is complete, this helper will no longer be
@@ -855,7 +859,7 @@ def _insert_after_intro(
 
         index = 0
         for index, ref_section in enumerate(self.all_sections):
-            if ref_section.kind.name in ("PLAIN", "INTRO"):
+            if ref_section.kind.name in ("INTRO", "DETAILS"):
                 continue
             break
         else:
@@ -871,7 +875,7 @@ def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
         if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
             return
 
-        # No MEMBER sections present. Insert after INTRO/PLAIN sections.
+        # No MEMBER sections present. Insert after INTRO/DETAILS sections.
         self._insert_after_intro(stub)
 
     def connect_member(self, member: 'QAPISchemaMember') -> None:
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
index eaaf119dea8..1d4fbd3762a 100644
--- a/tests/qapi-schema/doc-good.out
+++ b/tests/qapi-schema/doc-good.out
@@ -54,15 +54,15 @@ event EVT_BOXED Object
     boxed=True
     feature feat3
 doc freeform
-    Plain
+    Details
 *******
 Section
 *******
 doc freeform
-    Plain
+    Details
 Just text, no heading.
 doc freeform
-    Plain
+    Details
 Subsection
 ==========
 
@@ -116,7 +116,7 @@ The _one_ {and only}, description on the same line
 Also _one_ {and only}
     Feature=enum-member-feat
 a member feature
-    Plain
+    Details
 @two is undocumented
 doc symbol=Base
     Intro
@@ -131,7 +131,7 @@ doc symbol=Variant1
     Another paragraph
     Member=var1
 
-    Plain
+    Details
 @var1 is undocumented
     Feature=variant1-feat
 a feature
@@ -157,7 +157,7 @@ description starts on the same line
     Feature=alt-feat
 a feature
 doc freeform
-    Plain
+    Details
 Another subsection
 ==================
 doc symbol=cmd
@@ -175,7 +175,7 @@ description starts on the same line
 a feature
     Feature=cmd-feat2
 another feature
-    Plain
+    Details
 .. note:: @arg3 is undocumented
     Returns
 @Object
@@ -183,7 +183,7 @@ another feature
 some
     Todo
 frobnicate
-    Plain
+    Details
 .. admonition:: Notes
 
  - Lorem ipsum dolor sit amet
@@ -217,7 +217,7 @@ doc symbol=cmd-boxed
 a feature
     Feature=cmd-feat2
 another feature
-    Plain
+    Details
 .. qmp-example::
 
    -> "this example"
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 02/14] docs/qapi-code-gen: expand each section's syntax into a mini-section
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
  2026-09-28 22:36 ` [PATCH 01/14] qapi: rename "plain" sections to "details" John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-28 22:36 ` [PATCH 03/14] qapi: re-order QAPI doc block sections John Snow
                   ` (11 subsequent siblings)
  13 siblings, 0 replies; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

While grouping Features and Argument descriptions together is
syntactically elegant, it is more helpful to document how a QAPI
definition documentation block is laid out from start to finish.

Break apart and re-order the documentation for each section into a
logical order that follows the document flow. Add cross-references and
revise the documentation as we go.

Note that as of this commit, we do not enforce a total section
ordering for source documentation, but the order in which these
sections are being presented matches the order we wish to enforce in a
forthcoming commit:

 1. Intro
 2. Members/Arguments/etc
 3. Returns
 4. Errors
 5. Features
 6. Details
 7. Since
 X. TODO (Not rendered or ordered.)

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/devel/qapi-code-gen.rst | 110 ++++++++++++++++++++++++++++-------
 1 file changed, 90 insertions(+), 20 deletions(-)

diff --git a/docs/devel/qapi-code-gen.rst b/docs/devel/qapi-code-gen.rst
index b1cc5b5f0db..3ac1c2dae18 100644
--- a/docs/devel/qapi-code-gen.rst
+++ b/docs/devel/qapi-code-gen.rst
@@ -985,13 +985,23 @@ When documentation is required (see pragma_ 'doc-required'), every
 definition must have documentation.
 
 Definition documentation starts with a description naming the
-definition with an optional indented overview, a description of each
-argument (for 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.
+definition with an optional indented overview (the "intro"),
+:ref:`descriptions <doc-descrip>` of each argument (for commands and
+events), member (for structs and unions), branch (for alternates), or
+value (for enums), :ref:`feature descriptions <doc-feat>` of each
+feature (if any), and finally optional :ref:`tagged sections
+<doc-tagged>`.
 
-Descriptions start with '\@name:'.  The description text must be
-indented like this::
+.. _doc-descrip:
+
+Descriptions
+^^^^^^^^^^^^
+
+Descriptions start with '\@name:'.  These are used to document
+arguments (for commands and events), members (for structs and unions),
+branches (for alternates), or values (for enums).
+
+The description text must be indented like this::
 
  # @name: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
  #     do eiusmod tempor incididunt ut labore et dolore magna aliqua.
@@ -1001,15 +1011,12 @@ indented like this::
 .. FIXME union branches should be described, too.
 
 Extensions added after the definition was first released carry a
-"(since x.y.z)" comment.
+"(since x.y)" comment.
 
-The feature descriptions must be preceded by a blank line and then a
-line "Features:", like this::
+.. _doc-tagged:
 
-  #
-  # Features:
-  #
-  # @feature: Description text
+Tagged sections
+^^^^^^^^^^^^^^^
 
 A tagged section begins with a paragraph that starts with one of the
 following words: "Since:", "Returns:", "Errors:", "TODO:".  It ends with
@@ -1024,8 +1031,29 @@ like this::
  #     Duis aute irure dolor in reprehenderit in voluptate velit esse
  #     cillum dolore eu fugiat nulla pariatur.
 
-"Returns" and "Errors" sections are only valid for commands.  They
-document the success and the error response, respectively.
+
+Returns
+^^^^^^^
+
+"Returns" :ref:`doc-tagged` are only valid for commands.  They document the
+success response of the command. If there is some nuance to capture
+with the return value, it may be documented as::
+
+  #
+  # Returns: eBPF object encoded in base64. Long lines should be
+  #     wrapped and indented.
+  #
+
+The type of the return value will be added to the documentation
+automatically. If there is nothing relevant to add apart from type,
+this section may be omitted.
+
+
+Errors
+^^^^^^
+
+"Errors" :ref:`doc-tagged` are only valid for commands.  They document
+the error responses.
 
 "Errors" sections should be formatted as an rST list, each entry
 detailing a relevant error condition.  For example::
@@ -1034,12 +1062,33 @@ detailing a relevant error condition.  For example::
  #     - If @device does not exist, DeviceNotFound
  #     - Any other error returns a GenericError.
 
-A "Since: x.y.z" tagged section lists the release that introduced the
-definition.
 
-"TODO" sections are not rendered (they are for developers, not users of
-QMP).  In other sections, the text is formatted, and rST markup can be
-used.
+.. _doc-feat:
+
+Feature Descriptions
+^^^^^^^^^^^^^^^^^^^^
+
+Feature descriptions are a :ref:`doc-descrip` section that document features
+that apply to this definition.
+
+The feature descriptions must be preceded by a blank line and then a
+line "Features:", like this::
+
+  #
+  # Features:
+  #
+  # @feature: Description text
+
+
+Details
+^^^^^^^
+
+Details sections are comprised of one or more paragraphs of rST
+formatted text, but otherwise are not marked up in any specific
+way. In general, this text contains in-depth explanations of the
+definition being documented and may include various rST admonitions
+like ``.. seealso::``, ``.. note::``, etc. This is also the section
+where QMP usage examples should be placed.
 
 QMP Examples can be added by using the ``.. qmp-example::`` directive.
 In its simplest form, this can be used to contain a single QMP code
@@ -1089,6 +1138,27 @@ Highlighting in non-QMP languages can be accomplished by using the
 achieved by omitting the language argument.
 
 
+Tagged section: Since
+^^^^^^^^^^^^^^^^^^^^^
+
+A "Since: x.y" :ref:`doc-tagged` lists the release that introduced the
+definition::
+
+  #
+  # Since: 11.2
+  #
+
+
+TODO
+^^^^
+
+"TODO" tagged sections are not rendered (they are for developers, not users of
+QMP).
+
+
+Putting it all together
+^^^^^^^^^^^^^^^^^^^^^^^
+
 Examples of complete definition documentation::
 
  ##
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 03/14] qapi: re-order QAPI doc block sections
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
  2026-09-28 22:36 ` [PATCH 01/14] qapi: rename "plain" sections to "details" John Snow
  2026-09-28 22:36 ` [PATCH 02/14] docs/qapi-code-gen: expand each section's syntax into a mini-section John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-29 12:10   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 04/14] qapi/parser: enforce doc block section ordering John Snow
                   ` (10 subsequent siblings)
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

This patch is focused on making sure that "Returns" appears before
"Errors", and "Errors" appears before "Features".

Signed-off-by: John Snow <jsnow@redhat.com>
---
 qapi/accelerator.json           |  8 ++---
 qapi/block-core.json            | 44 +++++++++++++-------------
 qapi/block-export.json          | 20 ++++++------
 qapi/block.json                 |  8 ++---
 qapi/machine-s390x.json         |  8 ++---
 qapi/machine.json               | 56 ++++++++++++++++-----------------
 qapi/misc.json                  |  4 +--
 qapi/net.json                   | 16 +++++-----
 qapi/qom.json                   | 10 +++---
 qapi/virtio.json                | 32 +++++++++----------
 tests/qapi-schema/doc-good.json | 12 +++----
 tests/qapi-schema/doc-good.out  | 12 +++----
 tests/qapi-schema/doc-good.txt  | 18 +++++------
 13 files changed, 124 insertions(+), 124 deletions(-)

diff --git a/qapi/accelerator.json b/qapi/accelerator.json
index efc0fa81418..9f6a322685f 100644
--- a/qapi/accelerator.json
+++ b/qapi/accelerator.json
@@ -46,12 +46,12 @@
 # @x-accel-stats:
 #     Query accelerator statistics
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: accelerator statistics
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 10.1
 ##
 { 'command': 'x-accel-stats',
diff --git a/qapi/block-core.json b/qapi/block-core.json
index 741c8165c06..1fa6e34e884 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -1958,14 +1958,14 @@
 #     `job-dismiss`.  When true, this job will automatically disappear
 #     without user intervention.  Defaults to true.  (Since 3.1)
 #
+# Errors:
+#     - If @device does not exist, DeviceNotFound
+#
 # Features:
 #
 # @deprecated: Members @base and @top are deprecated.  Use @base-node
 #     and @top-node instead.
 #
-# Errors:
-#     - If @device does not exist, DeviceNotFound
-#
 # Since: 1.3
 #
 # .. qmp-example::
@@ -1996,14 +1996,14 @@
 #     can be stopped before it has completed using the `job-cancel` or
 #     `block-job-cancel` command.
 #
+# Errors:
+#     - If @device is not a valid block device, GenericError
+#
 # Features:
 #
 # @deprecated: This command is deprecated.  Use `blockdev-backup`
 #     instead.
 #
-# Errors:
-#     - If @device is not a valid block device, GenericError
-#
 # Since: 1.6
 #
 # .. qmp-example::
@@ -2551,14 +2551,14 @@
 # @x-debug-block-dirty-bitmap-sha256:
 #     Get bitmap SHA256.
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Errors:
 #     - If @node is not a valid block device, DeviceNotFound
 #     - If @name is not found or if hashing has failed, GenericError
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 2.10
 ##
 { 'command': 'x-debug-block-dirty-bitmap-sha256',
@@ -3053,15 +3053,15 @@
 #     the name of the parameter), but since QEMU 2.7 it can have other
 #     values.
 #
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
 # Features:
 #
 # @deprecated: This command is deprecated.  Use `job-pause`
 #     instead.
 #
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-pause', 'data': { 'device': 'str' },
@@ -3081,15 +3081,15 @@
 #     the name of the parameter), but since QEMU 2.7 it can have other
 #     values.
 #
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
 # Features:
 #
 # @deprecated: This command is deprecated.  Use `job-resume`
 #     instead.
 #
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-resume', 'data': { 'device': 'str' },
@@ -3121,15 +3121,15 @@
 #     the name of the parameter), but since QEMU 2.7 it can have other
 #     values.
 #
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
 # Features:
 #
 # @deprecated: This command is deprecated.  Use `job-complete`
 #     instead.
 #
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-complete', 'data': { 'device': 'str' },
diff --git a/qapi/block-export.json b/qapi/block-export.json
index e3cee2fb5ce..172d18b3556 100644
--- a/qapi/block-export.json
+++ b/qapi/block-export.json
@@ -239,15 +239,15 @@
 # The export name will be used as the id for the resulting block
 # export.
 #
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `block-export-add`
-#     instead.
-#
 # Errors:
 #     - if the server is not running
 #     - if an export with the same name already exists
 #
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `block-export-add`
+#     instead.
+#
 # Since: 1.3
 ##
 { 'command': 'nbd-server-add',
@@ -285,16 +285,16 @@
 # @mode: Mode of command operation.  See `BlockExportRemoveMode`
 #     description.  Default is 'safe'.
 #
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `block-export-del`
-#     instead.
-#
 # Errors:
 #     - if the server is not running
 #     - if export is not found
 #     - if mode is 'safe' and there are existing connections
 #
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `block-export-del`
+#     instead.
+#
 # Since: 2.12
 ##
 { 'command': 'nbd-server-remove',
diff --git a/qapi/block.json b/qapi/block.json
index 0a88d405a26..4b96a72fe70 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -105,13 +105,13 @@
 # @force: If true, eject regardless of whether the drive is locked.
 #     If not specified, the default value is false.
 #
-# Features:
-#
-# @deprecated: Member @device is deprecated.  Use @id instead.
-#
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
+# Features:
+#
+# @deprecated: Member @device is deprecated.  Use @id instead.
+#
 # .. note:: Ejecting a device with no media results in success.
 #
 # Since: 0.14
diff --git a/qapi/machine-s390x.json b/qapi/machine-s390x.json
index 8d8cbe2fb75..4a745d0d73f 100644
--- a/qapi/machine-s390x.json
+++ b/qapi/machine-s390x.json
@@ -104,12 +104,12 @@
 ##
 # @query-s390x-cpu-polarization:
 #
-# Features:
-#
-# @unstable: This command is experimental.
-#
 # Returns: the machine's CPU polarization
 #
+# Features:
+#
+# @unstable: This command is experimental.
+#
 # Since: 8.2
 ##
 { 'command': 'query-s390x-cpu-polarization', 'returns': 'CpuPolarizationInfo',
diff --git a/qapi/machine.json b/qapi/machine.json
index 9b145c635ba..911132918bd 100644
--- a/qapi/machine.json
+++ b/qapi/machine.json
@@ -1686,12 +1686,12 @@
 # @x-query-irq:
 #     Query interrupt statistics
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: interrupt statistics
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-irq',
@@ -1702,12 +1702,12 @@
 # @x-query-jit:
 #     Query TCG compiler statistics
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: TCG compiler statistics
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-jit',
@@ -1719,12 +1719,12 @@
 # @x-query-numa:
 #     Query NUMA topology information
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: topology information
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-numa',
@@ -1735,12 +1735,12 @@
 # @x-query-ramblock:
 #     Query system ramblock information
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: system ramblock information
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-ramblock',
@@ -1751,12 +1751,12 @@
 # @x-query-roms:
 #     Query information on the registered ROMS
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: registered ROMs
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-roms',
@@ -1767,12 +1767,12 @@
 # @x-query-usb:
 #     Query information on the USB devices
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: USB device information
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-usb',
@@ -1833,12 +1833,12 @@
 # @x-query-interrupt-controllers:
 #     Query information on interrupt controller devices
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: Interrupt controller devices information
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 9.1
 ##
 { 'command': 'x-query-interrupt-controllers',
diff --git a/qapi/misc.json b/qapi/misc.json
index 4469e44d057..6a96ece6d91 100644
--- a/qapi/misc.json
+++ b/qapi/misc.json
@@ -214,14 +214,14 @@
 #
 # @cpu-index: The CPU to use for commands that require an implicit CPU
 #
+# Returns: the output of the command as a string
+#
 # Features:
 #
 # @savevm-monitor-nodes: If present, HMP command savevm only snapshots
 #     monitor-owned nodes if they have no parents.  This allows the
 #     use of 'savevm' with -blockdev.  (since 4.2)
 #
-# Returns: the output of the command as a string
-#
 # Since: 0.14
 #
 # .. note:: This command only exists as a stop-gap.  Its use is highly
diff --git a/qapi/net.json b/qapi/net.json
index f0312dfb714..376cd01cb6a 100644
--- a/qapi/net.json
+++ b/qapi/net.json
@@ -1296,12 +1296,12 @@
 # @x-query-usernet:
 #     Query user-mode network stack connection states.
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: list of user-mode network stack information.
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 11.2
 #
 # .. qmp-example::
@@ -1412,12 +1412,12 @@
 # @x-query-network:
 #     Query the network configuration including hubs and clients.
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: @NetworkInfo describing hubs and clients.
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 11.2
 #
 # .. qmp-example::
diff --git a/qapi/qom.json b/qapi/qom.json
index e03ede34421..8d626797819 100644
--- a/qapi/qom.json
+++ b/qapi/qom.json
@@ -158,12 +158,12 @@
 # @paths: The absolute or partial path for each object, as described
 #     in `qom-get`.
 #
-# Errors:
-#     - If any path is not valid or is ambiguous
-#
 # Returns: A list where each element is the result for the
 #     corresponding element of @paths.
 #
+# Errors:
+#     - If any path is not valid or is ambiguous
+#
 # Since: 10.1
 ##
 { 'command': 'qom-list-get',
@@ -245,12 +245,12 @@
 #
 # @typename: the type name of an object
 #
+# Returns: a list describing object properties
+#
 # .. note:: Objects can create properties at runtime, for example to
 #    describe links between different devices and/or objects.  These
 #    properties are not included in the output of this command.
 #
-# Returns: a list describing object properties
-#
 # Since: 2.12
 ##
 { 'command': 'qom-list-properties',
diff --git a/qapi/virtio.json b/qapi/virtio.json
index 0a4753cc343..219c3f75043 100644
--- a/qapi/virtio.json
+++ b/qapi/virtio.json
@@ -26,12 +26,12 @@
 # @x-query-virtio:
 #     Return a list of all realized VirtIODevices
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: List of gathered VirtIODevices
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 7.2
 #
 # .. qmp-example::
@@ -189,12 +189,12 @@
 #
 # @path: Canonical QOM path of the VirtIODevice
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: Status of the virtio device
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 7.2
 #
 # .. qmp-example::
@@ -554,12 +554,12 @@
 #
 # @queue: VirtQueue index to examine
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: Status of the queue
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # .. note:: last_avail_idx will not be displayed in the case where the
 #    selected VirtIODevice has a running vhost device and the
 #    VirtIODevice VirtQueue index (queue) does not exist for the
@@ -678,12 +678,12 @@
 #
 # @queue: vhost_virtqueue index to examine
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: Status of the vhost_virtqueue
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 7.2
 #
 # .. qmp-example::
diff --git a/tests/qapi-schema/doc-good.json b/tests/qapi-schema/doc-good.json
index d9a84ccd729..50e4fdb54d1 100644
--- a/tests/qapi-schema/doc-good.json
+++ b/tests/qapi-schema/doc-good.json
@@ -160,16 +160,16 @@
 # @arg2: description starts on the same line
 #     remainder indented differently
 #
-# Features:
-# @cmd-feat1: a feature
-# @cmd-feat2: another feature
-#
-# .. note:: @arg3 is undocumented
-#
 # Returns: @Object
 #
 # Errors: some
 #
+# Features:
+# @cmd-feat1: a feature
+# @cmd-feat2: another feature
+#
+# .. note:: @arg3 is undocumented
+#
 # TODO: frobnicate
 #
 # .. admonition:: Notes
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
index 1d4fbd3762a..3858bfa861d 100644
--- a/tests/qapi-schema/doc-good.out
+++ b/tests/qapi-schema/doc-good.out
@@ -171,16 +171,16 @@ description starts on the same line
     remainder indented differently
     Member=arg3
 
-    Feature=cmd-feat1
-a feature
-    Feature=cmd-feat2
-another feature
-    Details
-.. note:: @arg3 is undocumented
     Returns
 @Object
     Errors
 some
+    Feature=cmd-feat1
+a feature
+    Feature=cmd-feat2
+another feature
+    Details
+.. note:: @arg3 is undocumented
     Todo
 frobnicate
     Details
diff --git a/tests/qapi-schema/doc-good.txt b/tests/qapi-schema/doc-good.txt
index ba089a7f942..1f8274ef1f3 100644
--- a/tests/qapi-schema/doc-good.txt
+++ b/tests/qapi-schema/doc-good.txt
@@ -115,21 +115,21 @@ Command cmd (Since: 2.10)
 
       * **arg3** ("boolean") -- Not documented
 
-   Features:
-      * **cmd-feat1** -- a feature
-
-      * **cmd-feat2** -- another feature
-
-   Note:
-
-     "arg3" is undocumented
-
    Return:
       "Object" -- "Object"
 
    Errors:
       some
 
+   Features:
+      * **cmd-feat1** -- a feature
+
+      * **cmd-feat2** -- another feature
+
+   Note:
+
+     "arg3" is undocumented
+
    Notes:
 
    * Lorem ipsum dolor sit amet
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 04/14] qapi/parser: enforce doc block section ordering
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (2 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 03/14] qapi: re-order QAPI doc block sections John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-29 12:36   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 05/14] qapi: move 'details' sections to the bottom of doc blocks John Snow
                   ` (9 subsequent siblings)
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

Enforce the relative ordering of "Intro", "Members", "Features",
"Errors" and "Returns" sections.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/devel/qapi-code-gen.rst | 19 +++++++++++------
 scripts/qapi/parser.py       | 41 ++++++++++++++++++++++++++++++------
 2 files changed, 48 insertions(+), 12 deletions(-)

diff --git a/docs/devel/qapi-code-gen.rst b/docs/devel/qapi-code-gen.rst
index 3ac1c2dae18..29e9c5a3da6 100644
--- a/docs/devel/qapi-code-gen.rst
+++ b/docs/devel/qapi-code-gen.rst
@@ -985,12 +985,19 @@ When documentation is required (see pragma_ 'doc-required'), every
 definition must have documentation.
 
 Definition documentation starts with a description naming the
-definition with an optional indented overview (the "intro"),
-:ref:`descriptions <doc-descrip>` of each argument (for commands and
-events), member (for structs and unions), branch (for alternates), or
-value (for enums), :ref:`feature descriptions <doc-feat>` of each
-feature (if any), and finally optional :ref:`tagged sections
-<doc-tagged>`.
+definition with an optional indented overview (the "intro").
+
+The introduction is followed by :ref:`descriptions <doc-descrip>` of
+each argument (for commands and events), member (for structs and
+unions), branch (for alternates), or value (for enums).
+
+:ref:`Tagged sections <doc-tagged>` follow in order: Returns (for
+commands), Errors (for commands), and lastly :ref:`feature
+descriptions <doc-feat>` if applicable.
+
+Definition documentation concludes with rST-formatted detail
+paragraphs (including examples, notes, &c), and finally an optional
+"Since" tagged section at the very end.
 
 .. _doc-descrip:
 
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index fe284a3f866..94a60a23482 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -545,6 +545,28 @@ def get_doc(self) -> 'QAPIDoc':
             doc.all_sections.append(QAPIDoc.Section(info, QAPIDoc.Kind.INTRO))
             line = self.get_doc_indented(doc)
             no_more_args = False
+            last_ordered_section = QAPIDoc.Kind.INTRO
+
+            def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
+                nonlocal last_ordered_section
+                if isinstance(this, str):
+                    this = QAPIDoc.Kind.from_string(this)
+
+                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE,
+                            QAPIDoc.Kind.DETAILS):
+                    return
+
+                if this.value < last_ordered_section.value:
+                    raise QAPIParseError(
+                        self,
+                        f"'{this}' section cannot appear after "
+                        f"'{last_ordered_section}' section, please re-order "
+                        "the sections and adjust phrasing as necessary to "
+                        "ensure consistent documentation flow between the "
+                        "source code and the rendered HTML manual"
+                    )
+
+                last_ordered_section = this
 
             while line is not None:
                 # Blank lines
@@ -558,6 +580,7 @@ def get_doc(self) -> 'QAPIDoc':
                     if doc.has_features:
                         raise QAPIParseError(
                             self, "duplicated 'Features:' line")
+                    _tag_check(QAPIDoc.Kind.FEATURE)
                     self.accept(False)
                     line = self.get_doc_line()
                     while line == '':
@@ -581,6 +604,7 @@ def get_doc(self) -> 'QAPIDoc':
                             self,
                             "description of '@%s:' follows a section"
                             % match.group(1))
+                    _tag_check(QAPIDoc.Kind.MEMBER)
                     while (line is not None
                            and (match := self._match_at_name_colon(line))):
                         doc.new_argument(self.info, match.group(1))
@@ -621,6 +645,7 @@ def get_doc(self) -> 'QAPIDoc':
                         )
                         raise QAPIParseError(self, emsg)
 
+                    _tag_check(match.group(1))
                     doc.new_tagged_section(
                         self.info,
                         QAPIDoc.Kind.from_string(match.group(1))
@@ -632,6 +657,7 @@ def get_doc(self) -> 'QAPIDoc':
                     no_more_args = True
                 else:
                     # plain paragraph(s)
+                    _tag_check(QAPIDoc.Kind.DETAILS)
                     doc.ensure_untagged_section(self.info)
                     doc.append_line(line)
                     line = self.get_doc_paragraph(doc)
@@ -674,14 +700,17 @@ class QAPIDoc:
     """
 
     class Kind(enum.Enum):
-        DETAILS = 0
+        # The order here is the order in which sections must appear in
+        # source code; with the exception of 'TODO' which may appear
+        # anywhere but is treated as a comment.
+        INTRO = 0
         MEMBER = 1
-        FEATURE = 2
-        RETURNS = 3
-        ERRORS = 4
-        SINCE = 5
+        RETURNS = 2
+        ERRORS = 3
+        FEATURE = 4
+        DETAILS = 5
         TODO = 6
-        INTRO = 7
+        SINCE = 7
 
         @staticmethod
         def from_string(kind: str) -> 'QAPIDoc.Kind':
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 05/14] qapi: move 'details' sections to the bottom of doc blocks
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (3 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 04/14] qapi/parser: enforce doc block section ordering John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-10-01 11:14   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 06/14] qapi/parser: enforce 'details' sections' relative position John Snow
                   ` (8 subsequent siblings)
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

This patch is focused on moving any and all "Details" sections to
below any Members/Returns/Errors/Features sections, generally
appearing last or after "Since:" sections.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/interop/firmware.json      |  54 +++---
 docs/interop/vhost-user.json    |  30 +--
 qapi/acpi.json                  |  28 +--
 qapi/block-core.json            | 334 ++++++++++++++++----------------
 qapi/block-export.json          |  16 +-
 qapi/block.json                 |  82 ++++----
 qapi/compat.json                |  14 +-
 qapi/crypto.json                |   4 +-
 qapi/introspect.json            |  14 +-
 qapi/job.json                   |  26 +--
 qapi/machine-s390x.json         |  16 +-
 qapi/machine.json               | 184 +++++++++---------
 qapi/migration.json             |  64 +++---
 qapi/misc-i386.json             |  28 +--
 qapi/misc.json                  |  26 +--
 qapi/net.json                   |   4 +-
 qapi/qdev.json                  |  12 +-
 qapi/qom.json                   |  26 +--
 qapi/stats.json                 |   6 +-
 qapi/transaction.json           |  34 ++--
 qapi/ui.json                    |  18 +-
 qga/qapi-schema.json            |  80 ++++----
 tests/qapi-schema/doc-good.json |   4 +-
 tests/qapi-schema/doc-good.out  |   4 +-
 tests/qapi-schema/doc-good.txt  |   4 +-
 25 files changed, 556 insertions(+), 556 deletions(-)

diff --git a/docs/interop/firmware.json b/docs/interop/firmware.json
index 1efd32c6315..c463fa6d05b 100644
--- a/docs/interop/firmware.json
+++ b/docs/interop/firmware.json
@@ -457,6 +457,33 @@
 #     Describes a firmware (or a firmware use case) to management
 #     software.
 #
+# @description: Provides a human-readable description of the firmware.
+#     Management software may or may not display @description.
+#
+# @interface-types: Lists the types of interfaces that the firmware
+#     can expose to the guest OS.  This is a non-empty, ordered list;
+#     entries near the beginning of @interface-types are considered
+#     more native to the firmware, and/or to have a higher quality
+#     implementation in the firmware, than entries near the end of
+#     @interface-types.
+#
+# @mapping: Describes the loading / mapping properties of the
+#     firmware.
+#
+# @targets: Collects the target architectures (QEMU system emulators)
+#     and their machine types that may execute the firmware.
+#
+# @features: Lists the features that the firmware supports, and the
+#     platform requirements it presents.
+#
+# @tags: A list of auxiliary strings associated with the firmware for
+#     which @description is not appropriate, due to the latter's
+#     possible exposure to the end-user.  @tags serves development and
+#     debugging purposes only, and management software shall
+#     explicitly ignore it.
+#
+# Since: 3.0
+#
 # It is possible for multiple @Firmware elements to match the search
 # criteria of management software.  Applications thus need rules to
 # pick one of the many matches, and users need the ability to override
@@ -512,33 +539,6 @@
 #
 #   $ vim /etc/qemu/firmware/99-ovmf.json
 #
-# @description: Provides a human-readable description of the firmware.
-#     Management software may or may not display @description.
-#
-# @interface-types: Lists the types of interfaces that the firmware
-#     can expose to the guest OS.  This is a non-empty, ordered list;
-#     entries near the beginning of @interface-types are considered
-#     more native to the firmware, and/or to have a higher quality
-#     implementation in the firmware, than entries near the end of
-#     @interface-types.
-#
-# @mapping: Describes the loading / mapping properties of the
-#     firmware.
-#
-# @targets: Collects the target architectures (QEMU system emulators)
-#     and their machine types that may execute the firmware.
-#
-# @features: Lists the features that the firmware supports, and the
-#     platform requirements it presents.
-#
-# @tags: A list of auxiliary strings associated with the firmware for
-#     which @description is not appropriate, due to the latter's
-#     possible exposure to the end-user.  @tags serves development and
-#     debugging purposes only, and management software shall
-#     explicitly ignore it.
-#
-# Since: 3.0
-#
 # .. qmp-example::
 #
 #     {
diff --git a/docs/interop/vhost-user.json b/docs/interop/vhost-user.json
index 65e681a166a..9a49ab6c14d 100644
--- a/docs/interop/vhost-user.json
+++ b/docs/interop/vhost-user.json
@@ -184,6 +184,21 @@
 # @VhostUserBackend:
 #     Describes a vhost user backend to management software.
 #
+# @type: The vhost user backend type.
+#
+# @description: Provides a human-readable description of the backend.
+#     Management software may or may not display @description.
+#
+# @binary: Absolute path to the backend binary.
+#
+# @tags: An optional list of auxiliary strings associated with the
+#     backend for which @description is not appropriate, due to the
+#     latter's possible exposure to the end-user.  @tags serves
+#     development and debugging purposes only, and management software
+#     shall explicitly ignore it.
+#
+# Since: 4.0
+#
 # It is possible for multiple @VhostUserBackend elements to match the
 # search criteria of management software.  Applications thus need
 # rules to pick one of the many matches, and users need the ability to
@@ -242,21 +257,6 @@
 #
 #   $ vim /etc/qemu/vhost-user/99-qemu-gpu.json
 #
-# @type: The vhost user backend type.
-#
-# @description: Provides a human-readable description of the backend.
-#     Management software may or may not display @description.
-#
-# @binary: Absolute path to the backend binary.
-#
-# @tags: An optional list of auxiliary strings associated with the
-#     backend for which @description is not appropriate, due to the
-#     latter's possible exposure to the end-user.  @tags serves
-#     development and debugging purposes only, and management software
-#     shall explicitly ignore it.
-#
-# Since: 4.0
-#
 # .. qmp-example:
 #
 # {
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 82e82d7cf89..8204d7ba52a 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -15,20 +15,6 @@
 # @AcpiTableOptions:
 #     Specify an ACPI table on the command line to load.
 #
-# At most one of @file and @data can be specified.  The list of files
-# specified by any one of them is loaded and concatenated in order.
-# If both are omitted, @data is implied.
-#
-# Other fields / optargs can be used to override fields of the generic
-# ACPI table header; refer to the ACPI specification 5.0, section
-# 5.2.6 System Description Table Header.  If a header field is not
-# overridden, then the corresponding value from the concatenated blob
-# is used (in case of @file), or it is filled in with a hard-coded
-# value (in case of @data).
-#
-# String fields are copied into the matching ACPI member from lowest
-# address upwards, and silently truncated / NUL-padded to length.
-#
 # @sig: table signature / identifier (4 bytes)
 #
 # @rev: table revision number (dependent on signature, 1 byte)
@@ -55,6 +41,20 @@
 #     table header.  At least one file is required.  This field
 #     excludes @file.
 #
+# At most one of @file and @data can be specified.  The list of files
+# specified by any one of them is loaded and concatenated in order.
+# If both are omitted, @data is implied.
+#
+# Other fields / optargs can be used to override fields of the generic
+# ACPI table header; refer to the ACPI specification 5.0, section
+# 5.2.6 System Description Table Header.  If a header field is not
+# overridden, then the corresponding value from the concatenated blob
+# is used (in case of @file), or it is filled in with a hard-coded
+# value (in case of @data).
+#
+# String fields are copied into the matching ACPI member from lowest
+# address upwards, and silently truncated / NUL-padded to length.
+#
 # Since: 1.5
 ##
 { 'struct': 'AcpiTableOptions',
diff --git a/qapi/block-core.json b/qapi/block-core.json
index 1fa6e34e884..c92dbb80818 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -846,6 +846,18 @@
 #     Image file size calculation information.  This structure
 #     describes the size requirements for creating a new image file.
 #
+# @required: Size required for a new image file, in bytes, when
+#     copying just allocated guest-visible contents.
+#
+# @fully-allocated: Image file size, in bytes, once data has been
+#     written to all sectors, when copying just guest-visible
+#     contents.
+#
+# @bitmaps: Additional size required if all the top-level bitmap
+#     metadata in the source image were to be copied to the
+#     destination, present only when source and destination both
+#     support persistent bitmaps.  (since 5.1)
+#
 # The size requirements depend on the new image file format.  File
 # size always equals virtual disk size for the 'raw' format, even for
 # sparse POSIX files.  Compact formats such as 'qcow2' represent
@@ -857,18 +869,6 @@
 # bitmap creation, may require additional space and is not covered
 # here.
 #
-# @required: Size required for a new image file, in bytes, when
-#     copying just allocated guest-visible contents.
-#
-# @fully-allocated: Image file size, in bytes, once data has been
-#     written to all sectors, when copying just guest-visible
-#     contents.
-#
-# @bitmaps: Additional size required if all the top-level bitmap
-#     metadata in the source image were to be copied to the
-#     destination, present only when source and destination both
-#     support persistent bitmaps.  (since 5.1)
-#
 # Since: 2.10
 ##
 { 'struct': 'BlockMeasureInfo',
@@ -1557,8 +1557,6 @@
 # @block_resize:
 #     Resize a block image while a guest is running.
 #
-# Either @device or @node-name must be set but not both.
-#
 # @device: the name of the device to get the image resized
 #
 # @node-name: graph node name to get the image resized (Since 2.0)
@@ -1570,6 +1568,8 @@
 #
 # Since: 0.14
 #
+# Either @device or @node-name must be set but not both.
+#
 # .. qmp-example::
 #
 #     -> { "execute": "block_resize",
@@ -1602,8 +1602,6 @@
 ##
 # @BlockdevSnapshotSync:
 #
-# Either @device or @node-name must be set but not both.
-#
 # @device: the name of the device to take a snapshot of.
 #
 # @node-name: graph node name to generate the snapshot from
@@ -1620,6 +1618,8 @@
 #
 # @mode: whether and how QEMU should create a new image, default is
 #     'absolute-paths'.
+#
+# Either @device or @node-name must be set but not both.
 ##
 { 'struct': 'BlockdevSnapshotSync',
   'data': { '*device': 'str', '*node-name': 'str',
@@ -1883,17 +1883,6 @@
 #     Live commit of data from overlay image nodes into backing nodes
 #     - i.e., writes data between 'top' and 'base' into 'base'.
 #
-# If top == base, that is an error.  If top has no overlays on top of
-# it, or if it is in use by a writer, the job will not be completed by
-# itself.  The user needs to complete the job with the `job-complete`
-# command after getting the ready event.  (Since 2.0)
-#
-# If the base image is smaller than top, then the base image will be
-# resized to be the same size as top.  If top is smaller than the base
-# image, the base will not be truncated.  If you want the base image
-# size to match the size of the smaller top, you can safely truncate
-# it yourself once the commit operation successfully completes.
-#
 # @job-id: identifier for the newly-created block job.  If omitted,
 #     the device name will be used.  (Since 2.7)
 #
@@ -1968,6 +1957,17 @@
 #
 # Since: 1.3
 #
+# If top == base, that is an error.  If top has no overlays on top of
+# it, or if it is in use by a writer, the job will not be completed by
+# itself.  The user needs to complete the job with the `job-complete`
+# command after getting the ready event.  (Since 2.0)
+#
+# If the base image is smaller than top, then the base image will be
+# resized to be the same size as top.  If top is smaller than the base
+# image, the base will not be truncated.  If you want the base image
+# size to match the size of the smaller top, you can safely truncate
+# it yourself once the commit operation successfully completes.
+#
 # .. qmp-example::
 #
 #     -> { "execute": "block-commit",
@@ -2862,6 +2862,66 @@
 # @block-stream:
 #     Copy data from a backing file into a block device.
 #
+# @job-id: identifier for the newly-created block job.  If omitted,
+#     the device name will be used.  (Since 2.7)
+#
+# @device: the device or node name of the top image
+#
+# @base: the common backing file name.  It cannot be set if @base-node
+#     or @bottom is also set.
+#
+# @base-node: the node name of the backing file.  It cannot be set if
+#     @base or @bottom is also set.  (Since 2.8)
+#
+# @bottom: the last node in the chain that should be streamed into
+#     top.  It cannot be set if @base or @base-node is also set.  It
+#     cannot be filter node.  (Since 6.0)
+#
+# @backing-file: The backing file string to write into the top image.
+#     This filename is not validated.
+#
+#     If a pathname string is such that it cannot be resolved by QEMU,
+#     that means that subsequent QMP or HMP commands must use
+#     node-names for the image in question, as filename lookup methods
+#     will fail.
+#
+#     If not specified, QEMU will automatically determine the backing
+#     file string to use, or error out if there is no obvious choice.
+#     Care should be taken when specifying the string, to specify a
+#     valid filename or protocol.  (Since 2.1)
+#
+# @backing-mask-protocol: If true, replace any protocol mentioned in
+#     the 'backing file format' with 'raw', rather than storing the
+#     protocol name as the backing format.  Can be used even when no
+#     image header will be updated (default false; since 9.0).
+#
+# @speed: the maximum speed, in bytes per second
+#
+# @on-error: the action to take on an error (default report).  'stop'
+#     and 'enospc' can only be used if the block device supports
+#     io-status (see `BlockInfo`).  (Since 1.3)
+#
+# @filter-node-name: the node name that should be assigned to the
+#     filter driver that the stream job inserts into the graph above
+#     @device.  If this option is not given, a node name is
+#     autogenerated.  (Since: 6.0)
+#
+# @auto-finalize: When false, this job will wait in a PENDING state
+#     after it has finished its work, waiting for `job-finalize`
+#     before making any block graph changes.  When true, this job will
+#     automatically perform its abort or commit actions.  Defaults to
+#     true.  (Since 3.1)
+#
+# @auto-dismiss: When false, this job will wait in a CONCLUDED state
+#     after it has completely ceased all work, and awaits
+#     `job-dismiss`.  When true, this job will automatically disappear
+#     without user intervention.  Defaults to true.  (Since 3.1)
+#
+# Errors:
+#     - If @device does not exist, DeviceNotFound.
+#
+# Since: 1.1
+#
 # The block streaming operation is performed in the background until
 # the entire backing file has been copied.  This command returns
 # immediately once streaming has started.  The status of ongoing block
@@ -2892,66 +2952,6 @@
 # non-filter overlay node below it to point to the new backing node
 # instead of modifying @device itself.
 #
-# @job-id: identifier for the newly-created block job.  If omitted,
-#     the device name will be used.  (Since 2.7)
-#
-# @device: the device or node name of the top image
-#
-# @base: the common backing file name.  It cannot be set if @base-node
-#     or @bottom is also set.
-#
-# @base-node: the node name of the backing file.  It cannot be set if
-#     @base or @bottom is also set.  (Since 2.8)
-#
-# @bottom: the last node in the chain that should be streamed into
-#     top.  It cannot be set if @base or @base-node is also set.  It
-#     cannot be filter node.  (Since 6.0)
-#
-# @backing-file: The backing file string to write into the top image.
-#     This filename is not validated.
-#
-#     If a pathname string is such that it cannot be resolved by QEMU,
-#     that means that subsequent QMP or HMP commands must use
-#     node-names for the image in question, as filename lookup methods
-#     will fail.
-#
-#     If not specified, QEMU will automatically determine the backing
-#     file string to use, or error out if there is no obvious choice.
-#     Care should be taken when specifying the string, to specify a
-#     valid filename or protocol.  (Since 2.1)
-#
-# @backing-mask-protocol: If true, replace any protocol mentioned in
-#     the 'backing file format' with 'raw', rather than storing the
-#     protocol name as the backing format.  Can be used even when no
-#     image header will be updated (default false; since 9.0).
-#
-# @speed: the maximum speed, in bytes per second
-#
-# @on-error: the action to take on an error (default report).  'stop'
-#     and 'enospc' can only be used if the block device supports
-#     io-status (see `BlockInfo`).  (Since 1.3)
-#
-# @filter-node-name: the node name that should be assigned to the
-#     filter driver that the stream job inserts into the graph above
-#     @device.  If this option is not given, a node name is
-#     autogenerated.  (Since: 6.0)
-#
-# @auto-finalize: When false, this job will wait in a PENDING state
-#     after it has finished its work, waiting for `job-finalize`
-#     before making any block graph changes.  When true, this job will
-#     automatically perform its abort or commit actions.  Defaults to
-#     true.  (Since 3.1)
-#
-# @auto-dismiss: When false, this job will wait in a CONCLUDED state
-#     after it has completely ceased all work, and awaits
-#     `job-dismiss`.  When true, this job will automatically disappear
-#     without user intervention.  Defaults to true.  (Since 3.1)
-#
-# Errors:
-#     - If @device does not exist, DeviceNotFound.
-#
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-stream",
@@ -2973,10 +2973,6 @@
 # @block-job-set-speed:
 #     Set maximum speed for a background block operation.
 #
-# This command can only be issued when there is an active block job.
-#
-# Throttling can be disabled by setting the speed to 0.
-#
 # @device: The job identifier.  This used to be a device name (hence
 #     the name of the parameter), but since QEMU 2.7 it can have other
 #     values.
@@ -2988,6 +2984,10 @@
 #     - If no background operation is active on this device,
 #       DeviceNotActive
 #
+# This command can only be issued when there is an active block job.
+#
+# Throttling can be disabled by setting the speed to 0.
+#
 # Since: 1.1
 ##
 { 'command': 'block-job-set-speed',
@@ -2998,6 +2998,19 @@
 # @block-job-cancel:
 #     Stop an active background block operation.
 #
+# @device: The job identifier.  This used to be a device name (hence
+#     the name of the parameter), but since QEMU 2.7 it can have other
+#     values.
+#
+# @force: If true, and the job has already emitted the event
+#     `BLOCK_JOB_READY`, abandon the job immediately (even if it is
+#     paused) instead of waiting for the destination to complete its
+#     final synchronization (since 1.3)
+#
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
 # This command returns immediately after marking the active background
 # block operation for cancellation.  It is an error to call this
 # command if no operation is in progress.
@@ -3018,19 +3031,6 @@
 # cancelled.  A new streaming operation can be started at a later time
 # to finish copying all data from the backing file.
 #
-# @device: The job identifier.  This used to be a device name (hence
-#     the name of the parameter), but since QEMU 2.7 it can have other
-#     values.
-#
-# @force: If true, and the job has already emitted the event
-#     `BLOCK_JOB_READY`, abandon the job immediately (even if it is
-#     paused) instead of waiting for the destination to complete its
-#     final synchronization (since 1.3)
-#
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
 # Since: 1.1
 ##
 { 'command': 'block-job-cancel', 'data': { 'device': 'str', '*force': 'bool' },
@@ -3040,6 +3040,19 @@
 # @block-job-pause:
 #     Pause an active background block operation.
 #
+# @device: The job identifier.  This used to be a device name (hence
+#     the name of the parameter), but since QEMU 2.7 it can have other
+#     values.
+#
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `job-pause`
+#     instead.
+#
 # This command returns immediately after marking the active job for
 # pausing.  Pausing an already paused job is an error.
 #
@@ -3049,19 +3062,6 @@
 #
 # Cancelling a paused job automatically resumes it.
 #
-# @device: The job identifier.  This used to be a device name (hence
-#     the name of the parameter), but since QEMU 2.7 it can have other
-#     values.
-#
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `job-pause`
-#     instead.
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-pause', 'data': { 'device': 'str' },
@@ -3072,24 +3072,24 @@
 # @block-job-resume:
 #     Resume an active background block operation.
 #
+# @device: The job identifier.  This used to be a device name (hence
+#     the name of the parameter), but since QEMU 2.7 it can have other
+#     values.
+#
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `job-resume`
+#     instead.
+#
 # This command returns immediately after resuming a paused job.
 # Resuming an already running job is an error.
 #
 # This command also clears the error status of the job.
 #
-# @device: The job identifier.  This used to be a device name (hence
-#     the name of the parameter), but since QEMU 2.7 it can have other
-#     values.
-#
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `job-resume`
-#     instead.
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-resume', 'data': { 'device': 'str' },
@@ -3102,6 +3102,19 @@
 #     STANDBY state.  Completing the job in any other state is an
 #     error.
 #
+# @device: The job identifier.  This used to be a device name (hence
+#     the name of the parameter), but since QEMU 2.7 it can have other
+#     values.
+#
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `job-complete`
+#     instead.
+#
 # This is supported only for drive mirroring, where it also switches
 # the device to write to the target path only.  Note that drive
 # mirroring includes `drive-mirror`, `blockdev-mirror` and
@@ -3117,19 +3130,6 @@
 # rerror/werror arguments that were specified when starting the
 # operation.
 #
-# @device: The job identifier.  This used to be a device name (hence
-#     the name of the parameter), but since QEMU 2.7 it can have other
-#     values.
-#
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `job-complete`
-#     instead.
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-complete', 'data': { 'device': 'str' },
@@ -3146,18 +3146,18 @@
 #     `blockdev-mirror`, `block-commit` and `block-stream`.
 #     @auto-dismiss is enabled by default for these jobs.
 #
+# @id: The job identifier.
+#
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `job-dismiss`
+#     instead.
+#
 # This command will refuse to operate on any job that has not yet
 # reached its terminal state, CONCLUDED.  For jobs that make use of
 # the `BLOCK_JOB_READY` event, `job-cancel`, `block-job-cancel` or
 # `job-complete` will still need to be used as appropriate.
 #
-# @id: The job identifier.
-#
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `job-dismiss`
-#     instead.
-#
 # Since: 2.12
 ##
 { 'command': 'block-job-dismiss', 'data': { 'id': 'str' },
@@ -3171,6 +3171,13 @@
 #     do any necessary cleanup.  This command requires that all
 #     involved jobs are in the PENDING state.
 #
+# @id: The job identifier.
+#
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `job-finalize`
+#     instead.
+#
 # For jobs in a transaction, instructing one job to finalize will
 # force ALL jobs in the transaction to finalize, so it is only
 # necessary to instruct a single member job to finalize.
@@ -3178,13 +3185,6 @@
 # The command is applicable only to jobs which have @auto-finalize
 # option and only when this option is set to false.
 #
-# @id: The job identifier.
-#
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `job-finalize`
-#     instead.
-#
 # Since: 2.12
 ##
 { 'command': 'block-job-finalize', 'data': { 'id': 'str' },
@@ -4986,10 +4986,6 @@
 #     Activate or deactivate a block device.  Use this to manage the
 #     handover of block devices on migration with qemu-storage-daemon.
 #
-# Activating a node automatically activates all of its child nodes
-# first.  Deactivating a node automatically deactivates any of its
-# child nodes that are not in use by a still active node.
-#
 # @node-name: Name of the graph node to activate or deactivate.  By
 #     default, all nodes are affected by the operation.
 #
@@ -4998,6 +4994,10 @@
 #
 # Since: 10.0
 #
+# Activating a node automatically activates all of its child nodes
+# first.  Deactivating a node automatically deactivates any of its
+# child nodes that are not in use by a still active node.
+#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-set-active",
@@ -5898,12 +5898,6 @@
 # @x-blockdev-change:
 #     Dynamically reconfigure the block driver state graph.
 #
-# Currently only supports adding and deleting quorum children.  A
-# child will be added at the end of the list of children.  Its
-# contents *must* be consistent with the other childrens' contents.
-# Deleting a child that is not last in the list of children is
-# problematic, because it "renumbers" the children following it.
-#
 # @parent: the id or name of the parent node.
 #
 # @child: the name of a child to be deleted.  Mutually exclusive with
@@ -5923,6 +5917,12 @@
 #
 # Since: 2.7
 #
+# Currently only supports adding and deleting quorum children.  A
+# child will be added at the end of the list of children.  Its
+# contents *must* be consistent with the other childrens' contents.
+# Deleting a child that is not last in the list of children is
+# problematic, because it "renumbers" the children following it.
+#
 # .. qmp-example::
 #    :title: Add a new node to a quorum
 #
diff --git a/qapi/block-export.json b/qapi/block-export.json
index 172d18b3556..b335b0620cc 100644
--- a/qapi/block-export.json
+++ b/qapi/block-export.json
@@ -157,11 +157,6 @@
 #     Options for exporting a block graph node on some (file)
 #     mountpoint as a raw image.
 #
-# Multi-threading note: The FUSE export supports multi-threading.
-# Currently, requests are distributed across these threads in a
-# round-robin fashion, i.e. independently of the CPU core from which a
-# request originates.
-#
 # @mountpoint: Path on which to export the block device via FUSE.
 #     This must point to an existing regular file.
 #
@@ -178,6 +173,11 @@
 #     mount the export with allow_other, and if that fails, try again
 #     without.  (since 6.1; default: auto)
 #
+# Multi-threading note: The FUSE export supports multi-threading.
+# Currently, requests are distributed across these threads in a
+# round-robin fashion, i.e. independently of the CPU core from which a
+# request originates.
+#
 # Since: 6.0
 ##
 { 'struct': 'BlockExportOptionsFuse',
@@ -236,9 +236,6 @@
 # @nbd-server-add:
 #     Export a block node to QEMU's embedded NBD server.
 #
-# The export name will be used as the id for the resulting block
-# export.
-#
 # Errors:
 #     - if the server is not running
 #     - if an export with the same name already exists
@@ -248,6 +245,9 @@
 # @deprecated: This command is deprecated.  Use `block-export-add`
 #     instead.
 #
+# The export name will be used as the id for the resulting block
+# export.
+#
 # Since: 1.3
 ##
 { 'command': 'nbd-server-add',
diff --git a/qapi/block.json b/qapi/block.json
index 4b96a72fe70..99b888d78e9 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -130,6 +130,21 @@
 # @blockdev-open-tray:
 #     Opens a block device's tray.
 #
+# @device: Block device name
+#
+# @id: The name or QOM path of the guest device (since: 2.8)
+#
+# @force: if false (the default), an eject request will be sent to the
+#     guest if it has locked the tray (and the tray will not be opened
+#     immediately); if true, the tray will be opened regardless of
+#     whether it is locked
+#
+# Features:
+#
+# @deprecated: Member @device is deprecated.  Use @id instead.
+#
+# Since: 2.5
+#
 # If there is a block driver state tree inserted as a medium, it will
 # become inaccessible to the guest (but it will remain associated to
 # the block device, so closing the tray will make it accessible
@@ -146,21 +161,6 @@
 #   device attached to it
 # - if the guest device does not have an actual tray
 #
-# @device: Block device name
-#
-# @id: The name or QOM path of the guest device (since: 2.8)
-#
-# @force: if false (the default), an eject request will be sent to the
-#     guest if it has locked the tray (and the tray will not be opened
-#     immediately); if true, the tray will be opened regardless of
-#     whether it is locked
-#
-# Features:
-#
-# @deprecated: Member @device is deprecated.  Use @id instead.
-#
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-open-tray",
@@ -184,22 +184,22 @@
 # @blockdev-close-tray:
 #     Closes a block device's tray.
 #
+# @device: Block device name
+#
+# @id: The name or QOM path of the guest device (since: 2.8)
+#
+# Features:
+#
+# @deprecated: Member @device is deprecated.  Use @id instead.
+#
+# Since: 2.5
+#
 # If there is a block driver state tree associated with the block
 # device (which is currently ejected), that tree will be loaded as the
 # medium.
 #
 # If the tray was already closed before, this will be a no-op.
 #
-# @device: Block device name
-#
-# @id: The name or QOM path of the guest device (since: 2.8)
-#
-# Features:
-#
-# @deprecated: Member @device is deprecated.  Use @id instead.
-#
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-close-tray",
@@ -224,13 +224,13 @@
 #     device.  That block device's tray must currently be open
 #     (unless there is no attached guest device).
 #
+# @id: The name or QOM path of the guest device
+#
+# Since: 2.12
+#
 # If the tray is open and there is no medium inserted, this will
 # be a no-op.
 #
-# @id: The name or QOM path of the guest device
-#
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-remove-medium",
@@ -432,6 +432,11 @@
 # @block_set_io_throttle:
 #     Change I/O throttle limits for a block drive.
 #
+# Errors:
+#     - If @device is not a valid block device, DeviceNotFound
+#
+# Since: 1.1
+#
 # Since QEMU 2.4, each device with I/O limits is member of a throttle
 # group.
 #
@@ -453,11 +458,6 @@
 # case the device will be removed from its group and the rest of its
 # members will not be affected.  The 'group' parameter is ignored.
 #
-# Errors:
-#     - If @device is not a valid block device, DeviceNotFound
-#
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block_set_io_throttle",
@@ -506,10 +506,6 @@
 # @block-latency-histogram-set:
 #     Manage read, write and flush latency histograms for the device.
 #
-# If only @id parameter is specified, remove all present latency
-# histograms for the device.  Otherwise, add/reset some of (or all)
-# latency histograms.
-#
 # @id: The name or QOM path of the guest device.
 #
 # @boundaries: list of interval boundary values (see description in
@@ -539,6 +535,10 @@
 #
 # Since: 4.0
 #
+# If only @id parameter is specified, remove all present latency
+# histograms for the device.  Otherwise, add/reset some of (or all)
+# latency histograms.
+#
 # .. qmp-example::
 #    :annotated:
 #
@@ -599,9 +599,6 @@
 # @x-qemu-io:
 #     Run a qemu-io command on a block device.
 #
-# Take either a block backend name or a qdev ID to identify the
-# device.
-#
 # @device: the block backend name, node-name to run the
 #     command on.
 #
@@ -616,6 +613,9 @@
 #
 # Since: 11.2
 #
+# Take either a block backend name or a qdev ID to identify the
+# device.
+#
 # .. qmp-example::
 #
 #     -> { "execute": "x-qemu-io",
diff --git a/qapi/compat.json b/qapi/compat.json
index 0f90ea1f524..16e227c61c6 100644
--- a/qapi/compat.json
+++ b/qapi/compat.json
@@ -57,13 +57,6 @@
 #
 #     This is intended for testing users of the management interfaces.
 #
-# Limitation: covers only syntactic aspects of QMP, i.e. stuff tagged
-# with feature 'deprecated' or 'unstable'.  We may want to extend it
-# to cover semantic aspects and CLI.
-#
-# Limitation: deprecated-output policy @hide is not implemented for
-# enumeration values.  They behave the same as with policy @accept.
-#
 # @deprecated-input: how to handle deprecated input (default 'accept')
 #
 # @deprecated-output: how to handle deprecated output (default
@@ -78,6 +71,13 @@
 # @insecure-types: how to handle types that are not declared secure
 #     (default 'accept') (since 11.2)
 #
+# Limitation: covers only syntactic aspects of QMP, i.e. stuff tagged
+# with feature 'deprecated' or 'unstable'.  We may want to extend it
+# to cover semantic aspects and CLI.
+#
+# Limitation: deprecated-output policy @hide is not implemented for
+# enumeration values.  They behave the same as with policy @accept.
+#
 # Since: 6.0
 ##
 { 'struct': 'CompatPolicy',
diff --git a/qapi/crypto.json b/qapi/crypto.json
index a8d753d360f..5d0bcdf2b40 100644
--- a/qapi/crypto.json
+++ b/qapi/crypto.json
@@ -431,12 +431,12 @@
 # @SecretProperties:
 #     Properties for secret objects.
 #
-# Either @data or @file must be provided, but not both.
-#
 # @data: the associated with the secret from
 #
 # @file: the filename to load the data associated with the secret from
 #
+# Either @data or @file must be provided, but not both.
+#
 # Since: 2.6
 ##
 { 'struct': 'SecretProperties',
diff --git a/qapi/introspect.json b/qapi/introspect.json
index d579e5c8554..693ecff6be7 100644
--- a/qapi/introspect.json
+++ b/qapi/introspect.json
@@ -22,6 +22,13 @@
 #     and events are available in this QEMU, and their parameters and
 #     results.
 #
+# Returns: an array where each element describes an entity in the ABI:
+#     command, event, type, ...
+#
+#     The order of the various elements is unspecified; however, all
+#     names are guaranteed to be unique (no name will be duplicated
+#     with different meta-types).
+#
 # The `SchemaInfo` can't reflect all the rules and restrictions that
 # apply to QMP.  It's interface introspection (figuring out what's
 # there), not interface specification.  The specification is in the
@@ -36,13 +43,6 @@
 # string into a specific enum or from one specific type into an
 # alternate that includes the original type alongside something else.
 #
-# Returns: an array where each element describes an entity in the ABI:
-#     command, event, type, ...
-#
-#     The order of the various elements is unspecified; however, all
-#     names are guaranteed to be unique (no name will be duplicated
-#     with different meta-types).
-#
 # .. note:: The QAPI schema is also used to help define *internal*
 #    interfaces, by defining QAPI types.  These are not part of the
 #    QMP wire ABI, and therefore not returned by this command.
diff --git a/qapi/job.json b/qapi/job.json
index aa75a8efa24..8ecb50f9228 100644
--- a/qapi/job.json
+++ b/qapi/job.json
@@ -130,6 +130,8 @@
 # @job-pause:
 #     Pause an active job.
 #
+# @id: The job identifier.
+#
 # This command returns immediately after marking the active job for
 # pausing.  Pausing an already paused job is an error.
 #
@@ -139,8 +141,6 @@
 #
 # Cancelling a paused job automatically resumes it.
 #
-# @id: The job identifier.
-#
 # Since: 3.0
 ##
 { 'command': 'job-pause', 'data': { 'id': 'str' } }
@@ -149,14 +149,14 @@
 # @job-resume:
 #     Resume a paused job.
 #
+# @id: The job identifier.
+#
 # This command returns immediately after resuming a paused job.
 # Resuming an already running job is an error.
 #
 # This command also clears the error status for block-jobs (stream,
 # commit, mirror, backup).
 #
-# @id: The job identifier.
-#
 # Since: 3.0
 ##
 { 'command': 'job-resume', 'data': { 'id': 'str' } }
@@ -167,14 +167,14 @@
 #     opportunity.  This command returns immediately after marking the
 #     active job for cancellation.
 #
+# @id: The job identifier.
+#
 # The job will cancel as soon as possible and then emit a
 # `JOB_STATUS_CHANGE` event.  Usually, the status will change to
 # ABORTING, but it is possible that a job successfully completes (e.g.
 # because it was almost done and there was no opportunity to cancel
 # earlier than completing the job) and transitions to PENDING instead.
 #
-# @id: The job identifier.
-#
 # Since: 3.0
 ##
 { 'command': 'job-cancel', 'data': { 'id': 'str' } }
@@ -185,6 +185,8 @@
 #     STANDBY state.  Completing the job in any other state is an
 #     error.
 #
+# @id: The job identifier.
+#
 # This is supported only for drive mirroring, where it also switches
 # the device to write to the target path only.  Note that drive
 # mirroring includes `drive-mirror`, `blockdev-mirror` and
@@ -200,8 +202,6 @@
 # rerror/werror arguments that were specified when starting the
 # operation.
 #
-# @id: The job identifier.
-#
 # Since: 3.0
 ##
 { 'command': 'job-complete', 'data': { 'id': 'str' } }
@@ -210,6 +210,8 @@
 # @job-dismiss:
 #     Deletes a job that is in the CONCLUDED state.
 #
+# @id: The job identifier.
+#
 # This command only needs to be run explicitly for jobs that don't
 # have automatic dismiss enabled.  In turn, automatic dismiss may be
 # enabled only for jobs that have @auto-dismiss option, which are
@@ -222,8 +224,6 @@
 # the JOB_READY event, `job-cancel` or `job-complete` will still need
 # to be used as appropriate.
 #
-# @id: The job identifier.
-#
 # Since: 3.0
 ##
 { 'command': 'job-dismiss', 'data': { 'id': 'str' } }
@@ -235,6 +235,9 @@
 #     do any necessary cleanup.  This command requires that all
 #     involved jobs are in the PENDING state.
 #
+# @id: The identifier of any job in the transaction, or of a job that
+#     is not part of any transaction.
+#
 # For jobs in a transaction, instructing one job to finalize will
 # force ALL jobs in the transaction to finalize, so it is only
 # necessary to instruct a single member job to finalize.
@@ -242,9 +245,6 @@
 # The command is applicable only to jobs which have @auto-finalize
 # option and only when this option is set to false.
 #
-# @id: The identifier of any job in the transaction, or of a job that
-#     is not part of any transaction.
-#
 # Since: 3.0
 ##
 { 'command': 'job-finalize', 'data': { 'id': 'str' } }
diff --git a/qapi/machine-s390x.json b/qapi/machine-s390x.json
index 4a745d0d73f..911989bb4f7 100644
--- a/qapi/machine-s390x.json
+++ b/qapi/machine-s390x.json
@@ -59,6 +59,14 @@
 # @CPU_POLARIZATION_CHANGE:
 #     Emitted when the guest asks to change the polarization.
 #
+# @polarization: polarization specified by the guest
+#
+# Features:
+#
+# @unstable: This event is experimental.
+#
+# Since: 8.2
+#
 # The guest can tell the host (via the PTF instruction) whether the
 # CPUs should be provisioned using horizontal or vertical
 # polarization.
@@ -70,14 +78,6 @@
 # differently.  The guest will get information on the details of the
 # provisioning the next time it uses the STSI(15) instruction.
 #
-# @polarization: polarization specified by the guest
-#
-# Features:
-#
-# @unstable: This event is experimental.
-#
-# Since: 8.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "CPU_POLARIZATION_CHANGE",
diff --git a/qapi/machine.json b/qapi/machine.json
index 911132918bd..8aae66fcfde 100644
--- a/qapi/machine.json
+++ b/qapi/machine.json
@@ -645,9 +645,6 @@
 #     Bandwidth Information Structure of HMAT (Heterogeneous Memory
 #     Attribute Table)
 #
-# For more information about `HmatLBMemoryHierarchy`, see chapter
-# 5.2.27.4: Table 5-146: Field "Flags" of ACPI 6.3 spec.
-#
 # @memory: the structure represents the memory performance
 #
 # @first-level: first level of memory side cache
@@ -656,6 +653,9 @@
 #
 # @third-level: third level of memory side cache
 #
+# For more information about `HmatLBMemoryHierarchy`, see chapter
+# 5.2.27.4: Table 5-146: Field "Flags" of ACPI 6.3 spec.
+#
 # Since: 5.0
 ##
 { 'enum': 'HmatLBMemoryHierarchy',
@@ -667,9 +667,6 @@
 #     Information Structure of HMAT (Heterogeneous Memory Attribute
 #     Table)
 #
-# For more information about `HmatLBDataType`, see chapter 5.2.27.4:
-# Table 5-146: Field "Data Type" of ACPI 6.3 spec.
-#
 # @access-latency: access latency (nanoseconds)
 #
 # @read-latency: read latency (nanoseconds)
@@ -682,6 +679,9 @@
 #
 # @write-bandwidth: write bandwidth (Bytes per second)
 #
+# For more information about `HmatLBDataType`, see chapter 5.2.27.4:
+# Table 5-146: Field "Data Type" of ACPI 6.3 spec.
+#
 # Since: 5.0
 ##
 { 'enum': 'HmatLBDataType',
@@ -693,9 +693,6 @@
 #     Set the system locality latency and bandwidth information
 #     between Initiator and Target proximity Domains.
 #
-# For more information about `NumaHmatLBOptions`, see chapter
-# 5.2.27.4: Table 5-146 of ACPI 6.3 spec.
-#
 # @initiator: the Initiator Proximity Domain.
 #
 # @target: the Target Proximity Domain.
@@ -712,6 +709,9 @@
 # @bandwidth: the value of bandwidth between @initiator and @target
 #     proximity domain, the bandwidth unit is "Bytes per second".
 #
+# For more information about `NumaHmatLBOptions`, see chapter
+# 5.2.27.4: Table 5-146 of ACPI 6.3 spec.
+#
 # Since: 5.0
 ##
 { 'struct': 'NumaHmatLBOptions',
@@ -728,9 +728,6 @@
 #     Cache associativity in the Memory Side Cache Information
 #     Structure of HMAT
 #
-# For more information of `HmatCacheAssociativity`, see chapter
-# 5.2.27.5: Table 5-147 of ACPI 6.3 spec.
-#
 # @none: None (no memory side cache in this proximity domain, or cache
 #     associativity unknown)
 #
@@ -738,6 +735,9 @@
 #
 # @complex: Complex Cache Indexing (implementation specific)
 #
+# For more information of `HmatCacheAssociativity`, see chapter
+# 5.2.27.5: Table 5-147 of ACPI 6.3 spec.
+#
 # Since: 5.0
 ##
 { 'enum': 'HmatCacheAssociativity',
@@ -748,9 +748,6 @@
 #     Cache write policy in the Memory Side Cache Information
 #     Structure of HMAT
 #
-# For more information of `HmatCacheWritePolicy`, see chapter
-# 5.2.27.5: Table 5-147: Field "Cache Attributes" of ACPI 6.3 spec.
-#
 # @none: None (no memory side cache in this proximity domain, or cache
 #     write policy unknown)
 #
@@ -758,6 +755,9 @@
 #
 # @write-through: Write Through (WT)
 #
+# For more information of `HmatCacheWritePolicy`, see chapter
+# 5.2.27.5: Table 5-147: Field "Cache Attributes" of ACPI 6.3 spec.
+#
 # Since: 5.0
 ##
 { 'enum': 'HmatCacheWritePolicy',
@@ -767,9 +767,6 @@
 # @NumaHmatCacheOptions:
 #     Set the memory side cache information for a given memory domain.
 #
-# For more information of `NumaHmatCacheOptions`, see chapter
-# 5.2.27.5: Table 5-147: Field "Cache Attributes" of ACPI 6.3 spec.
-#
 # @node-id: the memory proximity domain to which the memory belongs.
 #
 # @size: the size of memory side cache in bytes.
@@ -783,6 +780,9 @@
 #
 # @line: the cache line size in bytes.
 #
+# For more information of `NumaHmatCacheOptions`, see chapter
+# 5.2.27.5: Table 5-147: Field "Cache Attributes" of ACPI 6.3 spec.
+#
 # Since: 5.0
 ##
 { 'struct': 'NumaHmatCacheOptions',
@@ -932,15 +932,6 @@
 # @CpuInstanceProperties:
 #     Properties identifying a CPU.
 #
-# Which members are optional and which mandatory depends on the
-# architecture and board.
-#
-# For s390x see :ref:`cpu-topology-s390x`.
-#
-# The ids other than the node-id specify the position of the CPU
-# within the CPU topology (as defined by the machine property "smp",
-# thus see also type `SMPConfiguration`)
-#
 # @node-id: NUMA node ID the CPU belongs to
 #
 # @drawer-id: drawer number within CPU topology the CPU belongs to
@@ -964,6 +955,15 @@
 #
 # @thread-id: thread number within the core the CPU  belongs to
 #
+# Which members are optional and which mandatory depends on the
+# architecture and board.
+#
+# For s390x see :ref:`cpu-topology-s390x`.
+#
+# The ids other than the node-id specify the position of the CPU
+# within the CPU topology (as defined by the machine property "smp",
+# thus see also type `SMPConfiguration`)
+#
 # Since: 2.7
 ##
 { 'struct': 'CpuInstanceProperties',
@@ -1635,18 +1635,6 @@
 #     QEMU figure out a suitable value based on the ones that are
 #     provided.
 #
-# The members other than @cpus and @maxcpus define a topology of
-# containers.
-#
-# The ordering from highest/coarsest to lowest/finest is: @drawers,
-# @books, @sockets, @dies, @clusters, @cores, @threads.
-#
-# Different architectures support different subsets of topology
-# containers.
-#
-# For example, s390x does not have clusters and dies, and the socket
-# is the parent container of cores.
-#
 # @cpus: number of virtual CPUs in the virtual machine
 #
 # @maxcpus: maximum number of hotpluggable virtual CPUs in the virtual
@@ -1668,6 +1656,18 @@
 #
 # @threads: number of threads per core
 #
+# The members other than @cpus and @maxcpus define a topology of
+# containers.
+#
+# The ordering from highest/coarsest to lowest/finest is: @drawers,
+# @books, @sockets, @dies, @clusters, @cores, @threads.
+#
+# Different architectures support different subsets of topology
+# containers.
+#
+# For example, s390x does not have clusters and dies, and the socket
+# is the parent container of cores.
+#
 # Since: 6.1
 ##
 { 'struct': 'SMPConfiguration', 'data': {
@@ -1894,16 +1894,16 @@
 # @CpuModelInfo:
 #     Virtual CPU model.
 #
+# @name: the name of the CPU definition the model is based on
+#
+# @props: a dictionary of QOM properties to be applied
+#
 # A CPU model consists of the name of a CPU definition, to which delta
 # changes are applied (e.g. features added/removed).  Most magic
 # values that an architecture might require should be hidden behind
 # the name.  However, if required, architectures can expose relevant
 # properties.
 #
-# @name: the name of the CPU definition the model is based on
-#
-# @props: a dictionary of QOM properties to be applied
-#
 # Since: 2.8
 ##
 { 'struct': 'CpuModelInfo',
@@ -2004,6 +2004,21 @@
 #     Compares two CPU models, @modela and @modelb, returning how they
 #     compare in a specific configuration.
 #
+# @modela: description of the first CPU model to compare, referred to
+#     as "model A" in `CpuModelCompareResult`
+#
+# @modelb: description of the second CPU model to compare, referred to
+#     as "model B" in `CpuModelCompareResult`
+#
+# Returns: a `CpuModelCompareInfo` describing how both CPU models
+#     compare
+#
+# Errors:
+#     - if comparing CPU models is not supported by the target
+#     - if a model cannot be used
+#     - if a model contains an unknown cpu definition name, unknown
+#       properties or properties with wrong types.
+#
 # The results indicates how both models compare regarding runnability.
 # This result can be used by tooling to make decisions if a certain
 # CPU model will run in a certain configuration or if a compatible CPU
@@ -2034,21 +2049,6 @@
 # Some architectures may not support comparing CPU models.  s390x
 # supports comparing CPU models.
 #
-# @modela: description of the first CPU model to compare, referred to
-#     as "model A" in `CpuModelCompareResult`
-#
-# @modelb: description of the second CPU model to compare, referred to
-#     as "model B" in `CpuModelCompareResult`
-#
-# Returns: a `CpuModelCompareInfo` describing how both CPU models
-#     compare
-#
-# Errors:
-#     - if comparing CPU models is not supported by the target
-#     - if a model cannot be used
-#     - if a model contains an unknown cpu definition name, unknown
-#       properties or properties with wrong types.
-#
 # Since: 2.8
 ##
 { 'command': 'query-cpu-model-comparison',
@@ -2062,6 +2062,18 @@
 #     static, migration-safe CPU model (see "static" CPU model
 #     expansion for details).
 #
+# @modela: description of the first CPU model to baseline
+#
+# @modelb: description of the second CPU model to baseline
+#
+# Returns: a `CpuModelBaselineInfo` describing the baselined CPU model
+#
+# Errors:
+#     - if baselining CPU models is not supported by the target
+#     - if a model cannot be used
+#     - if a model contains an unknown cpu definition name, unknown
+#       properties or properties with wrong types.
+#
 # This interface can be used by tooling to create a compatible CPU
 # model out two CPU models.  The created CPU model will be identical
 # to or a subset of both CPU models when comparing them.  Therefore,
@@ -2088,18 +2100,6 @@
 # Some architectures may not support baselining CPU models.  s390x
 # supports baselining CPU models.
 #
-# @modela: description of the first CPU model to baseline
-#
-# @modelb: description of the second CPU model to baseline
-#
-# Returns: a `CpuModelBaselineInfo` describing the baselined CPU model
-#
-# Errors:
-#     - if baselining CPU models is not supported by the target
-#     - if a model cannot be used
-#     - if a model contains an unknown cpu definition name, unknown
-#       properties or properties with wrong types.
-#
 # Since: 2.8
 ##
 { 'command': 'query-cpu-model-baseline',
@@ -2134,28 +2134,6 @@
 #     what a specific CPU model looks like in QEMU under a certain
 #     configuration.
 #
-# This interface can be used to query the "host" CPU model.
-#
-# The data returned by this command may be affected by:
-#
-# * QEMU version: CPU models may look different depending on the QEMU
-#   version.  (Except for CPU models reported as "static" in
-#   `query-cpu-definitions`.)
-# * machine-type: CPU model may look different depending on the
-#   machine-type.  (Except for CPU models reported as "static" in
-#   `query-cpu-definitions`.)
-# * machine options (including accelerator): in some architectures,
-#   CPU models may look different depending on machine and accelerator
-#   options.  (Except for CPU models reported as "static" in
-#   `query-cpu-definitions`.)
-# * "-cpu" arguments and global properties: arguments to the -cpu
-#   option and global properties may affect expansion of CPU models.
-#   Using `query-cpu-model-expansion` while using these is not
-#   advised.
-#
-# Some architectures may not support all expansion types.  s390x
-# supports "full" and "static".  Arm only supports "full".
-#
 # @model: description of the CPU model to expand
 #
 # @type: expansion type, specifying how to expand the CPU model
@@ -2169,6 +2147,28 @@
 #       properties or properties with a wrong type
 #     - if an expansion type is not supported
 #
+# This interface can be used to query the "host" CPU model.
+#
+# The data returned by this command may be affected by:
+#
+# * QEMU version: CPU models may look different depending on the QEMU
+#   version.  (Except for CPU models reported as "static" in
+#   `query-cpu-definitions`.)
+# * machine-type: CPU model may look different depending on the
+#   machine-type.  (Except for CPU models reported as "static" in
+#   `query-cpu-definitions`.)
+# * machine options (including accelerator): in some architectures,
+#   CPU models may look different depending on machine and accelerator
+#   options.  (Except for CPU models reported as "static" in
+#   `query-cpu-definitions`.)
+# * "-cpu" arguments and global properties: arguments to the -cpu
+#   option and global properties may affect expansion of CPU models.
+#   Using `query-cpu-model-expansion` while using these is not
+#   advised.
+#
+# Some architectures may not support all expansion types.  s390x
+# supports "full" and "static".  Arm only supports "full".
+#
 # Since: 2.8
 ##
 { 'command': 'query-cpu-model-expansion',
diff --git a/qapi/migration.json b/qapi/migration.json
index 8d3951ae429..927942910d0 100644
--- a/qapi/migration.json
+++ b/qapi/migration.json
@@ -1830,6 +1830,28 @@
 #     retrieved with `query-dirty-rate` after measurements are
 #     completed.
 #
+# @calc-time: time period for which dirty page rate is calculated.  By
+#     default it is specified in seconds, but the unit can be set
+#     explicitly with @calc-time-unit.  Note that larger @calc-time
+#     values will typically result in smaller dirty page rates because
+#     page dirtying is a one-time event.  Once some page is counted as
+#     dirty during @calc-time period, further writes to this page will
+#     not increase dirty page rate anymore.
+#
+# @calc-time-unit: time unit in which @calc-time is specified.  By
+#     default it is seconds.  (Since 8.2)
+#
+# @sample-pages: number of sampled pages per each GiB of guest memory.
+#     Default value is 512.  For 4KiB guest pages this corresponds to
+#     sampling ratio of 0.2%.  This argument is used only in page
+#     sampling mode.  (Since 6.1)
+#
+# @mode: mechanism for tracking dirty pages.  Default value is
+#     'page-sampling'.  Others are 'dirty-bitmap' and 'dirty-ring'.
+#     (Since 6.1)
+#
+# Since: 5.2
+#
 # Dirty page rate is the number of pages changed in a given time
 # period expressed in MiB/s.  The following methods of calculation are
 # available:
@@ -1855,28 +1877,6 @@
 #    This mode tracks page modification per each vCPU separately.  It
 #    requires that KVM accelerator property "dirty-ring-size" is set.
 #
-# @calc-time: time period for which dirty page rate is calculated.  By
-#     default it is specified in seconds, but the unit can be set
-#     explicitly with @calc-time-unit.  Note that larger @calc-time
-#     values will typically result in smaller dirty page rates because
-#     page dirtying is a one-time event.  Once some page is counted as
-#     dirty during @calc-time period, further writes to this page will
-#     not increase dirty page rate anymore.
-#
-# @calc-time-unit: time unit in which @calc-time is specified.  By
-#     default it is seconds.  (Since 8.2)
-#
-# @sample-pages: number of sampled pages per each GiB of guest memory.
-#     Default value is 512.  For 4KiB guest pages this corresponds to
-#     sampling ratio of 0.2%.  This argument is used only in page
-#     sampling mode.  (Since 6.1)
-#
-# @mode: mechanism for tracking dirty pages.  Default value is
-#     'page-sampling'.  Others are 'dirty-bitmap' and 'dirty-ring'.
-#     (Since 6.1)
-#
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> {"execute": "calc-dirty-rate", "arguments": {"calc-time": 1,
@@ -1947,16 +1947,16 @@
 # @set-vcpu-dirty-limit:
 #     Set the upper limit of dirty page rate for virtual CPUs.
 #
+# @cpu-index: index of a virtual CPU, default is all.
+#
+# @dirty-rate: upper limit of dirty page rate (MB/s) for virtual CPUs.
+#
+# Since: 7.1
+#
 # Requires KVM with accelerator property "dirty-ring-size" set.  A
 # virtual CPU's dirty page rate is a measure of its memory load.  To
 # observe dirty page rates, use `calc-dirty-rate`.
 #
-# @cpu-index: index of a virtual CPU, default is all.
-#
-# @dirty-rate: upper limit of dirty page rate (MB/s) for virtual CPUs.
-#
-# Since: 7.1
-#
 # .. qmp-example::
 #
 #     -> {"execute": "set-vcpu-dirty-limit"}
@@ -1972,14 +1972,14 @@
 # @cancel-vcpu-dirty-limit:
 #     Cancel the upper limit of dirty page rate for virtual CPUs.
 #
+# @cpu-index: index of a virtual CPU, default is all.
+#
+# Since: 7.1
+#
 # Cancel the dirty page limit for the vCPU which has been set with
 # `set-vcpu-dirty-limit` command.  Note that this command requires
 # support from dirty ring, same as the `set-vcpu-dirty-limit`.
 #
-# @cpu-index: index of a virtual CPU, default is all.
-#
-# Since: 7.1
-#
 # .. qmp-example::
 #
 #     -> {"execute": "cancel-vcpu-dirty-limit"},
diff --git a/qapi/misc-i386.json b/qapi/misc-i386.json
index 6e0ab46b8ea..52ab5a6389a 100644
--- a/qapi/misc-i386.json
+++ b/qapi/misc-i386.json
@@ -149,10 +149,6 @@
 # @query-sev-launch-measure:
 #     Query the SEV/SEV-ES guest launch information.
 #
-# This is only valid on x86 machines configured with KVM and the
-# 'sev-guest' confidential virtualization object.  The launch
-# measurement for SEV-SNP guests is only available within the guest.
-#
 # Returns: The guest's SEV guest launch measurement info
 #
 # Errors:
@@ -162,6 +158,10 @@
 #
 # Since: 2.12
 #
+# This is only valid on x86 machines configured with KVM and the
+# 'sev-guest' confidential virtualization object.  The launch
+# measurement for SEV-SNP guests is only available within the guest.
+#
 # .. qmp-example::
 #
 #     -> { "execute": "query-sev-launch-measure" }
@@ -198,13 +198,13 @@
 # @query-sev-capabilities:
 #     Get SEV capabilities.
 #
-# This is only supported on AMD X86 platforms with KVM enabled.
-#
 # Errors:
 #     - If SEV is not available on the platform, GenericError
 #
 # Since: 2.12
 #
+# This is only supported on AMD X86 platforms with KVM enabled.
+#
 # .. qmp-example::
 #
 #     -> { "execute": "query-sev-capabilities" }
@@ -218,10 +218,6 @@
 # @sev-inject-launch-secret:
 #     Inject a secret blob into a SEV/SEV-ES guest's memory.
 #
-# This is only valid on x86 machines configured with KVM and the
-# 'sev-guest' confidential virtualization object.  SEV-SNP guests do
-# not support launch secret injection.
-#
 # @packet-header: the launch secret packet header encoded in base64
 #
 # @secret: the launch secret data to be injected encoded in base64
@@ -233,6 +229,10 @@
 #       an invalid guest configuration, or if the guest has not
 #       reached the required SEV state, GenericError
 #
+# This is only valid on x86 machines configured with KVM and the
+# 'sev-guest' confidential virtualization object.  SEV-SNP guests do
+# not support launch secret injection.
+#
 # Since: 6.0
 ##
 { 'command': 'sev-inject-launch-secret',
@@ -254,10 +254,6 @@
 # @query-sev-attestation-report:
 #     Get the SEV attestation report.
 #
-# This is only valid on x86 machines configured with KVM and the
-# 'sev-guest' confidential virtualization object.  The attestation
-# report for SEV-SNP guests is only available within the guest.
-#
 # @mnonce: a random 16 bytes value encoded in base64 (it will be
 #     included in report)
 #
@@ -268,6 +264,10 @@
 #
 # Since: 6.1
 #
+# This is only valid on x86 machines configured with KVM and the
+# 'sev-guest' confidential virtualization object.  The attestation
+# report for SEV-SNP guests is only available within the guest.
+#
 # .. qmp-example::
 #
 #     -> { "execute" : "query-sev-attestation-report",
diff --git a/qapi/misc.json b/qapi/misc.json
index 6a96ece6d91..228d1cc1cd7 100644
--- a/qapi/misc.json
+++ b/qapi/misc.json
@@ -15,9 +15,6 @@
 #     Allow client connections for VNC, Spice and socket based
 #     character devices to be passed in to QEMU via SCM_RIGHTS.
 #
-# If the FD associated with @fdname is not a socket, the command will
-# fail and the FD will be closed.
-#
 # @protocol: protocol name.  Valid names are "vnc", "spice",
 #     "@dbus-display" or the name of a character device (e.g. from
 #     -chardev id=XXXX)
@@ -31,6 +28,9 @@
 #
 # Since: 0.14
 #
+# If the FD associated with @fdname is not a socket, the command will
+# fail and the FD will be closed.
+#
 # .. qmp-example::
 #
 #     -> { "execute": "add_client", "arguments": { "protocol": "vnc",
@@ -104,14 +104,14 @@
 # @query-iothreads:
 #     Return a list of information about each iothread.
 #
+# Returns: a list of info for each iothread
+#
+# Since: 2.0
+#
 # .. note:: This list excludes the QEMU main loop thread, which is not
 #    declared using the ``-object iothread`` command-line option.  It
 #    is always the main thread of the process.
 #
-# Returns: a list of info for each iothread
-#
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-iothreads" }
@@ -186,18 +186,18 @@
 # @x-exit-preconfig:
 #     Exit from "preconfig" state
 #
+# Features:
+#
+# @unstable: This command is experimental.
+#
+# Since: 3.0
+#
 # This command makes QEMU exit the preconfig state and proceed with VM
 # initialization using configuration data provided on the command line
 # and via the QMP monitor during the preconfig state.  The command is
 # only available during the preconfig state (i.e. when the --preconfig
 # command line option was in use).
 #
-# Features:
-#
-# @unstable: This command is experimental.
-#
-# Since: 3.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "x-exit-preconfig" }
diff --git a/qapi/net.json b/qapi/net.json
index 376cd01cb6a..e760b0e84d5 100644
--- a/qapi/net.json
+++ b/qapi/net.json
@@ -39,13 +39,13 @@
 # @netdev_add:
 #     Add a network backend.
 #
-# Additional arguments depend on the type.
-#
 # Since: 0.14
 #
 # Errors:
 #     - If @type is not a valid network backend, DeviceNotFound
 #
+# Additional arguments depend on the type.
+#
 # .. qmp-example::
 #
 #     -> { "execute": "netdev_add",
diff --git a/qapi/qdev.json b/qapi/qdev.json
index f390354d68e..a3fb6a28d94 100644
--- a/qapi/qdev.json
+++ b/qapi/qdev.json
@@ -167,16 +167,16 @@
 #     the guest part (frontend).  Then notify guest software that
 #     device configuration changed.
 #
+# @id: the device's ID or QOM path
+#
+# Features:
+#
+# @unstable: The command is experimental.
+#
 # The command may be used to notify the guest about block device
 # capacity change.  Currently only vhost-user-blk device supports
 # this.
 #
-# @id: the device's ID or QOM path
-#
-# Features:
-#
-# @unstable: The command is experimental.
-#
 # Since: 9.2
 ##
 { 'command': 'device-sync-config',
diff --git a/qapi/qom.json b/qapi/qom.json
index 8d626797819..286fb987317 100644
--- a/qapi/qom.json
+++ b/qapi/qom.json
@@ -467,9 +467,6 @@
 # @FilterRedirectorProperties:
 #     Properties for filter-redirector objects.
 #
-# At least one of @indev or @outdev must be present.  If both are
-# present, they must not refer to the same character device backend.
-#
 # @indev: the name of a character device backend from which packets
 #     are received and redirected to the filtered network device
 #
@@ -479,6 +476,9 @@
 # @vnet_hdr_support: if true, vnet header support is enabled
 #     (default: false)
 #
+# At least one of @indev or @outdev must be present.  If both are
+# present, they must not refer to the same character device backend.
+#
 # Since: 2.6
 ##
 { 'struct': 'FilterRedirectorProperties',
@@ -1040,16 +1040,6 @@
 ##
 # @SevSnpGuestProperties:
 #
-# Properties for sev-snp-guest objects.  Most of these are direct
-# arguments for the KVM_SNP_* interfaces documented in the Linux
-# kernel source under
-# Documentation/arch/x86/amd-memory-encryption.rst, which are in turn
-# closely coupled with the SNP_INIT/SNP_LAUNCH_* firmware commands
-# documented in the SEV-SNP Firmware ABI Specification (Rev 0.9).
-#
-# More usage information is also available in the QEMU source tree
-# under docs/amd-memory-encryption.
-#
 # @policy: the 'POLICY' parameter to the SNP_LAUNCH_START command, as
 #     defined in the SEV-SNP firmware ABI (default: 0x30000)
 #
@@ -1086,6 +1076,16 @@
 # @tsc-frequency: set secure TSC frequency.  Only valid if Secure TSC
 #     is enabled (default: zero) (since 11.1)
 #
+# Properties for sev-snp-guest objects.  Most of these are direct
+# arguments for the KVM_SNP_* interfaces documented in the Linux
+# kernel source under
+# Documentation/arch/x86/amd-memory-encryption.rst, which are in turn
+# closely coupled with the SNP_INIT/SNP_LAUNCH_* firmware commands
+# documented in the SEV-SNP Firmware ABI Specification (Rev 0.9).
+#
+# More usage information is also available in the QEMU source tree
+# under docs/amd-memory-encryption.
+#
 # Since: 9.1
 ##
 { 'struct': 'SevSnpGuestProperties',
diff --git a/qapi/stats.json b/qapi/stats.json
index e9d05390c31..a4e4b0fa50b 100644
--- a/qapi/stats.json
+++ b/qapi/stats.json
@@ -178,12 +178,12 @@
 #     Return runtime-collected statistics for objects such as the VM
 #     or its vCPUs.
 #
-# The arguments are a `StatsFilter` and specify the provider and
-# objects to return statistics about.
-#
 # Returns: a list of statistics, one for each provider and object
 #     (e.g., for each vCPU).
 #
+# The arguments are a `StatsFilter` and specify the provider and
+# objects to return statistics about.
+#
 # Since: 7.1
 ##
 { 'command': 'query-stats',
diff --git a/qapi/transaction.json b/qapi/transaction.json
index daecf843c90..68df2df44ac 100644
--- a/qapi/transaction.json
+++ b/qapi/transaction.json
@@ -201,6 +201,23 @@
 #     If any operation fails, then the entire set of actions will be
 #     abandoned and the appropriate error returned.
 #
+# @actions: List of `TransactionAction`; information needed for the
+#     respective operations.
+#
+# @properties: structure of additional options to control the
+#     execution of the transaction.  See `TransactionProperties` for
+#     additional detail.
+#
+# Errors:
+#     - Any errors from commands in the transaction
+#
+# .. note:: The transaction aborts on the first failure.  Therefore,
+#    there will be information on only one failed operation returned
+#    in an error condition, and subsequent actions will not have been
+#    attempted.
+#
+# Since: 1.1
+#
 # For external snapshots, the dictionary contains the device, the file
 # to use for the new snapshot, and the format.  The default format, if
 # not specified, is qcow2.
@@ -224,23 +241,6 @@
 # in the transaction.  When an I/O error occurs during deletion, the
 # user needs to fix it later with qemu-img or other command.
 #
-# @actions: List of `TransactionAction`; information needed for the
-#     respective operations.
-#
-# @properties: structure of additional options to control the
-#     execution of the transaction.  See `TransactionProperties` for
-#     additional detail.
-#
-# Errors:
-#     - Any errors from commands in the transaction
-#
-# .. note:: The transaction aborts on the first failure.  Therefore,
-#    there will be information on only one failed operation returned
-#    in an error condition, and subsequent actions will not have been
-#    attempted.
-#
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "transaction",
diff --git a/qapi/ui.json b/qapi/ui.json
index 9e3e80a3d70..51e276baba4 100644
--- a/qapi/ui.json
+++ b/qapi/ui.json
@@ -1199,6 +1199,15 @@
 # @input-send-event:
 #     Send input event(s) to guest.
 #
+# @device: display device to send event(s) to.
+#
+# @head: head to send event(s) to, in case the display device supports
+#     multiple scanouts.
+#
+# @events: List of `InputEvent` union.
+#
+# Since: 2.6
+#
 # The @device and @head parameters can be used to send the input event
 # to specific input devices in case (a) multiple input devices of the
 # same kind are added to the virtual machine and (b) you have
@@ -1210,15 +1219,6 @@
 # are admissible, but devices with input routing config take
 # precedence.
 #
-# @device: display device to send event(s) to.
-#
-# @head: head to send event(s) to, in case the display device supports
-#     multiple scanouts.
-#
-# @events: List of `InputEvent` union.
-#
-# Since: 2.6
-#
 # .. note:: The consoles are visible in the qom tree, under
 #    ``/backend/console[$index]``.  They have a device link and head
 #    property, so it is possible to map which console belongs to which
diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json
index 05fe4ea484d..8efed573367 100644
--- a/qga/qapi-schema.json
+++ b/qga/qapi-schema.json
@@ -51,6 +51,10 @@
 #     Echo back a unique integer value, and prepend to response a
 #     leading sentinel byte (0xFF) the client can check scan for.
 #
+# @id: randomly generated 64-bit integer
+#
+# Returns: The unique integer id passed in by the client
+#
 # This is used by clients talking to the guest agent over the wire to
 # ensure the stream is in sync and doesn't contain stale data from
 # previous client.  It must be issued upon initial connection, and
@@ -70,10 +74,6 @@
 # byte to make sure the guest agent flushes any partially read JSON
 # data from a previous client connection.
 #
-# @id: randomly generated 64-bit integer
-#
-# Returns: The unique integer id passed in by the client
-#
 # Since: 1.1
 ##
 { 'command': 'guest-sync-delimited',
@@ -84,6 +84,10 @@
 # @guest-sync:
 #     Echo back a unique integer value
 #
+# @id: randomly generated 64-bit integer
+#
+# Returns: The unique integer id passed in by the client
+#
 # This is used by clients talking to the guest agent over the wire to
 # ensure the stream is in sync and doesn't contain stale data from
 # previous client.  All guest agent responses should be ignored until
@@ -106,10 +110,6 @@
 # make sure the guest agent flushes any partially read JSON data from
 # a previous session.
 #
-# @id: randomly generated 64-bit integer
-#
-# Returns: The unique integer id passed in by the client
-#
 # Since: 0.15.0
 ##
 { 'command': 'guest-sync',
@@ -140,6 +140,9 @@
 # @guest-set-time:
 #     Set guest time.
 #
+# @time: time of nanoseconds, relative to the Epoch of 1970-01-01 in
+#     UTC.
+#
 # When a guest is paused or migrated to a file then loaded from that
 # file, the guest OS has no idea that there was a big gap in the time.
 # Depending on how long the gap was, NTP might not be able to
@@ -153,9 +156,6 @@
 # Windows).  If that's the case users are advised to always pass a
 # value.
 #
-# @time: time of nanoseconds, relative to the Epoch of 1970-01-01 in
-#     UTC.
-#
 # Since: 1.5
 ##
 { 'command': 'guest-set-time',
@@ -526,6 +526,9 @@
 # @guest-suspend-disk:
 #     Suspend guest to disk.
 #
+# Errors:
+#     - If suspend to disk is not supported, Unsupported
+#
 # This command attempts to suspend the guest using three strategies,
 # in this order:
 #
@@ -540,9 +543,6 @@
 # VM could also exit (or set its status to "shutdown") due to other
 # reasons.
 #
-# Errors:
-#     - If suspend to disk is not supported, Unsupported
-#
 # .. note:: It's strongly recommended to issue the `guest-sync`
 #    command before sending commands when the guest resumes.
 #
@@ -555,33 +555,33 @@
 # @guest-suspend-ram:
 #     Suspend guest to ram.
 #
-# This command attempts to suspend the guest using three strategies,
-# in this order:
-#
-# - systemd hibernate
-# - pm-utils (via pm-hibernate)
-# - manual write into sysfs
-#
-# IMPORTANT: `guest-suspend-ram` requires working wakeup support in
-# QEMU.  You should check QMP command `query-current-machine` returns
-# wakeup-suspend-support: true before issuing this command.  Failure
-# in doing so can result in a suspended guest that QEMU will not be
-# able to awaken, forcing the user to power cycle the guest to bring
-# it back.
-#
-# This command does NOT return a response on success.  There are two
-# options to check for success:
-#
-# 1. Wait for the `SUSPEND` QMP event from QEMU
-# 2. Issue the `query-status` QMP command to confirm the VM status is
-#    "suspended"
-#
 # Errors:
 #     - If suspend to ram is not supported, Unsupported
 #
 # .. note:: It's strongly recommended to issue the `guest-sync`
 #    command before sending commands when the guest resumes.
 #
+# This command attempts to suspend the guest using three strategies,
+# in this order:
+#
+# - systemd hibernate
+# - pm-utils (via pm-hibernate)
+# - manual write into sysfs
+#
+# IMPORTANT: `guest-suspend-ram` requires working wakeup support in
+# QEMU.  You should check QMP command `query-current-machine` returns
+# wakeup-suspend-support: true before issuing this command.  Failure
+# in doing so can result in a suspended guest that QEMU will not be
+# able to awaken, forcing the user to power cycle the guest to bring
+# it back.
+#
+# This command does NOT return a response on success.  There are two
+# options to check for success:
+#
+# 1. Wait for the `SUSPEND` QMP event from QEMU
+# 2. Issue the `query-status` QMP command to confirm the VM status is
+#    "suspended"
+#
 # Since: 1.1
 ##
 { 'command': 'guest-suspend-ram', 'success-response': false,
@@ -591,6 +591,9 @@
 # @guest-suspend-hybrid:
 #     Save guest state to disk and suspend to ram.
 #
+# Errors:
+#     - If hybrid suspend is not supported, Unsupported
+#
 # This command attempts to suspend the guest by executing, in this
 # order:
 #
@@ -611,9 +614,6 @@
 # 2. Issue the `query-status` QMP command to confirm the VM status is
 #    "suspended"
 #
-# Errors:
-#     - If hybrid suspend is not supported, Unsupported
-#
 # .. note:: It's strongly recommended to issue the `guest-sync`
 #    command before sending commands when the guest resumes.
 #
@@ -1807,13 +1807,13 @@
 # @guest-get-load:
 #     Retrieve CPU process load information
 #
+# Returns: load information
+#
 # .. note:: Windows does not have load average API, so QGA emulates it
 #    by calculating the average CPU usage in the last 1, 5, 15 minutes
 #    similar as Linux does this.  Calculation starts from the first
 #    time this command is called.
 #
-# Returns: load information
-#
 # Since: 10.0
 ##
 { 'command': 'guest-get-load',
diff --git a/tests/qapi-schema/doc-good.json b/tests/qapi-schema/doc-good.json
index 50e4fdb54d1..89a548e1a8d 100644
--- a/tests/qapi-schema/doc-good.json
+++ b/tests/qapi-schema/doc-good.json
@@ -98,11 +98,11 @@
 #
 #     Another paragraph
 #
-# @var1 is undocumented
-#
 # Features:
 # @variant1-feat: a feature
 # @member-feat: a member feature
+#
+# @var1 is undocumented
 ##
 { 'struct': 'Variant1',
   'features': [ 'variant1-feat' ],
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
index 3858bfa861d..7a485e46037 100644
--- a/tests/qapi-schema/doc-good.out
+++ b/tests/qapi-schema/doc-good.out
@@ -131,12 +131,12 @@ doc symbol=Variant1
     Another paragraph
     Member=var1
 
-    Details
-@var1 is undocumented
     Feature=variant1-feat
 a feature
     Feature=member-feat
 a member feature
+    Details
+@var1 is undocumented
 doc symbol=Variant2
     Intro
 
diff --git a/tests/qapi-schema/doc-good.txt b/tests/qapi-schema/doc-good.txt
index 1f8274ef1f3..e08b838f3fb 100644
--- a/tests/qapi-schema/doc-good.txt
+++ b/tests/qapi-schema/doc-good.txt
@@ -68,13 +68,13 @@ Object Variant1
    Members:
       * **var1** ("string") -- Not documented
 
-   "var1" is undocumented
-
    Features:
       * **variant1-feat** -- a feature
 
       * **member-feat** -- a member feature
 
+   "var1" is undocumented
+
 Object Variant2
 
 Object Object
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 06/14] qapi/parser: enforce 'details' sections' relative position
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (4 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 05/14] qapi: move 'details' sections to the bottom of doc blocks John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-28 22:36 ` [PATCH 07/14] qapi: re-order 'since' sections to always be last John Snow
                   ` (7 subsequent siblings)
  13 siblings, 0 replies; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

This patch enforces that any "Details" sections must appear strictly
after any and all "Intro", "Members", "Features", "Errors" or
"Returns" sections.

The relative positioning of "Since:" is still unenforced: hold your
breath, it's coming in a very large code motion patch.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 94a60a23482..8b331aa0673 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -552,8 +552,7 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
                 if isinstance(this, str):
                     this = QAPIDoc.Kind.from_string(this)
 
-                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE,
-                            QAPIDoc.Kind.DETAILS):
+                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE):
                     return
 
                 if this.value < last_ordered_section.value:
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 07/14] qapi: re-order 'since' sections to always be last
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (5 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 06/14] qapi/parser: enforce 'details' sections' relative position John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-28 22:36 ` [PATCH 08/14] qapi/parser: enforce strict positioning for "Since:" section John Snow
                   ` (6 subsequent siblings)
  13 siblings, 0 replies; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

This patch moves "Since:" sections to always be the last section.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/interop/firmware.json   |   4 +-
 docs/interop/vhost-user.json |   3 +-
 qapi/accelerator.json        |   8 +--
 qapi/acpi.json               |   8 +--
 qapi/block-core.json         | 136 +++++++++++++++++------------------
 qapi/block.json              |  44 ++++++------
 qapi/char.json               |  36 +++++-----
 qapi/control.json            |  12 ++--
 qapi/dump.json               |  16 ++---
 qapi/machine-s390x.json      |   8 +--
 qapi/machine.json            |  84 +++++++++++-----------
 qapi/migration.json          |  92 ++++++++++++------------
 qapi/misc-arm.json           |   4 +-
 qapi/misc-i386.json          |  36 +++++-----
 qapi/misc.json               |  64 ++++++++---------
 qapi/net.json                |  48 ++++++-------
 qapi/pci.json                |   4 +-
 qapi/qdev.json               |  12 ++--
 qapi/qom.json                |  20 +++---
 qapi/replay.json             |  16 ++---
 qapi/rocker.json             |  16 ++---
 qapi/run-state.json          |  64 ++++++++---------
 qapi/tpm.json                |  12 ++--
 qapi/trace.json              |   8 +--
 qapi/transaction.json        |   4 +-
 qapi/ui.json                 |  76 ++++++++++----------
 qapi/vfio.json               |   4 +-
 qapi/virtio.json             |  20 +++---
 28 files changed, 429 insertions(+), 430 deletions(-)

diff --git a/docs/interop/firmware.json b/docs/interop/firmware.json
index c463fa6d05b..6366d01672f 100644
--- a/docs/interop/firmware.json
+++ b/docs/interop/firmware.json
@@ -482,8 +482,6 @@
 #     debugging purposes only, and management software shall
 #     explicitly ignore it.
 #
-# Since: 3.0
-#
 # It is possible for multiple @Firmware elements to match the search
 # criteria of management software.  Applications thus need rules to
 # pick one of the many matches, and users need the ability to override
@@ -738,6 +736,8 @@
 #             "-D DEBUG_PRINT_ERROR_LEVEL=0x80000000"
 #         ]
 #     }
+#
+# Since: 3.0
 ##
 { 'struct' : 'Firmware',
   'data'   : { 'description'     : 'str',
diff --git a/docs/interop/vhost-user.json b/docs/interop/vhost-user.json
index 9a49ab6c14d..a64da30bad7 100644
--- a/docs/interop/vhost-user.json
+++ b/docs/interop/vhost-user.json
@@ -197,8 +197,6 @@
 #     development and debugging purposes only, and management software
 #     shall explicitly ignore it.
 #
-# Since: 4.0
-#
 # It is possible for multiple @VhostUserBackend elements to match the
 # search criteria of management software.  Applications thus need
 # rules to pick one of the many matches, and users need the ability to
@@ -269,6 +267,7 @@
 #   ]
 # }
 #
+# Since: 4.0
 ##
 {
   'struct' : 'VhostUserBackend',
diff --git a/qapi/accelerator.json b/qapi/accelerator.json
index 9f6a322685f..a4e40f3286d 100644
--- a/qapi/accelerator.json
+++ b/qapi/accelerator.json
@@ -32,12 +32,12 @@
 # @deprecated: This command is deprecated.  Use `query-accelerators`
 #     instead.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-kvm" }
 #     <- { "return": { "enabled": true, "present": true } }
+#
+# Since: 0.14
 ##
 { 'command': 'query-kvm', 'returns': 'KvmInfo',
   'features': [ 'deprecated' ] }
@@ -101,11 +101,11 @@
 #
 # Returns: @AcceleratorInfo
 #
-# Since: 10.2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-accelerators" }
 #     <- { "return": { "enabled": "mshv", "present": ["kvm", "mshv", "qtest", "tcg"] } }
+#
+# Since: 10.2.0
 ##
 { 'command': 'query-accelerators', 'returns': 'AcceleratorInfo' }
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 8204d7ba52a..f52533300ea 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -108,8 +108,6 @@
 #     Return a list of `ACPIOSTInfo` for devices
 #     that support status reporting via ACPI _OST method.
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-acpi-ospm-status" }
@@ -118,6 +116,8 @@
 #                      { "slot": "2", "slot-type": "DIMM", "source": 0, "status": 0},
 #                      { "slot": "3", "slot-type": "DIMM", "source": 0, "status": 0}
 #        ]}
+#
+# Since: 2.1
 ##
 { 'command': 'query-acpi-ospm-status', 'returns': ['ACPIOSTInfo'] }
 
@@ -127,14 +127,14 @@
 #
 # @info: OSPM Status Indication
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "ACPI_DEVICE_OST",
 #          "data": { "info": { "device": "d1", "slot": "0",
 #                              "slot-type": "DIMM", "source": 1, "status": 0 } },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 2.1
 ##
 { 'event': 'ACPI_DEVICE_OST',
      'data': { 'info': 'ACPIOSTInfo' } }
diff --git a/qapi/block-core.json b/qapi/block-core.json
index c92dbb80818..3c7ef1ef251 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -885,8 +885,6 @@
 # Returns: a list describing each virtual block device.  Filter nodes
 #     that were created implicitly are skipped over.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-block" }
@@ -970,6 +968,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-block', 'returns': ['BlockInfo'],
   'data': { '*flat': 'bool' },
@@ -1283,8 +1283,6 @@
 #
 # Returns: A list of statistics for each virtual block device.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-blockstats" }
@@ -1386,6 +1384,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-blockstats',
   'data': { '*query-nodes': 'bool' },
@@ -1566,8 +1566,6 @@
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
-# Since: 0.14
-#
 # Either @device or @node-name must be set but not both.
 #
 # .. qmp-example::
@@ -1575,6 +1573,8 @@
 #     -> { "execute": "block_resize",
 #          "arguments": { "device": "scratch", "size": 1073741824 } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'block_resize',
   'data': { '*device': 'str',
@@ -1793,8 +1793,6 @@
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-snapshot-sync",
@@ -1803,6 +1801,8 @@
 #                         "/some/place/my-image",
 #                         "format": "qcow2" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'blockdev-snapshot-sync',
   'data': 'BlockdevSnapshotSync',
@@ -1824,8 +1824,6 @@
 #     backing file of a destination of a `blockdev-mirror`.
 #     (since 5.0)
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-add",
@@ -1841,6 +1839,8 @@
 #          "arguments": { "node": "ide-hd0",
 #                         "overlay": "node1534" } }
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'blockdev-snapshot',
   'data': 'BlockdevSnapshot',
@@ -1955,8 +1955,6 @@
 # @deprecated: Members @base and @top are deprecated.  Use @base-node
 #     and @top-node instead.
 #
-# Since: 1.3
-#
 # If top == base, that is an error.  If top has no overlays on top of
 # it, or if it is in use by a writer, the job will not be completed by
 # itself.  The user needs to complete the job with the `job-complete`
@@ -1974,6 +1972,8 @@
 #          "arguments": { "device": "virtio0",
 #                         "top": "/tmp/snap1.qcow2" } }
 #     <- { "return": {} }
+#
+# Since: 1.3
 ##
 { 'command': 'block-commit',
   'data': { '*job-id': 'str', 'device': 'str', '*base-node': 'str',
@@ -2004,8 +2004,6 @@
 # @deprecated: This command is deprecated.  Use `blockdev-backup`
 #     instead.
 #
-# Since: 1.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "drive-backup",
@@ -2013,6 +2011,8 @@
 #                         "sync": "full",
 #                         "target": "backup.img" } }
 #     <- { "return": {} }
+#
+# Since: 1.6
 ##
 { 'command': 'drive-backup', 'boxed': true,
   'data': 'DriveBackup', 'features': ['deprecated'],
@@ -2030,8 +2030,6 @@
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
-# Since: 2.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-backup",
@@ -2039,6 +2037,8 @@
 #                         "sync": "full",
 #                         "target": "tgt-id" } }
 #     <- { "return": {} }
+#
+# Since: 2.3
 ##
 { 'command': 'blockdev-backup', 'boxed': true,
   'data': 'BlockdevBackup',
@@ -2051,8 +2051,6 @@
 # @flat: Omit the nested data about backing image ("backing-image"
 #     key) if true.  Default is false (Since 5.0)
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-named-block-nodes" }
@@ -2101,6 +2099,8 @@
 #                               "virtual-size":2048000
 #                           }
 #                        } } ] }
+#
+# Since: 2.0
 ##
 { 'command': 'query-named-block-nodes',
   'returns': [ 'BlockDeviceInfo' ],
@@ -2228,8 +2228,6 @@
 # Errors:
 #     - If @device is not a valid block device, GenericError
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "drive-mirror",
@@ -2238,6 +2236,8 @@
 #                         "sync": "full",
 #                         "format": "qcow2" } }
 #     <- { "return": {} }
+#
+# Since: 1.3
 ##
 { 'command': 'drive-mirror', 'boxed': true,
   'data': 'DriveMirror',
@@ -2405,13 +2405,13 @@
 #     - If @node is not a valid block device or node, DeviceNotFound
 #     - If @name is already taken, GenericError
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-add",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 2.4
 ##
 { 'command': 'block-dirty-bitmap-add',
   'data': 'BlockDirtyBitmapAdd',
@@ -2428,13 +2428,13 @@
 #     - If @name is not found, GenericError
 #     - if @name is frozen by an operation, GenericError
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-remove",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 2.4
 ##
 { 'command': 'block-dirty-bitmap-remove',
   'data': 'BlockDirtyBitmap',
@@ -2450,13 +2450,13 @@
 #     - If @node is not a valid block device, DeviceNotFound
 #     - If @name is not found, GenericError
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-clear",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 2.4
 ##
 { 'command': 'block-dirty-bitmap-clear',
   'data': 'BlockDirtyBitmap',
@@ -2471,13 +2471,13 @@
 #     - If @node is not a valid block device, DeviceNotFound
 #     - If @name is not found, GenericError
 #
-# Since: 4.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-enable",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 4.0
 ##
 { 'command': 'block-dirty-bitmap-enable',
   'data': 'BlockDirtyBitmap',
@@ -2492,13 +2492,13 @@
 #     - If @node is not a valid block device, DeviceNotFound
 #     - If @name is not found, GenericError
 #
-# Since: 4.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-disable",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 4.0
 ##
 { 'command': 'block-dirty-bitmap-disable',
   'data': 'BlockDirtyBitmap',
@@ -2523,14 +2523,14 @@
 #     - If any of the bitmaps have different sizes or granularities,
 #       GenericError
 #
-# Since: 4.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-merge",
 #          "arguments": { "node": "drive0", "target": "bitmap0",
 #                         "bitmaps": ["bitmap1"] } }
 #     <- { "return": {} }
+#
+# Since: 4.0
 ##
 { 'command': 'block-dirty-bitmap-merge',
   'data': 'BlockDirtyBitmapMerge',
@@ -2629,8 +2629,6 @@
 #     mirror.  Setting this to true when the destination is not
 #     actually all zero can corrupt the destination.  (Since 10.1)
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-mirror",
@@ -2638,6 +2636,8 @@
 #                         "target": "target0",
 #                         "sync": "full" } }
 #     <- { "return": {} }
+#
+# Since: 2.6
 ##
 { 'command': 'blockdev-mirror',
   'data': { '*job-id': 'str', 'device': 'str', 'target': 'str',
@@ -2920,8 +2920,6 @@
 # Errors:
 #     - If @device does not exist, DeviceNotFound.
 #
-# Since: 1.1
-#
 # The block streaming operation is performed in the background until
 # the entire backing file has been copied.  This command returns
 # immediately once streaming has started.  The status of ongoing block
@@ -2958,6 +2956,8 @@
 #          "arguments": { "device": "virtio0",
 #                         "base": "/tmp/master.qcow2" } }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'block-stream',
   'data': { '*job-id': 'str', 'device': 'str', '*base': 'str',
@@ -4858,8 +4858,6 @@
 # @blockdev-add:
 #     Creates a new block device.
 #
-# Since: 2.9
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-add",
@@ -4899,6 +4897,8 @@
 #          }
 #
 #     <- { "return": {} }
+#
+# Since: 2.9
 ##
 { 'command': 'blockdev-add', 'data': 'BlockdevOptions', 'boxed': true,
   'allow-preconfig': true }
@@ -4957,8 +4957,6 @@
 #
 # @node-name: Name of the graph node to delete.
 #
-# Since: 2.9
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-add",
@@ -4977,6 +4975,8 @@
 #          "arguments": { "node-name": "node0" }
 #        }
 #     <- { "return": {} }
+#
+# Since: 2.9
 ##
 { 'command': 'blockdev-del', 'data': { 'node-name': 'str' },
   'allow-preconfig': true }
@@ -4992,8 +4992,6 @@
 # @active: true if the nodes should be active when the command returns
 #     success, false if they should be inactive.
 #
-# Since: 10.0
-#
 # Activating a node automatically activates all of its child nodes
 # first.  Deactivating a node automatically deactivates any of its
 # child nodes that are not in use by a still active node.
@@ -5007,6 +5005,8 @@
 #          }
 #        }
 #     <- { "return": {} }
+#
+# Since: 10.0
 ##
 { 'command': 'blockdev-set-active',
   'data': { '*node-name': 'str', 'active': 'bool' },
@@ -5646,8 +5646,6 @@
 #
 # .. note:: This event is rate-limited, except if action is "stop".
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_IO_ERROR",
@@ -5658,6 +5656,8 @@
 #                    "action": "stop",
 #                    "reason": "No space left on device" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 0.13
 ##
 { 'event': 'BLOCK_IO_ERROR',
   'data': { 'qom-path': 'str', 'device': 'str', '*node-name': 'str',
@@ -5686,8 +5686,6 @@
 #     other than that streaming has failed and clients should not try
 #     to interpret the error string
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_COMPLETED",
@@ -5695,6 +5693,8 @@
 #                    "len": 10737418240, "offset": 10737418240,
 #                    "speed": 0 },
 #          "timestamp": { "seconds": 1267061043, "microseconds": 959568 } }
+#
+# Since: 1.1
 ##
 { 'event': 'BLOCK_JOB_COMPLETED',
   'data': { 'type'  : 'JobType',
@@ -5720,8 +5720,6 @@
 #
 # @speed: rate limit, bytes per second
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_CANCELLED",
@@ -5729,6 +5727,8 @@
 #                    "len": 10737418240, "offset": 134217728,
 #                    "speed": 0 },
 #          "timestamp": { "seconds": 1267061043, "microseconds": 959568 } }
+#
+# Since: 1.1
 ##
 { 'event': 'BLOCK_JOB_CANCELLED',
   'data': { 'type'  : 'JobType',
@@ -5748,8 +5748,6 @@
 #
 # @action: action that has been taken
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_ERROR",
@@ -5757,6 +5755,8 @@
 #                    "operation": "write",
 #                    "action": "stop" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 1.3
 ##
 { 'event': 'BLOCK_JOB_ERROR',
   'data': { 'device'   : 'str',
@@ -5782,14 +5782,14 @@
 # .. note:: The "ready to complete" status is always reset by a
 #    `BLOCK_JOB_ERROR` event.
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_READY",
 #          "data": { "device": "drive0", "type": "mirror", "speed": 0,
 #                    "len": 2097152, "offset": 2097152 },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 1.3
 ##
 { 'event': 'BLOCK_JOB_READY',
   'data': { 'type'  : 'JobType',
@@ -5809,13 +5809,13 @@
 #
 # @id: The job identifier.
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_PENDING",
 #          "data": { "type": "mirror", "id": "backup_1" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 2.12
 ##
 { 'event': 'BLOCK_JOB_PENDING',
   'data': { 'type'  : 'JobType',
@@ -5881,14 +5881,14 @@
 # @write-threshold: configured threshold for the block device, bytes.
 #     Use 0 to disable the threshold.
 #
-# Since: 2.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-set-write-threshold",
 #          "arguments": { "node-name": "mydev",
 #                         "write-threshold": 17179869184 } }
 #     <- { "return": {} }
+#
+# Since: 2.3
 ##
 { 'command': 'block-set-write-threshold',
   'data': { 'node-name': 'str', 'write-threshold': 'uint64' },
@@ -5915,8 +5915,6 @@
 #     'children' list of `BlockdevOptionsQuorum`, as returned by
 #     .bdrv_refresh_filename().
 #
-# Since: 2.7
-#
 # Currently only supports adding and deleting quorum children.  A
 # child will be added at the end of the list of children.  Its
 # contents *must* be consistent with the other childrens' contents.
@@ -5945,6 +5943,8 @@
 #          "arguments": { "parent": "disk1",
 #                         "child": "children.1" } }
 #     <- { "return": {} }
+#
+# Since: 2.7
 ##
 { 'command': 'x-blockdev-change',
   'data' : { 'parent': 'str',
@@ -5972,8 +5972,6 @@
 # @unstable: This command is experimental and intended for test cases
 #     that need control over IOThreads only.
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #    :title: Move a node into an IOThread
 #
@@ -5989,6 +5987,8 @@
 #          "arguments": { "node-name": "disk1",
 #                         "iothread": null } }
 #     <- { "return": {} }
+#
+# Since: 2.12
 ##
 { 'command': 'x-blockdev-set-iothread',
   'data' : { 'node-name': 'str',
@@ -6025,13 +6025,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     <- { "event": "QUORUM_FAILURE",
 #          "data": { "reference": "usr1", "sector-num": 345435, "sectors-count": 5 },
 #          "timestamp": { "seconds": 1344522075, "microseconds": 745528 } }
+#
+# Since: 2.0
 ##
 { 'event': 'QUORUM_FAILURE',
   'data': { 'reference': 'str', 'sector-num': 'int', 'sectors-count': 'int' } }
@@ -6055,8 +6055,6 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #    :title: Read operation
 #
@@ -6072,6 +6070,8 @@
 #          "data": { "node-name": "node0", "sector-num": 0, "sectors-count": 2097120,
 #                    "type": "flush", "error": "Broken pipe" },
 #          "timestamp": { "seconds": 1456406829, "microseconds": 291763 } }
+#
+# Since: 2.0
 ##
 { 'event': 'QUORUM_REPORT_BAD',
   'data': { 'type': 'QuorumOpType', '*error': 'str', 'node-name': 'str',
@@ -6107,8 +6107,6 @@
 # .. note:: Only some image formats such as qcow2 and rbd support
 #    internal snapshots.
 #
-# Since: 1.7
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-snapshot-internal-sync",
@@ -6116,6 +6114,8 @@
 #                         "name": "snapshot0" }
 #        }
 #     <- { "return": {} }
+#
+# Since: 1.7
 ##
 { 'command': 'blockdev-snapshot-internal-sync',
   'data': 'BlockdevSnapshotInternal',
@@ -6143,8 +6143,6 @@
 #       GenericError
 #     - If @id and @name are both not specified, GenericError
 #
-# Since: 1.7
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-snapshot-delete-internal-sync",
@@ -6162,6 +6160,8 @@
 #                        "icount": 220414
 #          }
 #        }
+#
+# Since: 1.7
 ##
 { 'command': 'blockdev-snapshot-delete-internal-sync',
   'data': { 'device': 'str', '*id': 'str', '*name': 'str'},
diff --git a/qapi/block.json b/qapi/block.json
index 99b888d78e9..51be0c8eb0f 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -114,12 +114,12 @@
 #
 # .. note:: Ejecting a device with no media results in success.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "eject", "arguments": { "id": "ide1-0-1" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'eject',
   'data': { '*device': { 'type': 'str', 'features': [ 'deprecated' ] },
@@ -143,8 +143,6 @@
 #
 # @deprecated: Member @device is deprecated.  Use @id instead.
 #
-# Since: 2.5
-#
 # If there is a block driver state tree inserted as a medium, it will
 # become inaccessible to the guest (but it will remain associated to
 # the block device, so closing the tray will make it accessible
@@ -174,6 +172,8 @@
 #                    "tray-open": true } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'blockdev-open-tray',
   'data': { '*device': { 'type': 'str', 'features': [ 'deprecated' ] },
@@ -192,8 +192,6 @@
 #
 # @deprecated: Member @device is deprecated.  Use @id instead.
 #
-# Since: 2.5
-#
 # If there is a block driver state tree associated with the block
 # device (which is currently ejected), that tree will be loaded as the
 # medium.
@@ -213,6 +211,8 @@
 #                    "tray-open": false } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'blockdev-close-tray',
   'data': { '*device': { 'type': 'str', 'features': [ 'deprecated' ] },
@@ -226,8 +226,6 @@
 #
 # @id: The name or QOM path of the guest device
 #
-# Since: 2.12
-#
 # If the tray is open and there is no medium inserted, this will
 # be a no-op.
 #
@@ -255,6 +253,8 @@
 #          "arguments": { "id": "ide0-1-0" } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.12
 ##
 { 'command': 'blockdev-remove-medium',
   'data': { 'id': 'str' } }
@@ -270,8 +270,6 @@
 #
 # @node-name: name of a node in the block driver state graph
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-add",
@@ -287,6 +285,8 @@
 #                         "node-name": "node0" } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.12
 ##
 { 'command': 'blockdev-insert-medium',
   'data': { 'id': 'str',
@@ -338,8 +338,6 @@
 #
 # @deprecated: Member @device is deprecated.  Use @id instead.
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #    :title: Change a removable medium
 #
@@ -369,6 +367,8 @@
 #                         "read-only-mode": "read-only" } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'blockdev-change-medium',
   'data': { '*device': { 'type': 'str', 'features': [ 'deprecated' ] },
@@ -392,8 +392,6 @@
 # @tray-open: true if the tray has been opened or false if it has been
 #     closed
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "DEVICE_TRAY_MOVED",
@@ -402,6 +400,8 @@
 #                    "tray-open": true
 #          },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 1.1
 ##
 { 'event': 'DEVICE_TRAY_MOVED',
   'data': { 'device': 'str', 'id': 'str', 'tray-open': 'bool' } }
@@ -415,8 +415,6 @@
 #
 # @connected: true if the PR manager is connected to a backend
 #
-# Since: 3.0
-#
 # .. qmp-example::
 #
 #     <- { "event": "PR_MANAGER_STATUS_CHANGED",
@@ -424,6 +422,8 @@
 #                    "connected": true
 #          },
 #          "timestamp": { "seconds": 1519840375, "microseconds": 450486 } }
+#
+# Since: 3.0
 ##
 { 'event': 'PR_MANAGER_STATUS_CHANGED',
   'data': { 'id': 'str', 'connected': 'bool' } }
@@ -435,8 +435,6 @@
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
-# Since: 1.1
-#
 # Since QEMU 2.4, each device with I/O limits is member of a throttle
 # group.
 #
@@ -497,6 +495,8 @@
 #                         "bps_max_length": 60,
 #                         "iops_size": 0 } }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'block_set_io_throttle', 'boxed': true,
   'data': 'BlockIOThrottle',
@@ -533,8 +533,6 @@
 # Errors:
 #     - if device is not found or any boundary arrays are invalid.
 #
-# Since: 4.0
-#
 # If only @id parameter is specified, remove all present latency
 # histograms for the device.  Otherwise, add/reset some of (or all)
 # latency histograms.
@@ -585,6 +583,8 @@
 #     -> { "execute": "block-latency-histogram-set",
 #          "arguments": { "id": "drive0" } }
 #     <- { "return": {} }
+#
+# Since: 4.0
 ##
 { 'command': 'block-latency-histogram-set',
   'data': {'id': 'str',
@@ -611,8 +611,6 @@
 #
 # @unstable: This command is for testing only.
 #
-# Since: 11.2
-#
 # Take either a block backend name or a qdev ID to identify the
 # device.
 #
@@ -622,6 +620,8 @@
 #          "arguments": { "device": "virtio0",
 #                         "command": "read 0 512" } }
 #     <- { "return": {} }
+#
+# Since: 11.2
 ##
 { 'command': 'x-qemu-io',
   'data': { '*device': 'str',
diff --git a/qapi/char.json b/qapi/char.json
index 7ae67eedff8..cad2c0ecd5f 100644
--- a/qapi/char.json
+++ b/qapi/char.json
@@ -36,8 +36,6 @@
 # @query-chardev:
 #     Return information about current character devices.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-chardev" }
@@ -60,6 +58,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-chardev', 'returns': ['ChardevInfo'],
   'allow-preconfig': true }
@@ -78,8 +78,6 @@
 # @query-chardev-backends:
 #     Return information about character device backends.
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-chardev-backends" }
@@ -99,6 +97,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 2.0
 ##
 { 'command': 'query-chardev-backends', 'returns': ['ChardevBackendInfo'] }
 
@@ -131,8 +131,6 @@
 #     - data itself is always Unicode regardless of format, like any
 #       other string.
 #
-# Since: 1.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "ringbuf-write",
@@ -140,6 +138,8 @@
 #                         "data": "abcdefgh",
 #                         "format": "utf8" } }
 #     <- { "return": {} }
+#
+# Since: 1.4
 ##
 { 'command': 'ringbuf-write',
   'data': { 'device': 'str',
@@ -166,8 +166,6 @@
 #
 # Returns: data read from the device
 #
-# Since: 1.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "ringbuf-read",
@@ -175,6 +173,8 @@
 #                         "size": 1000,
 #                         "format": "utf8" } }
 #     <- { "return": "abcdefgh" }
+#
+# Since: 1.4
 ##
 { 'command': 'ringbuf-read',
   'data': {'device': 'str', 'size': 'int', '*format': 'DataFormat'},
@@ -761,8 +761,6 @@
 #
 # @backend: backend type and parameters
 #
-# Since: 1.4
-#
 # .. qmp-example::
 #
 #     -> { "execute" : "chardev-add",
@@ -784,6 +782,8 @@
 #          "arguments" : { "id" : "baz",
 #                          "backend" : { "type" : "pty", "data" : {} } } }
 #     <- { "return": { "pty" : "/dev/pty/42" } }
+#
+# Since: 1.4
 ##
 { 'command': 'chardev-add',
   'data': { 'id': 'str',
@@ -798,8 +798,6 @@
 #
 # @backend: new backend type and parameters
 #
-# Since: 2.10
-#
 # .. qmp-example::
 #
 #     -> { "execute" : "chardev-change",
@@ -824,6 +822,8 @@
 #                      "server" : true,
 #                      "wait" : false }}}}
 #     <- {"return": {}}
+#
+# Since: 2.10
 ##
 { 'command': 'chardev-change',
   'data': { 'id': 'str',
@@ -836,12 +836,12 @@
 #
 # @id: the chardev's ID, must not be in use
 #
-# Since: 1.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "chardev-remove", "arguments": { "id" : "foo" } }
 #     <- { "return": {} }
+#
+# Since: 1.4
 ##
 { 'command': 'chardev-remove',
   'data': { 'id': 'str' } }
@@ -852,12 +852,12 @@
 #
 # @id: the chardev's ID
 #
-# Since: 2.10
-#
 # .. qmp-example::
 #
 #     -> { "execute": "chardev-send-break", "arguments": { "id" : "foo" } }
 #     <- { "return": {} }
+#
+# Since: 2.10
 ##
 { 'command': 'chardev-send-break',
   'data': { 'id': 'str' } }
@@ -872,13 +872,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "VSERPORT_CHANGE",
 #          "data": { "id": "channel0", "open": true },
 #          "timestamp": { "seconds": 1401385907, "microseconds": 422329 } }
+#
+# Since: 2.1
 ##
 { 'event': 'VSERPORT_CHANGE',
   'data': { 'id': 'str',
diff --git a/qapi/control.json b/qapi/control.json
index ef63a8a261b..1f687d42b47 100644
--- a/qapi/control.json
+++ b/qapi/control.json
@@ -93,8 +93,6 @@
 #
 # Returns: An object describing the current version of QEMU.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-version" }
@@ -108,6 +106,8 @@
 #              "package":""
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-version', 'returns': 'VersionInfo',
   'allow-preconfig': true }
@@ -128,8 +128,6 @@
 #
 # Returns: A list of all supported commands
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-commands" }
@@ -146,6 +144,8 @@
 #        }
 #
 # This example has been shortened as the real response is too long.
+#
+# Since: 0.14
 ##
 { 'command': 'query-commands', 'returns': ['CommandInfo'],
   'allow-preconfig': true }
@@ -158,12 +158,12 @@
 # terminating, this is not guaranteed.  When using this interface, a
 # premature EOF would not be unexpected.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "quit" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'quit',
   'allow-preconfig': true }
diff --git a/qapi/dump.json b/qapi/dump.json
index 9e97e8431f4..4e238fb67cd 100644
--- a/qapi/dump.json
+++ b/qapi/dump.json
@@ -99,13 +99,13 @@
 #
 # .. note:: All boolean arguments default to false.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "dump-guest-memory",
 #          "arguments": { "paging": false, "protocol": "fd:dump" } }
 #     <- { "return": {} }
+#
+# Since: 1.2
 ##
 { 'command': 'dump-guest-memory',
   'data': { 'paging': 'bool', 'protocol': 'str', '*detach': 'bool',
@@ -153,13 +153,13 @@
 #
 # Returns: An object showing the dump status.
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-dump" }
 #     <- { "return": { "status": "active", "completed": 1024000,
 #                      "total": 2048000 } }
+#
+# Since: 2.6
 ##
 { 'command': 'query-dump', 'returns': 'DumpQueryResult' }
 
@@ -173,14 +173,14 @@
 #     failed.  Only presents on failure.  The user should not try to
 #     interpret the error string.
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     <- { "event": "DUMP_COMPLETED",
 #          "data": { "result": { "total": 1090650112, "status": "completed",
 #                                "completed": 1090650112 } },
 #          "timestamp": { "seconds": 1648244171, "microseconds": 950316 } }
+#
+# Since: 2.6
 ##
 { 'event': 'DUMP_COMPLETED' ,
   'data': { 'result': 'DumpQueryResult', '*error': 'str' } }
@@ -202,13 +202,13 @@
 #
 # Returns: An object listing available formats for `dump-guest-memory`
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-dump-guest-memory-capability" }
 #     <- { "return": { "formats":
 #                      ["elf", "kdump-zlib", "kdump-lzo", "kdump-snappy"] } }
+#
+# Since: 2.0
 ##
 { 'command': 'query-dump-guest-memory-capability',
   'returns': 'DumpGuestMemoryCapability' }
diff --git a/qapi/machine-s390x.json b/qapi/machine-s390x.json
index 911989bb4f7..ed8f424ddd5 100644
--- a/qapi/machine-s390x.json
+++ b/qapi/machine-s390x.json
@@ -65,8 +65,6 @@
 #
 # @unstable: This event is experimental.
 #
-# Since: 8.2
-#
 # The guest can tell the host (via the PTF instruction) whether the
 # CPUs should be provisioned using horizontal or vertical
 # polarization.
@@ -83,6 +81,8 @@
 #     <- { "event": "CPU_POLARIZATION_CHANGE",
 #          "data": { "polarization": "horizontal" },
 #          "timestamp": { "seconds": 1401385907, "microseconds": 422329 } }
+#
+# Since: 8.2
 ##
 { 'event': 'CPU_POLARIZATION_CHANGE',
   'data': { 'polarization': 'S390CpuPolarization' },
@@ -125,12 +125,12 @@
 #
 # @unstable: This event is experimental.
 #
-# Since: 10.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "SCLP_CPI_INFO_AVAILABLE",
 #          "timestamp": { "seconds": 1401385907, "microseconds": 422329 } }
+#
+# Since: 10.2
 ##
 { 'event': 'SCLP_CPI_INFO_AVAILABLE',
   'features': [ 'unstable' ]
diff --git a/qapi/machine.json b/qapi/machine.json
index 8aae66fcfde..7832708950d 100644
--- a/qapi/machine.json
+++ b/qapi/machine.json
@@ -103,8 +103,6 @@
 # @query-cpus-fast:
 #     Return information about all virtual CPUs.
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-cpus-fast" }
@@ -133,6 +131,8 @@
 #             }
 #         ]
 #     }
+#
+# Since: 2.12
 ##
 { 'command': 'query-cpus-fast', 'returns': [ 'CpuInfoFast' ] }
 
@@ -221,8 +221,6 @@
 #
 # @unstable: Argument @compat-props is experimental.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-machines", "arguments": { "compat-props": true } }
@@ -245,6 +243,8 @@
 #               },
 #               ...
 #        }
+#
+# Since: 1.2
 ##
 { 'command': 'query-machines',
   'data': { '*compat-props': { 'type': 'bool',
@@ -297,10 +297,10 @@
 #
 # @UUID: the UUID of the guest
 #
-# Since: 0.14
-#
 # .. note:: If no UUID was specified for the guest, the nil UUID (all
 #    zeroes) is returned.
+#
+# Since: 0.14
 ##
 { 'struct': 'UuidInfo', 'data': {'UUID': 'str'} }
 
@@ -308,12 +308,12 @@
 # @query-uuid:
 #     Query the guest UUID information.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-uuid" }
 #     <- { "return": { "UUID": "550e8400-e29b-41d4-a716-446655440000" } }
+#
+# Since: 0.14
 ##
 { 'command': 'query-uuid', 'returns': 'UuidInfo', 'allow-preconfig': true }
 
@@ -339,12 +339,12 @@
 # @system_reset:
 #     Performs a hard reset of a guest.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "system_reset" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'system_reset' }
 
@@ -352,8 +352,6 @@
 # @system_powerdown:
 #     Requests that a guest perform a powerdown operation.
 #
-# Since: 0.14
-#
 # .. note:: A guest may or may not respond to this command.  This
 #    command returning does not indicate that a guest has accepted the
 #    request or that it has shut down.  Many guests will respond to
@@ -363,6 +361,8 @@
 #
 #     -> { "execute": "system_powerdown" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'system_powerdown' }
 
@@ -373,8 +373,6 @@
 #     `query-current-machine`), wake-up guest from suspend if the
 #     guest is in SUSPENDED state.  Return an error otherwise.
 #
-# Since: 1.1
-#
 # .. note:: Prior to 4.0, this command does nothing in case the guest
 #    isn't suspended.
 #
@@ -382,6 +380,8 @@
 #
 #     -> { "execute": "system_wakeup" }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'system_wakeup' }
 
@@ -423,8 +423,6 @@
 #     machines all vCPUs receive it).  The command fails when the
 #     guest doesn't support injecting.
 #
-# Since: 0.14
-#
 # .. note:: Prior to 2.1, this command was only supported for x86 and
 #    s390 VMs.
 #
@@ -432,6 +430,8 @@
 #
 #     -> { "execute": "inject-nmi" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'inject-nmi' }
 
@@ -807,8 +807,6 @@
 # @cpu-index: the index of the virtual CPU to use for translating the
 #     virtual address (defaults to CPU 0)
 #
-# Since: 0.14
-#
 # .. caution:: Errors were not reliably returned until 1.1.
 #
 # .. qmp-example::
@@ -818,6 +816,8 @@
 #                         "size": 100,
 #                         "filename": "/tmp/virtual-mem-dump" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'memsave',
   'data': {
@@ -836,8 +836,6 @@
 #
 # @filename: the file to save the memory to as binary data
 #
-# Since: 0.14
-#
 # .. caution:: Errors were not reliably returned until 1.1.
 #
 # .. qmp-example::
@@ -847,6 +845,8 @@
 #                         "size": 100,
 #                         "filename": "/tmp/physical-mem-dump" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'pmemsave',
   'data': {
@@ -898,8 +898,6 @@
 # @query-memdev:
 #     Return information for all memory backends.
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-memdev" }
@@ -925,6 +923,8 @@
 #            }
 #          ]
 #        }
+#
+# Since: 2.1
 ##
 { 'command': 'query-memdev', 'returns': ['Memdev'], 'allow-preconfig': true }
 
@@ -1012,8 +1012,6 @@
 #
 # TODO: Better documentation; currently there is none.
 #
-# Since: 2.7
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -1064,6 +1062,8 @@
 #             "props": { "core-id": 0 }
 #          }
 #        ]}
+#
+# Since: 2.7
 ##
 { 'command': 'query-hotpluggable-cpus', 'returns': ['HotpluggableCPU'],
              'allow-preconfig': true }
@@ -1101,8 +1101,6 @@
 #    returns, the balloon size may not have changed.  A guest can
 #    change the balloon size independent of this command.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -1112,6 +1110,8 @@
 #      <- { "return": {} }
 #
 #    With a 2.5GiB guest this command inflated the ballon to 3GiB.
+#
+# Since: 0.14
 ##
 { 'command': 'balloon', 'data': {'value': 'int'} }
 
@@ -1135,8 +1135,6 @@
 #       the KVM kernel module cannot support it, KVMMissingCap
 #     - If no balloon device is present, DeviceNotActive
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-balloon" }
@@ -1144,6 +1142,8 @@
 #              "actual": 1073741824
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-balloon', 'returns': 'BalloonInfo' }
 
@@ -1158,13 +1158,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "BALLOON_CHANGE",
 #          "data": { "actual": 944766976 },
 #          "timestamp": { "seconds": 1267020223, "microseconds": 435656 } }
+#
+# Since: 1.2
 ##
 { 'event': 'BALLOON_CHANGE',
   'data': { 'actual': 'int' } }
@@ -1195,8 +1195,6 @@
 #       reporting is not enabled or no guest memory status report
 #       received yet, GenericError
 #
-# Since: 8.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-hv-balloon-status-report" }
@@ -1205,6 +1203,8 @@
 #              "available": 3333054464
 #           }
 #        }
+#
+# Since: 8.2
 ##
 { 'command': 'query-hv-balloon-status-report', 'returns': 'HvBalloonInfo' }
 
@@ -1215,13 +1215,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 8.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "HV_BALLOON_STATUS_REPORT",
 #          "data": { "committed": 816640000, "available": 3333054464 },
 #          "timestamp": { "seconds": 1600295492, "microseconds": 661044 } }
+#
+# Since: 8.2
 ##
 { 'event': 'HV_BALLOON_STATUS_REPORT',
   'data': 'HvBalloonInfo' }
@@ -1553,8 +1553,6 @@
 # @query-memory-devices:
 #     Lists available memory devices and their state
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-memory-devices" }
@@ -1569,6 +1567,8 @@
 #                             "slot": 0},
 #                        "type": "dimm"
 #                      } ] }
+#
+# Since: 2.1
 ##
 { 'command': 'query-memory-devices', 'returns': ['MemoryDeviceInfo'] }
 
@@ -1586,14 +1586,14 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 5.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "MEMORY_DEVICE_SIZE_CHANGE",
 #          "data": { "id": "vm0", "size": 1073741824,
 #                    "qom-path": "/machine/unattached/device[2]" },
 #          "timestamp": { "seconds": 1588168529, "microseconds": 201316 } }
+#
+# Since: 5.1
 ##
 { 'event': 'MEMORY_DEVICE_SIZE_CHANGE',
   'data': { '*id': 'str', 'size': 'size', 'qom-path' : 'str'} }
@@ -1817,13 +1817,13 @@
 #
 # @filename: name of the dtb file to be created
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "dumpdtb" }
 #          "arguments": { "filename": "fdt.dtb" } }
 #     <- { "return": {} }
+#
+# Since: 7.2
 ##
 { 'command': 'dumpdtb',
   'data': { 'filename': 'str' },
@@ -1879,13 +1879,13 @@
 #
 # @filename: the path to the file to dump to
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "dump-skeys",
 #          "arguments": { "filename": "/tmp/skeys" } }
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'dump-skeys',
   'data': { 'filename': 'str' } }
diff --git a/qapi/migration.json b/qapi/migration.json
index 927942910d0..86a9d139dca 100644
--- a/qapi/migration.json
+++ b/qapi/migration.json
@@ -337,8 +337,6 @@
 #     migration is active there will be another json-object with RAM
 #     migration status.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #    :title: Before the first migration
 #
@@ -424,6 +422,8 @@
 #              }
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-migrate', 'returns': 'MigrationInfo' }
 
@@ -556,13 +556,13 @@
 #
 # @capabilities: json array of capability modifications to make
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-set-capabilities" , "arguments":
 #          { "capabilities": [ { "capability": "xbzrle", "state": true } ] } }
 #     <- { "return": {} }
+#
+# Since: 1.2
 ##
 { 'command': 'migrate-set-capabilities',
   'data': { 'capabilities': ['MigrationCapabilityStatus'] } }
@@ -572,8 +572,6 @@
 #     Return information about the current migration capabilities
 #     status
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-migrate-capabilities" }
@@ -585,6 +583,8 @@
 #           {"state": false, "capability": "postcopy-ram"},
 #           {"state": false, "capability": "x-colo"}
 #        ]}
+#
+# Since: 1.2
 ##
 { 'command': 'query-migrate-capabilities', 'returns':   ['MigrationCapabilityStatus']}
 
@@ -826,13 +826,13 @@
 # @migrate-set-parameters:
 #     Set migration parameters.  All arguments are optional.
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-set-parameters" ,
 #          "arguments": { "multifd-channels": 5 } }
 #     <- { "return": {} }
+#
+# Since: 2.4
 ##
 { 'command': 'migrate-set-parameters', 'boxed': true,
   'data': 'MigrationParameters' }
@@ -1063,8 +1063,6 @@
 #     @block-bitmap-mapping, which is only present if it has been
 #     previously set.
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-migrate-parameters" }
@@ -1076,6 +1074,8 @@
 #              "downtime-limit": 300
 #           }
 #        }
+#
+# Since: 2.4
 ##
 { 'command': 'query-migrate-parameters',
   'returns': 'MigrationParameters' }
@@ -1086,12 +1086,12 @@
 #     postcopy mode.  The postcopy-ram capability must be set on both
 #     source and destination before the original migration command.
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-start-postcopy" }
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'migrate-start-postcopy' }
 
@@ -1101,13 +1101,13 @@
 #
 # @status: `MigrationStatus` describing the current migration status.
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     <- {"timestamp": {"seconds": 1432121972, "microseconds": 744001},
 #         "event": "MIGRATION",
 #         "data": {"status": "completed"} }
+#
+# Since: 2.4
 ##
 { 'event': 'MIGRATION',
   'data': {'status': 'MigrationStatus'}}
@@ -1119,12 +1119,12 @@
 #
 # @pass: An incrementing count (starting at 1 on the first pass)
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1449669631, "microseconds": 239225},
 #           "event": "MIGRATION_PASS", "data": {"pass": 2} }
+#
+# Since: 2.6
 ##
 { 'event': 'MIGRATION_PASS',
   'data': { 'pass': 'int' } }
@@ -1200,12 +1200,12 @@
 #
 # @reason: describes the reason for the COLO exit.
 #
-# Since: 3.1
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 2032141960, "microseconds": 417172},
 #          "event": "COLO_EXIT", "data": {"mode": "primary", "reason": "request" } }
+#
+# Since: 3.1
 ##
 { 'event': 'COLO_EXIT',
   'data': {'mode': 'COLOMode', 'reason': 'COLOExitReason' } }
@@ -1241,12 +1241,12 @@
 #
 # @unstable: This command is experimental.
 #
-# Since: 2.8
-#
 # .. qmp-example::
 #
 #     -> { "execute": "x-colo-lost-heartbeat" }
 #     <- { "return": {} }
+#
+# Since: 2.8
 ##
 { 'command': 'x-colo-lost-heartbeat',
   'features': [ 'unstable' ],
@@ -1262,12 +1262,12 @@
 # .. note:: This command succeeds even if there is no migration
 #    process running.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate_cancel" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'migrate_cancel' }
 
@@ -1277,13 +1277,13 @@
 #
 # @state: The state the migration is currently expected to be in
 #
-# Since: 2.11
-#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-continue" , "arguments":
 #          { "state": "pre-switchover" } }
 #     <- { "return": {} }
+#
+# Since: 2.11
 ##
 { 'command': 'migrate-continue', 'data': {'state': 'MigrationStatus'} }
 
@@ -1387,8 +1387,6 @@
 #     will fail unless migration is in "postcopy-paused" state.
 #     (default: false, since 3.0)
 #
-# Since: 0.14
-#
 # .. admonition:: Notes
 #
 #     1. The `query-migrate` command should be used to check
@@ -1442,6 +1440,8 @@
 #                                        "filename": "/tmp/migfile",
 #                                        "offset": "0x1000" } } ] } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'migrate',
   'data': {'*uri': 'str',
@@ -1464,8 +1464,6 @@
 #     :qapi:event:`MIGRATION` event, and error details could be
 #     retrieved with `query-migrate`.  (since 9.1)
 #
-# Since: 2.3
-#
 # .. admonition:: Notes
 #
 #     1. It's a bad idea to use a string for the uri, but it needs to
@@ -1513,6 +1511,8 @@
 #                                        "host": "10.12.34.9",
 #                                        "port": "1050" } } ] } }
 #     <- { "return": {} }
+#
+# Since: 2.3
 ##
 { 'command': 'migrate-incoming',
              'data': {'*uri': 'str',
@@ -1531,13 +1531,13 @@
 # @live: Optional argument to ask QEMU to treat this command as part
 #     of a live migration.  Default to true.  (since 2.11)
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-save-devices-state",
 #          "arguments": { "filename": "/tmp/save" } }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'xen-save-devices-state',
   'data': {'filename': 'str', '*live':'bool' } }
@@ -1548,13 +1548,13 @@
 #
 # @enable: true to enable, false to disable.
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-set-global-dirty-log",
 #          "arguments": { "enable": true } }
 #     <- { "return": {} }
+#
+# Since: 1.3
 ##
 { 'command': 'xen-set-global-dirty-log', 'data': { 'enable': 'bool' } }
 
@@ -1567,13 +1567,13 @@
 #     data.  See `xen-save-devices-state`.txt for a description of the
 #     binary format.
 #
-# Since: 2.7
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-load-devices-state",
 #          "arguments": { "filename": "/tmp/resume" } }
 #     <- { "return": {} }
+#
+# Since: 2.7
 ##
 { 'command': 'xen-load-devices-state', 'data': {'filename': 'str'} }
 
@@ -1720,13 +1720,13 @@
 #
 # @device-id: QEMU device id of the unplugged device
 #
-# Since: 4.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "UNPLUG_PRIMARY",
 #          "data": { "device-id": "hostdev0" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 4.2
 ##
 { 'event': 'UNPLUG_PRIMARY',
   'data': { 'device-id': 'str' } }
@@ -1850,8 +1850,6 @@
 #     'page-sampling'.  Others are 'dirty-bitmap' and 'dirty-ring'.
 #     (Since 6.1)
 #
-# Since: 5.2
-#
 # Dirty page rate is the number of pages changed in a given time
 # period expressed in MiB/s.  The following methods of calculation are
 # available:
@@ -1892,6 +1890,8 @@
 #         "calc-time-unit": "millisecond", "mode": "dirty-bitmap"} }
 #
 #     <- { "return": {} }
+#
+# Since: 5.2
 ##
 { 'command': 'calc-dirty-rate', 'data': {'calc-time': 'int64',
                                          '*calc-time-unit': 'TimeUnit',
@@ -1906,8 +1906,6 @@
 # @calc-time-unit: time unit in which to report calculation time.
 #     By default it is reported in seconds.  (Since 8.2)
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #    :title: Measurement is in progress
 #
@@ -1921,6 +1919,8 @@
 #     <- {"status": "measured", "sample-pages": 512, "dirty-rate": 108,
 #         "mode": "page-sampling", "start-time": 1693900454, "calc-time": 10,
 #         "calc-time-unit": "second"}
+#
+# Since: 5.2
 ##
 { 'command': 'query-dirty-rate', 'data': {'*calc-time-unit': 'TimeUnit' },
                                  'returns': 'DirtyRateInfo' }
@@ -1951,8 +1951,6 @@
 #
 # @dirty-rate: upper limit of dirty page rate (MB/s) for virtual CPUs.
 #
-# Since: 7.1
-#
 # Requires KVM with accelerator property "dirty-ring-size" set.  A
 # virtual CPU's dirty page rate is a measure of its memory load.  To
 # observe dirty page rates, use `calc-dirty-rate`.
@@ -1963,6 +1961,8 @@
 #         "arguments": { "dirty-rate": 200,
 #                        "cpu-index": 1 } }
 #     <- { "return": {} }
+#
+# Since: 7.1
 ##
 { 'command': 'set-vcpu-dirty-limit',
   'data': { '*cpu-index': 'int',
@@ -1974,8 +1974,6 @@
 #
 # @cpu-index: index of a virtual CPU, default is all.
 #
-# Since: 7.1
-#
 # Cancel the dirty page limit for the vCPU which has been set with
 # `set-vcpu-dirty-limit` command.  Note that this command requires
 # support from dirty ring, same as the `set-vcpu-dirty-limit`.
@@ -1985,6 +1983,8 @@
 #     -> {"execute": "cancel-vcpu-dirty-limit"},
 #         "arguments": { "cpu-index": 1 } }
 #     <- { "return": {} }
+#
+# Since: 7.1
 ##
 { 'command': 'cancel-vcpu-dirty-limit',
   'data': { '*cpu-index': 'int'} }
@@ -1994,14 +1994,14 @@
 #     Return information about virtual CPU dirty page rate limits, if
 #     any.
 #
-# Since: 7.1
-#
 # .. qmp-example::
 #
 #     -> {"execute": "query-vcpu-dirty-limit"}
 #     <- {"return": [
 #            { "limit-rate": 60, "current-rate": 3, "cpu-index": 0},
 #            { "limit-rate": 60, "current-rate": 3, "cpu-index": 1}]}
+#
+# Since: 7.1
 ##
 { 'command': 'query-vcpu-dirty-limit',
   'returns': [ 'DirtyLimitInfo' ] }
diff --git a/qapi/misc-arm.json b/qapi/misc-arm.json
index 70859b2c37d..a0287dd79d6 100644
--- a/qapi/misc-arm.json
+++ b/qapi/misc-arm.json
@@ -34,13 +34,13 @@
 # On non-ARM targets this command will report an error as the GIC
 # technology is not applicable.
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-gic-capabilities" }
 #     <- { "return": [{ "version": 2, "emulated": true, "kernel": false },
 #                     { "version": 3, "emulated": false, "kernel": true } ] }
+#
+# Since: 2.6
 ##
 { 'command': 'query-gic-capabilities', 'returns': ['GICCapability'] }
 
diff --git a/qapi/misc-i386.json b/qapi/misc-i386.json
index 52ab5a6389a..a0dc4c70d63 100644
--- a/qapi/misc-i386.json
+++ b/qapi/misc-i386.json
@@ -13,12 +13,12 @@
 # and on other machines will silently return without performing any
 # action.
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "rtc-reset-reinjection" }
 #     <- { "return": {} }
+#
+# Since: 2.1
 ##
 { 'command': 'rtc-reset-reinjection' }
 
@@ -124,14 +124,14 @@
 # @enabled field is set to 'false' and the state of all other fields
 # is unspecified.
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-sev" }
 #     <- { "return": { "enabled": true, "api-major" : 0, "api-minor" : 0,
 #                      "build-id" : 0, "policy" : 0, "state" : "running",
 #                      "handle" : 1 } }
+#
+# Since: 2.12
 ##
 { 'command': 'query-sev', 'returns': 'SevInfo' }
 
@@ -156,8 +156,6 @@
 #       invalid guest configuration or if the guest has not reached
 #       the required SEV state, GenericError
 #
-# Since: 2.12
-#
 # This is only valid on x86 machines configured with KVM and the
 # 'sev-guest' confidential virtualization object.  The launch
 # measurement for SEV-SNP guests is only available within the guest.
@@ -166,6 +164,8 @@
 #
 #     -> { "execute": "query-sev-launch-measure" }
 #     <- { "return": { "data": "4l8LXeNlSPUDlXPJG5966/8%YZ" } }
+#
+# Since: 2.12
 ##
 { 'command': 'query-sev-launch-measure', 'returns': 'SevLaunchMeasureInfo' }
 
@@ -201,8 +201,6 @@
 # Errors:
 #     - If SEV is not available on the platform, GenericError
 #
-# Since: 2.12
-#
 # This is only supported on AMD X86 platforms with KVM enabled.
 #
 # .. qmp-example::
@@ -211,6 +209,8 @@
 #     <- { "return": { "pdh": "8CCDD8DDD", "cert-chain": "888CCCDDDEE",
 #                      "cpu0-id": "2lvmGwo+...61iEinw==",
 #                      "cbitpos": 47, "reduced-phys-bits": 1}}
+#
+# Since: 2.12
 ##
 { 'command': 'query-sev-capabilities', 'returns': 'SevCapability' }
 
@@ -262,8 +262,6 @@
 #       invalid guest configuration or because the guest has not
 #       reached the required SEV state, GenericError
 #
-# Since: 6.1
-#
 # This is only valid on x86 machines configured with KVM and the
 # 'sev-guest' confidential virtualization object.  The attestation
 # report for SEV-SNP guests is only available within the guest.
@@ -273,6 +271,8 @@
 #     -> { "execute" : "query-sev-attestation-report",
 #                      "arguments": { "mnonce": "aaaaaaa" } }
 #     <- { "return" : { "data": "aaaaaaaabbbddddd"} }
+#
+# Since: 6.1
 ##
 { 'command': 'query-sev-attestation-report',
   'data': { 'mnonce': 'str' },
@@ -319,8 +319,6 @@
 # @query-sgx:
 #     Return information about configured SGX capabilities of guest
 #
-# Since: 6.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-sgx" }
@@ -328,6 +326,8 @@
 #                      "flc": true,
 #                      "sections": [{"node": 0, "size": 67108864},
 #                      {"node": 1, "size": 29360128}]} }
+#
+# Since: 6.2
 ##
 { 'command': 'query-sgx', 'returns': 'SgxInfo' }
 
@@ -335,8 +335,6 @@
 # @query-sgx-capabilities:
 #     Return information about SGX capabilities of host
 #
-# Since: 6.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-sgx-capabilities" }
@@ -344,6 +342,8 @@
 #                      "flc": true,
 #                      "section" : [{"node": 0, "size": 67108864},
 #                      {"node": 1, "size": 29360128}]} }
+#
+# Since: 6.2
 ##
 { 'command': 'query-sgx-capabilities', 'returns': 'SgxInfo' }
 
@@ -403,8 +403,6 @@
 #
 # Returns: list of open event channel ports.
 #
-# Since: 8.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-event-list" }
@@ -429,6 +427,8 @@
 #             }
 #          ]
 #        }
+#
+# Since: 8.0
 ##
 { 'command': 'xen-event-list',
   'returns': ['EvtchnInfo'] }
@@ -439,12 +439,12 @@
 #
 # @port: The port number
 #
-# Since: 8.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-event-inject", "arguments": { "port": 1 } }
 #     <- { "return": { } }
+#
+# Since: 8.0
 ##
 { 'command': 'xen-event-inject',
   'data': { 'port': 'uint32' } }
diff --git a/qapi/misc.json b/qapi/misc.json
index 228d1cc1cd7..3770dea41eb 100644
--- a/qapi/misc.json
+++ b/qapi/misc.json
@@ -26,8 +26,6 @@
 #
 # @tls: whether to perform TLS.  Only applies to the "spice" protocol
 #
-# Since: 0.14
-#
 # If the FD associated with @fdname is not a socket, the command will
 # fail and the FD will be closed.
 #
@@ -36,6 +34,8 @@
 #     -> { "execute": "add_client", "arguments": { "protocol": "vnc",
 #                                                  "fdname": "myclient" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'add_client',
   'data': { 'protocol': 'str', 'fdname': 'str', '*skipauth': 'bool',
@@ -55,12 +55,12 @@
 # @query-name:
 #     Return the name information of a guest.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-name" }
 #     <- { "return": { "name": "qemu-name" } }
+#
+# Since: 0.14
 ##
 { 'command': 'query-name', 'returns': 'NameInfo', 'allow-preconfig': true }
 
@@ -106,8 +106,6 @@
 #
 # Returns: a list of info for each iothread
 #
-# Since: 2.0
-#
 # .. note:: This list excludes the QEMU main loop thread, which is not
 #    declared using the ``-object iothread`` command-line option.  It
 #    is always the main thread of the process.
@@ -134,6 +132,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 2.0
 ##
 { 'command': 'query-iothreads', 'returns': ['IOThreadInfo'],
   'allow-preconfig': true }
@@ -142,8 +142,6 @@
 # @stop:
 #     Stop guest VM execution.
 #
-# Since: 0.14
-#
 # .. note:: This function will succeed even if the guest is already in
 #    the stopped state.  In "inmigrate" state, it will ensure that the
 #    guest remains paused once migration finishes, as if the ``-S``
@@ -156,6 +154,8 @@
 #
 #     -> { "execute": "stop" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'stop' }
 
@@ -163,8 +163,6 @@
 # @cont:
 #     Resume guest VM execution.
 #
-# Since: 0.14
-#
 # .. note:: This command will succeed if the guest is currently
 #    running.  It will also succeed if the guest is in the "inmigrate"
 #    state; in this case, the effect of the command is to make sure
@@ -179,6 +177,8 @@
 #
 #     -> { "execute": "cont" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'cont' }
 
@@ -190,8 +190,6 @@
 #
 # @unstable: This command is experimental.
 #
-# Since: 3.0
-#
 # This command makes QEMU exit the preconfig state and proceed with VM
 # initialization using configuration data provided on the command line
 # and via the QMP monitor during the preconfig state.  The command is
@@ -202,6 +200,8 @@
 #
 #     -> { "execute": "x-exit-preconfig" }
 #     <- { "return": {} }
+#
+# Since: 3.0
 ##
 { 'command': 'x-exit-preconfig', 'allow-preconfig': true,
   'features': [ 'unstable' ] }
@@ -222,8 +222,6 @@
 #     monitor-owned nodes if they have no parents.  This allows the
 #     use of 'savevm' with -blockdev.  (since 4.2)
 #
-# Since: 0.14
-#
 # .. note:: This command only exists as a stop-gap.  Its use is highly
 #    discouraged.  The semantics of this command are not guaranteed:
 #    this means that command names, arguments and responses can change
@@ -242,6 +240,8 @@
 #     -> { "execute": "human-monitor-command",
 #          "arguments": { "command-line": "info kvm" } }
 #     <- { "return": "kvm support: enabled\r\n" }
+#
+# Since: 0.14
 ##
 { 'command': 'human-monitor-command',
   'data': {'command-line': 'str', '*cpu-index': 'int'},
@@ -255,8 +255,6 @@
 #
 # @fdname: file descriptor name
 #
-# Since: 0.14
-#
 # .. note:: If @fdname already exists, the file descriptor assigned to
 #    it will be closed and replaced by the received file descriptor.
 #
@@ -267,6 +265,8 @@
 #
 #     -> { "execute": "getfd", "arguments": { "fdname": "fd1" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'getfd', 'data': {'fdname': 'str'}, 'if': 'CONFIG_POSIX' }
 
@@ -281,8 +281,6 @@
 #
 # @fdname: file descriptor name
 #
-# Since: 8.0
-#
 # .. note:: If @fdname already exists, the file descriptor assigned to
 #    it will be closed and replaced by the received file descriptor.
 #
@@ -294,6 +292,8 @@
 #     -> { "execute": "get-win32-socket",
 #          "arguments": { "info": "abcd123..", "fdname": "skclient" } }
 #     <- { "return": {} }
+#
+# Since: 8.0
 ##
 { 'command': 'get-win32-socket', 'data': {'info': 'str', 'fdname': 'str'}, 'if': 'CONFIG_WIN32' }
 
@@ -303,12 +303,12 @@
 #
 # @fdname: file descriptor name
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "closefd", "arguments": { "fdname": "fd1" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'closefd', 'data': {'fdname': 'str'} }
 
@@ -343,12 +343,12 @@
 # .. note:: If @fdset-id is not specified, a new fd set will be
 #    created.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "add-fd", "arguments": { "fdset-id": 1 } }
 #     <- { "return": { "fdset-id": 1, "fd": 3 } }
+#
+# Since: 1.2
 ##
 { 'command': 'add-fd',
   'data': { '*fdset-id': 'int',
@@ -366,8 +366,6 @@
 # Errors:
 #     - If @fdset-id or @fd is not found, GenericError
 #
-# Since: 1.2
-#
 # .. note:: The list of fd sets is shared by all monitor connections.
 #
 # .. note:: If @fd is not specified, all file descriptors in @fdset-id
@@ -377,6 +375,8 @@
 #
 #     -> { "execute": "remove-fd", "arguments": { "fdset-id": 1, "fd": 3 } }
 #     <- { "return": {} }
+#
+# Since: 1.2
 ##
 { 'command': 'remove-fd', 'data': {'fdset-id': 'int', '*fd': 'int'} }
 
@@ -410,8 +410,6 @@
 # @query-fdsets:
 #     Return information describing all fd sets.
 #
-# Since: 1.2
-#
 # .. note:: The list of fd sets is shared by all monitor connections.
 #
 # .. qmp-example::
@@ -444,6 +442,8 @@
 #            }
 #          ]
 #        }
+#
+# Since: 1.2
 ##
 { 'command': 'query-fdsets', 'returns': ['FdsetInfo'] }
 
@@ -510,8 +510,6 @@
 # Errors:
 #     - if the given @option doesn't exist
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-command-line-options",
@@ -532,6 +530,8 @@
 #             }
 #          ]
 #        }
+#
+# Since: 1.5
 ##
 {'command': 'query-command-line-options',
  'data': {'*option': 'str'},
@@ -551,13 +551,13 @@
 #    RTC in the system implements this event, or even that the system
 #    has an RTC at all.
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "RTC_CHANGE",
 #          "data": { "offset": 78 },
 #          "timestamp": { "seconds": 1267020223, "microseconds": 435656 } }
+#
+# Since: 0.13
 ##
 { 'event': 'RTC_CHANGE',
   'data': { 'offset': 'int', 'qom-path': 'str' } }
@@ -577,8 +577,6 @@
 #
 # @dev-qom-path: path to attached PCI device in the QOM tree
 #
-# Since: 7.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "VFU_CLIENT_HANGUP",
@@ -587,6 +585,8 @@
 #                    "dev-id": "sas1",
 #                    "dev-qom-path": "/machine/peripheral/sas1" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 7.1
 ##
 { 'event': 'VFU_CLIENT_HANGUP',
   'data': { 'vfu-id': 'str', 'vfu-qom-path': 'str',
diff --git a/qapi/net.json b/qapi/net.json
index e760b0e84d5..de0182f9931 100644
--- a/qapi/net.json
+++ b/qapi/net.json
@@ -21,8 +21,6 @@
 # Errors:
 #     - If @name is not a valid network device, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. note:: Not all network adapters support setting link status.
 #    This command will succeed even if the network adapter does not
 #    support link status notification.
@@ -32,6 +30,8 @@
 #     -> { "execute": "set_link",
 #          "arguments": { "name": "e1000.0", "up": false } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'set_link', 'data': {'name': 'str', 'up': 'bool'} }
 
@@ -39,8 +39,6 @@
 # @netdev_add:
 #     Add a network backend.
 #
-# Since: 0.14
-#
 # Errors:
 #     - If @type is not a valid network backend, DeviceNotFound
 #
@@ -52,6 +50,8 @@
 #          "arguments": { "type": "user", "id": "netdev1",
 #                         "dnssearch": [ { "str": "example.org" } ] } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'netdev_add', 'data': 'Netdev', 'boxed': true,
   'allow-preconfig': true }
@@ -65,12 +65,12 @@
 # Errors:
 #     - If @id is not a valid network backend, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "netdev_del", "arguments": { "id": "netdev1" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'netdev_del', 'data': {'id': 'str'},
   'allow-preconfig': true }
@@ -1066,8 +1066,6 @@
 #     - if the given NIC doesn't support rx-filter querying
 #     - if the given net client isn't a NIC
 #
-# Since: 1.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rx-filter", "arguments": { "name": "vnet0" } }
@@ -1096,6 +1094,8 @@
 #             }
 #           ]
 #        }
+#
+# Since: 1.6
 ##
 { 'command': 'query-rx-filter',
   'data': { '*name': 'str' },
@@ -1110,14 +1110,14 @@
 #
 # @path: device path
 #
-# Since: 1.6
-#
 # .. qmp-example::
 #
 #     <- { "event": "NIC_RX_FILTER_CHANGED",
 #          "data": { "name": "vnet0",
 #                    "path": "/machine/peripheral/vnet0/virtio-backend" },
 #          "timestamp": { "seconds": 1368697518, "microseconds": 326866 } }
+#
+# Since: 1.6
 ##
 { 'event': 'NIC_RX_FILTER_CHANGED',
   'data': { '*name': 'str', 'path': 'str' } }
@@ -1180,13 +1180,13 @@
 #
 # @device-id: QEMU device id of the unplugged device
 #
-# Since: 4.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "FAILOVER_NEGOTIATED",
 #          "data": { "device-id": "net1" },
 #          "timestamp": { "seconds": 1368697518, "microseconds": 326866 } }
+#
+# Since: 4.2
 ##
 { 'event': 'FAILOVER_NEGOTIATED',
   'data': {'device-id': 'str'} }
@@ -1199,8 +1199,6 @@
 #
 # @addr: The destination address
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "NETDEV_STREAM_CONNECTED",
@@ -1215,6 +1213,8 @@
 #          "data": { "netdev-id": "netdev0",
 #                    "addr": { "path": "/tmp/qemu0", "type": "unix" } },
 #          "timestamp": { "seconds": 1666269706, "microseconds": 413651 } }
+#
+# Since: 7.2
 ##
 { 'event': 'NETDEV_STREAM_CONNECTED',
   'data': { 'netdev-id': 'str',
@@ -1226,13 +1226,13 @@
 #
 # @netdev-id: QEMU netdev id that is disconnected
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "NETDEV_STREAM_DISCONNECTED",
 #          "data": {"netdev-id": "netdev0"},
 #          "timestamp": {"seconds": 1663330937, "microseconds": 526695} }
+#
+# Since: 7.2
 ##
 { 'event': 'NETDEV_STREAM_DISCONNECTED',
   'data': { 'netdev-id': 'str' } }
@@ -1245,13 +1245,13 @@
 #
 # @chardev-id: The character device id used by the QEMU netdev
 #
-# Since: 10.0
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1739538638, "microseconds": 354181 },
 #          "event": "NETDEV_VHOST_USER_CONNECTED",
 #          "data": { "netdev-id": "netdev0", "chardev-id": "chr0" } }
+#
+# Since: 10.0
 ##
 { 'event': 'NETDEV_VHOST_USER_CONNECTED',
   'data': { 'netdev-id': 'str', 'chardev-id': 'str' } }
@@ -1262,13 +1262,13 @@
 #
 # @netdev-id: QEMU netdev id that is disconnected
 #
-# Since: 10.0
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": { "seconds": 1739538634, "microseconds": 920450 },
 #          "event": "NETDEV_VHOST_USER_DISCONNECTED",
 #          "data": { "netdev-id": "netdev0" } }
+#
+# Since: 10.0
 ##
 { 'event': 'NETDEV_VHOST_USER_DISCONNECTED',
   'data': { 'netdev-id': 'str' } }
@@ -1302,13 +1302,13 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 11.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "x-query-usernet" }
 #     <- { "return": [ { "hub-id": 0, "hub-name": "vnet",
 #                        "info": "..." } ] }
+#
+# Since: 11.2
 ##
 { 'command': 'x-query-usernet',
   'returns': ['UsernetInfo'],
@@ -1418,14 +1418,14 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 11.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "x-query-network" }
 #     <- { "return": { "clients": [ { "name": "st0",
 #                      "queue-index": 0, "type": "stream",
 #                      "info-str": "listening" } ] } }
+#
+# Since: 11.2
 ##
 { 'command': 'x-query-network',
   'returns': 'NetworkInfo',
diff --git a/qapi/pci.json b/qapi/pci.json
index a6a79f91afc..8736393b6c5 100644
--- a/qapi/pci.json
+++ b/qapi/pci.json
@@ -173,8 +173,6 @@
 #     of all PCI devices attached to it.  Each device is represented
 #     by a json-object.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-pci" }
@@ -305,5 +303,7 @@
 #        }
 #
 # This example has been shortened as the real response is too long.
+#
+# Since: 0.14
 ##
 { 'command': 'query-pci', 'returns': ['PciInfo'] }
diff --git a/qapi/qdev.json b/qapi/qdev.json
index a3fb6a28d94..8a68be1d5f8 100644
--- a/qapi/qdev.json
+++ b/qapi/qdev.json
@@ -100,8 +100,6 @@
 #    device will not be removed and a `DEVICE_UNPLUG_GUEST_ERROR`
 #    event is sent.  Some errors cannot be detected.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "device_del",
@@ -113,6 +111,8 @@
 #     -> { "execute": "device_del",
 #          "arguments": { "id": "/machine/peripheral-anon/device[0]" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'device_del', 'data': {'id': 'str'} }
 
@@ -127,14 +127,14 @@
 #
 # @path: the device's QOM path
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     <- { "event": "DEVICE_DELETED",
 #          "data": { "device": "virtio-net-pci-0",
 #                    "path": "/machine/peripheral/virtio-net-pci-0" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 1.5
 ##
 { 'event': 'DEVICE_DELETED',
   'data': { '*device': 'str', 'path': 'str' } }
@@ -148,14 +148,14 @@
 #
 # @path: the device's QOM path
 #
-# Since: 6.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "DEVICE_UNPLUG_GUEST_ERROR",
 #          "data": { "device": "core1",
 #                    "path": "/machine/peripheral/core1" },
 #          "timestamp": { "seconds": 1615570772, "microseconds": 202844 } }
+#
+# Since: 6.2
 ##
 { 'event': 'DEVICE_UNPLUG_GUEST_ERROR',
   'data': { '*device': 'str', 'path': 'str' } }
diff --git a/qapi/qom.json b/qapi/qom.json
index 286fb987317..d765a59ce23 100644
--- a/qapi/qom.json
+++ b/qapi/qom.json
@@ -84,8 +84,6 @@
 #
 # Returns: a list that describe the properties of the object.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "qom-list",
@@ -94,6 +92,8 @@
 #                      { "name": "parallel0", "type": "child<chardev-vc>" },
 #                      { "name": "serial0", "type": "child<chardev-vc>" },
 #                      { "name": "mon0", "type": "child<chardev-stdio>" } ] }
+#
+# Since: 1.2
 ##
 { 'command': 'qom-list',
   'data': { 'path': 'str' },
@@ -127,8 +127,6 @@
 #     child<> and link<> properties are returned as #str pathnames.
 #     All integer property types (u8, u16, etc) are returned as #int.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #    :title: Use absolute path
 #
@@ -144,6 +142,8 @@
 #          "arguments": { "path": "unattached/sysbus",
 #                         "property": "type" } }
 #     <- { "return": "System" }
+#
+# Since: 1.2
 ##
 { 'command': 'qom-get',
   'data': { 'path': 'str', 'property': 'str' },
@@ -182,8 +182,6 @@
 # @value: a value who's type is appropriate for the property type.
 #     See `qom-get` for a description of type mapping.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "qom-set",
@@ -191,6 +189,8 @@
 #                         "property": "graphics",
 #                         "value": false } }
 #     <- { "return": {} }
+#
+# Since: 1.2
 ##
 { 'command': 'qom-set',
   'data': { 'path': 'str', 'property': 'str', 'value': 'any' },
@@ -1381,14 +1381,14 @@
 # Errors:
 #     - If @qom-type is not a valid class name
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "object-add",
 #          "arguments": { "qom-type": "rng-random", "id": "rng1",
 #                         "filename": "/dev/hwrng" } }
 #     <- { "return": {} }
+#
+# Since: 2.0
 ##
 { 'command': 'object-add', 'data': 'ObjectOptions', 'boxed': true,
   'allow-preconfig': true }
@@ -1402,12 +1402,12 @@
 # Errors:
 #     - If @id is not a valid id for a QOM object
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "object-del", "arguments": { "id": "rng1" } }
 #     <- { "return": {} }
+#
+# Since: 2.0
 ##
 { 'command': 'object-del', 'data': {'id': 'str'},
   'allow-preconfig': true }
diff --git a/qapi/replay.json b/qapi/replay.json
index db1440c09a6..ce7d084684f 100644
--- a/qapi/replay.json
+++ b/qapi/replay.json
@@ -51,12 +51,12 @@
 #
 # Returns: record/replay information.
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-replay" }
 #     <- { "return": { "mode": "play", "filename": "log.rr", "icount": 220414 } }
+#
+# Since: 5.2
 ##
 { 'command': 'query-replay',
   'returns': 'ReplayInfo' }
@@ -73,12 +73,12 @@
 #
 # @icount: instruction count to stop at
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "replay-break", "arguments": { "icount": 220414 } }
 #     <- { "return": {} }
+#
+# Since: 5.2
 ##
 { 'command': 'replay-break', 'data': { 'icount': 'int' } }
 
@@ -87,12 +87,12 @@
 #     Remove replay breakpoint which was set with `replay-break`.  The
 #     command is ignored when there are no replay breakpoints.
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "replay-delete-break" }
 #     <- { "return": {} }
+#
+# Since: 5.2
 ##
 { 'command': 'replay-delete-break' }
 
@@ -107,11 +107,11 @@
 #
 # @icount: target instruction count
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "replay-seek", "arguments": { "icount": 220414 } }
 #     <- { "return": {} }
+#
+# Since: 5.2
 ##
 { 'command': 'replay-seek', 'data': { 'icount': 'int' } }
diff --git a/qapi/rocker.json b/qapi/rocker.json
index 7eb0a88dafd..06e21721993 100644
--- a/qapi/rocker.json
+++ b/qapi/rocker.json
@@ -28,12 +28,12 @@
 #
 # @name: switch name
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rocker", "arguments": { "name": "sw1" } }
 #     <- { "return": {"name": "sw1", "ports": 2, "id": 1327446905938}}
+#
+# Since: 2.4
 ##
 { 'command': 'query-rocker',
   'data': { 'name': 'str' },
@@ -92,8 +92,6 @@
 #
 # @name: port name
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rocker-ports", "arguments": { "name": "sw1" } }
@@ -102,6 +100,8 @@
 #                      {"duplex": "full", "enabled": true, "name": "sw1.2",
 #                       "autoneg": "off", "link-up": true, "speed": 10000}
 #        ]}
+#
+# Since: 2.4
 ##
 { 'command': 'query-rocker-ports',
   'data': { 'name': 'str' },
@@ -229,8 +229,6 @@
 #
 # Returns: rocker OF-DPA flow information
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rocker-of-dpa-flows",
@@ -243,6 +241,8 @@
 #                      },
 #                      ...
 #        ]}
+#
+# Since: 2.4
 ##
 { 'command': 'query-rocker-of-dpa-flows',
   'data': { 'name': 'str', '*tbl-id': 'uint32' },
@@ -302,8 +302,6 @@
 #
 # Returns: rocker OF-DPA group information
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rocker-of-dpa-groups",
@@ -321,6 +319,8 @@
 #                       "pport": 0, "vlan-id": 3840,
 #                       "pop-vlan": 1, "id": 251658240}
 #        ]}
+#
+# Since: 2.4
 ##
 { 'command': 'query-rocker-of-dpa-groups',
   'data': { 'name': 'str', '*type': 'uint8' },
diff --git a/qapi/run-state.json b/qapi/run-state.json
index 8bbd9884087..3c8d790216a 100644
--- a/qapi/run-state.json
+++ b/qapi/run-state.json
@@ -117,13 +117,13 @@
 # @query-status:
 #     Query the run status of the VM
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-status" }
 #     <- { "return": { "running": true,
 #                      "status": "running" } }
+#
+# Since: 0.14
 ##
 { 'command': 'query-status', 'returns': 'StatusInfo',
   'allow-preconfig': true }
@@ -145,13 +145,13 @@
 #    specified, QEMU will not exit, and a `STOP` event will eventually
 #    follow the `SHUTDOWN` event.
 #
-# Since: 0.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "SHUTDOWN",
 #          "data": { "guest": true, "reason": "guest-shutdown" },
 #          "timestamp": { "seconds": 1267040730, "microseconds": 682951 } }
+#
+# Since: 0.12
 ##
 { 'event': 'SHUTDOWN', 'data': { 'guest': 'bool', 'reason': 'ShutdownCause' } }
 
@@ -160,12 +160,12 @@
 #     Emitted when the virtual machine is powered down through the
 #     power control system, such as via ACPI.
 #
-# Since: 0.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "POWERDOWN",
 #          "timestamp": { "seconds": 1267040730, "microseconds": 682951 } }
+#
+# Since: 0.12
 ##
 { 'event': 'POWERDOWN' }
 
@@ -180,13 +180,13 @@
 #
 # @reason: The `ShutdownCause` of the `RESET`.  (since 4.0)
 #
-# Since: 0.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "RESET",
 #          "data": { "guest": false, "reason": "guest-reset" },
 #          "timestamp": { "seconds": 1267041653, "microseconds": 9518 } }
+#
+# Since: 0.12
 ##
 { 'event': 'RESET', 'data': { 'guest': 'bool', 'reason': 'ShutdownCause' } }
 
@@ -194,12 +194,12 @@
 # @STOP:
 #     Emitted when the virtual machine is stopped
 #
-# Since: 0.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "STOP",
 #          "timestamp": { "seconds": 1267041730, "microseconds": 281295 } }
+#
+# Since: 0.12
 ##
 { 'event': 'STOP' }
 
@@ -207,12 +207,12 @@
 # @RESUME:
 #     Emitted when the virtual machine resumes execution
 #
-# Since: 0.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "RESUME",
 #          "timestamp": { "seconds": 1271770767, "microseconds": 582542 } }
+#
+# Since: 0.12
 ##
 { 'event': 'RESUME' }
 
@@ -221,12 +221,12 @@
 #     Emitted when guest enters a hardware suspension state, for
 #     example, S3 state, which is sometimes called standby state
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "SUSPEND",
 #          "timestamp": { "seconds": 1344456160, "microseconds": 309119 } }
+#
+# Since: 1.1
 ##
 { 'event': 'SUSPEND' }
 
@@ -239,12 +239,12 @@
 # .. note:: QEMU shuts down (similar to event `SHUTDOWN`) when
 #    entering this state.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "SUSPEND_DISK",
 #          "timestamp": { "seconds": 1344456160, "microseconds": 309119 } }
+#
+# Since: 1.2
 ##
 { 'event': 'SUSPEND_DISK' }
 
@@ -253,12 +253,12 @@
 #     Emitted when the guest has woken up from suspend state and is
 #     running
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "WAKEUP",
 #          "timestamp": { "seconds": 1344522075, "microseconds": 745528 } }
+#
+# Since: 1.1
 ##
 { 'event': 'WAKEUP' }
 
@@ -274,13 +274,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "WATCHDOG",
 #          "data": { "action": "reset" },
 #          "timestamp": { "seconds": 1267061043, "microseconds": 959568 } }
+#
+# Since: 0.13
 ##
 { 'event': 'WATCHDOG',
   'data': { 'action': 'WatchdogAction' } }
@@ -365,13 +365,13 @@
 #
 # @action: `WatchdogAction` action taken when watchdog timer expires.
 #
-# Since: 2.11
-#
 # .. qmp-example::
 #
 #     -> { "execute": "watchdog-set-action",
 #          "arguments": { "action": "inject-nmi" } }
 #     <- { "return": {} }
+#
+# Since: 2.11
 ##
 { 'command': 'watchdog-set-action', 'data' : {'action': 'WatchdogAction'} }
 
@@ -389,8 +389,6 @@
 # @watchdog: `WatchdogAction` action taken when watchdog timer
 #     expires.
 #
-# Since: 6.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "set-action",
@@ -399,6 +397,8 @@
 #                         "panic": "pause",
 #                         "watchdog": "inject-nmi" } }
 #     <- { "return": {} }
+#
+# Since: 6.0
 ##
 { 'command': 'set-action',
   'data': { '*reboot': 'RebootAction',
@@ -415,13 +415,13 @@
 #
 # @info: information about a panic (since 2.9)
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     <- { "event": "GUEST_PANICKED",
 #          "data": { "action": "pause" },
 #          "timestamp": { "seconds": 1648245231, "microseconds": 900001 } }
+#
+# Since: 1.5
 ##
 { 'event': 'GUEST_PANICKED',
   'data': { 'action': 'GuestPanicAction', '*info': 'GuestPanicInformation' } }
@@ -434,13 +434,13 @@
 #
 # @info: information about a panic
 #
-# Since: 5.0
-#
 # .. qmp-example::
 #
 #     <- { "event": "GUEST_CRASHLOADED",
 #          "data": { "action": "run" },
 #          "timestamp": { "seconds": 1648245259, "microseconds": 893771 } }
+#
+# Since: 5.0
 ##
 { 'event': 'GUEST_CRASHLOADED',
   'data': { 'action': 'GuestPanicAction', '*info': 'GuestPanicInformation' } }
@@ -450,12 +450,12 @@
 #     Emitted when guest submits a shutdown request via pvpanic
 #     interface
 #
-# Since: 9.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "GUEST_PVSHUTDOWN",
 #          "timestamp": { "seconds": 1648245259, "microseconds": 893771 } }
+#
+# Since: 9.1
 ##
 { 'event': 'GUEST_PVSHUTDOWN' }
 
@@ -629,8 +629,6 @@
 #
 # @flags: flags for `MemoryFailureAction`.
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "MEMORY_FAILURE",
@@ -639,6 +637,8 @@
 #                    "flags": { "action-required": false,
 #                               "recursive": false } },
 #          "timestamp": { "seconds": 1267061043, "microseconds": 959568 } }
+#
+# Since: 5.2
 ##
 { 'event': 'MEMORY_FAILURE',
   'data': { 'recipient': 'MemoryFailureRecipient',
diff --git a/qapi/tpm.json b/qapi/tpm.json
index 6d94be656ae..906ebd7d019 100644
--- a/qapi/tpm.json
+++ b/qapi/tpm.json
@@ -27,12 +27,12 @@
 # @query-tpm-models:
 #     Return a list of supported TPM models
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-tpm-models" }
 #     <- { "return": [ "tpm-tis", "tpm-crb", "tpm-spapr" ] }
+#
+# Since: 1.5
 ##
 { 'command': 'query-tpm-models', 'returns': ['TpmModel'],
   'if': 'CONFIG_TPM' }
@@ -54,12 +54,12 @@
 # @query-tpm-types:
 #     Return a list of supported TPM types
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-tpm-types" }
 #     <- { "return": [ "passthrough", "emulator" ] }
+#
+# Since: 1.5
 ##
 { 'command': 'query-tpm-types', 'returns': ['TpmType'],
   'if': 'CONFIG_TPM' }
@@ -155,8 +155,6 @@
 # @query-tpm:
 #     Return information about the TPM device
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-tpm" }
@@ -174,6 +172,8 @@
 #            }
 #          ]
 #        }
+#
+# Since: 1.5
 ##
 { 'command': 'query-tpm', 'returns': ['TPMInfo'],
   'if': 'CONFIG_TPM' }
diff --git a/qapi/trace.json b/qapi/trace.json
index 29c76256055..ccc68b69b6c 100644
--- a/qapi/trace.json
+++ b/qapi/trace.json
@@ -48,13 +48,13 @@
 #
 # Returns: a list of info for the matching events
 #
-# Since: 2.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "trace-event-get-state",
 #          "arguments": { "name": "qemu_memalign" } }
 #     <- { "return": [ { "name": "qemu_memalign", "state": "disabled", "vcpu": false } ] }
+#
+# Since: 2.2
 ##
 { 'command': 'trace-event-get-state',
   'data': {'name': 'str' },
@@ -70,13 +70,13 @@
 #
 # @ignore-unavailable: Do not match unavailable events with @name.
 #
-# Since: 2.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "trace-event-set-state",
 #          "arguments": { "name": "qemu_memalign", "enable": true } }
 #     <- { "return": {} }
+#
+# Since: 2.2
 ##
 { 'command': 'trace-event-set-state',
   'data': {'name': 'str', 'enable': 'bool', '*ignore-unavailable': 'bool' } }
diff --git a/qapi/transaction.json b/qapi/transaction.json
index 68df2df44ac..5026dc15a57 100644
--- a/qapi/transaction.json
+++ b/qapi/transaction.json
@@ -216,8 +216,6 @@
 #    in an error condition, and subsequent actions will not have been
 #    attempted.
 #
-# Since: 1.1
-#
 # For external snapshots, the dictionary contains the device, the file
 # to use for the new snapshot, and the format.  The default format, if
 # not specified, is qcow2.
@@ -261,6 +259,8 @@
 #                                          "device": "ide-hd2",
 #                                          "name": "snapshot0" } } ] } }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'transaction',
   'data': { 'actions': [ 'TransactionAction' ],
diff --git a/qapi/ui.json b/qapi/ui.json
index 51e276baba4..83ac180ff36 100644
--- a/qapi/ui.json
+++ b/qapi/ui.json
@@ -78,13 +78,13 @@
 # Errors:
 #     - If Spice is not enabled, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "set_password", "arguments": { "protocol": "vnc",
 #                                                    "password": "secret" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'set_password', 'boxed': true, 'data': 'SetPasswordOptions' }
 
@@ -137,13 +137,13 @@
 #     - If @protocol is 'spice' and Spice is not active,
 #       DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "expire_password", "arguments": { "protocol": "vnc",
 #                                                       "time": "+60" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'expire_password', 'boxed': true, 'data': 'ExpirePasswordOptions' }
 
@@ -177,13 +177,13 @@
 #
 # @format: image format for `screendump`.  (default: ppm) (Since 7.1)
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "screendump",
 #          "arguments": { "filename": "/tmp/image" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'screendump',
   'data': {'filename': 'str', '*device': 'str', '*head': 'int',
@@ -312,8 +312,6 @@
 # @query-spice:
 #     Return information about the current SPICE server
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-spice" }
@@ -348,6 +346,8 @@
 #              ]
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-spice', 'returns': 'SpiceInfo',
   'if': 'CONFIG_SPICE' }
@@ -360,8 +360,6 @@
 #
 # @client: client information
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1290688046, "microseconds": 388707},
@@ -370,6 +368,8 @@
 #            "server": { "port": "5920", "family": "ipv4", "host": "127.0.0.1"},
 #            "client": {"port": "52873", "family": "ipv4", "host": "127.0.0.1"}
 #        }}
+#
+# Since: 0.14
 ##
 { 'event': 'SPICE_CONNECTED',
   'data': { 'server': 'SpiceBasicInfo',
@@ -385,8 +385,6 @@
 #
 # @client: client information
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1290688046, "microseconds": 417172},
@@ -397,6 +395,8 @@
 #                              "connection-id": 1804289383, "host": "127.0.0.1",
 #                              "channel-id": 0, "tls": true}
 #        }}
+#
+# Since: 0.14
 ##
 { 'event': 'SPICE_INITIALIZED',
   'data': { 'server': 'SpiceServerInfo',
@@ -411,8 +411,6 @@
 #
 # @client: client information
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1290688046, "microseconds": 388707},
@@ -421,6 +419,8 @@
 #            "server": { "port": "5920", "family": "ipv4", "host": "127.0.0.1"},
 #            "client": {"port": "52873", "family": "ipv4", "host": "127.0.0.1"}
 #        }}
+#
+# Since: 0.14
 ##
 { 'event': 'SPICE_DISCONNECTED',
   'data': { 'server': 'SpiceBasicInfo',
@@ -431,12 +431,12 @@
 # @SPICE_MIGRATE_COMPLETED:
 #     Emitted when SPICE migration has completed
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1290688046, "microseconds": 417172},
 #          "event": "SPICE_MIGRATE_COMPLETED" }
+#
+# Since: 1.3
 ##
 { 'event': 'SPICE_MIGRATE_COMPLETED',
   'if': 'CONFIG_SPICE' }
@@ -629,8 +629,6 @@
 # @query-vnc:
 #     Return information about the current VNC server
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-vnc" }
@@ -650,6 +648,8 @@
 #              ]
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-vnc', 'returns': 'VncInfo',
   'if': 'CONFIG_VNC' }
@@ -669,11 +669,11 @@
 #
 # @password: the new password to use with VNC authentication
 #
-# Since: 1.1
-#
 # .. note:: An empty password in this command will set the password to
 #    the empty string.  Existing clients are unaffected by executing
 #    this command.
+#
+# Since: 1.1
 ##
 { 'command': 'change-vnc-password',
   'data': { 'password': 'str' },
@@ -690,8 +690,6 @@
 # .. note:: This event is emitted before any authentication takes
 #    place, thus the authentication ID is not provided.
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "VNC_CONNECTED",
@@ -701,6 +699,8 @@
 #                "client": { "family": "ipv4", "service": "58425",
 #                            "host": "127.0.0.1", "websocket": false } },
 #          "timestamp": { "seconds": 1262976601, "microseconds": 975795 } }
+#
+# Since: 0.13
 ##
 { 'event': 'VNC_CONNECTED',
   'data': { 'server': 'VncServerInfo',
@@ -716,8 +716,6 @@
 #
 # @client: client information
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <-  { "event": "VNC_INITIALIZED",
@@ -727,6 +725,8 @@
 #                "client": { "family": "ipv4", "service": "46089", "websocket": false,
 #                            "host": "127.0.0.1", "sasl_username": "luiz" } },
 #           "timestamp": { "seconds": 1263475302, "microseconds": 150772 } }
+#
+# Since: 0.13
 ##
 { 'event': 'VNC_INITIALIZED',
   'data': { 'server': 'VncServerInfo',
@@ -741,8 +741,6 @@
 #
 # @client: client information
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "VNC_DISCONNECTED",
@@ -752,6 +750,8 @@
 #                "client": { "family": "ipv4", "service": "58425", "websocket": false,
 #                            "host": "127.0.0.1", "sasl_username": "luiz" } },
 #          "timestamp": { "seconds": 1262976601, "microseconds": 975795 } }
+#
+# Since: 0.13
 ##
 { 'event': 'VNC_DISCONNECTED',
   'data': { 'server': 'VncServerInfo',
@@ -789,8 +789,6 @@
 #
 # Returns: a list of info for each device
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-mice" }
@@ -809,6 +807,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-mice', 'returns': ['MouseInfo'] }
 
@@ -996,8 +996,6 @@
 # Errors:
 #     - If key is unknown or redundant, GenericError
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "send-key",
@@ -1005,6 +1003,8 @@
 #                                   { "type": "qcode", "data": "alt" },
 #                                   { "type": "qcode", "data": "delete" } ] } }
 #     <- { "return": {} }
+#
+# Since: 1.3
 ##
 { 'command': 'send-key',
   'data': { 'keys': ['KeyValue'], '*hold-time': 'int' } }
@@ -1206,8 +1206,6 @@
 #
 # @events: List of `InputEvent` union.
 #
-# Since: 2.6
-#
 # The @device and @head parameters can be used to send the input event
 # to specific input devices in case (a) multiple input devices of the
 # same kind are added to the virtual machine and (b) you have
@@ -1260,6 +1258,8 @@
 #                    { "type": "abs", "data" : { "axis": "x", "value" : 20000 } },
 #                    { "type": "abs", "data" : { "axis": "y", "value" : 400 } } ] } }
 #     <- { "return": {} }
+#
+# Since: 2.6
 ##
 { 'command': 'input-send-event',
   'data': { '*device': 'str',
@@ -1562,13 +1562,13 @@
 # @display-reload:
 #     Reload display configuration.
 #
-# Since: 6.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "display-reload",
 #          "arguments": { "type": "vnc", "tls-certs": true  } }
 #     <- { "return": {} }
+#
+# Since: 6.0
 ##
 { 'command': 'display-reload',
   'data': 'DisplayReloadOptions',
@@ -1615,8 +1615,6 @@
 # @display-update:
 #     Update display configuration.
 #
-# Since: 7.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "display-update",
@@ -1624,6 +1622,8 @@
 #                         [ { "type": "inet", "host": "0.0.0.0",
 #                             "port": "5901" } ] } }
 #     <- { "return": {} }
+#
+# Since: 7.1
 ##
 { 'command': 'display-update',
   'data': 'DisplayUpdateOptions',
@@ -1646,8 +1646,6 @@
 #
 # @cert-subject: server certificate subject
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "client_migrate_info",
@@ -1655,6 +1653,8 @@
 #                         "hostname": "virt42.lab.kraxel.org",
 #                         "port": 1234 } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'client_migrate_info',
   'data': { 'protocol': 'str', 'hostname': 'str', '*port': 'int',
diff --git a/qapi/vfio.json b/qapi/vfio.json
index 47fff8b979d..a8936265052 100644
--- a/qapi/vfio.json
+++ b/qapi/vfio.json
@@ -57,8 +57,6 @@
 #
 # @device-state: The new changed device migration state.
 #
-# Since: 9.1
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": { "seconds": 1713771323, "microseconds": 212268 },
@@ -67,6 +65,8 @@
 #              "device-id": "vfio_dev1",
 #              "qom-path": "/machine/peripheral/vfio_dev1",
 #              "device-state": "stop" } }
+#
+# Since: 9.1
 ##
 { 'event': 'VFIO_MIGRATION',
   'data': {
diff --git a/qapi/virtio.json b/qapi/virtio.json
index 219c3f75043..fe818719b89 100644
--- a/qapi/virtio.json
+++ b/qapi/virtio.json
@@ -32,8 +32,6 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "x-query-virtio" }
@@ -60,6 +58,8 @@
 #              }
 #          ]
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio',
   'returns': [ 'VirtioInfo' ],
@@ -195,8 +195,6 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -429,6 +427,8 @@
 #              "use-started": true
 #          }
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio-status',
   'data': { 'path': 'str' },
@@ -567,8 +567,6 @@
 #    shadow_avail_idx will not be displayed in the case where the
 #    selected VirtIODevice has a running vhost device.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -623,6 +621,8 @@
 #              "vring-num": 128
 #          }
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio-queue-status',
   'data': { 'path': 'str', 'queue': 'uint16' },
@@ -684,8 +684,6 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #    :title: Get vhost_virtqueue status for vhost-crypto
 #
@@ -727,6 +725,8 @@
 #              "kick": 0
 #          }
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio-vhost-queue-status',
   'data': { 'path': 'str', 'queue': 'uint16' },
@@ -820,8 +820,6 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #    :title: Introspect on virtio-net's VirtQueue 0 at index 5
 #
@@ -909,6 +907,8 @@
 #              }
 #          }
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio-queue-element',
   'data': { 'path': 'str', 'queue': 'uint16', '*index': 'uint16' },
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 08/14] qapi/parser: enforce strict positioning for "Since:" section
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (6 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 07/14] qapi: re-order 'since' sections to always be last John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-29 12:53   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 09/14] qapi/parser: remove _insert_after_intro() helper John Snow
                   ` (5 subsequent siblings)
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

This patch finalizes the total ordering of all QAPI documentation
block sections.

... Except TODO, which can still be wherever.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 8b331aa0673..c14bff2cbc3 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -552,7 +552,7 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
                 if isinstance(this, str):
                     this = QAPIDoc.Kind.from_string(this)
 
-                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE):
+                if this == QAPIDoc.Kind.TODO:
                     return
 
                 if this.value < last_ordered_section.value:
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 09/14] qapi/parser: remove _insert_after_intro() helper
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (7 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 08/14] qapi/parser: enforce strict positioning for "Since:" section John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-29 13:09   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 10/14] qapi/parser: add _append() method John Snow
                   ` (4 subsequent siblings)
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

I promised I'd remove this.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 49 ++++++------------------------------------
 1 file changed, 7 insertions(+), 42 deletions(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index c14bff2cbc3..5fc9c18c81d 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -862,49 +862,15 @@ def _insert_near_kind(
                 return True
         return False
 
-    def _insert_after_intro(
-        self,
-        section: 'QAPIDoc.Section',
-    ) -> None:
-        """
-        Insert a section immediately after the intro section.
-
-        While we convert DETAILS sections to INTRO sections, all
-        contiguous INTRO/DETAILS sections at the start of a QAPIDoc
-        section list are treated as "the intro".
-
-        Once INTRO conversion is complete, this helper will no longer be
-        needed and ``_insert_near_kind(QAPIDoc.Kind.INTRO, ...)`` will
-        be sufficient.
-        """
-        first = self.all_sections[0]
-        if first.text and first.kind == QAPIDoc.Kind.INTRO:
-            # First section is introduction and is non-empty: insert here.
-            # Rest assured all of this ugliness will very soon go away.
-            # Pinkie-swear.
-            self.all_sections.insert(1, section)
-            return
-
-        index = 0
-        for index, ref_section in enumerate(self.all_sections):
-            if ref_section.kind.name in ("INTRO", "DETAILS"):
-                continue
-            break
-        else:
-            index += 1
-
-        self.all_sections.insert(index, section)
-
     def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
-
         """
         Append a stub section after any Member sections.
         """
         if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
             return
 
-        # No MEMBER sections present. Insert after INTRO/DETAILS sections.
-        self._insert_after_intro(stub)
+        # No MEMBER sections present. Insert after INTRO section.
+        self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)
 
     def connect_member(self, member: 'QAPISchemaMember') -> None:
         if member.name not in self._args:
@@ -932,10 +898,10 @@ def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
     def ensure_returns(self, info: QAPISourceInfo) -> None:
         # This is more complicated than it ought to be.  The doc
         # parser should already know where a generated RETURNS section
-        # should go.  It currently doesn't, mostly because it accepts
-        # tagged sections in any order.
+        # should go.  It currently doesn't.
         #
-        # TODO: Tighten doc syntax and simplify.
+        # TODO: Take advantage of the new strict section ordering and
+        # simplify this algorithm.
 
         if any(s.kind == QAPIDoc.Kind.RETURNS for s in self.all_sections):
             return
@@ -950,12 +916,11 @@ def ensure_returns(self, info: QAPISourceInfo) -> None:
                 (QAPIDoc.Kind.ERRORS, False),
                 # 3. Elif features, right *before* those.
                 (QAPIDoc.Kind.FEATURE, False),
+                # 4. Else, right *after* the intro.
+                (QAPIDoc.Kind.INTRO, True),
         )):
             return
 
-        # Otherwise, it should go right after the intro.
-        self._insert_after_intro(stub)
-
     def check_expr(self, expr: QAPIExpression) -> None:
         if 'command' in expr:
             if self._returns and 'returns' not in expr:
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 10/14] qapi/parser: add _append() method
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (8 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 09/14] qapi/parser: remove _insert_after_intro() helper John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-29 13:28   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 11/14] qapi/parser: privatize "all_sections" field as "_sections" John Snow
                   ` (3 subsequent siblings)
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

This method is intended to become the sole point of appending new
sections to the section list, prohibiting outside callers from
accessing the list of sections directly.

A forthcoming commit will actually convert the "all_sections" field to
be a private field to begin enforcing that no outside callers are
manipulating the section list from outside of this class.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 50 ++++++++++++++++++++++++++----------------
 1 file changed, 31 insertions(+), 19 deletions(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 5fc9c18c81d..0dca1acba4c 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -542,7 +542,7 @@ def get_doc(self) -> 'QAPIDoc':
             if not symbol:
                 raise QAPIParseError(self, "name required after '@'")
             doc = QAPIDoc(info, symbol)
-            doc.all_sections.append(QAPIDoc.Section(info, QAPIDoc.Kind.INTRO))
+            doc.new_tagged_section(info, QAPIDoc.Kind.INTRO)
             line = self.get_doc_indented(doc)
             no_more_args = False
             last_ordered_section = QAPIDoc.Kind.INTRO
@@ -772,6 +772,33 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
     def has_features(self) -> bool:
         return bool(self._features)
 
+    def _append(self, section: 'QAPIDoc.Section') -> None:
+        """
+        Primary internal interface for adding a new section.
+
+        It is intended to become the sole access point for adding new
+        sections to the QAPIDoc section list, but we are currently in
+        a transitional phase where other methods access the section
+        list directly to do insertions of generated sections at
+        targeted points.
+        """
+        if section.kind == QAPIDoc.Kind.RETURNS:
+            if self._returns:
+                raise QAPISemError(
+                    section.info, "duplicated '%s' section" % section.kind)
+            self._returns = section
+        elif section.kind == QAPIDoc.Kind.ERRORS:
+            if self._errors:
+                raise QAPISemError(
+                    section.info, "duplicated '%s' section" % section.kind)
+            self._errors = section
+        elif section.kind == QAPIDoc.Kind.SINCE:
+            if self.since:
+                raise QAPISemError(
+                    section.info, "duplicated '%s' section" % section.kind)
+            self.since = section
+        self.all_sections.append(section)
+
     def end(self) -> None:
         for section in self.all_sections:
             section.text = section.text.strip('\n')
@@ -797,7 +824,7 @@ def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
 
         # start new section
         section = self.Section(info, kind)
-        self.all_sections.append(section)
+        self._append(section)
 
     def new_tagged_section(
         self,
@@ -805,22 +832,7 @@ def new_tagged_section(
         kind: 'QAPIDoc.Kind',
     ) -> None:
         section = self.Section(info, kind)
-        if kind == QAPIDoc.Kind.RETURNS:
-            if self._returns:
-                raise QAPISemError(
-                    info, "duplicated '%s' section" % kind)
-            self._returns = section
-        elif kind == QAPIDoc.Kind.ERRORS:
-            if self._errors:
-                raise QAPISemError(
-                    info, "duplicated '%s' section" % kind)
-            self._errors = section
-        elif kind == QAPIDoc.Kind.SINCE:
-            if self.since:
-                raise QAPISemError(
-                    info, "duplicated '%s' section" % kind)
-            self.since = section
-        self.all_sections.append(section)
+        self._append(section)
 
     def _new_description(
         self,
@@ -834,7 +846,7 @@ def _new_description(
         if name in desc:
             raise QAPISemError(info, "'%s' parameter name duplicated" % name)
         section = self.ArgSection(info, kind, name)
-        self.all_sections.append(section)
+        self._append(section)
         desc[name] = section
 
     def new_argument(self, info: QAPISourceInfo, name: str) -> None:
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 11/14] qapi/parser: privatize "all_sections" field as "_sections"
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (9 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 10/14] qapi/parser: add _append() method John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-10-02  8:51   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 12/14] qapi/parser: add _active_section private member John Snow
                   ` (2 subsequent siblings)
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

In preparation for switching QAPIDoc to a more complex data structure
for keeping track of sections, convert the "all_sections" property to
a private property.

Rename it "_sections" to emphasize that there is not* some alternative
property that is somehow not all sections. This is what you get and
you're going to love it.

Implement the len() and iter() features for QAPIDoc as a replacement
for direct, public access to the all_sections property.

(*OK, currently there are some, but spoilers, they're going away in
the next commits.)

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/sphinx/qapidoc.py         |  7 ++++---
 scripts/qapi/parser.py         | 27 +++++++++++++++++----------
 tests/qapi-schema/test-qapi.py |  2 +-
 3 files changed, 22 insertions(+), 14 deletions(-)

diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index ae57056a393..fd666be131f 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -377,7 +377,7 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
                 )
             )
 
-        sections = ent.doc.all_sections if ent.doc else []
+        sections = list(ent.doc) if ent.doc else []
 
         # Add sections in source order:
         for section in sections:
@@ -416,8 +416,9 @@ def visit_module(self, path: str) -> None:
         self.ensure_blank_line()
 
     def visit_freeform(self, doc: QAPIDoc) -> None:
-        assert len(doc.all_sections) == 1, doc.all_sections
-        body = doc.all_sections[0]
+        sections = list(doc)
+        assert len(sections) == 1, sections
+        body = sections[0]
         self.add_lines(self.reformat_arobase(body.text), doc.info)
         self.ensure_blank_line()
 
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 0dca1acba4c..bdd0f1ec3ff 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -19,6 +19,7 @@
     TYPE_CHECKING,
     Any,
     Dict,
+    Iterator,
     List,
     Mapping,
     Match,
@@ -758,7 +759,7 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
         # 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] = []
+        self._sections: List[QAPIDoc.Section] = []
         # dicts mapping parameter/feature names to their description
         self._args: Dict[str, QAPIDoc.ArgSection] = {}
         self._features: Dict[str, QAPIDoc.ArgSection] = {}
@@ -797,10 +798,16 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
                 raise QAPISemError(
                     section.info, "duplicated '%s' section" % section.kind)
             self.since = section
-        self.all_sections.append(section)
+        self._sections.append(section)
+
+    def __iter__(self) -> Iterator['QAPIDoc.Section']:
+        return iter(self._sections)
+
+    def __len__(self) -> int:
+        return len(self._sections)
 
     def end(self) -> None:
-        for section in self.all_sections:
+        for section in self:
             section.text = section.text.strip('\n')
             if (
                     section.kind not in (
@@ -813,9 +820,9 @@ def end(self) -> None:
     def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
         kind = QAPIDoc.Kind.DETAILS
 
-        if self.all_sections and self.all_sections[-1].kind == kind:
+        if self._sections and self._sections[-1].kind == kind:
             # extend current section
-            section = self.all_sections[-1]
+            section = self._sections[-1]
             if not section.text:
                 # Section is empty so far; update info to start *here*.
                 section.info = info
@@ -856,7 +863,7 @@ def new_feature(self, info: QAPISourceInfo, name: str) -> None:
         self._new_description(info, name, QAPIDoc.Kind.FEATURE, self._features)
 
     def append_line(self, line: str) -> None:
-        self.all_sections[-1].append_line(line)
+        self._sections[-1].append_line(line)
 
     def _insert_near_kind(
         self,
@@ -865,12 +872,12 @@ def _insert_near_kind(
         after: bool = False,
     ) -> bool:
         """Insert or append a new doc section at a specific point."""
-        for idx, sect in enumerate(reversed(self.all_sections)):
+        for idx, sect in enumerate(reversed(list(self))):
             if sect.kind == kind:
-                pos = len(self.all_sections) - idx - 1
+                pos = len(self) - idx - 1
                 if after:
                     pos += 1
-                self.all_sections.insert(pos, new_sect)
+                self._sections.insert(pos, new_sect)
                 return True
         return False
 
@@ -915,7 +922,7 @@ def ensure_returns(self, info: QAPISourceInfo) -> None:
         # TODO: Take advantage of the new strict section ordering and
         # simplify this algorithm.
 
-        if any(s.kind == QAPIDoc.Kind.RETURNS for s in self.all_sections):
+        if self._returns:
             return
 
         # Stub "Returns" section for undocumented returns value
diff --git a/tests/qapi-schema/test-qapi.py b/tests/qapi-schema/test-qapi.py
index 5beb96e894f..d5b4ac3a682 100755
--- a/tests/qapi-schema/test-qapi.py
+++ b/tests/qapi-schema/test-qapi.py
@@ -117,7 +117,7 @@ def test_frontend(fname):
             print('doc symbol=%s' % doc.symbol)
         else:
             print('doc freeform')
-        for section in doc.all_sections:
+        for section in doc:
             if isinstance(section, QAPIDoc.ArgSection):
                 print('    %s=%s' % (section.kind, section.name))
             else:
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 12/14] qapi/parser: add _active_section private member
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (10 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 11/14] qapi/parser: privatize "all_sections" field as "_sections" John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-09-28 22:36 ` [PATCH 13/14] qapi/parser: add new _sections mapping John Snow
  2026-09-28 22:36 ` [PATCH 14/14] qapi/parser: convert single-section properties to getter properties John Snow
  13 siblings, 0 replies; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

Instead of peeking inside the section list and always pulling the -1th
item, keep track specifically of the last added section. This is
because as we allow generated documentation to be inserted into
arbitrary positions, the last item in the list will not always
actually be the active section under consideration.

(Also, the simple list representation is going away, and accessing the
-1th item won't actually work anymore.)

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index bdd0f1ec3ff..894b8979b9e 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -760,6 +760,10 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
         self.symbol: Optional[str] = symbol
         # the sections in textual order
         self._sections: List[QAPIDoc.Section] = []
+
+        # The most recently appended section, on which append_line operates
+        self._active_section: Optional[QAPIDoc.Section] = None
+
         # dicts mapping parameter/feature names to their description
         self._args: Dict[str, QAPIDoc.ArgSection] = {}
         self._features: Dict[str, QAPIDoc.ArgSection] = {}
@@ -799,6 +803,7 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
                     section.info, "duplicated '%s' section" % section.kind)
             self.since = section
         self._sections.append(section)
+        self._active_section = section
 
     def __iter__(self) -> Iterator['QAPIDoc.Section']:
         return iter(self._sections)
@@ -820,13 +825,12 @@ def end(self) -> None:
     def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
         kind = QAPIDoc.Kind.DETAILS
 
-        if self._sections and self._sections[-1].kind == kind:
+        if self._active_section and self._active_section.kind == kind:
             # extend current section
-            section = self._sections[-1]
-            if not section.text:
+            if not self._active_section.text:
                 # Section is empty so far; update info to start *here*.
-                section.info = info
-            section.text += '\n'
+                self._active_section.info = info
+            self._active_section.text += '\n'
             return
 
         # start new section
@@ -863,7 +867,8 @@ def new_feature(self, info: QAPISourceInfo, name: str) -> None:
         self._new_description(info, name, QAPIDoc.Kind.FEATURE, self._features)
 
     def append_line(self, line: str) -> None:
-        self._sections[-1].append_line(line)
+        assert self._active_section is not None
+        self._active_section.append_line(line)
 
     def _insert_near_kind(
         self,
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 13/14] qapi/parser: add new _sections mapping
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (11 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 12/14] qapi/parser: add _active_section private member John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-10-02  7:24   ` Markus Armbruster
  2026-09-28 22:36 ` [PATCH 14/14] qapi/parser: convert single-section properties to getter properties John Snow
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

Modify the _sections field to be a mapping between section kinds and a
list of sections instead of a simple, flat list.

This makes it trivial to insert new auto-generated sections at the
correct position in the collection instead of needing to iterate
carefully over the list every time.

As a result, a lot of helpers we had to help us manage these
insertions are now unnecessary and can be deleted with extreme
prejudice.

NOTE: The remaining fishy use of a member insertion by the qapidoc
generator still remains, though it uses a simpler code pathway
now. The addition of the actual inliner mechanism will be the final
piece to remove this hack.

NOTE 2: Because "TODO:" sections are not strictly ordered in source,
this new section collection mechanism just lumps them in at the
end. For our generated documentation, this is functionally identical
as we do not render those sections anyway, but there is a noticeable
impact on our unit tests which *do* render TODO sections.

NOTE 3: This patch is what makes it obvious as to why we need
_append(), len(), and iter() implementations.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/sphinx/qapidoc.py         |  6 +--
 scripts/qapi/parser.py         | 74 ++++++++--------------------------
 tests/qapi-schema/doc-good.out |  4 +-
 3 files changed, 20 insertions(+), 64 deletions(-)

diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index fd666be131f..41acb560628 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -371,11 +371,7 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
         # other users of the QAPISchema or QAPIDoc objects outside of
         # the document generator. Fishy, but not harmful.
         if ent.doc:
-            ent.doc.append_member_stub(
-                QAPIDoc.ArgSection(
-                    ent.doc.info, QAPIDoc.Kind.MEMBER, "q_dummy"
-                )
-            )
+            ent.doc.new_argument(ent.doc.info, "q_dummy")
 
         sections = list(ent.doc) if ent.doc else []
 
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 894b8979b9e..1d8f1d39ab1 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -758,8 +758,13 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
         self.info = info
         # definition doc's symbol, None for free-form doc
         self.symbol: Optional[str] = symbol
-        # the sections in textual order
-        self._sections: List[QAPIDoc.Section] = []
+
+        # all sections arranged by kind. This is identical to source
+        # order, with the exception of TODO sections and any generated
+        # sections that are appended post-hoc.
+        self._sections: Dict[QAPIDoc.Kind, List[QAPIDoc.Section]] = {
+            kind: [] for kind in QAPIDoc.Kind
+        }
 
         # The most recently appended section, on which append_line operates
         self._active_section: Optional[QAPIDoc.Section] = None
@@ -767,6 +772,7 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
         # dicts mapping parameter/feature names to their description
         self._args: Dict[str, QAPIDoc.ArgSection] = {}
         self._features: Dict[str, QAPIDoc.ArgSection] = {}
+
         # a command's "Returns" and "Errors" section
         self._returns: Optional[QAPIDoc.Section] = None
         self._errors: Optional[QAPIDoc.Section] = None
@@ -781,11 +787,8 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
         """
         Primary internal interface for adding a new section.
 
-        It is intended to become the sole access point for adding new
-        sections to the QAPIDoc section list, but we are currently in
-        a transitional phase where other methods access the section
-        list directly to do insertions of generated sections at
-        targeted points.
+        All other methods/helpers/etc ultimately use this and only
+        this interface for appending a new section.
         """
         if section.kind == QAPIDoc.Kind.RETURNS:
             if self._returns:
@@ -802,14 +805,15 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
                 raise QAPISemError(
                     section.info, "duplicated '%s' section" % section.kind)
             self.since = section
-        self._sections.append(section)
+        self._sections[section.kind].append(section)
         self._active_section = section
 
     def __iter__(self) -> Iterator['QAPIDoc.Section']:
-        return iter(self._sections)
+        for values in self._sections.values():
+            yield from values
 
     def __len__(self) -> int:
-        return len(self._sections)
+        return sum(len(x) for x in self._sections.values())
 
     def end(self) -> None:
         for section in self:
@@ -870,32 +874,6 @@ def append_line(self, line: str) -> None:
         assert self._active_section is not None
         self._active_section.append_line(line)
 
-    def _insert_near_kind(
-        self,
-        kind: 'QAPIDoc.Kind',
-        new_sect: 'QAPIDoc.Section',
-        after: bool = False,
-    ) -> bool:
-        """Insert or append a new doc section at a specific point."""
-        for idx, sect in enumerate(reversed(list(self))):
-            if sect.kind == kind:
-                pos = len(self) - idx - 1
-                if after:
-                    pos += 1
-                self._sections.insert(pos, new_sect)
-                return True
-        return False
-
-    def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
-        """
-        Append a stub section after any Member sections.
-        """
-        if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
-            return
-
-        # No MEMBER sections present. Insert after INTRO section.
-        self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)
-
     def connect_member(self, member: 'QAPISchemaMember') -> None:
         if member.name not in self._args:
             assert member.info
@@ -908,7 +886,7 @@ def connect_member(self, member: 'QAPISchemaMember') -> None:
             stub_section = QAPIDoc.ArgSection(
                 self.info, QAPIDoc.Kind.MEMBER, member.name)
             self._args[member.name] = stub_section
-            self.append_member_stub(stub_section)
+            self._append(stub_section)
 
         self._args[member.name].connect(member)
 
@@ -920,30 +898,12 @@ def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
         self._features[feature.name].connect(feature)
 
     def ensure_returns(self, info: QAPISourceInfo) -> None:
-        # This is more complicated than it ought to be.  The doc
-        # parser should already know where a generated RETURNS section
-        # should go.  It currently doesn't.
-        #
-        # TODO: Take advantage of the new strict section ordering and
-        # simplify this algorithm.
-
-        if self._returns:
+        if self._sections[QAPIDoc.Kind.RETURNS]:
             return
 
         # Stub "Returns" section for undocumented returns value
         stub = QAPIDoc.Section(info, QAPIDoc.Kind.RETURNS)
-
-        if any(self._insert_near_kind(kind, stub, after) for kind, after in (
-                # 1. If arguments, right after those.
-                (QAPIDoc.Kind.MEMBER, True),
-                # 2. Elif errors, right *before* those.
-                (QAPIDoc.Kind.ERRORS, False),
-                # 3. Elif features, right *before* those.
-                (QAPIDoc.Kind.FEATURE, False),
-                # 4. Else, right *after* the intro.
-                (QAPIDoc.Kind.INTRO, True),
-        )):
-            return
+        self._append(stub)
 
     def check_expr(self, expr: QAPIExpression) -> None:
         if 'command' in expr:
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
index 7a485e46037..4a0fae6fce2 100644
--- a/tests/qapi-schema/doc-good.out
+++ b/tests/qapi-schema/doc-good.out
@@ -181,8 +181,6 @@ a feature
 another feature
     Details
 .. note:: @arg3 is undocumented
-    Todo
-frobnicate
     Details
 .. admonition:: Notes
 
@@ -207,6 +205,8 @@ Examples::
 
 Note::
     Ceci n'est pas une note
+    Todo
+frobnicate
     Since
 2.10
 doc symbol=cmd-boxed
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* [PATCH 14/14] qapi/parser: convert single-section properties to getter properties
  2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
                   ` (12 preceding siblings ...)
  2026-09-28 22:36 ` [PATCH 13/14] qapi/parser: add new _sections mapping John Snow
@ 2026-09-28 22:36 ` John Snow
  2026-10-02  9:44   ` Markus Armbruster
  13 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-28 22:36 UTC (permalink / raw)
  To: qemu-devel
  Cc: Peter Xu, Daniel P. Berrangé, Stefan Berger, Fabiano Rosas,
	Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf, Ani Sinha,
	Pierrick Bouvier, Jiri Pirko, Alex Williamson, Markus Armbruster,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, John Snow, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

Remove dedicated pointers to single sections, instead preferring a
functional getter property that returns the same thing dynamically.

This is being done mainly for the benefit of being able to condense
the _append() function, but I'm not convinced it's worth it. I'm not
convinced it's not not worth it either, though.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 47 +++++++++++++++++++++++-------------------
 1 file changed, 26 insertions(+), 21 deletions(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 1d8f1d39ab1..d4b5b3e5ab6 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -773,12 +773,6 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
         self._args: Dict[str, QAPIDoc.ArgSection] = {}
         self._features: Dict[str, QAPIDoc.ArgSection] = {}
 
-        # a command's "Returns" and "Errors" section
-        self._returns: Optional[QAPIDoc.Section] = None
-        self._errors: Optional[QAPIDoc.Section] = None
-        # "Since" section
-        self.since: Optional[QAPIDoc.Section] = None
-
     @property
     def has_features(self) -> bool:
         return bool(self._features)
@@ -790,21 +784,10 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
         All other methods/helpers/etc ultimately use this and only
         this interface for appending a new section.
         """
-        if section.kind == QAPIDoc.Kind.RETURNS:
-            if self._returns:
-                raise QAPISemError(
-                    section.info, "duplicated '%s' section" % section.kind)
-            self._returns = section
-        elif section.kind == QAPIDoc.Kind.ERRORS:
-            if self._errors:
-                raise QAPISemError(
-                    section.info, "duplicated '%s' section" % section.kind)
-            self._errors = section
-        elif section.kind == QAPIDoc.Kind.SINCE:
-            if self.since:
-                raise QAPISemError(
-                    section.info, "duplicated '%s' section" % section.kind)
-            self.since = section
+        if str(section.kind) in ('Intro', 'Returns', 'Errors', 'Since'):
+            if self._sections[section.kind]:
+                raise QAPISemError(section.info,
+                                   "duplicated '%s' section" % section.kind)
         self._sections[section.kind].append(section)
         self._active_section = section
 
@@ -815,6 +798,28 @@ def __iter__(self) -> Iterator['QAPIDoc.Section']:
     def __len__(self) -> int:
         return sum(len(x) for x in self._sections.values())
 
+    def _single_section_property(
+        self,
+        kind: 'QAPIDoc.Kind'
+    ) -> Optional['QAPIDoc.Section']:
+        try:
+            return self._sections[kind][0]
+        except IndexError:
+            pass
+        return None
+
+    @property
+    def _returns(self) -> Optional['QAPIDoc.Section']:
+        return self._single_section_property(QAPIDoc.Kind.RETURNS)
+
+    @property
+    def _errors(self) -> Optional['QAPIDoc.Section']:
+        return self._single_section_property(QAPIDoc.Kind.ERRORS)
+
+    @property
+    def since(self) -> Optional['QAPIDoc.Section']:
+        return self._single_section_property(QAPIDoc.Kind.SINCE)
+
     def end(self) -> None:
         for section in self:
             section.text = section.text.strip('\n')
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 35+ messages in thread

* Re: [PATCH 01/14] qapi: rename "plain" sections to "details"
  2026-09-28 22:36 ` [PATCH 01/14] qapi: rename "plain" sections to "details" John Snow
@ 2026-09-29 11:49   ` Markus Armbruster
  2026-09-29 14:36     ` John Snow
  0 siblings, 1 reply; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 11:49 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> With this change, we no longer have "plaintext" sections at all: they
> are all now either "Intro" or "Details". This patch does not enforce
> where those details sections must occur; that change occurs in a later
> patch.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  docs/sphinx/qapidoc.py         |  2 +-
>  scripts/qapi/parser.py         | 18 +++++++++++-------
>  tests/qapi-schema/doc-good.out | 18 +++++++++---------
>  3 files changed, 21 insertions(+), 17 deletions(-)
>
> diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
> index 54a32e45f7e..ae57056a393 100644
> --- a/docs/sphinx/qapidoc.py
> +++ b/docs/sphinx/qapidoc.py
> @@ -383,7 +383,7 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
>          for section in sections:
>              section.text = self.reformat_arobase(section.text)
>  
> -            if section.kind.name in ("PLAIN", "INTRO"):
> +            if section.kind.name in ("INTRO", "DETAILS"):
>                  self.visit_plaintext(section)
>              elif section.kind == QAPIDoc.Kind.MEMBER:
>                  assert isinstance(section, QAPIDoc.ArgSection)
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index dcc04b2663e..fe284a3f866 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -674,7 +674,7 @@ class QAPIDoc:
>      """
>  
>      class Kind(enum.Enum):
> -        PLAIN = 0
> +        DETAILS = 0
>          MEMBER = 1
>          FEATURE = 2
>          RETURNS = 3

I'd like the order of enum values to match the order of sections
eventually.

> @@ -747,12 +747,16 @@ def has_features(self) -> bool:
>      def end(self) -> None:
>          for section in self.all_sections:
>              section.text = section.text.strip('\n')
> -            if not (section.kind.name in ("INTRO", "PLAIN") or section.text):
> +            if (
> +                    section.kind not in (
> +                        QAPIDoc.Kind.INTRO, QAPIDoc.Kind.DETAILS
> +                    ) and section.text == ''
> +            ):

This isn't just rename.

The old conditional is a bit "uh, why bother with enums if we do strings
anyway".  The conditional is ugly.

Are empty DETAILS even possible?  Got a reproducer?

If not, you could use

               if section.text == '' and section.kind != QAPIDoc.Kind.INTRO:

>                  raise QAPISemError(
>                      section.info, "text required after '%s:'" % section.kind)
>  
>      def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
> -        kind = QAPIDoc.Kind.PLAIN
> +        kind = QAPIDoc.Kind.DETAILS
>  
>          if self.all_sections and self.all_sections[-1].kind == kind:
>              # extend current section
> @@ -837,8 +841,8 @@ def _insert_after_intro(
>          """
>          Insert a section immediately after the intro section.
>  
> -        While we convert PLAIN sections to INTRO sections, all
> -        contiguous INTRO/PLAIN sections at the start of a QAPIDoc
> +        While we convert DETAILS sections to INTRO sections, all
> +        contiguous INTRO/DETAILS sections at the start of a QAPIDoc
>          section list are treated as "the intro".
>  
>          Once INTRO conversion is complete, this helper will no longer be

This goes away in PATCH 9.  I'd be tempted to delay the rename until
after that.  Up to you.

> @@ -855,7 +859,7 @@ def _insert_after_intro(
>  
>          index = 0
>          for index, ref_section in enumerate(self.all_sections):
> -            if ref_section.kind.name in ("PLAIN", "INTRO"):
> +            if ref_section.kind.name in ("INTRO", "DETAILS"):
>                  continue
>              break
>          else:
> @@ -871,7 +875,7 @@ def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
>          if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
>              return
>  
> -        # No MEMBER sections present. Insert after INTRO/PLAIN sections.
> +        # No MEMBER sections present. Insert after INTRO/DETAILS sections.
>          self._insert_after_intro(stub)
>  
>      def connect_member(self, member: 'QAPISchemaMember') -> None:
> diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
> index eaaf119dea8..1d4fbd3762a 100644

[...]



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 03/14] qapi: re-order QAPI doc block sections
  2026-09-28 22:36 ` [PATCH 03/14] qapi: re-order QAPI doc block sections John Snow
@ 2026-09-29 12:10   ` Markus Armbruster
  2026-09-29 14:32     ` John Snow
  0 siblings, 1 reply; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 12:10 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

What's a "block section"?

John Snow <jsnow@redhat.com> writes:

> This patch is focused on making sure that "Returns" appears before
> "Errors", and "Errors" appears before "Features".

Aha.  Suggest

  qapi: Move Returns:, Errors: and Features: into this order

>
> Signed-off-by: John Snow <jsnow@redhat.com>

Patch looks good to me.



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 04/14] qapi/parser: enforce doc block section ordering
  2026-09-28 22:36 ` [PATCH 04/14] qapi/parser: enforce doc block section ordering John Snow
@ 2026-09-29 12:36   ` Markus Armbruster
  2026-09-29 14:31     ` John Snow
  0 siblings, 1 reply; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 12:36 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> Enforce the relative ordering of "Intro", "Members", "Features",
> "Errors" and "Returns" sections.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  docs/devel/qapi-code-gen.rst | 19 +++++++++++------
>  scripts/qapi/parser.py       | 41 ++++++++++++++++++++++++++++++------
>  2 files changed, 48 insertions(+), 12 deletions(-)
>
> diff --git a/docs/devel/qapi-code-gen.rst b/docs/devel/qapi-code-gen.rst
> index 3ac1c2dae18..29e9c5a3da6 100644
> --- a/docs/devel/qapi-code-gen.rst
> +++ b/docs/devel/qapi-code-gen.rst
> @@ -985,12 +985,19 @@ When documentation is required (see pragma_ 'doc-required'), every
>  definition must have documentation.
>  
>  Definition documentation starts with a description naming the
> -definition with an optional indented overview (the "intro"),
> -:ref:`descriptions <doc-descrip>` of each argument (for commands and
> -events), member (for structs and unions), branch (for alternates), or
> -value (for enums), :ref:`feature descriptions <doc-feat>` of each
> -feature (if any), and finally optional :ref:`tagged sections
> -<doc-tagged>`.
> +definition with an optional indented overview (the "intro").
> +
> +The introduction is followed by :ref:`descriptions <doc-descrip>` of
> +each argument (for commands and events), member (for structs and
> +unions), branch (for alternates), or value (for enums).
> +
> +:ref:`Tagged sections <doc-tagged>` follow in order: Returns (for
> +commands), Errors (for commands), and lastly :ref:`feature
> +descriptions <doc-feat>` if applicable.

*Some* of the tagged sections do.

Does this document still need the concept "tagged section"?

> +
> +Definition documentation concludes with rST-formatted detail
> +paragraphs (including examples, notes, &c), and finally an optional
> +"Since" tagged section at the very end.
>  
>  .. _doc-descrip:
>  
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index fe284a3f866..94a60a23482 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -545,6 +545,28 @@ def get_doc(self) -> 'QAPIDoc':
>              doc.all_sections.append(QAPIDoc.Section(info, QAPIDoc.Kind.INTRO))
>              line = self.get_doc_indented(doc)
>              no_more_args = False
> +            last_ordered_section = QAPIDoc.Kind.INTRO
> +
> +            def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
> +                nonlocal last_ordered_section
> +                if isinstance(this, str):
> +                    this = QAPIDoc.Kind.from_string(this)

Simpler, I think: require a .Kind argument, and do the conversion from
string in the one caller that needs it.

> +
> +                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE,
> +                            QAPIDoc.Kind.DETAILS):
> +                    return
> +
> +                if this.value < last_ordered_section.value:
> +                    raise QAPIParseError(
> +                        self,
> +                        f"'{this}' section cannot appear after "
> +                        f"'{last_ordered_section}' section, please re-order "
> +                        "the sections and adjust phrasing as necessary to "
> +                        "ensure consistent documentation flow between the "
> +                        "source code and the rendered HTML manual"
> +                    )

That's a loooong error message.  It actually consists of the error
proper and advice on how to correct it.  Please use

    "<error>\n<advice>"

like we do in expr.py's check_keys(), parser.py's
._validate_doc_line_format(), and schema.py's ._def_definition().  Yes,
the embedded newline is a bit of a hack.

> +
> +                last_ordered_section = this
>  
>              while line is not None:
>                  # Blank lines
> @@ -558,6 +580,7 @@ def get_doc(self) -> 'QAPIDoc':
>                      if doc.has_features:
>                          raise QAPIParseError(
>                              self, "duplicated 'Features:' line")
> +                    _tag_check(QAPIDoc.Kind.FEATURE)
>                      self.accept(False)
>                      line = self.get_doc_line()
>                      while line == '':
> @@ -581,6 +604,7 @@ def get_doc(self) -> 'QAPIDoc':
>                              self,
>                              "description of '@%s:' follows a section"
>                              % match.group(1))
> +                    _tag_check(QAPIDoc.Kind.MEMBER)
>                      while (line is not None
>                             and (match := self._match_at_name_colon(line))):
>                          doc.new_argument(self.info, match.group(1))
> @@ -621,6 +645,7 @@ def get_doc(self) -> 'QAPIDoc':
>                          )
>                          raise QAPIParseError(self, emsg)
>  
> +                    _tag_check(match.group(1))

This is the caller that needs it.

>                      doc.new_tagged_section(
>                          self.info,
>                          QAPIDoc.Kind.from_string(match.group(1))
> @@ -632,6 +657,7 @@ def get_doc(self) -> 'QAPIDoc':
>                      no_more_args = True
>                  else:
>                      # plain paragraph(s)
> +                    _tag_check(QAPIDoc.Kind.DETAILS)
>                      doc.ensure_untagged_section(self.info)
>                      doc.append_line(line)
>                      line = self.get_doc_paragraph(doc)
> @@ -674,14 +700,17 @@ class QAPIDoc:
>      """
>  
>      class Kind(enum.Enum):
> -        DETAILS = 0
> +        # The order here is the order in which sections must appear in
> +        # source code; with the exception of 'TODO' which may appear
> +        # anywhere but is treated as a comment.
> +        INTRO = 0
>          MEMBER = 1
> -        FEATURE = 2
> -        RETURNS = 3
> -        ERRORS = 4
> -        SINCE = 5
> +        RETURNS = 2
> +        ERRORS = 3
> +        FEATURE = 4
> +        DETAILS = 5
>          TODO = 6
> -        INTRO = 7
> +        SINCE = 7
>  
>          @staticmethod
>          def from_string(kind: str) -> 'QAPIDoc.Kind':

Missing: negative test for the new error.



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 08/14] qapi/parser: enforce strict positioning for "Since:" section
  2026-09-28 22:36 ` [PATCH 08/14] qapi/parser: enforce strict positioning for "Since:" section John Snow
@ 2026-09-29 12:53   ` Markus Armbruster
  2026-09-29 14:25     ` John Snow
  0 siblings, 1 reply; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 12:53 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> This patch finalizes the total ordering of all QAPI documentation
> block sections.
>
> ... Except TODO, which can still be wherever.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  scripts/qapi/parser.py | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index 8b331aa0673..c14bff2cbc3 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -552,7 +552,7 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
>                  if isinstance(this, str):
>                      this = QAPIDoc.Kind.from_string(this)
>  
> -                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE):
> +                if this == QAPIDoc.Kind.TODO:
>                      return
>  
>                  if this.value < last_ordered_section.value:

The check for duplicate RETURNS, ERRORS, SINCE in new_tagged_section()
is now technically redundant.  Deleting it would make the error message
worse, though.  Do we care?



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 09/14] qapi/parser: remove _insert_after_intro() helper
  2026-09-28 22:36 ` [PATCH 09/14] qapi/parser: remove _insert_after_intro() helper John Snow
@ 2026-09-29 13:09   ` Markus Armbruster
  2026-09-29 14:19     ` John Snow
  0 siblings, 1 reply; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 13:09 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> I promised I'd remove this.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  scripts/qapi/parser.py | 49 ++++++------------------------------------
>  1 file changed, 7 insertions(+), 42 deletions(-)
>
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index c14bff2cbc3..5fc9c18c81d 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -862,49 +862,15 @@ def _insert_near_kind(
>                  return True
>          return False
>  
> -    def _insert_after_intro(
> -        self,
> -        section: 'QAPIDoc.Section',
> -    ) -> None:
> -        """
> -        Insert a section immediately after the intro section.
> -
> -        While we convert DETAILS sections to INTRO sections, all
> -        contiguous INTRO/DETAILS sections at the start of a QAPIDoc
> -        section list are treated as "the intro".
> -
> -        Once INTRO conversion is complete, this helper will no longer be
> -        needed and ``_insert_near_kind(QAPIDoc.Kind.INTRO, ...)`` will
> -        be sufficient.
> -        """
> -        first = self.all_sections[0]
> -        if first.text and first.kind == QAPIDoc.Kind.INTRO:
> -            # First section is introduction and is non-empty: insert here.
> -            # Rest assured all of this ugliness will very soon go away.
> -            # Pinkie-swear.
> -            self.all_sections.insert(1, section)
> -            return
> -
> -        index = 0
> -        for index, ref_section in enumerate(self.all_sections):
> -            if ref_section.kind.name in ("INTRO", "DETAILS"):
> -                continue
> -            break
> -        else:
> -            index += 1
> -
> -        self.all_sections.insert(index, section)
> -
>      def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
> -
>          """
>          Append a stub section after any Member sections.
>          """
>          if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
>              return
>  
> -        # No MEMBER sections present. Insert after INTRO/DETAILS sections.
> -        self._insert_after_intro(stub)
> +        # No MEMBER sections present. Insert after INTRO section.
> +        self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)

There can only be one INTRO section, and it must be first.  So this must
be equivalent to

           self.all_sections.(1, stub)

Correct?

The argument for using the more complex ._insert_near_kind() is of
course symmetry with the other case.  Okay.

We have three ._insert_near_kind() calls:

        if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):

        self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)

        if any(self._insert_near_kind(kind, stub, after) for kind, after in (
                [...]
        )):

None of them omit the optional last argument.  Two pass it positionally,
one as a keyword argument.  Let's dumb this down to mandatory argument.

>  
>      def connect_member(self, member: 'QAPISchemaMember') -> None:
>          if member.name not in self._args:
> @@ -932,10 +898,10 @@ def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
>      def ensure_returns(self, info: QAPISourceInfo) -> None:
>          # This is more complicated than it ought to be.  The doc
>          # parser should already know where a generated RETURNS section
> -        # should go.  It currently doesn't, mostly because it accepts
> -        # tagged sections in any order.
> +        # should go.  It currently doesn't.
>          #
> -        # TODO: Tighten doc syntax and simplify.
> +        # TODO: Take advantage of the new strict section ordering and
> +        # simplify this algorithm.
>  
>          if any(s.kind == QAPIDoc.Kind.RETURNS for s in self.all_sections):
>              return
> @@ -950,12 +916,11 @@ def ensure_returns(self, info: QAPISourceInfo) -> None:
>                  (QAPIDoc.Kind.ERRORS, False),
>                  # 3. Elif features, right *before* those.
>                  (QAPIDoc.Kind.FEATURE, False),
> +                # 4. Else, right *after* the intro.
> +                (QAPIDoc.Kind.INTRO, True),
>          )):
>              return
>  
> -        # Otherwise, it should go right after the intro.
> -        self._insert_after_intro(stub)
> -

We now have

           if any(call for ...)
               return
           [implicit return at end of function]

Please simplify to

           any(call for ...)

or maybe even

           for ...:
               if call:
                   break

>      def check_expr(self, expr: QAPIExpression) -> None:
>          if 'command' in expr:
>              if self._returns and 'returns' not in expr:



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 10/14] qapi/parser: add _append() method
  2026-09-28 22:36 ` [PATCH 10/14] qapi/parser: add _append() method John Snow
@ 2026-09-29 13:28   ` Markus Armbruster
  2026-09-29 14:14     ` John Snow
  0 siblings, 1 reply; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 13:28 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> This method is intended to become the sole point of appending new
> sections to the section list, prohibiting outside callers from
> accessing the list of sections directly.
>
> A forthcoming commit will actually convert the "all_sections" field to
> be a private field to begin enforcing that no outside callers are
> manipulating the section list from outside of this class.

This suggests you merely add the method now.  Not true, you also put it
to use.

>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  scripts/qapi/parser.py | 50 ++++++++++++++++++++++++++----------------
>  1 file changed, 31 insertions(+), 19 deletions(-)
>
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index 5fc9c18c81d..0dca1acba4c 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -542,7 +542,7 @@ def get_doc(self) -> 'QAPIDoc':
>              if not symbol:
>                  raise QAPIParseError(self, "name required after '@'")
>              doc = QAPIDoc(info, symbol)
> -            doc.all_sections.append(QAPIDoc.Section(info, QAPIDoc.Kind.INTRO))
> +            doc.new_tagged_section(info, QAPIDoc.Kind.INTRO)
>              line = self.get_doc_indented(doc)
>              no_more_args = False
>              last_ordered_section = QAPIDoc.Kind.INTRO
> @@ -772,6 +772,33 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
>      def has_features(self) -> bool:
>          return bool(self._features)
>  
> +    def _append(self, section: 'QAPIDoc.Section') -> None:

Maybe name it _append_section()?  We already have append_line()...

> +        """
> +        Primary internal interface for adding a new section.
> +
> +        It is intended to become the sole access point for adding new
> +        sections to the QAPIDoc section list, but we are currently in
> +        a transitional phase where other methods access the section
> +        list directly to do insertions of generated sections at
> +        targeted points.

Yes: ._insert_near_kind().

> +        """

This comment will soon change to

           """
           Primary internal interface for adding a new section.

           All other methods/helpers/etc ultimately use this and only
           this interface for appending a new section.
           """

There's a bit of redundancy.  Suggest

           """
           Add a new section at the end.

           It is intended to ...
           """

now, and

           """
           Add a new section at the end.

           All other ...
           """

later.

> +        if section.kind == QAPIDoc.Kind.RETURNS:
> +            if self._returns:
> +                raise QAPISemError(
> +                    section.info, "duplicated '%s' section" % section.kind)
> +            self._returns = section
> +        elif section.kind == QAPIDoc.Kind.ERRORS:
> +            if self._errors:
> +                raise QAPISemError(
> +                    section.info, "duplicated '%s' section" % section.kind)
> +            self._errors = section
> +        elif section.kind == QAPIDoc.Kind.SINCE:
> +            if self.since:
> +                raise QAPISemError(
> +                    section.info, "duplicated '%s' section" % section.kind)
> +            self.since = section
> +        self.all_sections.append(section)
> +
>      def end(self) -> None:
>          for section in self.all_sections:
>              section.text = section.text.strip('\n')
> @@ -797,7 +824,7 @@ def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
>  
>          # start new section
>          section = self.Section(info, kind)
> -        self.all_sections.append(section)
> +        self._append(section)
>  
>      def new_tagged_section(
>          self,
> @@ -805,22 +832,7 @@ def new_tagged_section(
>          kind: 'QAPIDoc.Kind',
>      ) -> None:
>          section = self.Section(info, kind)
> -        if kind == QAPIDoc.Kind.RETURNS:
> -            if self._returns:
> -                raise QAPISemError(
> -                    info, "duplicated '%s' section" % kind)
> -            self._returns = section
> -        elif kind == QAPIDoc.Kind.ERRORS:
> -            if self._errors:
> -                raise QAPISemError(
> -                    info, "duplicated '%s' section" % kind)
> -            self._errors = section
> -        elif kind == QAPIDoc.Kind.SINCE:
> -            if self.since:
> -                raise QAPISemError(
> -                    info, "duplicated '%s' section" % kind)
> -            self.since = section
> -        self.all_sections.append(section)
> +        self._append(section)
>  
>      def _new_description(
>          self,
> @@ -834,7 +846,7 @@ def _new_description(
>          if name in desc:
>              raise QAPISemError(info, "'%s' parameter name duplicated" % name)
>          section = self.ArgSection(info, kind, name)
> -        self.all_sections.append(section)
> +        self._append(section)
>          desc[name] = section
>  
>      def new_argument(self, info: QAPISourceInfo, name: str) -> None:



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 10/14] qapi/parser: add _append() method
  2026-09-29 13:28   ` Markus Armbruster
@ 2026-09-29 14:14     ` John Snow
  2026-09-29 15:09       ` Markus Armbruster
  0 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-29 14:14 UTC (permalink / raw)
  To: Markus Armbruster
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

[-- Attachment #1: Type: text/plain, Size: 6173 bytes --]

On Tue, Sep 29, 2026, 9:28 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > This method is intended to become the sole point of appending new
> > sections to the section list, prohibiting outside callers from
> > accessing the list of sections directly.
> >
> > A forthcoming commit will actually convert the "all_sections" field to
> > be a private field to begin enforcing that no outside callers are
> > manipulating the section list from outside of this class.
>
> This suggests you merely add the method now.  Not true, you also put it
> to use.
>

Didn't mean to suggest such a thing! I'll try to reword.

I mean only to convey that at this point, this doesn't *quite* become the
sole access point as intended, due to some transitional code that this
series is on its way to remove.

I'm trying to justify the partial conversion.


> >
> > Signed-off-by: John Snow <jsnow@redhat.com>
> > ---
> >  scripts/qapi/parser.py | 50 ++++++++++++++++++++++++++----------------
> >  1 file changed, 31 insertions(+), 19 deletions(-)
> >
> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> > index 5fc9c18c81d..0dca1acba4c 100644
> > --- a/scripts/qapi/parser.py
> > +++ b/scripts/qapi/parser.py
> > @@ -542,7 +542,7 @@ def get_doc(self) -> 'QAPIDoc':
> >              if not symbol:
> >                  raise QAPIParseError(self, "name required after '@'")
> >              doc = QAPIDoc(info, symbol)
> > -            doc.all_sections.append(QAPIDoc.Section(info,
> QAPIDoc.Kind.INTRO))
> > +            doc.new_tagged_section(info, QAPIDoc.Kind.INTRO)
> >              line = self.get_doc_indented(doc)
> >              no_more_args = False
> >              last_ordered_section = QAPIDoc.Kind.INTRO
> > @@ -772,6 +772,33 @@ def __init__(self, info: QAPISourceInfo, symbol:
> Optional[str] = None):
> >      def has_features(self) -> bool:
> >          return bool(self._features)
> >
> > +    def _append(self, section: 'QAPIDoc.Section') -> None:
>
> Maybe name it _append_section()?  We already have append_line()...
>

Eh, why not, sure.


> > +        """
> > +        Primary internal interface for adding a new section.
> > +
> > +        It is intended to become the sole access point for adding new
> > +        sections to the QAPIDoc section list, but we are currently in
> > +        a transitional phase where other methods access the section
> > +        list directly to do insertions of generated sections at
> > +        targeted points.
>
> Yes: ._insert_near_kind().
>
> > +        """
>
> This comment will soon change to
>
>            """
>            Primary internal interface for adding a new section.
>
>            All other methods/helpers/etc ultimately use this and only
>            this interface for appending a new section.
>            """
>
> There's a bit of redundancy.  Suggest
>
>            """
>            Add a new section at the end.
>
>            It is intended to ...
>            """
>
> now, and
>
>            """
>            Add a new section at the end.
>
>            All other ...
>            """
>
> later.
>

More or less correct, though "at the end" isn't really true, it's
contextually sensitive.

"Add a new section" is more correct.

Actually, why don't we name it _add_section() to make it clear that it
isn't *always* an append?


> > +        if section.kind == QAPIDoc.Kind.RETURNS:
> > +            if self._returns:
> > +                raise QAPISemError(
> > +                    section.info, "duplicated '%s' section" %
> section.kind)
> > +            self._returns = section
> > +        elif section.kind == QAPIDoc.Kind.ERRORS:
> > +            if self._errors:
> > +                raise QAPISemError(
> > +                    section.info, "duplicated '%s' section" %
> section.kind)
> > +            self._errors = section
> > +        elif section.kind == QAPIDoc.Kind.SINCE:
> > +            if self.since:
> > +                raise QAPISemError(
> > +                    section.info, "duplicated '%s' section" %
> section.kind)
> > +            self.since = section
> > +        self.all_sections.append(section)
> > +
> >      def end(self) -> None:
> >          for section in self.all_sections:
> >              section.text = section.text.strip('\n')
> > @@ -797,7 +824,7 @@ def ensure_untagged_section(self, info:
> QAPISourceInfo) -> None:
> >
> >          # start new section
> >          section = self.Section(info, kind)
> > -        self.all_sections.append(section)
> > +        self._append(section)
> >
> >      def new_tagged_section(
> >          self,
> > @@ -805,22 +832,7 @@ def new_tagged_section(
> >          kind: 'QAPIDoc.Kind',
> >      ) -> None:
> >          section = self.Section(info, kind)
> > -        if kind == QAPIDoc.Kind.RETURNS:
> > -            if self._returns:
> > -                raise QAPISemError(
> > -                    info, "duplicated '%s' section" % kind)
> > -            self._returns = section
> > -        elif kind == QAPIDoc.Kind.ERRORS:
> > -            if self._errors:
> > -                raise QAPISemError(
> > -                    info, "duplicated '%s' section" % kind)
> > -            self._errors = section
> > -        elif kind == QAPIDoc.Kind.SINCE:
> > -            if self.since:
> > -                raise QAPISemError(
> > -                    info, "duplicated '%s' section" % kind)
> > -            self.since = section
> > -        self.all_sections.append(section)
> > +        self._append(section)
> >
> >      def _new_description(
> >          self,
> > @@ -834,7 +846,7 @@ def _new_description(
> >          if name in desc:
> >              raise QAPISemError(info, "'%s' parameter name duplicated" %
> name)
> >          section = self.ArgSection(info, kind, name)
> > -        self.all_sections.append(section)
> > +        self._append(section)
> >          desc[name] = section
> >
> >      def new_argument(self, info: QAPISourceInfo, name: str) -> None:
>
>

[-- Attachment #2: Type: text/html, Size: 9276 bytes --]

^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 09/14] qapi/parser: remove _insert_after_intro() helper
  2026-09-29 13:09   ` Markus Armbruster
@ 2026-09-29 14:19     ` John Snow
  2026-09-29 15:08       ` Markus Armbruster
  0 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-29 14:19 UTC (permalink / raw)
  To: Markus Armbruster
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

[-- Attachment #1: Type: text/plain, Size: 5602 bytes --]

On Tue, Sep 29, 2026, 9:09 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > I promised I'd remove this.
> >
> > Signed-off-by: John Snow <jsnow@redhat.com>
> > ---
> >  scripts/qapi/parser.py | 49 ++++++------------------------------------
> >  1 file changed, 7 insertions(+), 42 deletions(-)
> >
> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> > index c14bff2cbc3..5fc9c18c81d 100644
> > --- a/scripts/qapi/parser.py
> > +++ b/scripts/qapi/parser.py
> > @@ -862,49 +862,15 @@ def _insert_near_kind(
> >                  return True
> >          return False
> >
> > -    def _insert_after_intro(
> > -        self,
> > -        section: 'QAPIDoc.Section',
> > -    ) -> None:
> > -        """
> > -        Insert a section immediately after the intro section.
> > -
> > -        While we convert DETAILS sections to INTRO sections, all
> > -        contiguous INTRO/DETAILS sections at the start of a QAPIDoc
> > -        section list are treated as "the intro".
> > -
> > -        Once INTRO conversion is complete, this helper will no longer be
> > -        needed and ``_insert_near_kind(QAPIDoc.Kind.INTRO, ...)`` will
> > -        be sufficient.
> > -        """
> > -        first = self.all_sections[0]
> > -        if first.text and first.kind == QAPIDoc.Kind.INTRO:
> > -            # First section is introduction and is non-empty: insert
> here.
> > -            # Rest assured all of this ugliness will very soon go away.
> > -            # Pinkie-swear.
> > -            self.all_sections.insert(1, section)
> > -            return
> > -
> > -        index = 0
> > -        for index, ref_section in enumerate(self.all_sections):
> > -            if ref_section.kind.name in ("INTRO", "DETAILS"):
> > -                continue
> > -            break
> > -        else:
> > -            index += 1
> > -
> > -        self.all_sections.insert(index, section)
> > -
> >      def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
> > -
> >          """
> >          Append a stub section after any Member sections.
> >          """
> >          if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
> >              return
> >
> > -        # No MEMBER sections present. Insert after INTRO/DETAILS
> sections.
> > -        self._insert_after_intro(stub)
> > +        # No MEMBER sections present. Insert after INTRO section.
> > +        self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)
>
> There can only be one INTRO section, and it must be first.  So this must
> be equivalent to
>
>            self.all_sections.(1, stub)
>
> Correct?
>
> The argument for using the more complex ._insert_near_kind() is of
> course symmetry with the other case.  Okay.
>

Yep, largely symmetry and opting to not write new logic as much as I can
get away with in this patch. There's a compelling reason for not wasting
much time writing new, cleaner logic...


> We have three ._insert_near_kind() calls:
>
>         if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
>
>         self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)
>
>         if any(self._insert_near_kind(kind, stub, after) for kind, after
> in (
>                 [...]
>         )):
>
> None of them omit the optional last argument.  Two pass it positionally,
> one as a keyword argument.  Let's dumb this down to mandatory argument.
>

I'll do you one better, that entire method goes away by the end of this
series and we don't have to worry about it.

No sense putting lipstick on the pig enroute to the slaughterhouse.


> >
> >      def connect_member(self, member: 'QAPISchemaMember') -> None:
> >          if member.name not in self._args:
> > @@ -932,10 +898,10 @@ def connect_feature(self, feature:
> 'QAPISchemaFeature') -> None:
> >      def ensure_returns(self, info: QAPISourceInfo) -> None:
> >          # This is more complicated than it ought to be.  The doc
> >          # parser should already know where a generated RETURNS section
> > -        # should go.  It currently doesn't, mostly because it accepts
> > -        # tagged sections in any order.
> > +        # should go.  It currently doesn't.
> >          #
> > -        # TODO: Tighten doc syntax and simplify.
> > +        # TODO: Take advantage of the new strict section ordering and
> > +        # simplify this algorithm.
> >
> >          if any(s.kind == QAPIDoc.Kind.RETURNS for s in
> self.all_sections):
> >              return
> > @@ -950,12 +916,11 @@ def ensure_returns(self, info: QAPISourceInfo) ->
> None:
> >                  (QAPIDoc.Kind.ERRORS, False),
> >                  # 3. Elif features, right *before* those.
> >                  (QAPIDoc.Kind.FEATURE, False),
> > +                # 4. Else, right *after* the intro.
> > +                (QAPIDoc.Kind.INTRO, True),
> >          )):
> >              return
> >
> > -        # Otherwise, it should go right after the intro.
> > -        self._insert_after_intro(stub)
> > -
>
> We now have
>
>            if any(call for ...)
>                return
>            [implicit return at end of function]
>
> Please simplify to
>
>            any(call for ...)
>
> or maybe even
>
>            for ...:
>                if call:
>                    break
>
> >      def check_expr(self, expr: QAPIExpression) -> None:
> >          if 'command' in expr:
> >              if self._returns and 'returns' not in expr:
>
>

[-- Attachment #2: Type: text/html, Size: 7934 bytes --]

^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 08/14] qapi/parser: enforce strict positioning for "Since:" section
  2026-09-29 12:53   ` Markus Armbruster
@ 2026-09-29 14:25     ` John Snow
  2026-09-29 15:04       ` Markus Armbruster
  0 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-29 14:25 UTC (permalink / raw)
  To: Markus Armbruster
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

[-- Attachment #1: Type: text/plain, Size: 1499 bytes --]

On Tue, Sep 29, 2026, 8:53 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > This patch finalizes the total ordering of all QAPI documentation
> > block sections.
> >
> > ... Except TODO, which can still be wherever.
> >
> > Signed-off-by: John Snow <jsnow@redhat.com>
> > ---
> >  scripts/qapi/parser.py | 2 +-
> >  1 file changed, 1 insertion(+), 1 deletion(-)
> >
> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> > index 8b331aa0673..c14bff2cbc3 100644
> > --- a/scripts/qapi/parser.py
> > +++ b/scripts/qapi/parser.py
> > @@ -552,7 +552,7 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) ->
> None:
> >                  if isinstance(this, str):
> >                      this = QAPIDoc.Kind.from_string(this)
> >
> > -                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE):
> > +                if this == QAPIDoc.Kind.TODO:
> >                      return
> >
> >                  if this.value < last_ordered_section.value:
>
> The check for duplicate RETURNS, ERRORS, SINCE in new_tagged_section()
> is now technically redundant.  Deleting it would make the error message
> worse, though.  Do we care?
>

I wondered that, and wasn't sure. I left them in for now because I was
being conservative with regards to not messing up existing unit tests.

I suspect your feelings will become clear when we start adding unit tests
for the new error cases.

Sound about right?

>

[-- Attachment #2: Type: text/html, Size: 2471 bytes --]

^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 04/14] qapi/parser: enforce doc block section ordering
  2026-09-29 12:36   ` Markus Armbruster
@ 2026-09-29 14:31     ` John Snow
  2026-09-29 15:00       ` Markus Armbruster
  0 siblings, 1 reply; 35+ messages in thread
From: John Snow @ 2026-09-29 14:31 UTC (permalink / raw)
  To: Markus Armbruster
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

[-- Attachment #1: Type: text/plain, Size: 7147 bytes --]

On Tue, Sep 29, 2026, 8:36 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > Enforce the relative ordering of "Intro", "Members", "Features",
> > "Errors" and "Returns" sections.
> >
> > Signed-off-by: John Snow <jsnow@redhat.com>
> > ---
> >  docs/devel/qapi-code-gen.rst | 19 +++++++++++------
> >  scripts/qapi/parser.py       | 41 ++++++++++++++++++++++++++++++------
> >  2 files changed, 48 insertions(+), 12 deletions(-)
> >
> > diff --git a/docs/devel/qapi-code-gen.rst b/docs/devel/qapi-code-gen.rst
> > index 3ac1c2dae18..29e9c5a3da6 100644
> > --- a/docs/devel/qapi-code-gen.rst
> > +++ b/docs/devel/qapi-code-gen.rst
> > @@ -985,12 +985,19 @@ When documentation is required (see pragma_
> 'doc-required'), every
> >  definition must have documentation.
> >
> >  Definition documentation starts with a description naming the
> > -definition with an optional indented overview (the "intro"),
> > -:ref:`descriptions <doc-descrip>` of each argument (for commands and
> > -events), member (for structs and unions), branch (for alternates), or
> > -value (for enums), :ref:`feature descriptions <doc-feat>` of each
> > -feature (if any), and finally optional :ref:`tagged sections
> > -<doc-tagged>`.
> > +definition with an optional indented overview (the "intro").
> > +
> > +The introduction is followed by :ref:`descriptions <doc-descrip>` of
> > +each argument (for commands and events), member (for structs and
> > +unions), branch (for alternates), or value (for enums).
> > +
> > +:ref:`Tagged sections <doc-tagged>` follow in order: Returns (for
> > +commands), Errors (for commands), and lastly :ref:`feature
> > +descriptions <doc-feat>` if applicable.
>
> *Some* of the tagged sections do.
>
> Does this document still need the concept "tagged section"?
>

I doubt it does, beyond an explanation of common syntax. Happy to hear
suggestions...

We could also stand to change some of the method names like "ensure
untagged section" could become "ensure details", and the like.

tagged vs untagged has become increasingly non-meaningful.


> > +
> > +Definition documentation concludes with rST-formatted detail
> > +paragraphs (including examples, notes, &c), and finally an optional
> > +"Since" tagged section at the very end.
> >
> >  .. _doc-descrip:
> >
> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> > index fe284a3f866..94a60a23482 100644
> > --- a/scripts/qapi/parser.py
> > +++ b/scripts/qapi/parser.py
> > @@ -545,6 +545,28 @@ def get_doc(self) -> 'QAPIDoc':
> >              doc.all_sections.append(QAPIDoc.Section(info,
> QAPIDoc.Kind.INTRO))
> >              line = self.get_doc_indented(doc)
> >              no_more_args = False
> > +            last_ordered_section = QAPIDoc.Kind.INTRO
> > +
> > +            def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
> > +                nonlocal last_ordered_section
> > +                if isinstance(this, str):
> > +                    this = QAPIDoc.Kind.from_string(this)
>
> Simpler, I think: require a .Kind argument, and do the conversion from
> string in the one caller that needs it.
>

You're right.


> > +
> > +                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE,
> > +                            QAPIDoc.Kind.DETAILS):
> > +                    return
> > +
> > +                if this.value < last_ordered_section.value:
> > +                    raise QAPIParseError(
> > +                        self,
> > +                        f"'{this}' section cannot appear after "
> > +                        f"'{last_ordered_section}' section, please
> re-order "
> > +                        "the sections and adjust phrasing as necessary
> to "
> > +                        "ensure consistent documentation flow between
> the "
> > +                        "source code and the rendered HTML manual"
> > +                    )
>
> That's a loooong error message.  It actually consists of the error
> proper and advice on how to correct it.  Please use
>
>     "<error>\n<advice>"
>
> like we do in expr.py's check_keys(), parser.py's
> ._validate_doc_line_format(), and schema.py's ._def_definition().  Yes,
> the embedded newline is a bit of a hack.
>

ACK.


> > +
> > +                last_ordered_section = this
> >
> >              while line is not None:
> >                  # Blank lines
> > @@ -558,6 +580,7 @@ def get_doc(self) -> 'QAPIDoc':
> >                      if doc.has_features:
> >                          raise QAPIParseError(
> >                              self, "duplicated 'Features:' line")
> > +                    _tag_check(QAPIDoc.Kind.FEATURE)
> >                      self.accept(False)
> >                      line = self.get_doc_line()
> >                      while line == '':
> > @@ -581,6 +604,7 @@ def get_doc(self) -> 'QAPIDoc':
> >                              self,
> >                              "description of '@%s:' follows a section"
> >                              % match.group(1))
> > +                    _tag_check(QAPIDoc.Kind.MEMBER)
> >                      while (line is not None
> >                             and (match :=
> self._match_at_name_colon(line))):
> >                          doc.new_argument(self.info, match.group(1))
> > @@ -621,6 +645,7 @@ def get_doc(self) -> 'QAPIDoc':
> >                          )
> >                          raise QAPIParseError(self, emsg)
> >
> > +                    _tag_check(match.group(1))
>
> This is the caller that needs it.
>

Yep.


> >                      doc.new_tagged_section(
> >                          self.info,
> >                          QAPIDoc.Kind.from_string(match.group(1))
> > @@ -632,6 +657,7 @@ def get_doc(self) -> 'QAPIDoc':
> >                      no_more_args = True
> >                  else:
> >                      # plain paragraph(s)
> > +                    _tag_check(QAPIDoc.Kind.DETAILS)
> >                      doc.ensure_untagged_section(self.info)
> >                      doc.append_line(line)
> >                      line = self.get_doc_paragraph(doc)
> > @@ -674,14 +700,17 @@ class QAPIDoc:
> >      """
> >
> >      class Kind(enum.Enum):
> > -        DETAILS = 0
> > +        # The order here is the order in which sections must appear in
> > +        # source code; with the exception of 'TODO' which may appear
> > +        # anywhere but is treated as a comment.
> > +        INTRO = 0
> >          MEMBER = 1
> > -        FEATURE = 2
> > -        RETURNS = 3
> > -        ERRORS = 4
> > -        SINCE = 5
> > +        RETURNS = 2
> > +        ERRORS = 3
> > +        FEATURE = 4
> > +        DETAILS = 5
> >          TODO = 6
> > -        INTRO = 7
> > +        SINCE = 7
> >
> >          @staticmethod
> >          def from_string(kind: str) -> 'QAPIDoc.Kind':
>
> Missing: negative test for the new error.
>

Yep. If you are overall pleased with this change enough (pending nits),
I'll add the tests now.

>

[-- Attachment #2: Type: text/html, Size: 10917 bytes --]

^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 03/14] qapi: re-order QAPI doc block sections
  2026-09-29 12:10   ` Markus Armbruster
@ 2026-09-29 14:32     ` John Snow
  0 siblings, 0 replies; 35+ messages in thread
From: John Snow @ 2026-09-29 14:32 UTC (permalink / raw)
  To: Markus Armbruster
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

[-- Attachment #1: Type: text/plain, Size: 542 bytes --]

On Tue, Sep 29, 2026, 8:10 AM Markus Armbruster <armbru@redhat.com> wrote:

> What's a "block section"?
>

"doc block" section. Documentation block. probably.

I dunno.


> John Snow <jsnow@redhat.com> writes:
>
> > This patch is focused on making sure that "Returns" appears before
> > "Errors", and "Errors" appears before "Features".
>
> Aha.  Suggest
>
>   qapi: Move Returns:, Errors: and Features: into this order
>
> >
> > Signed-off-by: John Snow <jsnow@redhat.com>
>
> Patch looks good to me.
>

ACK

>

[-- Attachment #2: Type: text/html, Size: 1632 bytes --]

^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 01/14] qapi: rename "plain" sections to "details"
  2026-09-29 11:49   ` Markus Armbruster
@ 2026-09-29 14:36     ` John Snow
  0 siblings, 0 replies; 35+ messages in thread
From: John Snow @ 2026-09-29 14:36 UTC (permalink / raw)
  To: Markus Armbruster
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

[-- Attachment #1: Type: text/plain, Size: 4841 bytes --]

On Tue, Sep 29, 2026, 7:49 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > With this change, we no longer have "plaintext" sections at all: they
> > are all now either "Intro" or "Details". This patch does not enforce
> > where those details sections must occur; that change occurs in a later
> > patch.
> >
> > Signed-off-by: John Snow <jsnow@redhat.com>
> > ---
> >  docs/sphinx/qapidoc.py         |  2 +-
> >  scripts/qapi/parser.py         | 18 +++++++++++-------
> >  tests/qapi-schema/doc-good.out | 18 +++++++++---------
> >  3 files changed, 21 insertions(+), 17 deletions(-)
> >
> > diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
> > index 54a32e45f7e..ae57056a393 100644
> > --- a/docs/sphinx/qapidoc.py
> > +++ b/docs/sphinx/qapidoc.py
> > @@ -383,7 +383,7 @@ def visit_sections(self, ent: QAPISchemaDefinition)
> -> None:
> >          for section in sections:
> >              section.text = self.reformat_arobase(section.text)
> >
> > -            if section.kind.name in ("PLAIN", "INTRO"):
> > +            if section.kind.name in ("INTRO", "DETAILS"):
> >                  self.visit_plaintext(section)
> >              elif section.kind == QAPIDoc.Kind.MEMBER:
> >                  assert isinstance(section, QAPIDoc.ArgSection)
> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> > index dcc04b2663e..fe284a3f866 100644
> > --- a/scripts/qapi/parser.py
> > +++ b/scripts/qapi/parser.py
> > @@ -674,7 +674,7 @@ class QAPIDoc:
> >      """
> >
> >      class Kind(enum.Enum):
> > -        PLAIN = 0
> > +        DETAILS = 0
> >          MEMBER = 1
> >          FEATURE = 2
> >          RETURNS = 3
>
> I'd like the order of enum values to match the order of sections
> eventually.
>

I've got a treat for you from the future!


> > @@ -747,12 +747,16 @@ def has_features(self) -> bool:
> >      def end(self) -> None:
> >          for section in self.all_sections:
> >              section.text = section.text.strip('\n')
> > -            if not (section.kind.name in ("INTRO", "PLAIN") or
> section.text):
> > +            if (
> > +                    section.kind not in (
> > +                        QAPIDoc.Kind.INTRO, QAPIDoc.Kind.DETAILS
> > +                    ) and section.text == ''
> > +            ):
>
> This isn't just rename.
>
> The old conditional is a bit "uh, why bother with enums if we do strings
> anyway".  The conditional is ugly.
>
> Are empty DETAILS even possible?  Got a reproducer?
>

Mmm.... I'm afraid of dragons. I can investigate, ...


> If not, you could use
>
>                if section.text == '' and section.kind !=
> QAPIDoc.Kind.INTRO:
>
> >                  raise QAPISemError(
> >                      section.info, "text required after '%s:'" %
> section.kind)
> >
> >      def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
> > -        kind = QAPIDoc.Kind.PLAIN
> > +        kind = QAPIDoc.Kind.DETAILS
> >
> >          if self.all_sections and self.all_sections[-1].kind == kind:
> >              # extend current section
> > @@ -837,8 +841,8 @@ def _insert_after_intro(
> >          """
> >          Insert a section immediately after the intro section.
> >
> > -        While we convert PLAIN sections to INTRO sections, all
> > -        contiguous INTRO/PLAIN sections at the start of a QAPIDoc
> > +        While we convert DETAILS sections to INTRO sections, all
> > +        contiguous INTRO/DETAILS sections at the start of a QAPIDoc
> >          section list are treated as "the intro".
> >
> >          Once INTRO conversion is complete, this helper will no longer be
>
> This goes away in PATCH 9.  I'd be tempted to delay the rename until
> after that.  Up to you.
>

I'll look.


> > @@ -855,7 +859,7 @@ def _insert_after_intro(
> >
> >          index = 0
> >          for index, ref_section in enumerate(self.all_sections):
> > -            if ref_section.kind.name in ("PLAIN", "INTRO"):
> > +            if ref_section.kind.name in ("INTRO", "DETAILS"):
> >                  continue
> >              break
> >          else:
> > @@ -871,7 +875,7 @@ def append_member_stub(self, stub:
> 'QAPIDoc.Section') -> None:
> >          if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
> >              return
> >
> > -        # No MEMBER sections present. Insert after INTRO/PLAIN sections.
> > +        # No MEMBER sections present. Insert after INTRO/DETAILS
> sections.
> >          self._insert_after_intro(stub)
> >
> >      def connect_member(self, member: 'QAPISchemaMember') -> None:
> > diff --git a/tests/qapi-schema/doc-good.out
> b/tests/qapi-schema/doc-good.out
> > index eaaf119dea8..1d4fbd3762a 100644
>
> [...]
>
>

[-- Attachment #2: Type: text/html, Size: 7666 bytes --]

^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 04/14] qapi/parser: enforce doc block section ordering
  2026-09-29 14:31     ` John Snow
@ 2026-09-29 15:00       ` Markus Armbruster
  0 siblings, 0 replies; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 15:00 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> On Tue, Sep 29, 2026, 8:36 AM Markus Armbruster <armbru@redhat.com> wrote:
>
>> John Snow <jsnow@redhat.com> writes:
>>
>> > Enforce the relative ordering of "Intro", "Members", "Features",
>> > "Errors" and "Returns" sections.
>> >
>> > Signed-off-by: John Snow <jsnow@redhat.com>
>> > ---
>> >  docs/devel/qapi-code-gen.rst | 19 +++++++++++------
>> >  scripts/qapi/parser.py       | 41 ++++++++++++++++++++++++++++++------
>> >  2 files changed, 48 insertions(+), 12 deletions(-)
>> >
>> > diff --git a/docs/devel/qapi-code-gen.rst b/docs/devel/qapi-code-gen.rst
>> > index 3ac1c2dae18..29e9c5a3da6 100644
>> > --- a/docs/devel/qapi-code-gen.rst
>> > +++ b/docs/devel/qapi-code-gen.rst
>> > @@ -985,12 +985,19 @@ When documentation is required (see pragma_
>> 'doc-required'), every
>> >  definition must have documentation.
>> >
>> >  Definition documentation starts with a description naming the
>> > -definition with an optional indented overview (the "intro"),
>> > -:ref:`descriptions <doc-descrip>` of each argument (for commands and
>> > -events), member (for structs and unions), branch (for alternates), or
>> > -value (for enums), :ref:`feature descriptions <doc-feat>` of each
>> > -feature (if any), and finally optional :ref:`tagged sections
>> > -<doc-tagged>`.
>> > +definition with an optional indented overview (the "intro").
>> > +
>> > +The introduction is followed by :ref:`descriptions <doc-descrip>` of
>> > +each argument (for commands and events), member (for structs and
>> > +unions), branch (for alternates), or value (for enums).
>> > +
>> > +:ref:`Tagged sections <doc-tagged>` follow in order: Returns (for
>> > +commands), Errors (for commands), and lastly :ref:`feature
>> > +descriptions <doc-feat>` if applicable.
>>
>> *Some* of the tagged sections do.
>>
>> Does this document still need the concept "tagged section"?
>>
>
> I doubt it does, beyond an explanation of common syntax. Happy to hear
> suggestions...

I'll keep it in mind when I review PATCH 02.

> We could also stand to change some of the method names like "ensure
> untagged section" could become "ensure details", and the like.

Fair!

> tagged vs untagged has become increasingly non-meaningful.

True.

>> > +
>> > +Definition documentation concludes with rST-formatted detail
>> > +paragraphs (including examples, notes, &c), and finally an optional
>> > +"Since" tagged section at the very end.
>> >
>> >  .. _doc-descrip:
>> >
>> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
>> > index fe284a3f866..94a60a23482 100644
>> > --- a/scripts/qapi/parser.py
>> > +++ b/scripts/qapi/parser.py
>> > @@ -545,6 +545,28 @@ def get_doc(self) -> 'QAPIDoc':
>> >              doc.all_sections.append(QAPIDoc.Section(info,
>> QAPIDoc.Kind.INTRO))
>> >              line = self.get_doc_indented(doc)
>> >              no_more_args = False
>> > +            last_ordered_section = QAPIDoc.Kind.INTRO
>> > +
>> > +            def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
>> > +                nonlocal last_ordered_section
>> > +                if isinstance(this, str):
>> > +                    this = QAPIDoc.Kind.from_string(this)
>>
>> Simpler, I think: require a .Kind argument, and do the conversion from
>> string in the one caller that needs it.
>>
>
> You're right.
>
>
>> > +
>> > +                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE,
>> > +                            QAPIDoc.Kind.DETAILS):
>> > +                    return
>> > +
>> > +                if this.value < last_ordered_section.value:
>> > +                    raise QAPIParseError(
>> > +                        self,
>> > +                        f"'{this}' section cannot appear after "
>> > +                        f"'{last_ordered_section}' section, please
>> re-order "
>> > +                        "the sections and adjust phrasing as necessary
>> to "
>> > +                        "ensure consistent documentation flow between
>> the "
>> > +                        "source code and the rendered HTML manual"
>> > +                    )
>>
>> That's a loooong error message.  It actually consists of the error
>> proper and advice on how to correct it.  Please use
>>
>>     "<error>\n<advice>"
>>
>> like we do in expr.py's check_keys(), parser.py's
>> ._validate_doc_line_format(), and schema.py's ._def_definition().  Yes,
>> the embedded newline is a bit of a hack.
>>
>
> ACK.
>
>
>> > +
>> > +                last_ordered_section = this
>> >
>> >              while line is not None:
>> >                  # Blank lines
>> > @@ -558,6 +580,7 @@ def get_doc(self) -> 'QAPIDoc':
>> >                      if doc.has_features:
>> >                          raise QAPIParseError(
>> >                              self, "duplicated 'Features:' line")
>> > +                    _tag_check(QAPIDoc.Kind.FEATURE)
>> >                      self.accept(False)
>> >                      line = self.get_doc_line()
>> >                      while line == '':
>> > @@ -581,6 +604,7 @@ def get_doc(self) -> 'QAPIDoc':
>> >                              self,
>> >                              "description of '@%s:' follows a section"
>> >                              % match.group(1))
>> > +                    _tag_check(QAPIDoc.Kind.MEMBER)
>> >                      while (line is not None
>> >                             and (match :=
>> self._match_at_name_colon(line))):
>> >                          doc.new_argument(self.info, match.group(1))
>> > @@ -621,6 +645,7 @@ def get_doc(self) -> 'QAPIDoc':
>> >                          )
>> >                          raise QAPIParseError(self, emsg)
>> >
>> > +                    _tag_check(match.group(1))
>>
>> This is the caller that needs it.
>>
>
> Yep.
>
>
>> >                      doc.new_tagged_section(
>> >                          self.info,
>> >                          QAPIDoc.Kind.from_string(match.group(1))
>> > @@ -632,6 +657,7 @@ def get_doc(self) -> 'QAPIDoc':
>> >                      no_more_args = True
>> >                  else:
>> >                      # plain paragraph(s)
>> > +                    _tag_check(QAPIDoc.Kind.DETAILS)
>> >                      doc.ensure_untagged_section(self.info)
>> >                      doc.append_line(line)
>> >                      line = self.get_doc_paragraph(doc)
>> > @@ -674,14 +700,17 @@ class QAPIDoc:
>> >      """
>> >
>> >      class Kind(enum.Enum):
>> > -        DETAILS = 0
>> > +        # The order here is the order in which sections must appear in
>> > +        # source code; with the exception of 'TODO' which may appear
>> > +        # anywhere but is treated as a comment.
>> > +        INTRO = 0
>> >          MEMBER = 1
>> > -        FEATURE = 2
>> > -        RETURNS = 3
>> > -        ERRORS = 4
>> > -        SINCE = 5
>> > +        RETURNS = 2
>> > +        ERRORS = 3
>> > +        FEATURE = 4
>> > +        DETAILS = 5
>> >          TODO = 6
>> > -        INTRO = 7
>> > +        SINCE = 7
>> >
>> >          @staticmethod
>> >          def from_string(kind: str) -> 'QAPIDoc.Kind':
>>
>> Missing: negative test for the new error.
>>
>
> Yep. If you are overall pleased with this change enough (pending nits),
> I'll add the tests now.

I am!



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 08/14] qapi/parser: enforce strict positioning for "Since:" section
  2026-09-29 14:25     ` John Snow
@ 2026-09-29 15:04       ` Markus Armbruster
  0 siblings, 0 replies; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 15:04 UTC (permalink / raw)
  To: John Snow
  Cc: Markus Armbruster, qemu-devel, Peter Xu, Daniel P. Berrangé,
	Stefan Berger, Fabiano Rosas, Hanna Reitz, Paolo Bonzini,
	Zhao Liu, Kevin Wolf, Ani Sinha, Pierrick Bouvier, Jiri Pirko,
	Alex Williamson, Kashyap Chamarthy, Peter Maydell,
	Kostiantyn Kostiuk, Stefan Hajnoczi, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> On Tue, Sep 29, 2026, 8:53 AM Markus Armbruster <armbru@redhat.com> wrote:
>
>> John Snow <jsnow@redhat.com> writes:
>>
>> > This patch finalizes the total ordering of all QAPI documentation
>> > block sections.
>> >
>> > ... Except TODO, which can still be wherever.
>> >
>> > Signed-off-by: John Snow <jsnow@redhat.com>
>> > ---
>> >  scripts/qapi/parser.py | 2 +-
>> >  1 file changed, 1 insertion(+), 1 deletion(-)
>> >
>> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
>> > index 8b331aa0673..c14bff2cbc3 100644
>> > --- a/scripts/qapi/parser.py
>> > +++ b/scripts/qapi/parser.py
>> > @@ -552,7 +552,7 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) ->
>> None:
>> >                  if isinstance(this, str):
>> >                      this = QAPIDoc.Kind.from_string(this)
>> >
>> > -                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE):
>> > +                if this == QAPIDoc.Kind.TODO:
>> >                      return
>> >
>> >                  if this.value < last_ordered_section.value:
>>
>> The check for duplicate RETURNS, ERRORS, SINCE in new_tagged_section()
>> is now technically redundant.  Deleting it would make the error message
>> worse, though.  Do we care?
>>
>
> I wondered that, and wasn't sure. I left them in for now because I was
> being conservative with regards to not messing up existing unit tests.
>
> I suspect your feelings will become clear when we start adding unit tests
> for the new error cases.
>
> Sound about right?

Yes.  No need to decide now.  Better to stay focused.



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 09/14] qapi/parser: remove _insert_after_intro() helper
  2026-09-29 14:19     ` John Snow
@ 2026-09-29 15:08       ` Markus Armbruster
  0 siblings, 0 replies; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 15:08 UTC (permalink / raw)
  To: John Snow
  Cc: Markus Armbruster, qemu-devel, Peter Xu, Daniel P. Berrangé,
	Stefan Berger, Fabiano Rosas, Hanna Reitz, Paolo Bonzini,
	Zhao Liu, Kevin Wolf, Ani Sinha, Pierrick Bouvier, Jiri Pirko,
	Alex Williamson, Kashyap Chamarthy, Peter Maydell,
	Kostiantyn Kostiuk, Stefan Hajnoczi, Philippe Mathieu-Daudé,
	Michael Roth, Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> On Tue, Sep 29, 2026, 9:09 AM Markus Armbruster <armbru@redhat.com> wrote:
>
>> John Snow <jsnow@redhat.com> writes:
>>
>> > I promised I'd remove this.
>> >
>> > Signed-off-by: John Snow <jsnow@redhat.com>
>> > ---
>> >  scripts/qapi/parser.py | 49 ++++++------------------------------------
>> >  1 file changed, 7 insertions(+), 42 deletions(-)
>> >
>> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
>> > index c14bff2cbc3..5fc9c18c81d 100644
>> > --- a/scripts/qapi/parser.py
>> > +++ b/scripts/qapi/parser.py
>> > @@ -862,49 +862,15 @@ def _insert_near_kind(
>> >                  return True
>> >          return False
>> >
>> > -    def _insert_after_intro(
>> > -        self,
>> > -        section: 'QAPIDoc.Section',
>> > -    ) -> None:
>> > -        """
>> > -        Insert a section immediately after the intro section.
>> > -
>> > -        While we convert DETAILS sections to INTRO sections, all
>> > -        contiguous INTRO/DETAILS sections at the start of a QAPIDoc
>> > -        section list are treated as "the intro".
>> > -
>> > -        Once INTRO conversion is complete, this helper will no longer be
>> > -        needed and ``_insert_near_kind(QAPIDoc.Kind.INTRO, ...)`` will
>> > -        be sufficient.
>> > -        """
>> > -        first = self.all_sections[0]
>> > -        if first.text and first.kind == QAPIDoc.Kind.INTRO:
>> > -            # First section is introduction and is non-empty: insert here.
>> > -            # Rest assured all of this ugliness will very soon go away.
>> > -            # Pinkie-swear.
>> > -            self.all_sections.insert(1, section)
>> > -            return
>> > -
>> > -        index = 0
>> > -        for index, ref_section in enumerate(self.all_sections):
>> > -            if ref_section.kind.name in ("INTRO", "DETAILS"):
>> > -                continue
>> > -            break
>> > -        else:
>> > -            index += 1
>> > -
>> > -        self.all_sections.insert(index, section)
>> > -
>> >      def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
>> > -
>> >          """
>> >          Append a stub section after any Member sections.
>> >          """
>> >          if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
>> >              return
>> >
>> > -        # No MEMBER sections present. Insert after INTRO/DETAILS sections.
>> > -        self._insert_after_intro(stub)
>> > +        # No MEMBER sections present. Insert after INTRO section.
>> > +        self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)
>>
>> There can only be one INTRO section, and it must be first.  So this must
>> be equivalent to
>>
>>            self.all_sections.(1, stub)
>>
>> Correct?
>>
>> The argument for using the more complex ._insert_near_kind() is of
>> course symmetry with the other case.  Okay.
>>
>
> Yep, largely symmetry and opting to not write new logic as much as I can
> get away with in this patch. There's a compelling reason for not wasting
> much time writing new, cleaner logic...
>
>
>> We have three ._insert_near_kind() calls:
>>
>>         if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
>>
>>         self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)
>>
>>         if any(self._insert_near_kind(kind, stub, after) for kind, after
>> in (
>>                 [...]
>>         )):
>>
>> None of them omit the optional last argument.  Two pass it positionally,
>> one as a keyword argument.  Let's dumb this down to mandatory argument.
>>
>
> I'll do you one better, that entire method goes away by the end of this
> series and we don't have to worry about it.
>
> No sense putting lipstick on the pig enroute to the slaughterhouse.

I see.

Recommend to keep the new call similar to the existing one nearby
regardless, i.e. pass True, not after=True.

[...]



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 10/14] qapi/parser: add _append() method
  2026-09-29 14:14     ` John Snow
@ 2026-09-29 15:09       ` Markus Armbruster
  0 siblings, 0 replies; 35+ messages in thread
From: Markus Armbruster @ 2026-09-29 15:09 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, Qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> On Tue, Sep 29, 2026, 9:28 AM Markus Armbruster <armbru@redhat.com> wrote:
>
>> John Snow <jsnow@redhat.com> writes:
>>
>> > This method is intended to become the sole point of appending new
>> > sections to the section list, prohibiting outside callers from
>> > accessing the list of sections directly.
>> >
>> > A forthcoming commit will actually convert the "all_sections" field to
>> > be a private field to begin enforcing that no outside callers are
>> > manipulating the section list from outside of this class.
>>
>> This suggests you merely add the method now.  Not true, you also put it
>> to use.
>>
>
> Didn't mean to suggest such a thing! I'll try to reword.
>
> I mean only to convey that at this point, this doesn't *quite* become the
> sole access point as intended, due to some transitional code that this
> series is on its way to remove.
>
> I'm trying to justify the partial conversion.
>
>
>> >
>> > Signed-off-by: John Snow <jsnow@redhat.com>
>> > ---
>> >  scripts/qapi/parser.py | 50 ++++++++++++++++++++++++++----------------
>> >  1 file changed, 31 insertions(+), 19 deletions(-)
>> >
>> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
>> > index 5fc9c18c81d..0dca1acba4c 100644
>> > --- a/scripts/qapi/parser.py
>> > +++ b/scripts/qapi/parser.py
>> > @@ -542,7 +542,7 @@ def get_doc(self) -> 'QAPIDoc':
>> >              if not symbol:
>> >                  raise QAPIParseError(self, "name required after '@'")
>> >              doc = QAPIDoc(info, symbol)
>> > -            doc.all_sections.append(QAPIDoc.Section(info, QAPIDoc.Kind.INTRO))
>> > +            doc.new_tagged_section(info, QAPIDoc.Kind.INTRO)
>> >              line = self.get_doc_indented(doc)
>> >              no_more_args = False
>> >              last_ordered_section = QAPIDoc.Kind.INTRO
>> > @@ -772,6 +772,33 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
>> >      def has_features(self) -> bool:
>> >          return bool(self._features)
>> >
>> > +    def _append(self, section: 'QAPIDoc.Section') -> None:
>>
>> Maybe name it _append_section()?  We already have append_line()...
>>
>
> Eh, why not, sure.
>
>
>> > +        """
>> > +        Primary internal interface for adding a new section.
>> > +
>> > +        It is intended to become the sole access point for adding new
>> > +        sections to the QAPIDoc section list, but we are currently in
>> > +        a transitional phase where other methods access the section
>> > +        list directly to do insertions of generated sections at
>> > +        targeted points.
>>
>> Yes: ._insert_near_kind().
>>
>> > +        """
>>
>> This comment will soon change to
>>
>>            """
>>            Primary internal interface for adding a new section.
>>
>>            All other methods/helpers/etc ultimately use this and only
>>            this interface for appending a new section.
>>            """
>>
>> There's a bit of redundancy.  Suggest
>>
>>            """
>>            Add a new section at the end.
>>
>>            It is intended to ...
>>            """
>>
>> now, and
>>
>>            """
>>            Add a new section at the end.
>>
>>            All other ...
>>            """
>>
>> later.
>>
>
> More or less correct, though "at the end" isn't really true, it's
> contextually sensitive.
>
> "Add a new section" is more correct.
>
> Actually, why don't we name it _add_section() to make it clear that it
> isn't *always* an append?

Sold!

[...]



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 05/14] qapi: move 'details' sections to the bottom of doc blocks
  2026-09-28 22:36 ` [PATCH 05/14] qapi: move 'details' sections to the bottom of doc blocks John Snow
@ 2026-10-01 11:14   ` Markus Armbruster
  0 siblings, 0 replies; 35+ messages in thread
From: Markus Armbruster @ 2026-10-01 11:14 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> This patch is focused on moving any and all "Details" sections to
> below any Members/Returns/Errors/Features sections, generally
> appearing last or after "Since:" sections.

You mean right before "Since:", don't you?

> Signed-off-by: John Snow <jsnow@redhat.com>

Patch looks good to me.



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 13/14] qapi/parser: add new _sections mapping
  2026-09-28 22:36 ` [PATCH 13/14] qapi/parser: add new _sections mapping John Snow
@ 2026-10-02  7:24   ` Markus Armbruster
  0 siblings, 0 replies; 35+ messages in thread
From: Markus Armbruster @ 2026-10-02  7:24 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> Modify the _sections field to be a mapping between section kinds and a
> list of sections instead of a simple, flat list.
>
> This makes it trivial to insert new auto-generated sections at the
> correct position in the collection instead of needing to iterate
> carefully over the list every time.
>
> As a result, a lot of helpers we had to help us manage these
> insertions are now unnecessary and can be deleted with extreme
> prejudice.
>
> NOTE: The remaining fishy use of a member insertion by the qapidoc
> generator still remains, though it uses a simpler code pathway
> now. The addition of the actual inliner mechanism will be the final
> piece to remove this hack.
>
> NOTE 2: Because "TODO:" sections are not strictly ordered in source,
> this new section collection mechanism just lumps them in at the
> end. For our generated documentation, this is functionally identical
> as we do not render those sections anyway, but there is a noticeable
> impact on our unit tests which *do* render TODO sections.

This takes a path between the two extremes:

1. Throw them away.  Makes them completely invisible to the tests.

2. Fully preserve order information somehow.

   Consider this doc comment from doc-good.json:

    ##
    # @cmd:
    #
    # @arg1:
    #     description starts on a new line,
    #     indented
    #
    # @arg2: description starts on the same line
    #     remainder indented differently
    #
    # Returns: @Object
    #
    # Errors: some
    #
    # Features:
    # @cmd-feat1: a feature
    # @cmd-feat2: another feature
    #
    # .. note:: @arg3 is undocumented
    #
    # TODO: frobnicate
    #
    # .. admonition:: Notes
    #
    #  - Lorem ipsum dolor sit amet
    #  - Ut enim ad minim veniam
    [...]
    # Since: 2.10
    ##

  Its sections are:

    kind                source order
    --------------------------------------------
    Intro               0
    Member=arg1         1
    Member=arg2         2
    Member=arg3         none (it's synthesized)
    Returns             3
    Errors              4
    Feature=cmd-feat1   5
    Feature=cmd-feat2   6
    Details             7
    TODO                8
    Details             9
    Since               10

  We could record the number in the right column.

  Or we could try to use existing .info.line instead.  Slight
  complication: synthesized section Member=arg3 has a funny value there.

Mind, I'm not making demands here, I'm throwing you ideas.

> NOTE 3: This patch is what makes it obvious as to why we need
> _append(), len(), and iter() implementations.
>
> Signed-off-by: John Snow <jsnow@redhat.com>

I'd like the commit message to point out that and how we don't lose
source order, except for TODO.

Before the patch, ._sections is a flat list in source order.

The patch turns it into a dict of lists.  The strict section ordering
established in prior patches ensures that walking the dict's lists in
order is in source order: we add sections in source order, all sections
of the same kind are together, we keep the dict elements (lists) in
source order by adding at the end, and dict order is insertion order
since Python 3.6.

Except for the TODOs, because these are not strictly ordered.

> ---
>  docs/sphinx/qapidoc.py         |  6 +--
>  scripts/qapi/parser.py         | 74 ++++++++--------------------------
>  tests/qapi-schema/doc-good.out |  4 +-
>  3 files changed, 20 insertions(+), 64 deletions(-)
>
> diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
> index fd666be131f..41acb560628 100644
> --- a/docs/sphinx/qapidoc.py
> +++ b/docs/sphinx/qapidoc.py
> @@ -371,11 +371,7 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
>          # other users of the QAPISchema or QAPIDoc objects outside of
>          # the document generator. Fishy, but not harmful.
>          if ent.doc:
> -            ent.doc.append_member_stub(
> -                QAPIDoc.ArgSection(
> -                    ent.doc.info, QAPIDoc.Kind.MEMBER, "q_dummy"
> -                )
> -            )
> +            ent.doc.new_argument(ent.doc.info, "q_dummy")
>  
>          sections = list(ent.doc) if ent.doc else []
>  
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index 894b8979b9e..1d8f1d39ab1 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -758,8 +758,13 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
>          self.info = info
>          # definition doc's symbol, None for free-form doc
>          self.symbol: Optional[str] = symbol
> -        # the sections in textual order
> -        self._sections: List[QAPIDoc.Section] = []
> +
> +        # all sections arranged by kind. This is identical to source

All

> +        # order, with the exception of TODO sections and any generated
> +        # sections that are appended post-hoc.

post hoc (no dash), I think.

> +        self._sections: Dict[QAPIDoc.Kind, List[QAPIDoc.Section]] = {
> +            kind: [] for kind in QAPIDoc.Kind
> +        }
>  
>          # The most recently appended section, on which append_line operates
>          self._active_section: Optional[QAPIDoc.Section] = None
> @@ -767,6 +772,7 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
>          # dicts mapping parameter/feature names to their description
>          self._args: Dict[str, QAPIDoc.ArgSection] = {}
>          self._features: Dict[str, QAPIDoc.ArgSection] = {}
> +
>          # a command's "Returns" and "Errors" section
>          self._returns: Optional[QAPIDoc.Section] = None
>          self._errors: Optional[QAPIDoc.Section] = None

This looks out of place.

> @@ -781,11 +787,8 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
>          """
>          Primary internal interface for adding a new section.
>  
> -        It is intended to become the sole access point for adding new
> -        sections to the QAPIDoc section list, but we are currently in
> -        a transitional phase where other methods access the section
> -        list directly to do insertions of generated sections at
> -        targeted points.
> +        All other methods/helpers/etc ultimately use this and only
> +        this interface for appending a new section.
>          """
>          if section.kind == QAPIDoc.Kind.RETURNS:
>              if self._returns:
> @@ -802,14 +805,15 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
>                  raise QAPISemError(
>                      section.info, "duplicated '%s' section" % section.kind)
>              self.since = section
> -        self._sections.append(section)
> +        self._sections[section.kind].append(section)
>          self._active_section = section
>  
>      def __iter__(self) -> Iterator['QAPIDoc.Section']:

Worth a comment about source order?  There's already one next to
self._sections.

> -        return iter(self._sections)
> +        for values in self._sections.values():
> +            yield from values
>  
>      def __len__(self) -> int:
> -        return len(self._sections)
> +        return sum(len(x) for x in self._sections.values())

No longer used as far as I can tell.

>  
>      def end(self) -> None:
>          for section in self:
> @@ -870,32 +874,6 @@ def append_line(self, line: str) -> None:
>          assert self._active_section is not None
>          self._active_section.append_line(line)
>  
> -    def _insert_near_kind(
> -        self,
> -        kind: 'QAPIDoc.Kind',
> -        new_sect: 'QAPIDoc.Section',
> -        after: bool = False,
> -    ) -> bool:
> -        """Insert or append a new doc section at a specific point."""
> -        for idx, sect in enumerate(reversed(list(self))):
> -            if sect.kind == kind:
> -                pos = len(self) - idx - 1

This is where it's used before this patch.

> -                if after:
> -                    pos += 1
> -                self._sections.insert(pos, new_sect)
> -                return True
> -        return False
> -
> -    def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
> -        """
> -        Append a stub section after any Member sections.
> -        """
> -        if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
> -            return
> -
> -        # No MEMBER sections present. Insert after INTRO section.
> -        self._insert_near_kind(QAPIDoc.Kind.INTRO, stub, after=True)
> -
>      def connect_member(self, member: 'QAPISchemaMember') -> None:
>          if member.name not in self._args:
>              assert member.info
> @@ -908,7 +886,7 @@ def connect_member(self, member: 'QAPISchemaMember') -> None:
>              stub_section = QAPIDoc.ArgSection(
>                  self.info, QAPIDoc.Kind.MEMBER, member.name)
>              self._args[member.name] = stub_section
> -            self.append_member_stub(stub_section)
> +            self._append(stub_section)
>  
>          self._args[member.name].connect(member)
>  
> @@ -920,30 +898,12 @@ def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
>          self._features[feature.name].connect(feature)
>  
>      def ensure_returns(self, info: QAPISourceInfo) -> None:
> -        # This is more complicated than it ought to be.  The doc
> -        # parser should already know where a generated RETURNS section
> -        # should go.  It currently doesn't.
> -        #
> -        # TODO: Take advantage of the new strict section ordering and
> -        # simplify this algorithm.
> -
> -        if self._returns:
> +        if self._sections[QAPIDoc.Kind.RETURNS]:

Why not keep the more concise self._returns?

>              return
>  
>          # Stub "Returns" section for undocumented returns value
>          stub = QAPIDoc.Section(info, QAPIDoc.Kind.RETURNS)
> -
> -        if any(self._insert_near_kind(kind, stub, after) for kind, after in (
> -                # 1. If arguments, right after those.
> -                (QAPIDoc.Kind.MEMBER, True),
> -                # 2. Elif errors, right *before* those.
> -                (QAPIDoc.Kind.ERRORS, False),
> -                # 3. Elif features, right *before* those.
> -                (QAPIDoc.Kind.FEATURE, False),
> -                # 4. Else, right *after* the intro.
> -                (QAPIDoc.Kind.INTRO, True),
> -        )):
> -            return
> +        self._append(stub)
>  
>      def check_expr(self, expr: QAPIExpression) -> None:
>          if 'command' in expr:
> diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
> index 7a485e46037..4a0fae6fce2 100644
> --- a/tests/qapi-schema/doc-good.out
> +++ b/tests/qapi-schema/doc-good.out
> @@ -181,8 +181,6 @@ a feature
>  another feature
>      Details
>  .. note:: @arg3 is undocumented
> -    Todo
> -frobnicate
>      Details
>  .. admonition:: Notes
>  
> @@ -207,6 +205,8 @@ Examples::
>  
>  Note::
>      Ceci n'est pas une note
> +    Todo
> +frobnicate
>      Since
>  2.10
>  doc symbol=cmd-boxed



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 11/14] qapi/parser: privatize "all_sections" field as "_sections"
  2026-09-28 22:36 ` [PATCH 11/14] qapi/parser: privatize "all_sections" field as "_sections" John Snow
@ 2026-10-02  8:51   ` Markus Armbruster
  0 siblings, 0 replies; 35+ messages in thread
From: Markus Armbruster @ 2026-10-02  8:51 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> In preparation for switching QAPIDoc to a more complex data structure
> for keeping track of sections, convert the "all_sections" property to
> a private property.

s/property/attribute/

> Rename it "_sections" to emphasize that there is not* some alternative
> property that is somehow not all sections. This is what you get and
> you're going to love it.
>
> Implement the len() and iter() features for QAPIDoc as a replacement
> for direct, public access to the all_sections property.
>
> (*OK, currently there are some, but spoilers, they're going away in
> the next commits.)
>
> Signed-off-by: John Snow <jsnow@redhat.com>

This took a bit of work for me to understand.

I figure "alternative that is somehow not all sections" refers to .args,
._features, ._returns, ._errors, and .since.

You replace the latter three by properties in PATCH 14.  The former two
stay.  "They're going away" is not entirely true.

Or am I confused?

Here'a another way to justify the renaming.  When I added .all_sections
in commit 3d035cd2cca, there was also .sections, which held only the
"sections other than .body, .args, .features".  You dropped .sections in
commit d678c5aff31.  No real need for "all_" since.

> ---
>  docs/sphinx/qapidoc.py         |  7 ++++---
>  scripts/qapi/parser.py         | 27 +++++++++++++++++----------
>  tests/qapi-schema/test-qapi.py |  2 +-
>  3 files changed, 22 insertions(+), 14 deletions(-)
>
> diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
> index ae57056a393..fd666be131f 100644
> --- a/docs/sphinx/qapidoc.py
> +++ b/docs/sphinx/qapidoc.py
> @@ -377,7 +377,7 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
>                  )
>              )
>  
> -        sections = ent.doc.all_sections if ent.doc else []
> +        sections = list(ent.doc) if ent.doc else []

Why listify?  All you need is an iterator as far as I can tell.

>  
>          # Add sections in source order:
>          for section in sections:
> @@ -416,8 +416,9 @@ def visit_module(self, path: str) -> None:
>          self.ensure_blank_line()
>  
>      def visit_freeform(self, doc: QAPIDoc) -> None:
> -        assert len(doc.all_sections) == 1, doc.all_sections
> -        body = doc.all_sections[0]
> +        sections = list(doc)
> +        assert len(sections) == 1, sections
> +        body = sections[0]
>          self.add_lines(self.reformat_arobase(body.text), doc.info)
>          self.ensure_blank_line()

The code here checks "single section", then formats that section.

Works because a free-form doc comment is a single DETAILS section.

Aside: calling it DETAILS is a bit odd, but we'll live.

Aside: if we recognized TODO: there, it could be several, but we don't.
Maybe we should.

Your patch looks fine to me, but I wonder whether we could instead do

           for section in doc:
               assert section.kind == QAPIDoc.Kind.DETAILS
               self.add_lines(self.reformat_arobase(section.text), section.info)
               self.ensure_blank_line()

Separate cleanup patch, I guess.

>  
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index 0dca1acba4c..bdd0f1ec3ff 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -19,6 +19,7 @@
>      TYPE_CHECKING,
>      Any,
>      Dict,
> +    Iterator,
>      List,
>      Mapping,
>      Match,
> @@ -758,7 +759,7 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
>          # 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] = []
> +        self._sections: List[QAPIDoc.Section] = []
>          # dicts mapping parameter/feature names to their description
>          self._args: Dict[str, QAPIDoc.ArgSection] = {}
>          self._features: Dict[str, QAPIDoc.ArgSection] = {}
> @@ -797,10 +798,16 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
>                  raise QAPISemError(
>                      section.info, "duplicated '%s' section" % section.kind)
>              self.since = section
> -        self.all_sections.append(section)
> +        self._sections.append(section)
> +
> +    def __iter__(self) -> Iterator['QAPIDoc.Section']:
> +        return iter(self._sections)
> +
> +    def __len__(self) -> int:
> +        return len(self._sections)
>  
>      def end(self) -> None:
> -        for section in self.all_sections:
> +        for section in self:
>              section.text = section.text.strip('\n')
>              if (
>                      section.kind not in (
> @@ -813,9 +820,9 @@ def end(self) -> None:
>      def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
>          kind = QAPIDoc.Kind.DETAILS
>  
> -        if self.all_sections and self.all_sections[-1].kind == kind:
> +        if self._sections and self._sections[-1].kind == kind:
>              # extend current section
> -            section = self.all_sections[-1]
> +            section = self._sections[-1]
>              if not section.text:
>                  # Section is empty so far; update info to start *here*.
>                  section.info = info
> @@ -856,7 +863,7 @@ def new_feature(self, info: QAPISourceInfo, name: str) -> None:
>          self._new_description(info, name, QAPIDoc.Kind.FEATURE, self._features)
>  
>      def append_line(self, line: str) -> None:
> -        self.all_sections[-1].append_line(line)
> +        self._sections[-1].append_line(line)
>  
>      def _insert_near_kind(
>          self,
> @@ -865,12 +872,12 @@ def _insert_near_kind(
>          after: bool = False,
>      ) -> bool:
>          """Insert or append a new doc section at a specific point."""
> -        for idx, sect in enumerate(reversed(self.all_sections)):
> +        for idx, sect in enumerate(reversed(list(self))):

Having to listify an iterator just to run it backwards is always sad.
Fortunately, this code is about to go away.

>              if sect.kind == kind:
> -                pos = len(self.all_sections) - idx - 1
> +                pos = len(self) - idx - 1
>                  if after:
>                      pos += 1
> -                self.all_sections.insert(pos, new_sect)
> +                self._sections.insert(pos, new_sect)
>                  return True
>          return False
>  
> @@ -915,7 +922,7 @@ def ensure_returns(self, info: QAPISourceInfo) -> None:
>          # TODO: Take advantage of the new strict section ordering and
>          # simplify this algorithm.
>  
> -        if any(s.kind == QAPIDoc.Kind.RETURNS for s in self.all_sections):
> +        if self._returns:
>              return

Instead of translating the loop, you replace it.  I suspect the loop was
always unnecessary.  Consider a separate commit.

>  
>          # Stub "Returns" section for undocumented returns value
> diff --git a/tests/qapi-schema/test-qapi.py b/tests/qapi-schema/test-qapi.py
> index 5beb96e894f..d5b4ac3a682 100755
> --- a/tests/qapi-schema/test-qapi.py
> +++ b/tests/qapi-schema/test-qapi.py
> @@ -117,7 +117,7 @@ def test_frontend(fname):
>              print('doc symbol=%s' % doc.symbol)
>          else:
>              print('doc freeform')
> -        for section in doc.all_sections:
> +        for section in doc:
>              if isinstance(section, QAPIDoc.ArgSection):
>                  print('    %s=%s' % (section.kind, section.name))
>              else:



^ permalink raw reply	[flat|nested] 35+ messages in thread

* Re: [PATCH 14/14] qapi/parser: convert single-section properties to getter properties
  2026-09-28 22:36 ` [PATCH 14/14] qapi/parser: convert single-section properties to getter properties John Snow
@ 2026-10-02  9:44   ` Markus Armbruster
  0 siblings, 0 replies; 35+ messages in thread
From: Markus Armbruster @ 2026-10-02  9:44 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Peter Xu, Daniel P. Berrangé, Stefan Berger,
	Fabiano Rosas, Hanna Reitz, Paolo Bonzini, Zhao Liu, Kevin Wolf,
	Ani Sinha, Pierrick Bouvier, Jiri Pirko, Alex Williamson,
	Kashyap Chamarthy, Peter Maydell, Kostiantyn Kostiuk,
	Stefan Hajnoczi, Philippe Mathieu-Daudé, Michael Roth,
	Mauro Carvalho Chehab, Vladimir Sementsov-Ogievskiy,
	Marc-André Lureau, qemu-block, Cédric Le Goater,
	Stefano Garzarella, Richard Henderson, Michael S. Tsirkin,
	Igor Mammedov, Jason Wang, Alex Bennée, Eric Blake

John Snow <jsnow@redhat.com> writes:

> Remove dedicated pointers to single sections, instead preferring a
> functional getter property that returns the same thing dynamically.
>
> This is being done mainly for the benefit of being able to condense
> the _append() function, but I'm not convinced it's worth it. I'm not
> convinced it's not not worth it either, though.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  scripts/qapi/parser.py | 47 +++++++++++++++++++++++-------------------
>  1 file changed, 26 insertions(+), 21 deletions(-)

Hmm.

> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index 1d8f1d39ab1..d4b5b3e5ab6 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -773,12 +773,6 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
>          self._args: Dict[str, QAPIDoc.ArgSection] = {}
>          self._features: Dict[str, QAPIDoc.ArgSection] = {}
>  
> -        # a command's "Returns" and "Errors" section
> -        self._returns: Optional[QAPIDoc.Section] = None
> -        self._errors: Optional[QAPIDoc.Section] = None
> -        # "Since" section
> -        self.since: Optional[QAPIDoc.Section] = None
> -
>      @property
>      def has_features(self) -> bool:
>          return bool(self._features)
> @@ -790,21 +784,10 @@ def _append(self, section: 'QAPIDoc.Section') -> None:
>          All other methods/helpers/etc ultimately use this and only
>          this interface for appending a new section.
>          """
> -        if section.kind == QAPIDoc.Kind.RETURNS:
> -            if self._returns:
> -                raise QAPISemError(
> -                    section.info, "duplicated '%s' section" % section.kind)
> -            self._returns = section
> -        elif section.kind == QAPIDoc.Kind.ERRORS:
> -            if self._errors:
> -                raise QAPISemError(
> -                    section.info, "duplicated '%s' section" % section.kind)
> -            self._errors = section
> -        elif section.kind == QAPIDoc.Kind.SINCE:
> -            if self.since:
> -                raise QAPISemError(
> -                    section.info, "duplicated '%s' section" % section.kind)
> -            self.since = section
> +        if str(section.kind) in ('Intro', 'Returns', 'Errors', 'Since'):
> +            if self._sections[section.kind]:
> +                raise QAPISemError(section.info,
> +                                   "duplicated '%s' section" % section.kind)
>          self._sections[section.kind].append(section)
>          self._active_section = section
>  

Likable so far.

> @@ -815,6 +798,28 @@ def __iter__(self) -> Iterator['QAPIDoc.Section']:
>      def __len__(self) -> int:
>          return sum(len(x) for x in self._sections.values())
>  
> +    def _single_section_property(
> +        self,
> +        kind: 'QAPIDoc.Kind'
> +    ) -> Optional['QAPIDoc.Section']:
> +        try:
> +            return self._sections[kind][0]
> +        except IndexError:
> +            pass
> +        return None

Returns the first section of a kind.  Let's name it after what it does:
_first_section().

The try works, but

           return self._sections[kind][0] if self._sections[kind] else None

seems to work as well, and with less ceremony :)

> +
> +    @property
> +    def _returns(self) -> Optional['QAPIDoc.Section']:
> +        return self._single_section_property(QAPIDoc.Kind.RETURNS)
> +
> +    @property
> +    def _errors(self) -> Optional['QAPIDoc.Section']:
> +        return self._single_section_property(QAPIDoc.Kind.ERRORS)
> +
> +    @property
> +    def since(self) -> Optional['QAPIDoc.Section']:
> +        return self._single_section_property(QAPIDoc.Kind.SINCE)
> +
>      def end(self) -> None:
>          for section in self:
>              section.text = section.text.strip('\n')



^ permalink raw reply	[flat|nested] 35+ messages in thread

end of thread, other threads:[~2026-10-02  9:44 UTC | newest]

Thread overview: 35+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-28 22:36 [PATCH 00/14] qapi: enforce section ordering John Snow
2026-09-28 22:36 ` [PATCH 01/14] qapi: rename "plain" sections to "details" John Snow
2026-09-29 11:49   ` Markus Armbruster
2026-09-29 14:36     ` John Snow
2026-09-28 22:36 ` [PATCH 02/14] docs/qapi-code-gen: expand each section's syntax into a mini-section John Snow
2026-09-28 22:36 ` [PATCH 03/14] qapi: re-order QAPI doc block sections John Snow
2026-09-29 12:10   ` Markus Armbruster
2026-09-29 14:32     ` John Snow
2026-09-28 22:36 ` [PATCH 04/14] qapi/parser: enforce doc block section ordering John Snow
2026-09-29 12:36   ` Markus Armbruster
2026-09-29 14:31     ` John Snow
2026-09-29 15:00       ` Markus Armbruster
2026-09-28 22:36 ` [PATCH 05/14] qapi: move 'details' sections to the bottom of doc blocks John Snow
2026-10-01 11:14   ` Markus Armbruster
2026-09-28 22:36 ` [PATCH 06/14] qapi/parser: enforce 'details' sections' relative position John Snow
2026-09-28 22:36 ` [PATCH 07/14] qapi: re-order 'since' sections to always be last John Snow
2026-09-28 22:36 ` [PATCH 08/14] qapi/parser: enforce strict positioning for "Since:" section John Snow
2026-09-29 12:53   ` Markus Armbruster
2026-09-29 14:25     ` John Snow
2026-09-29 15:04       ` Markus Armbruster
2026-09-28 22:36 ` [PATCH 09/14] qapi/parser: remove _insert_after_intro() helper John Snow
2026-09-29 13:09   ` Markus Armbruster
2026-09-29 14:19     ` John Snow
2026-09-29 15:08       ` Markus Armbruster
2026-09-28 22:36 ` [PATCH 10/14] qapi/parser: add _append() method John Snow
2026-09-29 13:28   ` Markus Armbruster
2026-09-29 14:14     ` John Snow
2026-09-29 15:09       ` Markus Armbruster
2026-09-28 22:36 ` [PATCH 11/14] qapi/parser: privatize "all_sections" field as "_sections" John Snow
2026-10-02  8:51   ` Markus Armbruster
2026-09-28 22:36 ` [PATCH 12/14] qapi/parser: add _active_section private member John Snow
2026-09-28 22:36 ` [PATCH 13/14] qapi/parser: add new _sections mapping John Snow
2026-10-02  7:24   ` Markus Armbruster
2026-09-28 22:36 ` [PATCH 14/14] qapi/parser: convert single-section properties to getter properties John Snow
2026-10-02  9:44   ` Markus Armbruster

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.