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.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 KernelFiles(): 90 """ 91 Parse kernel-doc tags on multiple kernel source files. 92 93 There are two type of parsers defined here: 94 - self.parse_file(): parses both kernel-doc markups and 95 ``EXPORT_SYMBOL*`` macros; 96 - self.process_export_file(): parses only ``EXPORT_SYMBOL*`` macros. 97 """ 98 99 def warning(self, msg): 100 """Ancillary routine to output a warning and increment error count.""" 101 102 self.config.log.warning(msg) 103 self.errors += 1 104 105 def error(self, msg): 106 """Ancillary routine to output an error and increment error count.""" 107 108 self.config.log.error(msg) 109 self.errors += 1 110 111 def parse_file(self, fname): 112 """ 113 Parse a single Kernel source. 114 """ 115 116 # Prevent parsing the same file twice if results are cached 117 if fname in self.files: 118 return 119 120 doc = KernelDoc(self.config, fname) 121 export_table, entries = doc.parse_kdoc() 122 123 self.export_table[fname] = export_table 124 125 self.files.add(fname) 126 self.export_files.add(fname) # parse_kdoc() already check exports 127 128 self.results[fname] = entries 129 130 def process_export_file(self, fname): 131 """ 132 Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file. 133 """ 134 135 # Prevent parsing the same file twice if results are cached 136 if fname in self.export_files: 137 return 138 139 doc = KernelDoc(self.config, fname) 140 export_table = doc.parse_export() 141 142 if not export_table: 143 self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}") 144 export_table = set() 145 146 self.export_table[fname] = export_table 147 self.export_files.add(fname) 148 149 def file_not_found_cb(self, fname): 150 """ 151 Callback to warn if a file was not found. 152 """ 153 154 self.error(f"Cannot find file {fname}") 155 156 def __init__(self, verbose=False, out_style=None, 157 werror=False, wreturn=False, wshort_desc=False, 158 wcontents_before_sections=False, 159 logger=None): 160 """ 161 Initialize startup variables and parse all files. 162 """ 163 164 if not verbose: 165 verbose = bool(os.environ.get("KBUILD_VERBOSE", 0)) 166 167 if out_style is None: 168 out_style = OutputFormat() 169 170 if not werror: 171 kcflags = os.environ.get("KCFLAGS", None) 172 if kcflags: 173 match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags) 174 if match: 175 werror = True 176 177 # reading this variable is for backwards compat just in case 178 # someone was calling it with the variable from outside the 179 # kernel's build system 180 kdoc_werror = os.environ.get("KDOC_WERROR", None) 181 if kdoc_werror: 182 werror = kdoc_werror 183 184 # Some variables are global to the parser logic as a whole as they are 185 # used to send control configuration to KernelDoc class. As such, 186 # those variables are read-only inside the KernelDoc. 187 self.config = argparse.Namespace 188 189 self.config.verbose = verbose 190 self.config.werror = werror 191 self.config.wreturn = wreturn 192 self.config.wshort_desc = wshort_desc 193 self.config.wcontents_before_sections = wcontents_before_sections 194 195 if not logger: 196 self.config.log = logging.getLogger("kernel-doc") 197 else: 198 self.config.log = logger 199 200 self.config.warning = self.warning 201 202 self.config.src_tree = os.environ.get("SRCTREE", None) 203 204 # Initialize variables that are internal to KernelFiles 205 206 self.out_style = out_style 207 208 self.errors = 0 209 self.results = {} 210 211 self.files = set() 212 self.export_files = set() 213 self.export_table = {} 214 215 def parse(self, file_list, export_file=None): 216 """ 217 Parse all files. 218 """ 219 220 glob = GlobSourceFiles(srctree=self.config.src_tree) 221 222 for fname in glob.parse_files(file_list, self.file_not_found_cb): 223 self.parse_file(fname) 224 225 for fname in glob.parse_files(export_file, self.file_not_found_cb): 226 self.process_export_file(fname) 227 228 def out_msg(self, fname, name, arg): 229 """ 230 Return output messages from a file name using the output style 231 filtering. 232 233 If output type was not handled by the styler, return None. 234 """ 235 236 # NOTE: we can add rules here to filter out unwanted parts, 237 # although OutputFormat.msg already does that. 238 239 return self.out_style.msg(fname, name, arg) 240 241 def msg(self, enable_lineno=False, export=False, internal=False, 242 symbol=None, nosymbol=None, no_doc_sections=False, 243 filenames=None, export_file=None): 244 """ 245 Interacts over the kernel-doc results and output messages, 246 returning kernel-doc markups on each interaction. 247 """ 248 249 self.out_style.set_config(self.config) 250 251 if not filenames: 252 filenames = sorted(self.results.keys()) 253 254 glob = GlobSourceFiles(srctree=self.config.src_tree) 255 256 for fname in filenames: 257 function_table = set() 258 259 if internal or export: 260 if not export_file: 261 export_file = [fname] 262 263 for f in glob.parse_files(export_file, self.file_not_found_cb): 264 function_table |= self.export_table[f] 265 266 if symbol: 267 for s in symbol: 268 function_table.add(s) 269 270 self.out_style.set_filter(export, internal, symbol, nosymbol, 271 function_table, enable_lineno, 272 no_doc_sections) 273 274 msg = "" 275 if fname not in self.results: 276 self.config.log.warning("No kernel-doc for file %s", fname) 277 continue 278 279 symbols = self.results[fname] 280 self.out_style.set_symbols(symbols) 281 282 for arg in symbols: 283 m = self.out_msg(fname, arg.name, arg) 284 285 if m is None: 286 ln = arg.get("ln", 0) 287 dtype = arg.get('type', "") 288 289 self.config.log.warning("%s:%d Can't handle %s", 290 fname, ln, dtype) 291 else: 292 msg += m 293 294 if msg: 295 yield fname, msg 296