xref: /linux/tools/lib/python/kdoc/kdoc_files.py (revision d842057c4a205084fb3036122c7426963f04e826)
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, CTransforms())
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,
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 not logger:
197            self.config.log = logging.getLogger("kernel-doc")
198        else:
199            self.config.log = logger
200
201        self.config.warning = self.warning
202
203        self.config.src_tree = os.environ.get("SRCTREE", None)
204
205        # Initialize variables that are internal to KernelFiles
206
207        self.out_style = out_style
208
209        self.errors = 0
210        self.results = {}
211
212        self.files = set()
213        self.export_files = set()
214        self.export_table = {}
215
216    def parse(self, file_list, export_file=None):
217        """
218        Parse all files.
219        """
220
221        glob = GlobSourceFiles(srctree=self.config.src_tree)
222
223        for fname in glob.parse_files(file_list, self.file_not_found_cb):
224            self.parse_file(fname)
225
226        for fname in glob.parse_files(export_file, self.file_not_found_cb):
227            self.process_export_file(fname)
228
229    def out_msg(self, fname, name, arg):
230        """
231        Return output messages from a file name using the output style
232        filtering.
233
234        If output type was not handled by the styler, return None.
235        """
236
237        # NOTE: we can add rules here to filter out unwanted parts,
238        # although OutputFormat.msg already does that.
239
240        return self.out_style.msg(fname, name, arg)
241
242    def msg(self, enable_lineno=False, export=False, internal=False,
243            symbol=None, nosymbol=None, no_doc_sections=False,
244            filenames=None, export_file=None):
245        """
246        Interacts over the kernel-doc results and output messages,
247        returning kernel-doc markups on each interaction.
248        """
249
250        self.out_style.set_config(self.config)
251
252        if not filenames:
253            filenames = sorted(self.results.keys())
254
255        glob = GlobSourceFiles(srctree=self.config.src_tree)
256
257        for fname in filenames:
258            function_table = set()
259
260            if internal or export:
261                if not export_file:
262                    export_file = [fname]
263
264                for f in glob.parse_files(export_file, self.file_not_found_cb):
265                    function_table |= self.export_table[f]
266
267            if symbol:
268                for s in symbol:
269                    function_table.add(s)
270
271            self.out_style.set_filter(export, internal, symbol, nosymbol,
272                                      function_table, enable_lineno,
273                                      no_doc_sections)
274
275            msg = ""
276            if fname not in self.results:
277                self.config.log.warning("No kernel-doc for file %s", fname)
278                continue
279
280            symbols = self.results[fname]
281            self.out_style.set_symbols(symbols)
282
283            for arg in symbols:
284                m = self.out_msg(fname, arg.name, arg)
285
286                if m is None:
287                    ln = arg.get("ln", 0)
288                    dtype = arg.get('type', "")
289
290                    self.config.log.warning("%s:%d Can't handle %s",
291                                            fname, ln, dtype)
292                else:
293                    msg += m
294
295            if msg:
296                yield fname, msg
297