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"""
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