@samitouri / QOSamiQemu / commits / 43e7ad1a3f

qapi: adjust doc comment stub member insertion algorithm

A forthcoming patch removes the implicit PLAIN section that always starts a QAPIDoc section list. Further future changes begin converting "PLAIN" sections to "INTRO" sections. This will affect the code that inserts "Not documented" descriptions for undocumented members ("stub sections") and the dummy section that marks the spot for "The members of ..." references. Adjust the algorithm to cope with not only the finished state, but temporary intermediate states while the series is merged. This algorithm can handle zero-or-more PLAIN *or* INTRO sections at the beginning of a QAPIDoc object, in contrast to the previous algorithm which assumed and relied upon there being always one PLAIN section at the beginning of every QAPIDoc section list. In other words: (PLAIN | INTRO)* <EverythingElse> This does not impact what the parser itself will actually produce. As of this patch, the parser will still always generate QAPIDoc section lists that start with precisely one PLAIN section (whether or not it is empty), followed by the remaining sections. Those remaining sections may or may not include additional PLAIN sections, but never two such sections contiguously as the parser will always treat that layout as one PLAIN section consisting of multiple paragraph(s). In other other words: This insertion algorithm is more lenient than the parser, but this is on purpose for flexibility mid-stream as we convert QAPI to using explicit introductory sections. The allowed order of sections will eventually become strictly enforced in the parser, which will in turn allow dramatic simplifications to the insertion algorithm. This only exists as transitory code until we are able to enforce that order. Fear not: the intermediate ReST output before and after this patch are byte identical, so failing all else, we at least know it doesn't make anything worse. Lastly, because we have three places in the code that need to insert stub/dummy sections, we take the opportunity to consolidate this code to handle all three cases with one function. This winds up necessitating the qapidoc.py generator actually modify the section list to insert a "dummy" member that acts as a placeholder for "The members of ..." text. While it looks like a code smell to modify the caller's argument, it is ultimately safe because the QAPI Schema object is re-parsed and re-constructed in memory for each individual process that needs to operate on it. In other words, the Sphinx document generator already does have "its own copy" of the section lists, so it is "safe" to modify here without regards to other consumers of the QAPIDoc objects. It only *looks* like it smells bad. Ultimately, this code will also be removed once the inliner is merged, so it is only a temporary aesthetic issue regardless. That's my story and I'm sticking to it. Signed-off-by: John Snow <jsnow@redhat.com> Message-ID: <20260611042332.482979-11-jsnow@redhat.com> Reviewed-by: Markus Armbruster <armbru@redhat.com> [Commit message tweaked] Signed-off-by: Markus Armbruster <armbru@redhat.com>

