xref: /linux/tools/lib/python/kdoc/kdoc_files.py (revision d642acfd597e3ec37138f9a8f5a634845e3612fd)
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            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()
381