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""" 8Parse lernel-doc tags on multiple kernel source files. 9""" 10 11import argparse 12import logging 13import os 14import re 15 16from kdoc_parser import KernelDoc 17from kdoc_output import OutputFormat 18 19 20class GlobSourceFiles: 21 """ 22 Parse C source code file names and directories via an Interactor. 23 """ 24 25 def __init__(self, srctree=None, valid_extensions=None): 26 """ 27 Initialize valid extensions with a tuple. 28 29 If not defined, assume default C extensions (.c and .h) 30 31 It would be possible to use python's glob function, but it is 32 very slow, and it is not interactive. So, it would wait to read all 33 directories before actually do something. 34 35 So, let's use our own implementation. 36 """ 37 38 if not valid_extensions: 39 self.extensions = (".c", ".h") 40 else: 41 self.extensions = valid_extensions 42 43 self.srctree = srctree 44 45 def _parse_dir(self, dirname): 46 """Internal function to parse files recursively""" 47 48 with os.scandir(dirname) as obj: 49 for entry in obj: 50 name = os.path.join(dirname, entry.name) 51 52 if entry.is_dir(): 53 yield from self._parse_dir(name) 54 55 if not entry.is_file(): 56 continue 57 58 basename = os.path.basename(name) 59 60 if not basename.endswith(self.extensions): 61 continue 62 63 yield name 64 65 def parse_files(self, file_list, file_not_found_cb): 66 """ 67 Define an interator to parse all source files from file_list, 68 handling directories if any 69 """ 70 71 if not file_list: 72 return 73 74 for fname in file_list: 75 if self.srctree: 76 f = os.path.join(self.srctree, fname) 77 else: 78 f = fname 79 80 if os.path.isdir(f): 81 yield from self._parse_dir(f) 82 elif os.path.isfile(f): 83 yield f 84 elif file_not_found_cb: 85 file_not_found_cb(fname) 86 87 88class KernelFiles(): 89 """ 90 Parse kernel-doc tags on multiple kernel source files. 91 92 There are two type of parsers defined here: 93 - self.parse_file(): parses both kernel-doc markups and 94 EXPORT_SYMBOL* macros; 95 - self.process_export_file(): parses only EXPORT_SYMBOL* macros. 96 """ 97 98 def warning(self, msg): 99 """Ancillary routine to output a warning and increment error count""" 100 101 self.config.log.warning(msg) 102 self.errors += 1 103 104 def error(self, msg): 105 """Ancillary routine to output an error and increment error count""" 106 107 self.config.log.error(msg) 108 self.errors += 1 109 110 def parse_file(self, fname): 111 """ 112 Parse a single Kernel source. 113 """ 114 115 # Prevent parsing the same file twice if results are cached 116 if fname in self.files: 117 return 118 119 doc = KernelDoc(self.config, fname) 120 export_table, entries = doc.parse_kdoc() 121 122 self.export_table[fname] = export_table 123 124 self.files.add(fname) 125 self.export_files.add(fname) # parse_kdoc() already check exports 126 127 self.results[fname] = entries 128 129 def process_export_file(self, fname): 130 """ 131 Parses EXPORT_SYMBOL* macros from a single Kernel source file. 132 """ 133 134 # Prevent parsing the same file twice if results are cached 135 if fname in self.export_files: 136 return 137 138 doc = KernelDoc(self.config, fname) 139 export_table = doc.parse_export() 140 141 if not export_table: 142 self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}") 143 export_table = set() 144 145 self.export_table[fname] = export_table 146 self.export_files.add(fname) 147 148 def file_not_found_cb(self, fname): 149 """ 150 Callback to warn if a file was not found. 151 """ 152 153 self.error(f"Cannot find file {fname}") 154 155 def __init__(self, verbose=False, out_style=None, 156 werror=False, wreturn=False, wshort_desc=False, 157 wcontents_before_sections=False, 158 logger=None): 159 """ 160 Initialize startup variables and parse all files 161 """ 162 163 if not verbose: 164 verbose = bool(os.environ.get("KBUILD_VERBOSE", 0)) 165 166 if out_style is None: 167 out_style = OutputFormat() 168 169 if not werror: 170 kcflags = os.environ.get("KCFLAGS", None) 171 if kcflags: 172 match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags) 173 if match: 174 werror = True 175 176 # reading this variable is for backwards compat just in case 177 # someone was calling it with the variable from outside the 178 # kernel's build system 179 kdoc_werror = os.environ.get("KDOC_WERROR", None) 180 if kdoc_werror: 181 werror = kdoc_werror 182 183 # Some variables are global to the parser logic as a whole as they are 184 # used to send control configuration to KernelDoc class. As such, 185 # those variables are read-only inside the KernelDoc. 186 self.config = argparse.Namespace 187 188 self.config.verbose = verbose 189 self.config.werror = werror 190 self.config.wreturn = wreturn 191 self.config.wshort_desc = wshort_desc 192 self.config.wcontents_before_sections = wcontents_before_sections 193 194 if not logger: 195 self.config.log = logging.getLogger("kernel-doc") 196 else: 197 self.config.log = logger 198 199 self.config.warning = self.warning 200 201 self.config.src_tree = os.environ.get("SRCTREE", None) 202 203 # Initialize variables that are internal to KernelFiles 204 205 self.out_style = out_style 206 207 self.errors = 0 208 self.results = {} 209 210 self.files = set() 211 self.export_files = set() 212 self.export_table = {} 213 214 def parse(self, file_list, export_file=None): 215 """ 216 Parse all files 217 """ 218 219 glob = GlobSourceFiles(srctree=self.config.src_tree) 220 221 for fname in glob.parse_files(file_list, self.file_not_found_cb): 222 self.parse_file(fname) 223 224 for fname in glob.parse_files(export_file, self.file_not_found_cb): 225 self.process_export_file(fname) 226 227 def out_msg(self, fname, name, arg): 228 """ 229 Return output messages from a file name using the output style 230 filtering. 231 232 If output type was not handled by the syler, return None. 233 """ 234 235 # NOTE: we can add rules here to filter out unwanted parts, 236 # although OutputFormat.msg already does that. 237 238 return self.out_style.msg(fname, name, arg) 239 240 def msg(self, enable_lineno=False, export=False, internal=False, 241 symbol=None, nosymbol=None, no_doc_sections=False, 242 filenames=None, export_file=None): 243 """ 244 Interacts over the kernel-doc results and output messages, 245 returning kernel-doc markups on each interaction 246 """ 247 248 self.out_style.set_config(self.config) 249 250 if not filenames: 251 filenames = sorted(self.results.keys()) 252 253 glob = GlobSourceFiles(srctree=self.config.src_tree) 254 255 for fname in filenames: 256 function_table = set() 257 258 if internal or export: 259 if not export_file: 260 export_file = [fname] 261 262 for f in glob.parse_files(export_file, self.file_not_found_cb): 263 function_table |= self.export_table[f] 264 265 if symbol: 266 for s in symbol: 267 function_table.add(s) 268 269 self.out_style.set_filter(export, internal, symbol, nosymbol, 270 function_table, enable_lineno, 271 no_doc_sections) 272 273 msg = "" 274 if fname not in self.results: 275 self.config.log.warning("No kernel-doc for file %s", fname) 276 continue 277 278 for name, arg in self.results[fname]: 279 m = self.out_msg(fname, name, arg) 280 281 if m is None: 282 ln = arg.get("ln", 0) 283 dtype = arg.get('type', "") 284 285 self.config.log.warning("%s:%d Can't handle %s", 286 fname, ln, dtype) 287 else: 288 msg += m 289 290 if msg: 291 yield fname, msg 292