John Snow committed Jun 11, 2026 at 00:23 UTC 43e7ad1a3fa55f86970de605295e513a565ba67e
2 files changed +64 -36
docs/sphinx/qapidoc.py
+23 -19
@@ -349,30 +349,38 @@ class Transmogrifier:
349 )
350
351 def visit_sections(self, ent: QAPISchemaDefinition) -> None:
352 - sections = ent.doc.all_sections if ent.doc else []
352 + # Generate a placeholder right after the member section(s) which
353 + # may be used to generate documentation for "The members of..."
354 + # pointers in the rendered document.
355 + #
356 + # This is a temporary hack until the inliner is merged. Note
357 + # that although we modify the caller's section list, the
358 + # Sphinx document generator has its own copy of the parsed
359 + # schema in memory, so this action does not interfere with
360 + # other users of the QAPISchema or QAPIDoc objects outside of
361 + # the document generator. Fishy, but not harmful.
362 + if ent.doc:
363 + ent.doc.append_member_stub(
364 + QAPIDoc.ArgSection(
365 + ent.doc.info, QAPIDoc.Kind.MEMBER, "q_dummy"
366 + )
367 + )
368
354 - # Determine the index location at which we should generate
355 - # documentation for "The members of ..." pointers. This should
356 - # go at the end of the members section(s) if any. Note that
357 - # index 0 is assumed to be a plain intro section, even if it is
358 - # empty; and that a members section if present will always
359 - # immediately follow the opening PLAIN section.
360 - gen_index = 1
361 - if len(sections) > 1:
362 - while sections[gen_index].kind == QAPIDoc.Kind.MEMBER:
363 - gen_index += 1
364 - if gen_index >= len(sections):
365 - break
369 + sections = ent.doc.all_sections if ent.doc else []
370
371 # Add sections in source order:
368 - for i, section in enumerate(sections):
372 + for section in sections:
373 section.text = self.reformat_arobase(section.text)
374
375 if section.kind.name in ("PLAIN", "INTRO"):
376 self.visit_paragraph(section)
377 elif section.kind == QAPIDoc.Kind.MEMBER:
378 assert isinstance(section, QAPIDoc.ArgSection)
375 - self.visit_member(section)
379 + if section.name == "q_dummy":
380 + # Generate "The members of ..." entries if necessary
381 + self._insert_member_pointer(ent)
382 + else:
383 + self.visit_member(section)
384 elif section.kind == QAPIDoc.Kind.FEATURE:
385 assert isinstance(section, QAPIDoc.ArgSection)
386 self.visit_feature(section)
@@ -386,10 +394,6 @@ class Transmogrifier:
394 else:
395 assert False
396
389 - # Generate "The members of ..." entries if necessary:
390 - if i == gen_index - 1:
391 - self._insert_member_pointer(ent)
392 -
397 self.ensure_blank_line()
398
399 # Transmogrification core methods
scripts/qapi/parser.py
+41 -17
@@ -832,6 +832,42 @@ class QAPIDoc:
832 return True
833 return False
834
835 + def _insert_after_intro(
836 + self,
837 + section: 'QAPIDoc.Section',
838 + ) -> None:
839 + """
840 + Insert a section immediately after the intro section.
841 +
842 + While we convert PLAIN sections to INTRO sections, all
843 + contiguous INTRO/PLAIN sections at the start of a QAPIDoc
844 + section list are treated as "the intro".
845 +
846 + Once INTRO conversion is complete, this helper will no longer be
847 + needed and ``_insert_near_kind(QAPIDoc.Kind.INTRO, ...)`` will
848 + be sufficient.
849 + """
850 + index = 0
851 + for index, ref_section in enumerate(self.all_sections):
852 + if ref_section.kind.name in ("PLAIN", "INTRO"):
853 + continue
854 + break
855 + else:
856 + index += 1
857 +
858 + self.all_sections.insert(index, section)
859 +
860 + def append_member_stub(self, stub: 'QAPIDoc.Section') -> None:
861 +
862 + """
863 + Append a stub section after any Member sections.
864 + """
865 + if self._insert_near_kind(QAPIDoc.Kind.MEMBER, stub, True):
866 + return
867 +
868 + # No MEMBER sections present. Insert after INTRO/PLAIN sections.
869 + self._insert_after_intro(stub)
870 +
871 def connect_member(self, member: 'QAPISchemaMember') -> None:
872 if member.name not in self._args:
873 assert member.info
@@ -841,20 +877,10 @@ class QAPIDoc:
877 % (member.role, member.name))
878 # Insert stub documentation section for missing member docs.
879 # TODO: drop when undocumented members are outlawed
844 -
845 - section = QAPIDoc.ArgSection(
880 + stub_section = QAPIDoc.ArgSection(
881 self.info, QAPIDoc.Kind.MEMBER, member.name)
847 - self._args[member.name] = section
848 -
849 - # Determine where to insert stub doc - it should go at the
850 - # end of the members section(s), if any. Note that index 0
851 - # is assumed to be an untagged intro section, even if it is
852 - # empty.
853 - index = 1
854 - if len(self.all_sections) > 1:
855 - while self.all_sections[index].kind == QAPIDoc.Kind.MEMBER:
856 - index += 1
857 - self.all_sections.insert(index, section)
882 + self._args[member.name] = stub_section
883 + self.append_member_stub(stub_section)
884
885 self._args[member.name].connect(member)
886
@@ -889,10 +915,8 @@ class QAPIDoc:
915 )):
916 return
917
892 - # Otherwise, it should go right after the intro. The intro
893 - # is always the first section and is always present (even
894 - # when empty), so we can insert directly at index=1 blindly.
895 - self.all_sections.insert(1, stub)
918 + # Otherwise, it should go right after the intro.
919 + self._insert_after_intro(stub)
920
921 def check_expr(self, expr: QAPIExpression) -> None:
922 if 'command' in expr: