xref: /linux/tools/lib/python/kdoc/kdoc_files.py (revision 8d08c7c6ffc14abe584738843c8577c691ffcf22)
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.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 KernelFiles():
90    """
91    Parse kernel-doc tags on multiple kernel source files.
92
93    There are two type of parsers defined here:
94        - self.parse_file(): parses both kernel-doc markups and
95          ``EXPORT_SYMBOL*`` macros;
96        - self.process_export_file(): parses only ``EXPORT_SYMBOL*`` macros.
97    """
98
99    def warning(self, msg):
100        """Ancillary routine to output a warning and increment error count."""
101
102        self.config.log.warning(msg)
103        self.errors += 1
104
105    def error(self, msg):
106        """Ancillary routine to output an error and increment error count."""
107
108        self.config.log.error(msg)
109        self.errors += 1
110
111    def parse_file(self, fname):
112        """
113        Parse a single Kernel source.
114        """
115
116        # Prevent parsing the same file twice if results are cached
117        if fname in self.files:
118            return
119
120        doc = KernelDoc(self.config, fname)
121        export_table, entries = doc.parse_kdoc()
122
123        self.export_table[fname] = export_table
124
125        self.files.add(fname)
126        self.export_files.add(fname)      # parse_kdoc() already check exports
127
128        self.results[fname] = entries
129
130    def process_export_file(self, fname):
131        """
132        Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file.
133        """
134
135        # Prevent parsing the same file twice if results are cached
136        if fname in self.export_files:
137            return
138
139        doc = KernelDoc(self.config, fname)
140        export_table = doc.parse_export()
141
142        if not export_table:
143            self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}")
144            export_table = set()
145
146        self.export_table[fname] = export_table
147        self.export_files.add(fname)
148
149    def file_not_found_cb(self, fname):
150        """
151        Callback to warn if a file was not found.
152        """
153
154        self.error(f"Cannot find file {fname}")
155
156    def __init__(self, verbose=False, out_style=None,
157                 werror=False, wreturn=False, wshort_desc=False,
158                 wcontents_before_sections=False,
159                 logger=None):
160        """
161        Initialize startup variables and parse all files.
162        """
163
164        if not verbose:
165            verbose = bool(os.environ.get("KBUILD_VERBOSE", 0))
166
167        if out_style is None:
168            out_style = OutputFormat()
169
170        if not werror:
171            kcflags = os.environ.get("KCFLAGS", None)
172            if kcflags:
173                match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags)
174                if match:
175                    werror = True
176
177            # reading this variable is for backwards compat just in case
178            # someone was calling it with the variable from outside the
179            # kernel's build system
180            kdoc_werror = os.environ.get("KDOC_WERROR", None)
181            if kdoc_werror:
182                werror = kdoc_werror
183
184        # Some variables are global to the parser logic as a whole as they are
185        # used to send control configuration to KernelDoc class. As such,
186        # those variables are read-only inside the KernelDoc.
187        self.config = argparse.Namespace
188
189        self.config.verbose = verbose
190        self.config.werror = werror
191        self.config.wreturn = wreturn
192        self.config.wshort_desc = wshort_desc
193        self.config.wcontents_before_sections = wcontents_before_sections
194
195        if not logger:
196            self.config.log = logging.getLogger("kernel-doc")
197        else:
198            self.config.log = logger
199
200        self.config.warning = self.warning
201
202        self.config.src_tree = os.environ.get("SRCTREE", None)
203
204        # Initialize variables that are internal to KernelFiles
205
206        self.out_style = out_style
207
208        self.errors = 0
209        self.results = {}
210
211        self.files = set()
212        self.export_files = set()
213        self.export_table = {}
214
215    def parse(self, file_list, export_file=None):
216        """
217        Parse all files.
218        """
219
220        glob = GlobSourceFiles(srctree=self.config.src_tree)
221
222        for fname in glob.parse_files(file_list, self.file_not_found_cb):
223            self.parse_file(fname)
224
225        for fname in glob.parse_files(export_file, self.file_not_found_cb):
226            self.process_export_file(fname)
227
228    def out_msg(self, fname, name, arg):
229        """
230        Return output messages from a file name using the output style
231        filtering.
232
233        If output type was not handled by the styler, return None.
234        """
235
236        # NOTE: we can add rules here to filter out unwanted parts,
237        # although OutputFormat.msg already does that.
238
239        return self.out_style.msg(fname, name, arg)
240
241    def msg(self, enable_lineno=False, export=False, internal=False,
242            symbol=None, nosymbol=None, no_doc_sections=False,
243            filenames=None, export_file=None):
244        """
245        Interacts over the kernel-doc results and output messages,
246        returning kernel-doc markups on each interaction.
247        """
248
249        self.out_style.set_config(self.config)
250
251        if not filenames:
252            filenames = sorted(self.results.keys())
253
254        glob = GlobSourceFiles(srctree=self.config.src_tree)
255
256        for fname in filenames:
257            function_table = set()
258
259            if internal or export:
260                if not export_file:
261                    export_file = [fname]
262
263                for f in glob.parse_files(export_file, self.file_not_found_cb):
264                    function_table |= self.export_table[f]
265
266            if symbol:
267                for s in symbol:
268                    function_table.add(s)
269
270            self.out_style.set_filter(export, internal, symbol, nosymbol,
271                                      function_table, enable_lineno,
272                                      no_doc_sections)
273
274            msg = ""
275            if fname not in self.results:
276                self.config.log.warning("No kernel-doc for file %s", fname)
277                continue
278
279            symbols = self.results[fname]
280            self.out_style.set_symbols(symbols)
281
282            for arg in symbols:
283                m = self.out_msg(fname, arg.name, arg)
284
285                if m is None:
286                    ln = arg.get("ln", 0)
287                    dtype = arg.get('type', "")
288
289                    self.config.log.warning("%s:%d Can't handle %s",
290                                            fname, ln, dtype)
291                else:
292                    msg += m
293
294            if msg:
295                yield fname, msg
296