Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
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.kdoc_parser import KernelDoc
17from kdoc.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(follow_symlinks=False):
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 iterator 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 styler, 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 symbols = self.results[fname]
279 self.out_style.set_symbols(symbols)
280
281 for arg in symbols:
282 m = self.out_msg(fname, arg.name, arg)
283
284 if m is None:
285 ln = arg.get("ln", 0)
286 dtype = arg.get('type', "")
287
288 self.config.log.warning("%s:%d Can't handle %s",
289 fname, ln, dtype)
290 else:
291 msg += m
292
293 if msg:
294 yield fname, msg