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 verbose = bool(os.environ.get("KBUILD_VERBOSE", 0)) 242 243 if out_style is None: 244 out_style = OutputFormat() 245 246 if not werror: 247 kcflags = os.environ.get("KCFLAGS", None) 248 if kcflags: 249 match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags) 250 if match: 251 werror = True 252 253 # reading this variable is for backwards compat just in case 254 # someone was calling it with the variable from outside the 255 # kernel's build system 256 kdoc_werror = os.environ.get("KDOC_WERROR", None) 257 if kdoc_werror: 258 werror = kdoc_werror 259 260 if not logger: 261 logger = logging.getLogger("kernel-doc") 262 else: 263 logger = logger 264 265 # Some variables are global to the parser logic as a whole as they are 266 # used to send control configuration to KernelDoc class. As such, 267 # those variables are read-only inside the KernelDoc. 268 self.config = KdocConfig(verbose, werror, wreturn, wshort_desc, 269 wcontents_before_sections, logger) 270 271 # Override log warning, as we want to count errors 272 self.config.warning = self.warning 273 274 if yaml_file: 275 self.test_file = KDocTestFile(self.config, yaml_file, yaml_content) 276 else: 277 self.test_file = None 278 279 if xforms: 280 self.xforms = xforms 281 else: 282 self.xforms = CTransforms() 283 284 self.config.src_tree = os.environ.get("SRCTREE", None) 285 286 # Initialize variables that are internal to KernelFiles 287 288 self.out_style = out_style 289 self.out_style.set_config(self.config) 290 291 self.errors = 0 292 self.results = {} 293 294 self.files = set() 295 self.export_files = set() 296 self.export_table = {} 297 298 def parse(self, file_list, export_file=None): 299 """ 300 Parse all files. 301 """ 302 303 glob = GlobSourceFiles(srctree=self.config.src_tree) 304 305 for fname in glob.parse_files(file_list, self.file_not_found_cb): 306 self.parse_file(fname) 307 308 for fname in glob.parse_files(export_file, self.file_not_found_cb): 309 self.process_export_file(fname) 310 311 def out_msg(self, fname, name, arg): 312 """ 313 Return output messages from a file name using the output style 314 filtering. 315 316 If output type was not handled by the styler, return None. 317 """ 318 319 # NOTE: we can add rules here to filter out unwanted parts, 320 # although OutputFormat.msg already does that. 321 322 return self.out_style.msg(fname, name, arg) 323 324 def msg(self, enable_lineno=False, export=False, internal=False, 325 symbol=None, nosymbol=None, no_doc_sections=False, 326 filenames=None, export_file=None): 327 """ 328 Interacts over the kernel-doc results and output messages, 329 returning kernel-doc markups on each interaction. 330 """ 331 332 if not filenames: 333 filenames = sorted(self.results.keys()) 334 335 glob = GlobSourceFiles(srctree=self.config.src_tree) 336 337 for fname in filenames: 338 function_table = set() 339 340 if internal or export: 341 if not export_file: 342 export_file = [fname] 343 344 for f in glob.parse_files(export_file, self.file_not_found_cb): 345 function_table |= self.export_table[f] 346 347 if symbol: 348 for s in symbol: 349 function_table.add(s) 350 351 if fname not in self.results: 352 self.config.log.warning("No kernel-doc for file %s", fname) 353 continue 354 355 symbols = self.results[fname] 356 357 if self.test_file: 358 self.test_file.set_filter(export, internal, symbol, nosymbol, 359 function_table, enable_lineno, 360 no_doc_sections) 361 362 self.test_file.output_symbols(fname, symbols) 363 364 continue 365 366 self.out_style.set_filter(export, internal, symbol, nosymbol, 367 function_table, enable_lineno, 368 no_doc_sections) 369 370 msg = self.out_style.output_symbols(fname, symbols) 371 if msg: 372 yield fname, msg 373 374 if self.test_file: 375 self.test_file.write() 376