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 19 20 21class GlobSourceFiles: 22 """ 23 Parse C source code file names and directories via an Interactor. 24 """ 25 26 def __init__(self, srctree=None, valid_extensions=None): 27 """ 28 Initialize valid extensions with a tuple. 29 30 If not defined, assume default C extensions (.c and .h) 31 32 It would be possible to use python's glob function, but it is 33 very slow, and it is not interactive. So, it would wait to read all 34 directories before actually do something. 35 36 So, let's use our own implementation. 37 """ 38 39 if not valid_extensions: 40 self.extensions = (".c", ".h") 41 else: 42 self.extensions = valid_extensions 43 44 self.srctree = srctree 45 46 def _parse_dir(self, dirname): 47 """Internal function to parse files recursively.""" 48 49 with os.scandir(dirname) as obj: 50 for entry in obj: 51 name = os.path.join(dirname, entry.name) 52 53 if entry.is_dir(follow_symlinks=False): 54 yield from self._parse_dir(name) 55 56 if not entry.is_file(): 57 continue 58 59 basename = os.path.basename(name) 60 61 if not basename.endswith(self.extensions): 62 continue 63 64 yield name 65 66 def parse_files(self, file_list, file_not_found_cb): 67 """ 68 Define an iterator to parse all source files from file_list, 69 handling directories if any. 70 """ 71 72 if not file_list: 73 return 74 75 for fname in file_list: 76 if self.srctree: 77 f = os.path.join(self.srctree, fname) 78 else: 79 f = fname 80 81 if os.path.isdir(f): 82 yield from self._parse_dir(f) 83 elif os.path.isfile(f): 84 yield f 85 elif file_not_found_cb: 86 file_not_found_cb(fname) 87 88 89class KdocConfig(): 90 """ 91 Stores all configuration attributes that kdoc_parser and kdoc_output 92 needs. 93 """ 94 def __init__(self, verbose=False, werror=False, wreturn=False, 95 wshort_desc=False, wcontents_before_sections=False, 96 logger=None): 97 98 self.verbose = verbose 99 self.werror = werror 100 self.wreturn = wreturn 101 self.wshort_desc = wshort_desc 102 self.wcontents_before_sections = wcontents_before_sections 103 104 if logger: 105 self.log = logger 106 else: 107 self.log = logging.getLogger(__file__) 108 109 self.warning = self.log.warning 110 111class KernelFiles(): 112 """ 113 Parse kernel-doc tags on multiple kernel source files. 114 115 This is the main entry point to run kernel-doc. This class is initialized 116 using a series of optional arguments: 117 118 ``verbose`` 119 If True, enables kernel-doc verbosity. Default: False. 120 121 ``out_style`` 122 Class to be used to format output. If None (default), 123 only report errors. 124 125 ``xforms`` 126 Transforms to be applied to C prototypes and data structs. 127 If not specified, defaults to xforms = CFunction() 128 129 ``werror`` 130 If True, treat warnings as errors, retuning an error code on warnings. 131 132 Default: False. 133 134 ``wreturn`` 135 If True, warns about the lack of a return markup on functions. 136 137 Default: False. 138 ``wshort_desc`` 139 If True, warns if initial short description is missing. 140 141 Default: False. 142 143 ``wcontents_before_sections`` 144 If True, warn if there are contents before sections (deprecated). 145 This option is kept just for backward-compatibility, but it does 146 nothing, neither here nor at the original Perl script. 147 148 Default: False. 149 150 ``logger`` 151 Optional logger class instance. 152 153 If not specified, defaults to use: ``logging.getLogger("kernel-doc")`` 154 155 Note: 156 There are two type of parsers defined here: 157 158 - self.parse_file(): parses both kernel-doc markups and 159 ``EXPORT_SYMBOL*`` macros; 160 - self.process_export_file(): parses only ``EXPORT_SYMBOL*`` macros. 161 """ 162 163 def warning(self, msg): 164 """Ancillary routine to output a warning and increment error count.""" 165 166 self.config.log.warning(msg) 167 self.errors += 1 168 169 def error(self, msg): 170 """Ancillary routine to output an error and increment error count.""" 171 172 self.config.log.error(msg) 173 self.errors += 1 174 175 def parse_file(self, fname): 176 """ 177 Parse a single Kernel source. 178 """ 179 180 # Prevent parsing the same file twice if results are cached 181 if fname in self.files: 182 return 183 184 doc = KernelDoc(self.config, fname, self.xforms) 185 export_table, entries = doc.parse_kdoc() 186 187 self.export_table[fname] = export_table 188 189 self.files.add(fname) 190 self.export_files.add(fname) # parse_kdoc() already check exports 191 192 self.results[fname] = entries 193 194 def process_export_file(self, fname): 195 """ 196 Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file. 197 """ 198 199 # Prevent parsing the same file twice if results are cached 200 if fname in self.export_files: 201 return 202 203 doc = KernelDoc(self.config, fname) 204 export_table = doc.parse_export() 205 206 if not export_table: 207 self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}") 208 export_table = set() 209 210 self.export_table[fname] = export_table 211 self.export_files.add(fname) 212 213 def file_not_found_cb(self, fname): 214 """ 215 Callback to warn if a file was not found. 216 """ 217 218 self.error(f"Cannot find file {fname}") 219 220 def __init__(self, verbose=False, out_style=None, xforms=None, 221 werror=False, wreturn=False, wshort_desc=False, 222 wcontents_before_sections=False, 223 logger=None): 224 """ 225 Initialize startup variables and parse all files. 226 """ 227 228 if not verbose: 229 verbose = bool(os.environ.get("KBUILD_VERBOSE", 0)) 230 231 if out_style is None: 232 out_style = OutputFormat() 233 234 if not werror: 235 kcflags = os.environ.get("KCFLAGS", None) 236 if kcflags: 237 match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags) 238 if match: 239 werror = True 240 241 # reading this variable is for backwards compat just in case 242 # someone was calling it with the variable from outside the 243 # kernel's build system 244 kdoc_werror = os.environ.get("KDOC_WERROR", None) 245 if kdoc_werror: 246 werror = kdoc_werror 247 248 if not logger: 249 logger = logging.getLogger("kernel-doc") 250 else: 251 logger = logger 252 253 # Some variables are global to the parser logic as a whole as they are 254 # used to send control configuration to KernelDoc class. As such, 255 # those variables are read-only inside the KernelDoc. 256 self.config = KdocConfig(verbose, werror, wreturn, wshort_desc, 257 wcontents_before_sections, logger) 258 259 # Override log warning, as we want to count errors 260 self.config.warning = self.warning 261 262 if xforms: 263 self.xforms = xforms 264 else: 265 self.xforms = CTransforms() 266 267 self.config.src_tree = os.environ.get("SRCTREE", None) 268 269 # Initialize variables that are internal to KernelFiles 270 271 self.out_style = out_style 272 self.out_style.set_config(self.config) 273 274 self.errors = 0 275 self.results = {} 276 277 self.files = set() 278 self.export_files = set() 279 self.export_table = {} 280 281 def parse(self, file_list, export_file=None): 282 """ 283 Parse all files. 284 """ 285 286 glob = GlobSourceFiles(srctree=self.config.src_tree) 287 288 for fname in glob.parse_files(file_list, self.file_not_found_cb): 289 self.parse_file(fname) 290 291 for fname in glob.parse_files(export_file, self.file_not_found_cb): 292 self.process_export_file(fname) 293 294 def out_msg(self, fname, name, arg): 295 """ 296 Return output messages from a file name using the output style 297 filtering. 298 299 If output type was not handled by the styler, return None. 300 """ 301 302 # NOTE: we can add rules here to filter out unwanted parts, 303 # although OutputFormat.msg already does that. 304 305 return self.out_style.msg(fname, name, arg) 306 307 def msg(self, enable_lineno=False, export=False, internal=False, 308 symbol=None, nosymbol=None, no_doc_sections=False, 309 filenames=None, export_file=None): 310 """ 311 Interacts over the kernel-doc results and output messages, 312 returning kernel-doc markups on each interaction. 313 """ 314 315 if not filenames: 316 filenames = sorted(self.results.keys()) 317 318 glob = GlobSourceFiles(srctree=self.config.src_tree) 319 320 for fname in filenames: 321 function_table = set() 322 323 if internal or export: 324 if not export_file: 325 export_file = [fname] 326 327 for f in glob.parse_files(export_file, self.file_not_found_cb): 328 function_table |= self.export_table[f] 329 330 if symbol: 331 for s in symbol: 332 function_table.add(s) 333 334 self.out_style.set_filter(export, internal, symbol, nosymbol, 335 function_table, enable_lineno, 336 no_doc_sections) 337 338 if fname not in self.results: 339 self.config.log.warning("No kernel-doc for file %s", fname) 340 continue 341 342 symbols = self.results[fname] 343 344 msg = self.out_style.output_symbols(fname, symbols) 345 if msg: 346 yield fname, msg 347