1"""
2Copyright (c) 2021 Nordic Semiconductor ASA
3Copyright (c) 2024 The Linux Foundation
4SPDX-License-Identifier: Apache-2.0
5"""
6
7import concurrent.futures
8import os
9from typing import Any
10
11import doxmlparser
12from docutils import nodes
13from docutils.parsers.rst import directives
14from doxmlparser.compound import DoxCompoundKind, DoxMemberKind
15from sphinx import addnodes
16from sphinx.application import Sphinx
17from sphinx.domains.c import CXRefRole
18from sphinx.transforms.post_transforms import SphinxPostTransform
19from sphinx.util import logging
20from sphinx.util.docutils import SphinxDirective
21
22logger = logging.getLogger(__name__)
23
24
25KIND_D2S = {
26    DoxMemberKind.DEFINE: "macro",
27    DoxMemberKind.VARIABLE: "var",
28    DoxMemberKind.TYPEDEF: "type",
29    DoxMemberKind.ENUM: "enum",
30    DoxMemberKind.FUNCTION: "func",
31}
32
33
34class DoxygenGroupDirective(SphinxDirective):
35    has_content = False
36    required_arguments = 1
37    optional_arguments = 0
38    option_spec = {
39        "project": directives.unchanged,
40    }
41
42    def run(self):
43
44        desc_node = addnodes.desc()
45        desc_node["domain"] = "c"
46        desc_node["objtype"] = "group"
47
48        title_signode = addnodes.desc_signature()
49        group_xref = addnodes.pending_xref(
50            "",
51            refdomain="c",
52            reftype="group",
53            reftarget=self.arguments[0],
54            refwarn=True,
55            project=self.options.get("project")
56        )
57        group_xref += nodes.Text(self.arguments[0])
58        title_signode += group_xref
59
60        desc_node.append(title_signode)
61
62        return [desc_node]
63
64
65class DoxygenReferencer(SphinxPostTransform):
66    """Mapping between Doxygen memberdef kind and Sphinx kinds"""
67
68    default_priority = 5
69
70    def run(self, **kwargs: Any) -> None:
71        for node in self.document.traverse(addnodes.pending_xref):
72            if node.get("refdomain") != "c":
73                continue
74
75            reftype = node.get("reftype")
76
77            # "member", "data" and "var" are equivalent as per Sphinx documentation for C domain
78            if reftype in ("member", "data"):
79                reftype = "var"
80
81            found_name = None
82            found_id = None
83            for name in self.app.config.doxybridge_projects:
84                entry = self.app.env.doxybridge_cache[name].get(reftype)
85                if not entry:
86                    continue
87
88                reftarget = node.get("reftarget").replace(".", "::").rstrip("()")
89                id = entry.get(reftarget)
90                if not id:
91                    if reftype == "func":
92                        # macros are sometimes referenced as functions, so try that
93                        id = self.app.env.doxybridge_cache[name].get("macro").get(reftarget)
94                        if not id:
95                            continue
96                    else:
97                        continue
98
99                found_name = name
100                found_id = id
101                break
102
103            if not found_name or not found_id:
104                continue
105
106            if reftype in ("struct", "union", "group"):
107                doxygen_target = f"{id}.html"
108            else:
109                split = found_id.split("_")
110                doxygen_target = f"{'_'.join(split[:-1])}.html#{split[-1][1:]}"
111
112            doxygen_target = (
113                str(self.app.config.doxybridge_projects[found_name]) + "/html/" + doxygen_target
114            )
115
116            doc_dir = os.path.dirname(self.document.get("source"))
117            doc_dest = os.path.join(
118                self.app.outdir,
119                os.path.relpath(doc_dir, self.app.srcdir),
120            )
121            rel_uri = os.path.relpath(doxygen_target, doc_dest)
122
123            refnode = nodes.reference("", "", internal=True, refuri=rel_uri, reftitle="")
124
125            refnode.append(node[0].deepcopy())
126
127            if reftype == "group":
128                refnode["classes"].append("doxygroup")
129                title = self.app.env.doxybridge_group_titles[found_name].get(reftarget, "group")
130                refnode[0] = nodes.Text(title)
131
132            node.replace_self([refnode])
133
134
135def parse_members(sectiondef):
136    cache = {}
137
138    for memberdef in sectiondef.get_memberdef():
139        kind = KIND_D2S.get(memberdef.get_kind())
140        if not kind:
141            continue
142
143        id = memberdef.get_id()
144        if memberdef.get_kind() == DoxMemberKind.VARIABLE:
145            name = memberdef.get_qualifiedname() or memberdef.get_name()
146        else:
147            name = memberdef.get_name()
148
149        cache.setdefault(kind, {})[name] = id
150
151        if memberdef.get_kind() == DoxMemberKind.ENUM:
152            for enumvalue in memberdef.get_enumvalue():
153                enumname = enumvalue.get_name()
154                enumid = enumvalue.get_id()
155                cache.setdefault("enumerator", {})[enumname] = enumid
156
157    return cache
158
159
160def parse_sections(compounddef):
161    cache = {}
162
163    for sectiondef in compounddef.get_sectiondef():
164        members = parse_members(sectiondef)
165        for kind, data in members.items():
166            cache.setdefault(kind, {}).update(data)
167
168    return cache
169
170
171def parse_compound(inDirName, baseName) -> dict:
172    rootObj = doxmlparser.compound.parse(inDirName + "/" + baseName + ".xml", True)
173    cache = {}
174    group_titles = {}
175
176    for compounddef in rootObj.get_compounddef():
177        name = compounddef.get_compoundname()
178        id = compounddef.get_id()
179        kind = None
180        if compounddef.get_kind() == DoxCompoundKind.STRUCT:
181            kind = "struct"
182        elif compounddef.get_kind() == DoxCompoundKind.UNION:
183            kind = "union"
184        elif compounddef.get_kind() == DoxCompoundKind.GROUP:
185            kind = "group"
186            group_titles[name] = compounddef.get_title()
187
188        if kind:
189            cache.setdefault(kind, {})[name] = id
190
191        sections = parse_sections(compounddef)
192        for kind, data in sections.items():
193            cache.setdefault(kind, {}).update(data)
194
195    return cache, group_titles
196
197
198def parse_index(app: Sphinx, name, inDirName):
199    rootObj = doxmlparser.index.parse(inDirName + "/index.xml", True)
200    compounds = rootObj.get_compound()
201
202    with concurrent.futures.ProcessPoolExecutor() as executor:
203        futures = [
204            executor.submit(parse_compound, inDirName, compound.get_refid())
205            for compound in compounds
206        ]
207        for future in concurrent.futures.as_completed(futures):
208            cache, group_titles = future.result()
209            for kind, data in cache.items():
210                app.env.doxybridge_cache[name].setdefault(kind, {}).update(data)
211            app.env.doxybridge_group_titles[name].update(group_titles)
212
213
214def doxygen_parse(app: Sphinx) -> None:
215    if not hasattr(app.env, "doxybridge_cache"):
216        app.env.doxybridge_cache = dict()
217
218    if not hasattr(app.env, "doxybridge_group_titles"):
219        app.env.doxybridge_group_titles = dict()
220
221    for project, path in app.config.doxybridge_projects.items():
222        if project in app.env.doxygen_input_changed and not app.env.doxygen_input_changed[project]:
223            return
224
225        app.env.doxybridge_cache[project] = {
226            "macro": {},
227            "var": {},
228            "type": {},
229            "enum": {},
230            "enumerator": {},
231            "func": {},
232            "union": {},
233            "struct": {},
234            "group": {},
235        }
236
237        app.env.doxybridge_group_titles[project] = dict()
238
239        parse_index(app, project, str(path / "xml"))
240
241
242def setup(app: Sphinx) -> dict[str, Any]:
243    app.add_config_value("doxybridge_projects", None, "env")
244
245    app.add_directive("doxygengroup", DoxygenGroupDirective)
246
247    app.add_role_to_domain("c", "group", CXRefRole())
248
249    app.add_post_transform(DoxygenReferencer)
250    app.connect("builder-inited", doxygen_parse)
251
252    return {
253        "version": "0.1",
254        "parallel_read_safe": True,
255        "parallel_write_safe": True,
256    }
257