xref: /llvm-project/libc/utils/docgen/docgen.py (revision 88bcf7283b35b979ace0c6be32736b13f6b771ae)
1#!/usr/bin/env python
2#
3# ====- Generate documentation for libc functions  ------------*- python -*--==#
4#
5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6# See https://llvm.org/LICENSE.txt for license information.
7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8#
9# ==-------------------------------------------------------------------------==#
10from argparse import ArgumentParser, Namespace
11from pathlib import Path
12from typing import Dict
13import os
14import sys
15import yaml
16
17from header import Header
18
19
20class DocgenAPIFormatError(Exception):
21    """Raised on fatal formatting errors with a description of a formatting error"""
22
23
24def check_api(header: Header, api: Dict):
25    """
26    Checks that docgen yaml files are properly formatted. If there are any
27    fatal formatting errors, raises exceptions with error messages useful for
28    fixing formatting. Warnings are printed to stderr on non-fatal formatting
29    errors. The code that runs after ``check_api(api)`` is called expects that
30    ``check_api`` executed without raising formatting exceptions so the yaml
31    matches the formatting specified here.
32
33    The yaml file may contain:
34    * an optional macros object
35    * an optional functions object
36
37    Formatting of ``macros`` and ``functions`` objects
38    ==================================================
39
40    If a macros or functions object is present, then it may contain nested
41    objects. Each of these nested objects should have a name matching a macro
42    or function's name, and each nested object must have the property:
43    ``"c-definition"`` or ``"posix-definition"``.
44
45    Description of properties
46    =========================
47    The defined property is intended to be a reference to a part of the
48    standard that defines the function or macro. For the ``"c-definition"`` property,
49    this should be a C standard section number. For the ``"posix-definition"`` property,
50    this should be a link to the definition.
51
52    :param api: docgen yaml file contents parsed into a dict
53    """
54    errors = []
55    # We require entries to have at least one of these.
56    possible_keys = [
57        "c-definition",
58        "in-latest-posix",
59        "removed-in-posix-2008",
60    ]
61
62    # Validate macros
63    if "macros" in api:
64        if not header.macro_file_exists():
65            print(
66                f"warning: Macro definitions are listed for {header.name}, but no macro file can be found in the directory tree rooted at {header.macros_dir}. All macros will be listed as not implemented.",
67                file=sys.stderr,
68            )
69
70        macros = api["macros"]
71
72        for name, obj in macros.items():
73            if not any(k in obj for k in possible_keys):
74                err = f"error: Macro {name} does not contain at least one required property: {possible_keys}"
75                errors.append(err)
76
77    # Validate functions
78    if "functions" in api:
79        if not header.fns_dir_exists():
80            print(
81                f"warning: Function definitions are listed for {header.name}, but no function implementation directory exists at {header.fns_dir}. All functions will be listed as not implemented.",
82                file=sys.stderr,
83            )
84
85        fns = api["functions"]
86        for name, obj in fns.items():
87            if not any(k in obj for k in possible_keys):
88                err = f"error: function {name} does not contain at least one required property: {possible_keys}"
89                errors.append(err)
90
91    if errors:
92        raise DocgenAPIFormatError("\n".join(errors))
93
94
95def load_api(header: Header) -> Dict:
96    api = header.docgen_yaml.read_text(encoding="utf-8")
97    return yaml.safe_load(api)
98
99
100def print_tbl_dir(name):
101    print(
102        f"""
103.. list-table::
104  :widths: auto
105  :align: center
106  :header-rows: 1
107
108  * - {name}
109    - Implemented
110    - C23 Standard Section
111    - POSIX Docs"""
112    )
113
114
115def print_functions_rst(header: Header, functions: Dict):
116    tbl_hdr = "Functions"
117    print(tbl_hdr)
118    print("=" * len(tbl_hdr))
119
120    print_tbl_dir("Function")
121
122    for name in sorted(functions.keys()):
123        print(f"  * - {name}")
124
125        if header.fns_dir_exists() and header.implements_fn(name):
126            print("    - |check|")
127        else:
128            print("    -")
129
130        if "c-definition" in functions[name]:
131            print(f'    - {functions[name]["c-definition"]}')
132        else:
133            print("    -")
134
135        if "in-latest-posix" in functions[name]:
136            print(
137                f"    - `POSIX.1-2024 <https://pubs.opengroup.org/onlinepubs/9799919799/functions/{name}.html>`__"
138            )
139        elif "removed-in-posix-2008" in functions[name]:
140            print(
141                f"    - `removed in POSIX.1-2008 <https://pubs.opengroup.org/onlinepubs/007904875/functions/{name}.html>`__"
142            )
143        else:
144            print("    -")
145
146
147def print_macros_rst(header: Header, macros: Dict):
148    tbl_hdr = "Macros"
149    print(tbl_hdr)
150    print("=" * len(tbl_hdr))
151
152    print_tbl_dir("Macro")
153
154    for name in sorted(macros.keys()):
155        print(f"  * - {name}")
156
157        if header.macro_file_exists() and header.implements_macro(name):
158            print("    - |check|")
159        else:
160            print("    -")
161
162        if "c-definition" in macros[name]:
163            print(f'    - {macros[name]["c-definition"]}')
164        else:
165            print("    -")
166
167        if "in-latest-posix" in macros[name]:
168            print(
169                f"    - `POSIX.1-2024 <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/{header.name}.html>`__"
170            )
171        else:
172            print("    -")
173    print()
174
175
176def print_impl_status_rst(header: Header, api: Dict):
177    if os.sep in header.name:
178        print(".. include:: ../../check.rst\n")
179    else:
180        print(".. include:: ../check.rst\n")
181
182    print("=" * len(header.name))
183    print(header.name)
184    print("=" * len(header.name))
185    print()
186
187    # the macro and function sections are both optional
188    if "macros" in api:
189        print_macros_rst(header, api["macros"])
190
191    if "functions" in api:
192        print_functions_rst(header, api["functions"])
193
194
195# This code implicitly relies on docgen.py being in the same dir as the yaml
196# files and is likely to need to be fixed when re-integrating docgen into
197# hdrgen.
198def get_choices() -> list:
199    choices = []
200    for path in Path(__file__).parent.rglob("*.yaml"):
201        fname = path.with_suffix(".h").name
202        if path.parent != Path(__file__).parent:
203            fname = path.parent.name + os.sep + fname
204        choices.append(fname)
205    return choices
206
207
208def parse_args() -> Namespace:
209    parser = ArgumentParser()
210    parser.add_argument("header_name", choices=get_choices())
211    return parser.parse_args()
212
213
214if __name__ == "__main__":
215    args = parse_args()
216    header = Header(args.header_name)
217    api = load_api(header)
218    check_api(header, api)
219
220    print_impl_status_rst(header, api)
221