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 This is the main entry point to run kernel-doc. This class is initialized 95 using a series of optional arguments: 96 97 ``verbose`` 98 If True, enables kernel-doc verbosity. Default: False. 99 100 ``out_style`` 101 Class to be used to format output. If None (default), 102 only report errors. 103 104 ``xforms`` 105 Transforms to be applied to C prototypes and data structs. 106 If not specified, defaults to xforms = CFunction() 107 108 ``werror`` 109 If True, treat warnings as errors, retuning an error code on warnings. 110 111 Default: False. 112 113 ``wreturn`` 114 If True, warns about the lack of a return markup on functions. 115 116 Default: False. 117 ``wshort_desc`` 118 If True, warns if initial short description is missing. 119 120 Default: False. 121 122 ``wcontents_before_sections`` 123 If True, warn if there are contents before sections (deprecated). 124 This option is kept just for backward-compatibility, but it does 125 nothing, neither here nor at the original Perl script. 126 127 Default: False. 128 129 ``logger`` 130 Optional logger class instance. 131 132 If not specified, defaults to use: ``logging.getLogger("kernel-doc")`` 133 134 Note: 135 There are two type of parsers defined here: 136 137 - self.parse_file(): parses both kernel-doc markups and 138 ``EXPORT_SYMBOL*`` macros; 139 - self.process_export_file(): parses only ``EXPORT_SYMBOL*`` macros. 140 """ 141 142 def warning(self, msg): 143 """Ancillary routine to output a warning and increment error count.""" 144 145 self.config.log.warning(msg) 146 self.errors += 1 147 148 def error(self, msg): 149 """Ancillary routine to output an error and increment error count.""" 150 151 self.config.log.error(msg) 152 self.errors += 1 153 154 def parse_file(self, fname): 155 """ 156 Parse a single Kernel source. 157 """ 158 159 # Prevent parsing the same file twice if results are cached 160 if fname in self.files: 161 return 162 163 doc = KernelDoc(self.config, fname, self.xforms) 164 export_table, entries = doc.parse_kdoc() 165 166 self.export_table[fname] = export_table 167 168 self.files.add(fname) 169 self.export_files.add(fname) # parse_kdoc() already check exports 170 171 self.results[fname] = entries 172 173 def process_export_file(self, fname): 174 """ 175 Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file. 176 """ 177 178 # Prevent parsing the same file twice if results are cached 179 if fname in self.export_files: 180 return 181 182 doc = KernelDoc(self.config, fname) 183 export_table = doc.parse_export() 184 185 if not export_table: 186 self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}") 187 export_table = set() 188 189 self.export_table[fname] = export_table 190 self.export_files.add(fname) 191 192 def file_not_found_cb(self, fname): 193 """ 194 Callback to warn if a file was not found. 195 """ 196 197 self.error(f"Cannot find file {fname}") 198 199 def __init__(self, verbose=False, out_style=None, xforms=None, 200 werror=False, wreturn=False, wshort_desc=False, 201 wcontents_before_sections=False, 202 logger=None): 203 """ 204 Initialize startup variables and parse all files. 205 """ 206 207 if not verbose: 208 verbose = bool(os.environ.get("KBUILD_VERBOSE", 0)) 209 210 if out_style is None: 211 out_style = OutputFormat() 212 213 if not werror: 214 kcflags = os.environ.get("KCFLAGS", None) 215 if kcflags: 216 match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags) 217 if match: 218 werror = True 219 220 # reading this variable is for backwards compat just in case 221 # someone was calling it with the variable from outside the 222 # kernel's build system 223 kdoc_werror = os.environ.get("KDOC_WERROR", None) 224 if kdoc_werror: 225 werror = kdoc_werror 226 227 # Some variables are global to the parser logic as a whole as they are 228 # used to send control configuration to KernelDoc class. As such, 229 # those variables are read-only inside the KernelDoc. 230 self.config = argparse.Namespace 231 232 self.config.verbose = verbose 233 self.config.werror = werror 234 self.config.wreturn = wreturn 235 self.config.wshort_desc = wshort_desc 236 self.config.wcontents_before_sections = wcontents_before_sections 237 238 if xforms: 239 self.xforms = xforms 240 else: 241 self.xforms = CTransforms() 242 243 if not logger: 244 self.config.log = logging.getLogger("kernel-doc") 245 else: 246 self.config.log = logger 247 248 self.config.warning = self.warning 249 250 self.config.src_tree = os.environ.get("SRCTREE", None) 251 252 # Initialize variables that are internal to KernelFiles 253 254 self.out_style = out_style 255 256 self.errors = 0 257 self.results = {} 258 259 self.files = set() 260 self.export_files = set() 261 self.export_table = {} 262 263 def parse(self, file_list, export_file=None): 264 """ 265 Parse all files. 266 """ 267 268 glob = GlobSourceFiles(srctree=self.config.src_tree) 269 270 for fname in glob.parse_files(file_list, self.file_not_found_cb): 271 self.parse_file(fname) 272 273 for fname in glob.parse_files(export_file, self.file_not_found_cb): 274 self.process_export_file(fname) 275 276 def out_msg(self, fname, name, arg): 277 """ 278 Return output messages from a file name using the output style 279 filtering. 280 281 If output type was not handled by the styler, return None. 282 """ 283 284 # NOTE: we can add rules here to filter out unwanted parts, 285 # although OutputFormat.msg already does that. 286 287 return self.out_style.msg(fname, name, arg) 288 289 def msg(self, enable_lineno=False, export=False, internal=False, 290 symbol=None, nosymbol=None, no_doc_sections=False, 291 filenames=None, export_file=None): 292 """ 293 Interacts over the kernel-doc results and output messages, 294 returning kernel-doc markups on each interaction. 295 """ 296 297 self.out_style.set_config(self.config) 298 299 if not filenames: 300 filenames = sorted(self.results.keys()) 301 302 glob = GlobSourceFiles(srctree=self.config.src_tree) 303 304 for fname in filenames: 305 function_table = set() 306 307 if internal or export: 308 if not export_file: 309 export_file = [fname] 310 311 for f in glob.parse_files(export_file, self.file_not_found_cb): 312 function_table |= self.export_table[f] 313 314 if symbol: 315 for s in symbol: 316 function_table.add(s) 317 318 self.out_style.set_filter(export, internal, symbol, nosymbol, 319 function_table, enable_lineno, 320 no_doc_sections) 321 322 msg = "" 323 if fname not in self.results: 324 self.config.log.warning("No kernel-doc for file %s", fname) 325 continue 326 327 symbols = self.results[fname] 328 self.out_style.set_symbols(symbols) 329 330 for arg in symbols: 331 m = self.out_msg(fname, arg.name, arg) 332 333 if m is None: 334 ln = arg.get("ln", 0) 335 dtype = arg.get('type', "") 336 337 self.config.log.warning("%s:%d Can't handle %s", 338 fname, ln, dtype) 339 else: 340 msg += m 341 342 if msg: 343 yield fname, msg 344