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