| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: GPL-2.0 | |
| # Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>. | |
| # | |
| # pylint: disable=R0903,R0913,R0914,R0917 | |
| """ | |
| Parse lernel-doc tags on multiple kernel source files. | |
| """ | |
| import argparse | |
| import logging | |
| import os | |
| import re | |
| from kdoc_parser import KernelDoc | |
| from kdoc_output import OutputFormat | |
| class GlobSourceFiles: | |
| """ | |
| Parse C source code file names and directories via an Interactor. | |
| """ | |
| def __init__(self, srctree=None, valid_extensions=None): | |
| """ | |
| Initialize valid extensions with a tuple. | |
| If not defined, assume default C extensions (.c and .h) | |
| It would be possible to use python's glob function, but it is | |
| very slow, and it is not interactive. So, it would wait to read all | |
| directories before actually do something. | |
| So, let's use our own implementation. | |
| """ | |
| if not valid_extensions: | |
| self.extensions = (".c", ".h") | |
| else: | |
| self.extensions = valid_extensions | |
| self.srctree = srctree | |
| def _parse_dir(self, dirname): | |
| """Internal function to parse files recursively""" | |
| with os.scandir(dirname) as obj: | |
| for entry in obj: | |
| name = os.path.join(dirname, entry.name) | |
| if entry.is_dir(follow_symlinks=False): | |
| yield from self._parse_dir(name) | |
| if not entry.is_file(): | |
| continue | |
| basename = os.path.basename(name) | |
| if not basename.endswith(self.extensions): | |
| continue | |
| yield name | |
| def parse_files(self, file_list, file_not_found_cb): | |
| """ | |
| Define an iterator to parse all source files from file_list, | |
| handling directories if any | |
| """ | |
| if not file_list: | |
| return | |
| for fname in file_list: | |
| if self.srctree: | |
| f = os.path.join(self.srctree, fname) | |
| else: | |
| f = fname | |
| if os.path.isdir(f): | |
| yield from self._parse_dir(f) | |
| elif os.path.isfile(f): | |
| yield f | |
| elif file_not_found_cb: | |
| file_not_found_cb(fname) | |
| class KernelFiles(): | |
| """ | |
| Parse kernel-doc tags on multiple kernel source files. | |
| There are two type of parsers defined here: | |
| - self.parse_file(): parses both kernel-doc markups and | |
| EXPORT_SYMBOL* macros; | |
| - self.process_export_file(): parses only EXPORT_SYMBOL* macros. | |
| """ | |
| def warning(self, msg): | |
| """Ancillary routine to output a warning and increment error count""" | |
| self.config.log.warning(msg) | |
| self.errors += 1 | |
| def error(self, msg): | |
| """Ancillary routine to output an error and increment error count""" | |
| self.config.log.error(msg) | |
| self.errors += 1 | |
| def parse_file(self, fname): | |
| """ | |
| Parse a single Kernel source. | |
| """ | |
| # Prevent parsing the same file twice if results are cached | |
| if fname in self.files: | |
| return | |
| doc = KernelDoc(self.config, fname) | |
| export_table, entries = doc.parse_kdoc() | |
| self.export_table[fname] = export_table | |
| self.files.add(fname) | |
| self.export_files.add(fname) # parse_kdoc() already check exports | |
| self.results[fname] = entries | |
| def process_export_file(self, fname): | |
| """ | |
| Parses EXPORT_SYMBOL* macros from a single Kernel source file. | |
| """ | |
| # Prevent parsing the same file twice if results are cached | |
| if fname in self.export_files: | |
| return | |
| doc = KernelDoc(self.config, fname) | |
| export_table = doc.parse_export() | |
| if not export_table: | |
| self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}") | |
| export_table = set() | |
| self.export_table[fname] = export_table | |
| self.export_files.add(fname) | |
| def file_not_found_cb(self, fname): | |
| """ | |
| Callback to warn if a file was not found. | |
| """ | |
| self.error(f"Cannot find file {fname}") | |
| def __init__(self, verbose=False, out_style=None, | |
| werror=False, wreturn=False, wshort_desc=False, | |
| wcontents_before_sections=False, | |
| logger=None): | |
| """ | |
| Initialize startup variables and parse all files | |
| """ | |
| if not verbose: | |
| verbose = bool(os.environ.get("KBUILD_VERBOSE", 0)) | |
| if out_style is None: | |
| out_style = OutputFormat() | |
| if not werror: | |
| kcflags = os.environ.get("KCFLAGS", None) | |
| if kcflags: | |
| match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags) | |
| if match: | |
| werror = True | |
| # reading this variable is for backwards compat just in case | |
| # someone was calling it with the variable from outside the | |
| # kernel's build system | |
| kdoc_werror = os.environ.get("KDOC_WERROR", None) | |
| if kdoc_werror: | |
| werror = kdoc_werror | |
| # Some variables are global to the parser logic as a whole as they are | |
| # used to send control configuration to KernelDoc class. As such, | |
| # those variables are read-only inside the KernelDoc. | |
| self.config = argparse.Namespace | |
| self.config.verbose = verbose | |
| self.config.werror = werror | |
| self.config.wreturn = wreturn | |
| self.config.wshort_desc = wshort_desc | |
| self.config.wcontents_before_sections = wcontents_before_sections | |
| if not logger: | |
| self.config.log = logging.getLogger("kernel-doc") | |
| else: | |
| self.config.log = logger | |
| self.config.warning = self.warning | |
| self.config.src_tree = os.environ.get("SRCTREE", None) | |
| # Initialize variables that are internal to KernelFiles | |
| self.out_style = out_style | |
| self.errors = 0 | |
| self.results = {} | |
| self.files = set() | |
| self.export_files = set() | |
| self.export_table = {} | |
| def parse(self, file_list, export_file=None): | |
| """ | |
| Parse all files | |
| """ | |
| glob = GlobSourceFiles(srctree=self.config.src_tree) | |
| for fname in glob.parse_files(file_list, self.file_not_found_cb): | |
| self.parse_file(fname) | |
| for fname in glob.parse_files(export_file, self.file_not_found_cb): | |
| self.process_export_file(fname) | |
| def out_msg(self, fname, name, arg): | |
| """ | |
| Return output messages from a file name using the output style | |
| filtering. | |
| If output type was not handled by the styler, return None. | |
| """ | |
| # NOTE: we can add rules here to filter out unwanted parts, | |
| # although OutputFormat.msg already does that. | |
| return self.out_style.msg(fname, name, arg) | |
| def msg(self, enable_lineno=False, export=False, internal=False, | |
| symbol=None, nosymbol=None, no_doc_sections=False, | |
| filenames=None, export_file=None): | |
| """ | |
| Interacts over the kernel-doc results and output messages, | |
| returning kernel-doc markups on each interaction | |
| """ | |
| self.out_style.set_config(self.config) | |
| if not filenames: | |
| filenames = sorted(self.results.keys()) | |
| glob = GlobSourceFiles(srctree=self.config.src_tree) | |
| for fname in filenames: | |
| function_table = set() | |
| if internal or export: | |
| if not export_file: | |
| export_file = [fname] | |
| for f in glob.parse_files(export_file, self.file_not_found_cb): | |
| function_table |= self.export_table[f] | |
| if symbol: | |
| for s in symbol: | |
| function_table.add(s) | |
| self.out_style.set_filter(export, internal, symbol, nosymbol, | |
| function_table, enable_lineno, | |
| no_doc_sections) | |
| msg = "" | |
| if fname not in self.results: | |
| self.config.log.warning("No kernel-doc for file %s", fname) | |
| continue | |
| symbols = self.results[fname] | |
| self.out_style.set_symbols(symbols) | |
| for arg in symbols: | |
| m = self.out_msg(fname, arg.name, arg) | |
| if m is None: | |
| ln = arg.get("ln", 0) | |
| dtype = arg.get('type', "") | |
| self.config.log.warning("%s:%d Can't handle %s", | |
| fname, ln, dtype) | |
| else: | |
| msg += m | |
| if msg: | |
| yield fname, msg |