projects/linux·blob master · refs

380 lines · 11.2 KiB

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