| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- #!/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)
|