xref: /linux/scripts/lib/kdoc/kdoc_files.py (revision 3e443d167327b10966166c1953631936547b03d0)
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"""
8Parse lernel-doc tags on multiple kernel source files.
9"""
10
11import argparse
12import logging
13import os
14import re
15
16from kdoc_parser import KernelDoc
17from kdoc_output import OutputFormat
18
19
20class GlobSourceFiles:
21    """
22    Parse C source code file names and directories via an Interactor.
23    """
24
25    def __init__(self, srctree=None, valid_extensions=None):
26        """
27        Initialize valid extensions with a tuple.
28
29        If not defined, assume default C extensions (.c and .h)
30
31        It would be possible to use python's glob function, but it is
32        very slow, and it is not interactive. So, it would wait to read all
33        directories before actually do something.
34
35        So, let's use our own implementation.
36        """
37
38        if not valid_extensions:
39            self.extensions = (".c", ".h")
40        else:
41            self.extensions = valid_extensions
42
43        self.srctree = srctree
44
45    def _parse_dir(self, dirname):
46        """Internal function to parse files recursively"""
47
48        with os.scandir(dirname) as obj:
49            for entry in obj:
50                name = os.path.join(dirname, entry.name)
51
52                if entry.is_dir():
53                    yield from self._parse_dir(name)
54
55                if not entry.is_file():
56                    continue
57
58                basename = os.path.basename(name)
59
60                if not basename.endswith(self.extensions):
61                    continue
62
63                yield name
64
65    def parse_files(self, file_list, file_not_found_cb):
66        """
67        Define an interator to parse all source files from file_list,
68        handling directories if any
69        """
70
71        if not file_list:
72            return
73
74        for fname in file_list:
75            if self.srctree:
76                f = os.path.join(self.srctree, fname)
77            else:
78                f = fname
79
80            if os.path.isdir(f):
81                yield from self._parse_dir(f)
82            elif os.path.isfile(f):
83                yield f
84            elif file_not_found_cb:
85                file_not_found_cb(fname)
86
87
88class KernelFiles():
89    """
90    Parse kernel-doc tags on multiple kernel source files.
91
92    There are two type of parsers defined here:
93        - self.parse_file(): parses both kernel-doc markups and
94          EXPORT_SYMBOL* macros;
95        - self.process_export_file(): parses only EXPORT_SYMBOL* macros.
96    """
97
98    def warning(self, msg):
99        """Ancillary routine to output a warning and increment error count"""
100
101        self.config.log.warning(msg)
102        self.errors += 1
103
104    def error(self, msg):
105        """Ancillary routine to output an error and increment error count"""
106
107        self.config.log.error(msg)
108        self.errors += 1
109
110    def parse_file(self, fname):
111        """
112        Parse a single Kernel source.
113        """
114
115        # Prevent parsing the same file twice if results are cached
116        if fname in self.files:
117            return
118
119        doc = KernelDoc(self.config, fname)
120        export_table, entries = doc.parse_kdoc()
121
122        self.export_table[fname] = export_table
123
124        self.files.add(fname)
125        self.export_files.add(fname)      # parse_kdoc() already check exports
126
127        self.results[fname] = entries
128
129    def process_export_file(self, fname):
130        """
131        Parses EXPORT_SYMBOL* macros from a single Kernel source file.
132        """
133
134        # Prevent parsing the same file twice if results are cached
135        if fname in self.export_files:
136            return
137
138        doc = KernelDoc(self.config, fname)
139        export_table = doc.parse_export()
140
141        if not export_table:
142            self.error(f"Error: Cannot check EXPORT_SYMBOL* on {fname}")
143            export_table = set()
144
145        self.export_table[fname] = export_table
146        self.export_files.add(fname)
147
148    def file_not_found_cb(self, fname):
149        """
150        Callback to warn if a file was not found.
151        """
152
153        self.error(f"Cannot find file {fname}")
154
155    def __init__(self, verbose=False, out_style=None,
156                 werror=False, wreturn=False, wshort_desc=False,
157                 wcontents_before_sections=False,
158                 logger=None):
159        """
160        Initialize startup variables and parse all files
161        """
162
163        if not verbose:
164            verbose = bool(os.environ.get("KBUILD_VERBOSE", 0))
165
166        if out_style is None:
167            out_style = OutputFormat()
168
169        if not werror:
170            kcflags = os.environ.get("KCFLAGS", None)
171            if kcflags:
172                match = re.search(r"(\s|^)-Werror(\s|$)/", kcflags)
173                if match:
174                    werror = True
175
176            # reading this variable is for backwards compat just in case
177            # someone was calling it with the variable from outside the
178            # kernel's build system
179            kdoc_werror = os.environ.get("KDOC_WERROR", None)
180            if kdoc_werror:
181                werror = kdoc_werror
182
183        # Some variables are global to the parser logic as a whole as they are
184        # used to send control configuration to KernelDoc class. As such,
185        # those variables are read-only inside the KernelDoc.
186        self.config = argparse.Namespace
187
188        self.config.verbose = verbose
189        self.config.werror = werror
190        self.config.wreturn = wreturn
191        self.config.wshort_desc = wshort_desc
192        self.config.wcontents_before_sections = wcontents_before_sections
193
194        if not logger:
195            self.config.log = logging.getLogger("kernel-doc")
196        else:
197            self.config.log = logger
198
199        self.config.warning = self.warning
200
201        self.config.src_tree = os.environ.get("SRCTREE", None)
202
203        # Initialize variables that are internal to KernelFiles
204
205        self.out_style = out_style
206
207        self.errors = 0
208        self.results = {}
209
210        self.files = set()
211        self.export_files = set()
212        self.export_table = {}
213
214    def parse(self, file_list, export_file=None):
215        """
216        Parse all files
217        """
218
219        glob = GlobSourceFiles(srctree=self.config.src_tree)
220
221        for fname in glob.parse_files(file_list, self.file_not_found_cb):
222            self.parse_file(fname)
223
224        for fname in glob.parse_files(export_file, self.file_not_found_cb):
225            self.process_export_file(fname)
226
227    def out_msg(self, fname, name, arg):
228        """
229        Return output messages from a file name using the output style
230        filtering.
231
232        If output type was not handled by the syler, return None.
233        """
234
235        # NOTE: we can add rules here to filter out unwanted parts,
236        # although OutputFormat.msg already does that.
237
238        return self.out_style.msg(fname, name, arg)
239
240    def msg(self, enable_lineno=False, export=False, internal=False,
241            symbol=None, nosymbol=None, no_doc_sections=False,
242            filenames=None, export_file=None):
243        """
244        Interacts over the kernel-doc results and output messages,
245        returning kernel-doc markups on each interaction
246        """
247
248        self.out_style.set_config(self.config)
249
250        if not filenames:
251            filenames = sorted(self.results.keys())
252
253        glob = GlobSourceFiles(srctree=self.config.src_tree)
254
255        for fname in filenames:
256            function_table = set()
257
258            if internal or export:
259                if not export_file:
260                    export_file = [fname]
261
262                for f in glob.parse_files(export_file, self.file_not_found_cb):
263                    function_table |= self.export_table[f]
264
265            if symbol:
266                for s in symbol:
267                    function_table.add(s)
268
269            self.out_style.set_filter(export, internal, symbol, nosymbol,
270                                      function_table, enable_lineno,
271                                      no_doc_sections)
272
273            msg = ""
274            if fname not in self.results:
275                self.config.log.warning("No kernel-doc for file %s", fname)
276                continue
277
278            for name, arg in self.results[fname]:
279                m = self.out_msg(fname, name, arg)
280
281                if m is None:
282                    ln = arg.get("ln", 0)
283                    dtype = arg.get('type', "")
284
285                    self.config.log.warning("%s:%d Can't handle %s",
286                                            fname, ln, dtype)
287                else:
288                    msg += m
289
290            if msg:
291                yield fname, msg
292