xref: /aosp_15_r20/external/pigweed/pw_docgen/py/pw_docgen/sphinx/kconfig.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2023 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Auto-generate the Kconfig reference in //docs/os/zephyr/kconfig.rst"""
15
16
17import re
18from typing import Iterable
19
20import docutils
21from docutils.core import publish_doctree
22from sphinx.application import Sphinx
23from sphinx.addnodes import document
24
25
26try:
27    import kconfiglib  # type: ignore
28
29    KCONFIGLIB_AVAILABLE = True
30except ImportError:
31    KCONFIGLIB_AVAILABLE = False
32
33
34def rst_to_doctree(rst: str) -> Iterable[docutils.nodes.Node]:
35    """Convert raw reStructuredText into doctree nodes."""
36    # TODO: b/288127315 - Properly resolve references within the rst so that
37    # links are generated more robustly.
38    while ':ref:`module-' in rst:
39        rst = re.sub(
40            r':ref:`module-(.*?)`', r'`\1 <https://pigweed.dev/\1>`_', rst
41        )
42    doctree = publish_doctree(rst)
43    return doctree.children
44
45
46def create_source_paragraph(name_and_loc: str) -> Iterable[docutils.nodes.Node]:
47    """Convert kconfiglib's name and location string into a source code link."""
48    start = name_and_loc.index('pw_')
49    end = name_and_loc.index(':')
50    file_path = name_and_loc[start:end]
51    url = f'https://cs.opensource.google/pigweed/pigweed/+/main:{file_path}'
52    link = f'`//{file_path} <{url}>`_'
53    return rst_to_doctree(f'Source: {link}')
54
55
56def process_node(
57    node: kconfiglib.MenuNode, parent: docutils.nodes.Node
58) -> None:
59    """Recursively generate documentation for the Kconfig nodes."""
60    while node:
61        if node.item == kconfiglib.MENU:
62            name = node.prompt[0]
63            # All auto-generated sections must have an ID or else the
64            # get_secnumber() function in Sphinx's HTML5 writer throws an
65            # IndexError.
66            menu_section = docutils.nodes.section(ids=[name])
67            menu_section += docutils.nodes.title(text=f'{name} options')
68            if node.list:
69                process_node(node.list, menu_section)
70            parent += menu_section
71        elif isinstance(node.item, kconfiglib.Symbol):
72            name = f'CONFIG_{node.item.name}'
73            symbol_section = docutils.nodes.section(ids=[name])
74            symbol_section += docutils.nodes.title(text=name)
75            symbol_section += docutils.nodes.paragraph(
76                text=f'Type: {kconfiglib.TYPE_TO_STR[node.item.type]}'
77            )
78            if node.item.defaults:
79                try:
80                    default_value = node.item.defaults[0][0].str_value
81                    symbol_section += docutils.nodes.paragraph(
82                        text=f'Default value: {default_value}'
83                    )
84                # If the data wasn't found, just contine trying to process
85                # rest of the documentation for the node.
86                except IndexError:
87                    pass
88            if node.item.ranges:
89                try:
90                    low = node.item.ranges[0][0].str_value
91                    high = node.item.ranges[0][1].str_value
92                    symbol_section += docutils.nodes.paragraph(
93                        text=f'Range of valid values: {low} to {high}'
94                    )
95                except IndexError:
96                    pass
97            if node.prompt:
98                try:
99                    symbol_section += docutils.nodes.paragraph(
100                        text=f'Description: {node.prompt[0]}'
101                    )
102                except IndexError:
103                    pass
104            if node.help:
105                symbol_section += rst_to_doctree(node.help)
106            if node.list:
107                process_node(node.list, symbol_section)
108            symbol_section += create_source_paragraph(node.item.name_and_loc)
109            parent += symbol_section
110        # TODO: b/288127315 - Render choices?
111        # elif isinstance(node.item, kconfiglib.Choice):
112        node = node.next
113
114
115def generate_kconfig_reference(
116    app: Sphinx, doctree: document, docname: str
117) -> None:
118    """Parse the Kconfig and kick off the doc generation process."""
119    # Only run this logic on one specific page.
120    if 'docs/os/zephyr/kconfig' not in docname:
121        return
122    # Get the last `section` node in the doc. This is where we'll append the
123    # auto-generated content.
124    for child in doctree.children:
125        if isinstance(child, docutils.nodes.section):
126            root = child
127    file_path = f'{app.srcdir}/Kconfig.zephyr'
128    kconfig = kconfiglib.Kconfig(file_path)
129    # There's no need to process kconfig.top_node (the main menu) or
130    # kconfig.top_node.list (ZEPHYR_PIGWEED_MODULE) because they don't
131    # contain data that should be documented.
132    first_data_node = kconfig.top_node.list.next
133    process_node(first_data_node, root)
134
135
136def setup(app: Sphinx) -> dict[str, bool]:
137    """Initialize the Sphinx extension."""
138    if KCONFIGLIB_AVAILABLE:
139        app.connect('doctree-resolved', generate_kconfig_reference)
140    return {'parallel_read_safe': True, 'parallel_write_safe': True}
141