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