xref: /linux/tools/lib/python/kdoc/kdoc_files.py (revision 1329cc0b8d28454c3e87a28c62a4426cc53ba254)
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
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            verbose = bool(os.environ.get("KBUILD_VERBOSE", 0))
242
243        if out_style is None:
244            out_style = OutputFormat()
245
246        if not werror:
247            kcflags = os.environ.get("KCFLAGS", None)
248            if kcflags:
249                match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags)
250                if match:
251                    werror = True
252
253            # reading this variable is for backwards compat just in case
254            # someone was calling it with the variable from outside the
255            # kernel's build system
256            kdoc_werror = os.environ.get("KDOC_WERROR", None)
257            if kdoc_werror:
258                werror = kdoc_werror
259
260        if not logger:
261           logger = logging.getLogger("kernel-doc")
262        else:
263            logger = logger
264
265        # Some variables are global to the parser logic as a whole as they are
266        # used to send control configuration to KernelDoc class. As such,
267        # those variables are read-only inside the KernelDoc.
268        self.config = KdocConfig(verbose, werror, wreturn, wshort_desc,
269                                 wcontents_before_sections, logger)
270
271        # Override log warning, as we want to count errors
272        self.config.warning = self.warning
273
274        if yaml_file:
275            self.test_file = KDocTestFile(self.config, yaml_file, yaml_content)
276        else:
277            self.test_file = None
278
279        if xforms:
280            self.xforms = xforms
281        else:
282            self.xforms = CTransforms()
283
284        self.config.src_tree = os.environ.get("SRCTREE", None)
285
286        # Initialize variables that are internal to KernelFiles
287
288        self.out_style = out_style
289        self.out_style.set_config(self.config)
290
291        self.errors = 0
292        self.results = {}
293
294        self.files = set()
295        self.export_files = set()
296        self.export_table = {}
297
298    def parse(self, file_list, export_file=None):
299        """
300        Parse all files.
301        """
302
303        glob = GlobSourceFiles(srctree=self.config.src_tree)
304
305        for fname in glob.parse_files(file_list, self.file_not_found_cb):
306            self.parse_file(fname)
307
308        for fname in glob.parse_files(export_file, self.file_not_found_cb):
309            self.process_export_file(fname)
310
311    def out_msg(self, fname, name, arg):
312        """
313        Return output messages from a file name using the output style
314        filtering.
315
316        If output type was not handled by the styler, return None.
317        """
318
319        # NOTE: we can add rules here to filter out unwanted parts,
320        # although OutputFormat.msg already does that.
321
322        return self.out_style.msg(fname, name, arg)
323
324    def msg(self, enable_lineno=False, export=False, internal=False,
325            symbol=None, nosymbol=None, no_doc_sections=False,
326            filenames=None, export_file=None):
327        """
328        Interacts over the kernel-doc results and output messages,
329        returning kernel-doc markups on each interaction.
330        """
331
332        if not filenames:
333            filenames = sorted(self.results.keys())
334
335        glob = GlobSourceFiles(srctree=self.config.src_tree)
336
337        for fname in filenames:
338            function_table = set()
339
340            if internal or export:
341                if not export_file:
342                    export_file = [fname]
343
344                for f in glob.parse_files(export_file, self.file_not_found_cb):
345                    function_table |= self.export_table[f]
346
347            if symbol:
348                for s in symbol:
349                    function_table.add(s)
350
351            if fname not in self.results:
352                self.config.log.warning("No kernel-doc for file %s", fname)
353                continue
354
355            symbols = self.results[fname]
356
357            if self.test_file:
358                self.test_file.set_filter(export, internal, symbol, nosymbol,
359                                          function_table, enable_lineno,
360                                          no_doc_sections)
361
362                self.test_file.output_symbols(fname, symbols)
363
364                continue
365
366            self.out_style.set_filter(export, internal, symbol, nosymbol,
367                                      function_table, enable_lineno,
368                                      no_doc_sections)
369
370            msg = self.out_style.output_symbols(fname, symbols)
371            if msg:
372                yield fname, msg
373
374        if self.test_file:
375            self.test_file.write()
376