xref: /linux/tools/lib/python/kdoc/kdoc_files.py (revision c991b7ef2fb658c186df56d16b3ebcd0afb555cc)
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.xforms_lists import CTransforms
19from kdoc.kdoc_output import OutputFormat
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 KernelFiles():
91    """
92    Parse kernel-doc tags on multiple kernel source files.
93
94    This is the main entry point to run kernel-doc. This class is initialized
95    using a series of optional arguments:
96
97    ``verbose``
98        If True, enables kernel-doc verbosity. Default: False.
99
100    ``out_style``
101        Class to be used to format output. If None (default),
102        only report errors.
103
104    ``xforms``
105        Transforms to be applied to C prototypes and data structs.
106        If not specified, defaults to xforms = CFunction()
107
108    ``werror``
109        If True, treat warnings as errors, retuning an error code on warnings.
110
111        Default: False.
112
113    ``wreturn``
114        If True, warns about the lack of a return markup on functions.
115
116        Default: False.
117    ``wshort_desc``
118        If True, warns if initial short description is missing.
119
120        Default: False.
121
122    ``wcontents_before_sections``
123        If True, warn if there are contents before sections (deprecated).
124        This option is kept just for backward-compatibility, but it does
125        nothing, neither here nor at the original Perl script.
126
127        Default: False.
128
129    ``logger``
130        Optional logger class instance.
131
132        If not specified, defaults to use: ``logging.getLogger("kernel-doc")``
133
134    Note:
135        There are two type of parsers defined here:
136
137        - self.parse_file(): parses both kernel-doc markups and
138          ``EXPORT_SYMBOL*`` macros;
139        - self.process_export_file(): parses only ``EXPORT_SYMBOL*`` macros.
140    """
141
142    def warning(self, msg):
143        """Ancillary routine to output a warning and increment error count."""
144
145        self.config.log.warning(msg)
146        self.errors += 1
147
148    def error(self, msg):
149        """Ancillary routine to output an error and increment error count."""
150
151        self.config.log.error(msg)
152        self.errors += 1
153
154    def parse_file(self, fname):
155        """
156        Parse a single Kernel source.
157        """
158
159        # Prevent parsing the same file twice if results are cached
160        if fname in self.files:
161            return
162
163        doc = KernelDoc(self.config, fname, self.xforms)
164        export_table, entries = doc.parse_kdoc()
165
166        self.export_table[fname] = export_table
167
168        self.files.add(fname)
169        self.export_files.add(fname)      # parse_kdoc() already check exports
170
171        self.results[fname] = entries
172
173    def process_export_file(self, fname):
174        """
175        Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file.
176        """
177
178        # Prevent parsing the same file twice if results are cached
179        if fname in self.export_files:
180            return
181
182        doc = KernelDoc(self.config, fname)
183        export_table = doc.parse_export()
184
185        if not export_table:
186            self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}")
187            export_table = set()
188
189        self.export_table[fname] = export_table
190        self.export_files.add(fname)
191
192    def file_not_found_cb(self, fname):
193        """
194        Callback to warn if a file was not found.
195        """
196
197        self.error(f"Cannot find file {fname}")
198
199    def __init__(self, verbose=False, out_style=None, xforms=None,
200                 werror=False, wreturn=False, wshort_desc=False,
201                 wcontents_before_sections=False,
202                 logger=None):
203        """
204        Initialize startup variables and parse all files.
205        """
206
207        if not verbose:
208            verbose = bool(os.environ.get("KBUILD_VERBOSE", 0))
209
210        if out_style is None:
211            out_style = OutputFormat()
212
213        if not werror:
214            kcflags = os.environ.get("KCFLAGS", None)
215            if kcflags:
216                match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags)
217                if match:
218                    werror = True
219
220            # reading this variable is for backwards compat just in case
221            # someone was calling it with the variable from outside the
222            # kernel's build system
223            kdoc_werror = os.environ.get("KDOC_WERROR", None)
224            if kdoc_werror:
225                werror = kdoc_werror
226
227        # Some variables are global to the parser logic as a whole as they are
228        # used to send control configuration to KernelDoc class. As such,
229        # those variables are read-only inside the KernelDoc.
230        self.config = argparse.Namespace
231
232        self.config.verbose = verbose
233        self.config.werror = werror
234        self.config.wreturn = wreturn
235        self.config.wshort_desc = wshort_desc
236        self.config.wcontents_before_sections = wcontents_before_sections
237
238        if xforms:
239            self.xforms = xforms
240        else:
241            self.xforms = CTransforms()
242
243        if not logger:
244            self.config.log = logging.getLogger("kernel-doc")
245        else:
246            self.config.log = logger
247
248        self.config.warning = self.warning
249
250        self.config.src_tree = os.environ.get("SRCTREE", None)
251
252        # Initialize variables that are internal to KernelFiles
253
254        self.out_style = out_style
255
256        self.errors = 0
257        self.results = {}
258
259        self.files = set()
260        self.export_files = set()
261        self.export_table = {}
262
263    def parse(self, file_list, export_file=None):
264        """
265        Parse all files.
266        """
267
268        glob = GlobSourceFiles(srctree=self.config.src_tree)
269
270        for fname in glob.parse_files(file_list, self.file_not_found_cb):
271            self.parse_file(fname)
272
273        for fname in glob.parse_files(export_file, self.file_not_found_cb):
274            self.process_export_file(fname)
275
276    def out_msg(self, fname, name, arg):
277        """
278        Return output messages from a file name using the output style
279        filtering.
280
281        If output type was not handled by the styler, return None.
282        """
283
284        # NOTE: we can add rules here to filter out unwanted parts,
285        # although OutputFormat.msg already does that.
286
287        return self.out_style.msg(fname, name, arg)
288
289    def msg(self, enable_lineno=False, export=False, internal=False,
290            symbol=None, nosymbol=None, no_doc_sections=False,
291            filenames=None, export_file=None):
292        """
293        Interacts over the kernel-doc results and output messages,
294        returning kernel-doc markups on each interaction.
295        """
296
297        self.out_style.set_config(self.config)
298
299        if not filenames:
300            filenames = sorted(self.results.keys())
301
302        glob = GlobSourceFiles(srctree=self.config.src_tree)
303
304        for fname in filenames:
305            function_table = set()
306
307            if internal or export:
308                if not export_file:
309                    export_file = [fname]
310
311                for f in glob.parse_files(export_file, self.file_not_found_cb):
312                    function_table |= self.export_table[f]
313
314            if symbol:
315                for s in symbol:
316                    function_table.add(s)
317
318            self.out_style.set_filter(export, internal, symbol, nosymbol,
319                                      function_table, enable_lineno,
320                                      no_doc_sections)
321
322            msg = ""
323            if fname not in self.results:
324                self.config.log.warning("No kernel-doc for file %s", fname)
325                continue
326
327            symbols = self.results[fname]
328            self.out_style.set_symbols(symbols)
329
330            for arg in symbols:
331                m = self.out_msg(fname, arg.name, arg)
332
333                if m is None:
334                    ln = arg.get("ln", 0)
335                    dtype = arg.get('type', "")
336
337                    self.config.log.warning("%s:%d Can't handle %s",
338                                            fname, ln, dtype)
339                else:
340                    msg += m
341
342            if msg:
343                yield fname, msg
344