1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>. 4# 5# pylint: disable=R0903,R0913,R0914,R0917 6 7""" 8Classes for navigating through the files that kernel-doc needs to handle 9to generate documentation. 10""" 11 12import argparse 13import logging 14import os 15import re 16 17from kdoc.kdoc_parser import KernelDoc 18from kdoc.xforms_lists import CTransforms 19from kdoc.kdoc_output import OutputFormat 20 21 22class GlobSourceFiles: 23 """ 24 Parse C source code file names and directories via an Interactor. 25 """ 26 27 def __init__(self, srctree=None, valid_extensions=None): 28 """ 29 Initialize valid extensions with a tuple. 30 31 If not defined, assume default C extensions (.c and .h) 32 33 It would be possible to use python's glob function, but it is 34 very slow, and it is not interactive. So, it would wait to read all 35 directories before actually do something. 36 37 So, let's use our own implementation. 38 """ 39 40 if not valid_extensions: 41 self.extensions = (".c", ".h") 42 else: 43 self.extensions = valid_extensions 44 45 self.srctree = srctree 46 47 def _parse_dir(self, dirname): 48 """Internal function to parse files recursively.""" 49 50 with os.scandir(dirname) as obj: 51 for entry in obj: 52 name = os.path.join(dirname, entry.name) 53 54 if entry.is_dir(follow_symlinks=False): 55 yield from self._parse_dir(name) 56 57 if not entry.is_file(): 58 continue 59 60 basename = os.path.basename(name) 61 62 if not basename.endswith(self.extensions): 63 continue 64 65 yield name 66 67 def parse_files(self, file_list, file_not_found_cb): 68 """ 69 Define an iterator to parse all source files from file_list, 70 handling directories if any. 71 """ 72 73 if not file_list: 74 return 75 76 for fname in file_list: 77 if self.srctree: 78 f = os.path.join(self.srctree, fname) 79 else: 80 f = fname 81 82 if os.path.isdir(f): 83 yield from self._parse_dir(f) 84 elif os.path.isfile(f): 85 yield f 86 elif file_not_found_cb: 87 file_not_found_cb(fname) 88 89 90class KernelFiles(): 91 """ 92 Parse kernel-doc tags on multiple kernel source files. 93 94 There are two type of parsers defined here: 95 - self.parse_file(): parses both kernel-doc markups and 96 ``EXPORT_SYMBOL*`` macros; 97 - self.process_export_file(): parses only ``EXPORT_SYMBOL*`` macros. 98 """ 99 100 def warning(self, msg): 101 """Ancillary routine to output a warning and increment error count.""" 102 103 self.config.log.warning(msg) 104 self.errors += 1 105 106 def error(self, msg): 107 """Ancillary routine to output an error and increment error count.""" 108 109 self.config.log.error(msg) 110 self.errors += 1 111 112 def parse_file(self, fname): 113 """ 114 Parse a single Kernel source. 115 """ 116 117 # Prevent parsing the same file twice if results are cached 118 if fname in self.files: 119 return 120 121 doc = KernelDoc(self.config, fname, CTransforms()) 122 export_table, entries = doc.parse_kdoc() 123 124 self.export_table[fname] = export_table 125 126 self.files.add(fname) 127 self.export_files.add(fname) # parse_kdoc() already check exports 128 129 self.results[fname] = entries 130 131 def process_export_file(self, fname): 132 """ 133 Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file. 134 """ 135 136 # Prevent parsing the same file twice if results are cached 137 if fname in self.export_files: 138 return 139 140 doc = KernelDoc(self.config, fname) 141 export_table = doc.parse_export() 142 143 if not export_table: 144 self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}") 145 export_table = set() 146 147 self.export_table[fname] = export_table 148 self.export_files.add(fname) 149 150 def file_not_found_cb(self, fname): 151 """ 152 Callback to warn if a file was not found. 153 """ 154 155 self.error(f"Cannot find file {fname}") 156 157 def __init__(self, verbose=False, out_style=None, 158 werror=False, wreturn=False, wshort_desc=False, 159 wcontents_before_sections=False, 160 logger=None): 161 """ 162 Initialize startup variables and parse all files. 163 """ 164 165 if not verbose: 166 verbose = bool(os.environ.get("KBUILD_VERBOSE", 0)) 167 168 if out_style is None: 169 out_style = OutputFormat() 170 171 if not werror: 172 kcflags = os.environ.get("KCFLAGS", None) 173 if kcflags: 174 match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags) 175 if match: 176 werror = True 177 178 # reading this variable is for backwards compat just in case 179 # someone was calling it with the variable from outside the 180 # kernel's build system 181 kdoc_werror = os.environ.get("KDOC_WERROR", None) 182 if kdoc_werror: 183 werror = kdoc_werror 184 185 # Some variables are global to the parser logic as a whole as they are 186 # used to send control configuration to KernelDoc class. As such, 187 # those variables are read-only inside the KernelDoc. 188 self.config = argparse.Namespace 189 190 self.config.verbose = verbose 191 self.config.werror = werror 192 self.config.wreturn = wreturn 193 self.config.wshort_desc = wshort_desc 194 self.config.wcontents_before_sections = wcontents_before_sections 195 196 if not logger: 197 self.config.log = logging.getLogger("kernel-doc") 198 else: 199 self.config.log = logger 200 201 self.config.warning = self.warning 202 203 self.config.src_tree = os.environ.get("SRCTREE", None) 204 205 # Initialize variables that are internal to KernelFiles 206 207 self.out_style = out_style 208 209 self.errors = 0 210 self.results = {} 211 212 self.files = set() 213 self.export_files = set() 214 self.export_table = {} 215 216 def parse(self, file_list, export_file=None): 217 """ 218 Parse all files. 219 """ 220 221 glob = GlobSourceFiles(srctree=self.config.src_tree) 222 223 for fname in glob.parse_files(file_list, self.file_not_found_cb): 224 self.parse_file(fname) 225 226 for fname in glob.parse_files(export_file, self.file_not_found_cb): 227 self.process_export_file(fname) 228 229 def out_msg(self, fname, name, arg): 230 """ 231 Return output messages from a file name using the output style 232 filtering. 233 234 If output type was not handled by the styler, return None. 235 """ 236 237 # NOTE: we can add rules here to filter out unwanted parts, 238 # although OutputFormat.msg already does that. 239 240 return self.out_style.msg(fname, name, arg) 241 242 def msg(self, enable_lineno=False, export=False, internal=False, 243 symbol=None, nosymbol=None, no_doc_sections=False, 244 filenames=None, export_file=None): 245 """ 246 Interacts over the kernel-doc results and output messages, 247 returning kernel-doc markups on each interaction. 248 """ 249 250 self.out_style.set_config(self.config) 251 252 if not filenames: 253 filenames = sorted(self.results.keys()) 254 255 glob = GlobSourceFiles(srctree=self.config.src_tree) 256 257 for fname in filenames: 258 function_table = set() 259 260 if internal or export: 261 if not export_file: 262 export_file = [fname] 263 264 for f in glob.parse_files(export_file, self.file_not_found_cb): 265 function_table |= self.export_table[f] 266 267 if symbol: 268 for s in symbol: 269 function_table.add(s) 270 271 self.out_style.set_filter(export, internal, symbol, nosymbol, 272 function_table, enable_lineno, 273 no_doc_sections) 274 275 msg = "" 276 if fname not in self.results: 277 self.config.log.warning("No kernel-doc for file %s", fname) 278 continue 279 280 symbols = self.results[fname] 281 self.out_style.set_symbols(symbols) 282 283 for arg in symbols: 284 m = self.out_msg(fname, arg.name, arg) 285 286 if m is None: 287 ln = arg.get("ln", 0) 288 dtype = arg.get('type', "") 289 290 self.config.log.warning("%s:%d Can't handle %s", 291 fname, ln, dtype) 292 else: 293 msg += m 294 295 if msg: 296 yield fname, msg 297