xref: /linux/tools/lib/python/kdoc/kdoc_files.py (revision 19dcccbc064d6c58eaafae1ecb94821a2535cc26)
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        source = doc.get_source()
207        if source:
208            self.source[fname] = source
209
210    def process_export_file(self, fname):
211        """
212        Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file.
213        """
214
215        # Prevent parsing the same file twice if results are cached
216        if fname in self.export_files:
217            return
218
219        doc = KernelDoc(self.config, fname)
220        export_table = doc.parse_export()
221
222        if not export_table:
223            self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}")
224            export_table = set()
225
226        self.export_table[fname] = export_table
227        self.export_files.add(fname)
228
229    def file_not_found_cb(self, fname):
230        """
231        Callback to warn if a file was not found.
232        """
233
234        self.error(f"Cannot find file {fname}")
235
236    def __init__(self, verbose=False, out_style=None, xforms=None,
237                 werror=False, wreturn=False, wshort_desc=False,
238                 wcontents_before_sections=False,
239                 yaml_file=None, yaml_content=None, logger=None):
240        """
241        Initialize startup variables and parse all files.
242        """
243
244        if not verbose:
245            verbose = bool(os.environ.get("KBUILD_VERBOSE", 0))
246
247        if out_style is None:
248            out_style = OutputFormat()
249
250        if not werror:
251            kcflags = os.environ.get("KCFLAGS", None)
252            if kcflags:
253                match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags)
254                if match:
255                    werror = True
256
257            # reading this variable is for backwards compat just in case
258            # someone was calling it with the variable from outside the
259            # kernel's build system
260            kdoc_werror = os.environ.get("KDOC_WERROR", None)
261            if kdoc_werror:
262                werror = kdoc_werror
263
264        if not logger:
265           logger = logging.getLogger("kernel-doc")
266        else:
267            logger = logger
268
269        # Some variables are global to the parser logic as a whole as they are
270        # used to send control configuration to KernelDoc class. As such,
271        # those variables are read-only inside the KernelDoc.
272        self.config = KdocConfig(verbose, werror, wreturn, wshort_desc,
273                                 wcontents_before_sections, logger)
274
275        # Override log warning, as we want to count errors
276        self.config.warning = self.warning
277
278        if yaml_file:
279            self.test_file = KDocTestFile(self.config, yaml_file, yaml_content)
280        else:
281            self.test_file = None
282
283        if xforms:
284            self.xforms = xforms
285        else:
286            self.xforms = CTransforms()
287
288        self.config.src_tree = os.environ.get("SRCTREE", None)
289
290        # Initialize variables that are internal to KernelFiles
291
292        self.out_style = out_style
293        self.out_style.set_config(self.config)
294
295        self.errors = 0
296        self.results = {}
297        self.source = {}
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                                              self.source.get(fname))
369
370                continue
371
372            self.out_style.set_filter(export, internal, symbol, nosymbol,
373                                      function_table, enable_lineno,
374                                      no_doc_sections)
375
376            msg = self.out_style.output_symbols(fname, symbols)
377            if msg:
378                yield fname, msg
379
380        if self.test_file:
381            self.test_file.write()
382