| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: GPL-2.0 | |
| # Copyright (c) 2016-2025 by Mauro Carvalho Chehab <mchehab@kernel.org>. | |
| # pylint: disable=R0912,R0915 | |
| """ | |
| Parse a source file or header, creating ReStructured Text cross references. | |
| It accepts an optional file to change the default symbol reference or to | |
| suppress symbols from the output. | |
| It is capable of identifying ``define``, function, ``struct``, ``typedef``, | |
| ``enum`` and ``enum`` symbols and create cross-references for all of them. | |
| It is also capable of distinguish #define used for specifying a Linux | |
| ioctl. | |
| The optional rules file contains a set of rules like:: | |
| ignore ioctl VIDIOC_ENUM_FMT | |
| replace ioctl VIDIOC_DQBUF vidioc_qbuf | |
| replace define V4L2_EVENT_MD_FL_HAVE_FRAME_SEQ :c:type:`v4l2_event_motion_det` | |
| """ | |
| import os | |
| import re | |
| import sys | |
| class ParseDataStructs: | |
| """ | |
| Creates an enriched version of a Kernel header file with cross-links | |
| to each C data structure type. | |
| It is meant to allow having a more comprehensive documentation, where | |
| uAPI headers will create cross-reference links to the code. | |
| It is capable of identifying ``define``, function, ``struct``, ``typedef``, | |
| ``enum`` and ``enum`` symbols and create cross-references for all of them. | |
| It is also capable of distinguish #define used for specifying a Linux | |
| ioctl. | |
| By default, it create rules for all symbols and defines, but it also | |
| allows parsing an exception file. Such file contains a set of rules | |
| using the syntax below: | |
| 1. Ignore rules:: | |
| ignore <type> <symbol>` | |
| Removes the symbol from reference generation. | |
| 2. Replace rules:: | |
| replace <type> <old_symbol> <new_reference> | |
| Replaces how old_symbol with a new reference. The new_reference can be: | |
| - A simple symbol name; | |
| - A full Sphinx reference. | |
| 3. Namespace rules:: | |
| namespace <namespace> | |
| Sets C namespace to be used during cross-reference generation. Can | |
| be overridden by replace rules. | |
| On ignore and replace rules, ``<type>`` can be: | |
| - ``ioctl``: for defines that end with ``_IO*``, e.g. ioctl definitions | |
| - ``define``: for other defines | |
| - ``symbol``: for symbols defined within enums; | |
| - ``typedef``: for typedefs; | |
| - ``enum``: for the name of a non-anonymous enum; | |
| - ``struct``: for structs. | |
| Examples:: | |
| ignore define __LINUX_MEDIA_H | |
| ignore ioctl VIDIOC_ENUM_FMT | |
| replace ioctl VIDIOC_DQBUF vidioc_qbuf | |
| replace define V4L2_EVENT_MD_FL_HAVE_FRAME_SEQ :c:type:`v4l2_event_motion_det` | |
| namespace MC | |
| """ | |
| #: Parser regex with multiple ways to capture enums. | |
| RE_ENUMS = [ | |
| re.compile(r"^\s*enum\s+([\w_]+)\s*\{"), | |
| re.compile(r"^\s*enum\s+([\w_]+)\s*$"), | |
| re.compile(r"^\s*typedef\s*enum\s+([\w_]+)\s*\{"), | |
| re.compile(r"^\s*typedef\s*enum\s+([\w_]+)\s*$"), | |
| ] | |
| #: Parser regex with multiple ways to capture structs. | |
| RE_STRUCTS = [ | |
| re.compile(r"^\s*struct\s+([_\w][\w\d_]+)\s*\{"), | |
| re.compile(r"^\s*struct\s+([_\w][\w\d_]+)$"), | |
| re.compile(r"^\s*typedef\s*struct\s+([_\w][\w\d_]+)\s*\{"), | |
| re.compile(r"^\s*typedef\s*struct\s+([_\w][\w\d_]+)$"), | |
| ] | |
| # NOTE: the original code was written a long time before Sphinx C | |
| # domain to have multiple namespaces. To avoid to much turn at the | |
| # existing hyperlinks, the code kept using "c:type" instead of the | |
| # right types. To change that, we need to change the types not only | |
| # here, but also at the uAPI media documentation. | |
| #: Dictionary containing C type identifiers to be transformed. | |
| DEF_SYMBOL_TYPES = { | |
| "ioctl": { | |
| "prefix": "\\ ", | |
| "suffix": "\\ ", | |
| "ref_type": ":ref", | |
| "description": "IOCTL Commands", | |
| }, | |
| "define": { | |
| "prefix": "\\ ", | |
| "suffix": "\\ ", | |
| "ref_type": ":ref", | |
| "description": "Macros and Definitions", | |
| }, | |
| # We're calling each definition inside an enum as "symbol" | |
| "symbol": { | |
| "prefix": "\\ ", | |
| "suffix": "\\ ", | |
| "ref_type": ":ref", | |
| "description": "Enumeration values", | |
| }, | |
| "typedef": { | |
| "prefix": "\\ ", | |
| "suffix": "\\ ", | |
| "ref_type": ":c:type", | |
| "description": "Type Definitions", | |
| }, | |
| # This is the description of the enum itself | |
| "enum": { | |
| "prefix": "\\ ", | |
| "suffix": "\\ ", | |
| "ref_type": ":c:type", | |
| "description": "Enumerations", | |
| }, | |
| "struct": { | |
| "prefix": "\\ ", | |
| "suffix": "\\ ", | |
| "ref_type": ":c:type", | |
| "description": "Structures", | |
| }, | |
| } | |
| def __init__(self, debug: bool = False): | |
| """Initialize internal vars""" | |
| self.debug = debug | |
| self.data = "" | |
| self.symbols = {} | |
| self.namespace = None | |
| self.ignore = [] | |
| self.replace = [] | |
| for symbol_type in self.DEF_SYMBOL_TYPES: | |
| self.symbols[symbol_type] = {} | |
| def read_exceptions(self, fname: str): | |
| """ | |
| Read an optional exceptions file, used to override defaults. | |
| """ | |
| if not fname: | |
| return | |
| name = os.path.basename(fname) | |
| with open(fname, "r", encoding="utf-8", errors="backslashreplace") as f: | |
| for ln, line in enumerate(f): | |
| ln += 1 | |
| line = line.strip() | |
| if not line or line.startswith("#"): | |
| continue | |
| # ignore rules | |
| match = re.match(r"^ignore\s+(\w+)\s+(\S+)", line) | |
| if match: | |
| self.ignore.append((ln, match.group(1), match.group(2))) | |
| continue | |
| # replace rules | |
| match = re.match(r"^replace\s+(\S+)\s+(\S+)\s+(\S+)", line) | |
| if match: | |
| self.replace.append((ln, match.group(1), match.group(2), | |
| match.group(3))) | |
| continue | |
| match = re.match(r"^namespace\s+(\S+)", line) | |
| if match: | |
| self.namespace = match.group(1) | |
| continue | |
| sys.exit(f"{name}:{ln}: invalid line: {line}") | |
| def apply_exceptions(self): | |
| """ | |
| Process exceptions file with rules to ignore or replace references. | |
| """ | |
| # Handle ignore rules | |
| for ln, c_type, symbol in self.ignore: | |
| if c_type not in self.DEF_SYMBOL_TYPES: | |
| sys.exit(f"{name}:{ln}: {c_type} is invalid") | |
| d = self.symbols[c_type] | |
| if symbol in d: | |
| del d[symbol] | |
| # Handle replace rules | |
| for ln, c_type, old, new in self.replace: | |
| if c_type not in self.DEF_SYMBOL_TYPES: | |
| sys.exit(f"{name}:{ln}: {c_type} is invalid") | |
| reftype = None | |
| # Parse reference type when the type is specified | |
| match = re.match(r"^\:c\:(\w+)\:\`(.+)\`", new) | |
| if match: | |
| reftype = f":c:{match.group(1)}" | |
| new = match.group(2) | |
| else: | |
| match = re.search(r"(\:ref)\:\`(.+)\`", new) | |
| if match: | |
| reftype = match.group(1) | |
| new = match.group(2) | |
| # If the replacement rule doesn't have a type, get default | |
| if not reftype: | |
| reftype = self.DEF_SYMBOL_TYPES[c_type].get("ref_type") | |
| if not reftype: | |
| reftype = self.DEF_SYMBOL_TYPES[c_type].get("real_type") | |
| new_ref = f"{reftype}:`{old} <{new}>`" | |
| # Change self.symbols to use the replacement rule | |
| if old in self.symbols[c_type]: | |
| (_, ln) = self.symbols[c_type][old] | |
| self.symbols[c_type][old] = (new_ref, ln) | |
| else: | |
| print(f"{name}:{ln}: Warning: can't find {old} {c_type}") | |
| def store_type(self, ln, symbol_type: str, symbol: str, | |
| ref_name: str = None, replace_underscores: bool = True): | |
| """ | |
| Store a new symbol at self.symbols under symbol_type. | |
| By default, underscores are replaced by ``-``. | |
| """ | |
| defs = self.DEF_SYMBOL_TYPES[symbol_type] | |
| prefix = defs.get("prefix", "") | |
| suffix = defs.get("suffix", "") | |
| ref_type = defs.get("ref_type") | |
| # Determine ref_link based on symbol type | |
| if ref_type or self.namespace: | |
| if not ref_name: | |
| ref_name = symbol.lower() | |
| # c-type references don't support hash | |
| if ref_type == ":ref" and replace_underscores: | |
| ref_name = ref_name.replace("_", "-") | |
| # C domain references may have namespaces | |
| if ref_type.startswith(":c:"): | |
| if self.namespace: | |
| ref_name = f"{self.namespace}.{ref_name}" | |
| if ref_type: | |
| ref_link = f"{ref_type}:`{symbol} <{ref_name}>`" | |
| else: | |
| ref_link = f"`{symbol} <{ref_name}>`" | |
| else: | |
| ref_link = symbol | |
| self.symbols[symbol_type][symbol] = (f"{prefix}{ref_link}{suffix}", ln) | |
| def store_line(self, line): | |
| """ | |
| Store a line at self.data, properly indented. | |
| """ | |
| line = " " + line.expandtabs() | |
| self.data += line.rstrip(" ") | |
| def parse_file(self, file_in: str, exceptions: str = None): | |
| """ | |
| Read a C source file and get identifiers. | |
| """ | |
| self.data = "" | |
| is_enum = False | |
| is_comment = False | |
| multiline = "" | |
| self.read_exceptions(exceptions) | |
| with open(file_in, "r", | |
| encoding="utf-8", errors="backslashreplace") as f: | |
| for line_no, line in enumerate(f): | |
| self.store_line(line) | |
| line = line.strip("\n") | |
| # Handle continuation lines | |
| if line.endswith(r"\\"): | |
| multiline += line[-1] | |
| continue | |
| if multiline: | |
| line = multiline + line | |
| multiline = "" | |
| # Handle comments. They can be multilined | |
| if not is_comment: | |
| if re.search(r"/\*.*", line): | |
| is_comment = True | |
| else: | |
| # Strip C99-style comments | |
| line = re.sub(r"(//.*)", "", line) | |
| if is_comment: | |
| if re.search(r".*\*/", line): | |
| is_comment = False | |
| else: | |
| multiline = line | |
| continue | |
| # At this point, line variable may be a multilined statement, | |
| # if lines end with \ or if they have multi-line comments | |
| # With that, it can safely remove the entire comments, | |
| # and there's no need to use re.DOTALL for the logic below | |
| line = re.sub(r"(/\*.*\*/)", "", line) | |
| if not line.strip(): | |
| continue | |
| # It can be useful for debug purposes to print the file after | |
| # having comments stripped and multi-lines grouped. | |
| if self.debug > 1: | |
| print(f"line {line_no + 1}: {line}") | |
| # Now the fun begins: parse each type and store it. | |
| # We opted for a two parsing logic here due to: | |
| # 1. it makes easier to debug issues not-parsed symbols; | |
| # 2. we want symbol replacement at the entire content, not | |
| # just when the symbol is detected. | |
| if is_enum: | |
| match = re.match(r"^\s*([_\w][\w\d_]+)\s*[\,=]?", line) | |
| if match: | |
| self.store_type(line_no, "symbol", match.group(1)) | |
| if "}" in line: | |
| is_enum = False | |
| continue | |
| match = re.match(r"^\s*#\s*define\s+([\w_]+)\s+_IO", line) | |
| if match: | |
| self.store_type(line_no, "ioctl", match.group(1), | |
| replace_underscores=False) | |
| continue | |
| match = re.match(r"^\s*#\s*define\s+([\w_]+)(\s+|$)", line) | |
| if match: | |
| self.store_type(line_no, "define", match.group(1)) | |
| continue | |
| match = re.match(r"^\s*typedef\s+([_\w][\w\d_]+)\s+(.*)\s+([_\w][\w\d_]+);", | |
| line) | |
| if match: | |
| name = match.group(2).strip() | |
| symbol = match.group(3) | |
| self.store_type(line_no, "typedef", symbol, ref_name=name) | |
| continue | |
| for re_enum in self.RE_ENUMS: | |
| match = re_enum.match(line) | |
| if match: | |
| self.store_type(line_no, "enum", match.group(1)) | |
| is_enum = True | |
| break | |
| for re_struct in self.RE_STRUCTS: | |
| match = re_struct.match(line) | |
| if match: | |
| self.store_type(line_no, "struct", match.group(1)) | |
| break | |
| self.apply_exceptions() | |
| def debug_print(self): | |
| """ | |
| Print debug information containing the replacement rules per symbol. | |
| To make easier to check, group them per type. | |
| """ | |
| if not self.debug: | |
| return | |
| for c_type, refs in self.symbols.items(): | |
| if not refs: # Skip empty dictionaries | |
| continue | |
| print(f"{c_type}:") | |
| for symbol, (ref, ln) in sorted(refs.items()): | |
| print(f" #{ln:<5d} {symbol} -> {ref}") | |
| print() | |
| def gen_output(self): | |
| """Write the formatted output to a file.""" | |
| # Avoid extra blank lines | |
| text = re.sub(r"\s+$", "", self.data) + "\n" | |
| text = re.sub(r"\n\s+\n", "\n\n", text) | |
| # Escape Sphinx special characters | |
| text = re.sub(r"([\_\`\*\<\>\&\\\\:\/\|\%\$\#\{\}\~\^])", r"\\\1", text) | |
| # Source uAPI files may have special notes. Use bold font for them | |
| text = re.sub(r"DEPRECATED", "**DEPRECATED**", text) | |
| # Delimiters to catch the entire symbol after escaped | |
| start_delim = r"([ \n\t\(=\*\@])" | |
| end_delim = r"(\s|,|\\=|\\:|\;|\)|\}|\{)" | |
| # Process all reference types | |
| for ref_dict in self.symbols.values(): | |
| for symbol, (replacement, _) in ref_dict.items(): | |
| symbol = re.escape(re.sub(r"([\_\`\*\<\>\&\\\\:\/])", r"\\\1", symbol)) | |
| text = re.sub(fr'{start_delim}{symbol}{end_delim}', | |
| fr'\1{replacement}\2', text) | |
| # Remove "\ " where not needed: before spaces and at the end of lines | |
| text = re.sub(r"\\ ([\n ])", r"\1", text) | |
| text = re.sub(r" \\ ", " ", text) | |
| return text | |
| def gen_toc(self): | |
| """ | |
| Create a list of symbols to be part of a TOC contents table. | |
| """ | |
| text = [] | |
| # Sort symbol types per description | |
| symbol_descriptions = [] | |
| for k, v in self.DEF_SYMBOL_TYPES.items(): | |
| symbol_descriptions.append((v['description'], k)) | |
| symbol_descriptions.sort() | |
| # Process each category | |
| for description, c_type in symbol_descriptions: | |
| refs = self.symbols[c_type] | |
| if not refs: # Skip empty categories | |
| continue | |
| text.append(f"{description}") | |
| text.append("-" * len(description)) | |
| text.append("") | |
| # Sort symbols alphabetically | |
| for symbol, (ref, ln) in sorted(refs.items()): | |
| text.append(f"- LINENO_{ln}: {ref}") | |
| text.append("") # Add empty line between categories | |
| return "\n".join(text) | |
| def write_output(self, file_in: str, file_out: str, toc: bool): | |
| """ | |
| Write a ReST output file. | |
| """ | |
| title = os.path.basename(file_in) | |
| if toc: | |
| text = self.gen_toc() | |
| else: | |
| text = self.gen_output() | |
| with open(file_out, "w", encoding="utf-8", errors="backslashreplace") as f: | |
| f.write(".. -*- coding: utf-8; mode: rst -*-\n\n") | |
| f.write(f"{title}\n") | |
| f.write("=" * len(title) + "\n\n") | |
| if not toc: | |
| f.write(".. parsed-literal::\n\n") | |
| f.write(text) |