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