Source code for symbol_needs

# Copyright (c) 2026 inovex GmbH
#
# SPDX-License-Identifier: Apache-2.0

"""Sphinx extension: the symbolneeds directive — one need per API symbol that
satisfies a requirement.

A Doxygen 1.16 ``\\satisfies <UID>`` on a function or macro becomes, in the XML,
a ``<satisfies>`` child of its memberdef. This directive turns each such symbol
into a need (the ``implementation`` role) linked to those requirements (the
``satisfies`` role), so a requirement's page shows what implements it next to
what verifies it.

Loaded only for a document whose registry entry has a ``symbol_needs:`` block,
which also names the Doxygen document whose XML is read.
"""

import xml.etree.ElementTree as ET
from pathlib import Path

from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.statemachine import ViewList
from doxygen_parser import load_group_index, parse_symbol
from input_tracking import _note_input
from needs_fields import depends_field
from rst_builders import build_symbol_need_rst

from sphinx.util import logging

logger = logging.getLogger(__name__)

#: Compound kinds whose XML holds member definitions a symbol need can come
#: from. Pages, directories and requirements hold none.
_MEMBER_COMPOUNDS = ("group", "file", "namespace", "struct", "union", "class")


def _need_names_from_config(config):
    return {
        **getattr(config, "symbolneeds_need_types", {}),
        **getattr(config, "symbolneeds_need_links", {}),
    }


def _compounds(xml_dir, group=None):
    """``[(refid, title)]`` of the compounds to read: one group, or all of them."""
    if group is not None:
        refid = load_group_index(xml_dir).get(group)
        return [(refid, group)] if refid else []
    root = ET.parse(xml_dir / "index.xml").getroot()
    return [
        (c.get("refid"), c.findtext("name", c.get("refid")))
        for c in root.findall("compound")
        if c.get("kind") in _MEMBER_COMPOUNDS
    ]


[docs] def symbol_needs_rst( xml_dir, html_dir, group=None, need_names=None, note_input=None, depends_field=None ): """RST lines for the symbol needs of ``group`` (or the whole project). With no group, the needs are sectioned by compound (group or file), each heading taken from the compound's title. A member is emitted once, where Doxygen defines it, however many compounds list it. ``note_input`` is called with every XML file read. ``depends_field(conditions, subject)`` decides whether a need gets the ``depends_on`` field (`needs_fields.depends_field`); without it, none does. """ xml_dir = Path(xml_dir) lines, seen = [], set() for refid, name in _compounds(xml_dir, group): xml = xml_dir / f"{refid}.xml" if note_input: note_input(xml) if not xml.exists(): logger.warning(f"symbolneeds: compound XML not found: {xml}") continue cdef = ET.parse(xml).getroot().find("compounddef") blocks = [] for md in cdef.iter("memberdef"): if md.find("satisfies") is None or md.get("id") in seen: continue seen.add(md.get("id")) info = parse_symbol(md, html_dir) with_depends = bool(depends_field) and depends_field( info["depends_on"], f"symbolneeds: {info['name']}" ) blocks += build_symbol_need_rst(info, need_names, with_depends).splitlines() blocks.append("") if not blocks: continue if group is None: title = cdef.findtext("title") or name lines += [title, "-" * len(title), ""] lines += blocks return lines
[docs] class SymbolNeedsDirective(Directive): """Emit one need per API symbol carrying a Doxygen ``\\satisfies``. Usage:: .. symbolneeds:: .. symbolneeds:: queue_apis The optional argument is a Doxygen group name; without it every annotated symbol in the project is emitted, sectioned by group or file. """ required_arguments = 0 optional_arguments = 1 has_content = False option_spec = {} def run(self): group = self.arguments[0].strip() if self.arguments else None env = self.state.document.settings.env config = env.app.config xml_dir = Path(config.symbolneeds_xml_dir) _note_input(env, xml_dir / "index.xml") if not (xml_dir / "index.xml").is_file(): msg = f"symbolneeds: Doxygen XML not found: {xml_dir / 'index.xml'}" logger.warning(msg, location=(env.docname, self.lineno)) return [nodes.paragraph(text="[symbolneeds: Doxygen XML not found: index.xml]")] page_prefix = "../" * (len(Path(env.docname).parts) - 1) doxygen_url = config.symbolneeds_doxygen_url html_dir = page_prefix + doxygen_url if doxygen_url else "" try: rst = symbol_needs_rst( xml_dir, html_dir, group, _need_names_from_config(config), note_input=lambda path: _note_input(env, path), depends_field=lambda conditions, subject: depends_field( env, conditions, subject ), ) except (OSError, ET.ParseError) as exc: logger.warning(f"symbolneeds: cannot read {xml_dir}: {exc}") return [nodes.paragraph(text="[symbolneeds: cannot read the Doxygen XML]")] if group is not None and not rst: logger.warning( f"symbolneeds: group '{group}' not found or has no symbol with \\satisfies", location=(env.docname, self.lineno), ) return [] container = nodes.container() self.state.nested_parse( ViewList(rst, source="<symbolneeds>"), self.content_offset, container, match_titles=True, ) return container.children
def setup(app): app.add_config_value("symbolneeds_xml_dir", "", "env") app.add_config_value("symbolneeds_doxygen_url", "", "env") app.add_config_value("symbolneeds_need_types", {"implementation": "impl"}, "env") app.add_config_value("symbolneeds_need_links", {"satisfies": "satisfies"}, "env") app.setup_extension("input_tracking") app.add_directive("symbolneeds", SymbolNeedsDirective) return {"version": "0.1", "parallel_read_safe": True}