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 source = doc.get_source() 207 if source: 208 self.source[fname] = source 209 210 def process_export_file(self, fname): 211 """ 212 Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file. 213 """ 214 215 # Prevent parsing the same file twice if results are cached 216 if fname in self.export_files: 217 return 218 219 doc = KernelDoc(self.config, fname) 220 export_table = doc.parse_export() 221 222 if not export_table: 223 self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}") 224 export_table = set() 225 226 self.export_table[fname] = export_table 227 self.export_files.add(fname) 228 229 def file_not_found_cb(self, fname): 230 """ 231 Callback to warn if a file was not found. 232 """ 233 234 self.error(f"Cannot find file {fname}") 235 236 def __init__(self, verbose=False, out_style=None, xforms=None, 237 werror=False, wreturn=False, wshort_desc=False, 238 wcontents_before_sections=False, 239 yaml_file=None, yaml_content=None, logger=None): 240 """ 241 Initialize startup variables and parse all files. 242 """ 243 244 if not verbose: 245 verbose = bool(os.environ.get("KBUILD_VERBOSE", 0)) 246 247 if out_style is None: 248 out_style = OutputFormat() 249 250 if not werror: 251 kcflags = os.environ.get("KCFLAGS", None) 252 if kcflags: 253 match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags) 254 if match: 255 werror = True 256 257 # reading this variable is for backwards compat just in case 258 # someone was calling it with the variable from outside the 259 # kernel's build system 260 kdoc_werror = os.environ.get("KDOC_WERROR", None) 261 if kdoc_werror: 262 werror = kdoc_werror 263 264 if not logger: 265 logger = logging.getLogger("kernel-doc") 266 else: 267 logger = logger 268 269 # Some variables are global to the parser logic as a whole as they are 270 # used to send control configuration to KernelDoc class. As such, 271 # those variables are read-only inside the KernelDoc. 272 self.config = KdocConfig(verbose, werror, wreturn, wshort_desc, 273 wcontents_before_sections, logger) 274 275 # Override log warning, as we want to count errors 276 self.config.warning = self.warning 277 278 if yaml_file: 279 self.test_file = KDocTestFile(self.config, yaml_file, yaml_content) 280 else: 281 self.test_file = None 282 283 if xforms: 284 self.xforms = xforms 285 else: 286 self.xforms = CTransforms() 287 288 self.config.src_tree = os.environ.get("SRCTREE", None) 289 290 # Initialize variables that are internal to KernelFiles 291 292 self.out_style = out_style 293 self.out_style.set_config(self.config) 294 295 self.errors = 0 296 self.results = {} 297 self.source = {} 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 self.source.get(fname)) 369 370 continue 371 372 self.out_style.set_filter(export, internal, symbol, nosymbol, 373 function_table, enable_lineno, 374 no_doc_sections) 375 376 msg = self.out_style.output_symbols(fname, symbols) 377 if msg: 378 yield fname, msg 379 380 if self.test_file: 381 self.test_file.write() 382