xref: /linux/scripts/lib/kdoc/kdoc_output.py (revision de6f7ac91a08d723a6eaa9c5bbce30c5a126c861)
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=C0301,R0902,R0911,R0912,R0913,R0914,R0915,R0917
6
7"""
8Implement output filters to print kernel-doc documentation.
9
10The implementation uses a virtual base class (OutputFormat) which
11contains a dispatches to virtual methods, and some code to filter
12out output messages.
13
14The actual implementation is done on one separate class per each type
15of output. Currently, there are output classes for ReST and man/troff.
16"""
17
18import os
19import re
20from datetime import datetime
21
22from kdoc_parser import KernelDoc, type_param
23from kdoc_re import KernRe
24
25
26function_pointer = KernRe(r"([^\(]*\(\*)\s*\)\s*\(([^\)]*)\)", cache=False)
27
28# match expressions used to find embedded type information
29type_constant = KernRe(r"\b``([^\`]+)``\b", cache=False)
30type_constant2 = KernRe(r"\%([-_*\w]+)", cache=False)
31type_func = KernRe(r"(\w+)\(\)", cache=False)
32type_param_ref = KernRe(r"([\!~\*]?)\@(\w*((\.\w+)|(->\w+))*(\.\.\.)?)", cache=False)
33
34# Special RST handling for func ptr params
35type_fp_param = KernRe(r"\@(\w+)\(\)", cache=False)
36
37# Special RST handling for structs with func ptr params
38type_fp_param2 = KernRe(r"\@(\w+->\S+)\(\)", cache=False)
39
40type_env = KernRe(r"(\$\w+)", cache=False)
41type_enum = KernRe(r"\&(enum\s*([_\w]+))", cache=False)
42type_struct = KernRe(r"\&(struct\s*([_\w]+))", cache=False)
43type_typedef = KernRe(r"\&(typedef\s*([_\w]+))", cache=False)
44type_union = KernRe(r"\&(union\s*([_\w]+))", cache=False)
45type_member = KernRe(r"\&([_\w]+)(\.|->)([_\w]+)", cache=False)
46type_fallback = KernRe(r"\&([_\w]+)", cache=False)
47type_member_func = type_member + KernRe(r"\(\)", cache=False)
48
49
50class OutputFormat:
51    """
52    Base class for OutputFormat. If used as-is, it means that only
53    warnings will be displayed.
54    """
55
56    # output mode.
57    OUTPUT_ALL          = 0 # output all symbols and doc sections
58    OUTPUT_INCLUDE      = 1 # output only specified symbols
59    OUTPUT_EXPORTED     = 2 # output exported symbols
60    OUTPUT_INTERNAL     = 3 # output non-exported symbols
61
62    # Virtual member to be overriden at the  inherited classes
63    highlights = []
64
65    def __init__(self):
66        """Declare internal vars and set mode to OUTPUT_ALL"""
67
68        self.out_mode = self.OUTPUT_ALL
69        self.enable_lineno = None
70        self.nosymbol = {}
71        self.symbol = None
72        self.function_table = None
73        self.config = None
74        self.no_doc_sections = False
75
76        self.data = ""
77
78    def set_config(self, config):
79        """
80        Setup global config variables used by both parser and output.
81        """
82
83        self.config = config
84
85    def set_filter(self, export, internal, symbol, nosymbol, function_table,
86                   enable_lineno, no_doc_sections):
87        """
88        Initialize filter variables according with the requested mode.
89
90        Only one choice is valid between export, internal and symbol.
91
92        The nosymbol filter can be used on all modes.
93        """
94
95        self.enable_lineno = enable_lineno
96        self.no_doc_sections = no_doc_sections
97        self.function_table = function_table
98
99        if symbol:
100            self.out_mode = self.OUTPUT_INCLUDE
101        elif export:
102            self.out_mode = self.OUTPUT_EXPORTED
103        elif internal:
104            self.out_mode = self.OUTPUT_INTERNAL
105        else:
106            self.out_mode = self.OUTPUT_ALL
107
108        if nosymbol:
109            self.nosymbol = set(nosymbol)
110
111
112    def highlight_block(self, block):
113        """
114        Apply the RST highlights to a sub-block of text.
115        """
116
117        for r, sub in self.highlights:
118            block = r.sub(sub, block)
119
120        return block
121
122    def out_warnings(self, args):
123        """
124        Output warnings for identifiers that will be displayed.
125        """
126
127        warnings = args.get('warnings', [])
128
129        for log_msg in warnings:
130            self.config.warning(log_msg)
131
132    def check_doc(self, name, args):
133        """Check if DOC should be output"""
134
135        if self.no_doc_sections:
136            return False
137
138        if name in self.nosymbol:
139            return False
140
141        if self.out_mode == self.OUTPUT_ALL:
142            self.out_warnings(args)
143            return True
144
145        if self.out_mode == self.OUTPUT_INCLUDE:
146            if name in self.function_table:
147                self.out_warnings(args)
148                return True
149
150        return False
151
152    def check_declaration(self, dtype, name, args):
153        """
154        Checks if a declaration should be output or not based on the
155        filtering criteria.
156        """
157
158        if name in self.nosymbol:
159            return False
160
161        if self.out_mode == self.OUTPUT_ALL:
162            self.out_warnings(args)
163            return True
164
165        if self.out_mode in [self.OUTPUT_INCLUDE, self.OUTPUT_EXPORTED]:
166            if name in self.function_table:
167                return True
168
169        if self.out_mode == self.OUTPUT_INTERNAL:
170            if dtype != "function":
171                self.out_warnings(args)
172                return True
173
174            if name not in self.function_table:
175                self.out_warnings(args)
176                return True
177
178        return False
179
180    def msg(self, fname, name, args):
181        """
182        Handles a single entry from kernel-doc parser
183        """
184
185        self.data = ""
186
187        dtype = args.get('type', "")
188
189        if dtype == "doc":
190            self.out_doc(fname, name, args)
191            return self.data
192
193        if not self.check_declaration(dtype, name, args):
194            return self.data
195
196        if dtype == "function":
197            self.out_function(fname, name, args)
198            return self.data
199
200        if dtype == "enum":
201            self.out_enum(fname, name, args)
202            return self.data
203
204        if dtype == "typedef":
205            self.out_typedef(fname, name, args)
206            return self.data
207
208        if dtype in ["struct", "union"]:
209            self.out_struct(fname, name, args)
210            return self.data
211
212        # Warn if some type requires an output logic
213        self.config.log.warning("doesn't now how to output '%s' block",
214                                dtype)
215
216        return None
217
218    # Virtual methods to be overridden by inherited classes
219    # At the base class, those do nothing.
220    def out_doc(self, fname, name, args):
221        """Outputs a DOC block"""
222
223    def out_function(self, fname, name, args):
224        """Outputs a function"""
225
226    def out_enum(self, fname, name, args):
227        """Outputs an enum"""
228
229    def out_typedef(self, fname, name, args):
230        """Outputs a typedef"""
231
232    def out_struct(self, fname, name, args):
233        """Outputs a struct"""
234
235
236class RestFormat(OutputFormat):
237    """Consts and functions used by ReST output"""
238
239    highlights = [
240        (type_constant, r"``\1``"),
241        (type_constant2, r"``\1``"),
242
243        # Note: need to escape () to avoid func matching later
244        (type_member_func, r":c:type:`\1\2\3\\(\\) <\1>`"),
245        (type_member, r":c:type:`\1\2\3 <\1>`"),
246        (type_fp_param, r"**\1\\(\\)**"),
247        (type_fp_param2, r"**\1\\(\\)**"),
248        (type_func, r"\1()"),
249        (type_enum, r":c:type:`\1 <\2>`"),
250        (type_struct, r":c:type:`\1 <\2>`"),
251        (type_typedef, r":c:type:`\1 <\2>`"),
252        (type_union, r":c:type:`\1 <\2>`"),
253
254        # in rst this can refer to any type
255        (type_fallback, r":c:type:`\1`"),
256        (type_param_ref, r"**\1\2**")
257    ]
258    blankline = "\n"
259
260    sphinx_literal = KernRe(r'^[^.].*::$', cache=False)
261    sphinx_cblock = KernRe(r'^\.\.\ +code-block::', cache=False)
262
263    def __init__(self):
264        """
265        Creates class variables.
266
267        Not really mandatory, but it is a good coding style and makes
268        pylint happy.
269        """
270
271        super().__init__()
272        self.lineprefix = ""
273
274    def print_lineno(self, ln):
275        """Outputs a line number"""
276
277        if self.enable_lineno and ln is not None:
278            ln += 1
279            self.data += f".. LINENO {ln}\n"
280
281    def output_highlight(self, args):
282        """
283        Outputs a C symbol that may require being converted to ReST using
284        the self.highlights variable
285        """
286
287        input_text = args
288        output = ""
289        in_literal = False
290        litprefix = ""
291        block = ""
292
293        for line in input_text.strip("\n").split("\n"):
294
295            # If we're in a literal block, see if we should drop out of it.
296            # Otherwise, pass the line straight through unmunged.
297            if in_literal:
298                if line.strip():  # If the line is not blank
299                    # If this is the first non-blank line in a literal block,
300                    # figure out the proper indent.
301                    if not litprefix:
302                        r = KernRe(r'^(\s*)')
303                        if r.match(line):
304                            litprefix = '^' + r.group(1)
305                        else:
306                            litprefix = ""
307
308                        output += line + "\n"
309                    elif not KernRe(litprefix).match(line):
310                        in_literal = False
311                    else:
312                        output += line + "\n"
313                else:
314                    output += line + "\n"
315
316            # Not in a literal block (or just dropped out)
317            if not in_literal:
318                block += line + "\n"
319                if self.sphinx_literal.match(line) or self.sphinx_cblock.match(line):
320                    in_literal = True
321                    litprefix = ""
322                    output += self.highlight_block(block)
323                    block = ""
324
325        # Handle any remaining block
326        if block:
327            output += self.highlight_block(block)
328
329        # Print the output with the line prefix
330        for line in output.strip("\n").split("\n"):
331            self.data += self.lineprefix + line + "\n"
332
333    def out_section(self, args, out_docblock=False):
334        """
335        Outputs a block section.
336
337        This could use some work; it's used to output the DOC: sections, and
338        starts by putting out the name of the doc section itself, but that
339        tends to duplicate a header already in the template file.
340        """
341        for section, text in args.sections.items():
342            # Skip sections that are in the nosymbol_table
343            if section in self.nosymbol:
344                continue
345
346            if out_docblock:
347                if not self.out_mode == self.OUTPUT_INCLUDE:
348                    self.data += f".. _{section}:\n\n"
349                    self.data += f'{self.lineprefix}**{section}**\n\n'
350            else:
351                self.data += f'{self.lineprefix}**{section}**\n\n'
352
353            self.print_lineno(args.section_start_lines.get(section, 0))
354            self.output_highlight(text)
355            self.data += "\n"
356        self.data += "\n"
357
358    def out_doc(self, fname, name, args):
359        if not self.check_doc(name, args):
360            return
361        self.out_section(args, out_docblock=True)
362
363    def out_function(self, fname, name, args):
364
365        oldprefix = self.lineprefix
366        signature = ""
367
368        func_macro = args.get('func_macro', False)
369        if func_macro:
370            signature = args['function']
371        else:
372            if args.get('functiontype'):
373                signature = args['functiontype'] + " "
374            signature += args['function'] + " ("
375
376        ln = args.get('declaration_start_line', 0)
377        count = 0
378        for parameter in args.parameterlist:
379            if count != 0:
380                signature += ", "
381            count += 1
382            dtype = args.parametertypes.get(parameter, "")
383
384            if function_pointer.search(dtype):
385                signature += function_pointer.group(1) + parameter + function_pointer.group(3)
386            else:
387                signature += dtype
388
389        if not func_macro:
390            signature += ")"
391
392        self.print_lineno(ln)
393        if args.get('typedef') or not args.get('functiontype'):
394            self.data += f".. c:macro:: {args['function']}\n\n"
395
396            if args.get('typedef'):
397                self.data += "   **Typedef**: "
398                self.lineprefix = ""
399                self.output_highlight(args.get('purpose', ""))
400                self.data += "\n\n**Syntax**\n\n"
401                self.data += f"  ``{signature}``\n\n"
402            else:
403                self.data += f"``{signature}``\n\n"
404        else:
405            self.data += f".. c:function:: {signature}\n\n"
406
407        if not args.get('typedef'):
408            self.print_lineno(ln)
409            self.lineprefix = "   "
410            self.output_highlight(args.get('purpose', ""))
411            self.data += "\n"
412
413        # Put descriptive text into a container (HTML <div>) to help set
414        # function prototypes apart
415        self.lineprefix = "  "
416
417        if args.parameterlist:
418            self.data += ".. container:: kernelindent\n\n"
419            self.data += f"{self.lineprefix}**Parameters**\n\n"
420
421        for parameter in args.parameterlist:
422            parameter_name = KernRe(r'\[.*').sub('', parameter)
423            dtype = args.parametertypes.get(parameter, "")
424
425            if dtype:
426                self.data += f"{self.lineprefix}``{dtype}``\n"
427            else:
428                self.data += f"{self.lineprefix}``{parameter}``\n"
429
430            self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
431
432            self.lineprefix = "    "
433            if parameter_name in args.parameterdescs and \
434               args.parameterdescs[parameter_name] != KernelDoc.undescribed:
435
436                self.output_highlight(args.parameterdescs[parameter_name])
437                self.data += "\n"
438            else:
439                self.data += f"{self.lineprefix}*undescribed*\n\n"
440            self.lineprefix = "  "
441
442        self.out_section(args)
443        self.lineprefix = oldprefix
444
445    def out_enum(self, fname, name, args):
446
447        oldprefix = self.lineprefix
448        name = args.get('enum', '')
449        ln = args.get('declaration_start_line', 0)
450
451        self.data += f"\n\n.. c:enum:: {name}\n\n"
452
453        self.print_lineno(ln)
454        self.lineprefix = "  "
455        self.output_highlight(args.get('purpose', ''))
456        self.data += "\n"
457
458        self.data += ".. container:: kernelindent\n\n"
459        outer = self.lineprefix + "  "
460        self.lineprefix = outer + "  "
461        self.data += f"{outer}**Constants**\n\n"
462
463        for parameter in args.parameterlist:
464            self.data += f"{outer}``{parameter}``\n"
465
466            if args.parameterdescs.get(parameter, '') != KernelDoc.undescribed:
467                self.output_highlight(args.parameterdescs[parameter])
468            else:
469                self.data += f"{self.lineprefix}*undescribed*\n\n"
470            self.data += "\n"
471
472        self.lineprefix = oldprefix
473        self.out_section(args)
474
475    def out_typedef(self, fname, name, args):
476
477        oldprefix = self.lineprefix
478        name = args.get('typedef', '')
479        ln = args.get('declaration_start_line', 0)
480
481        self.data += f"\n\n.. c:type:: {name}\n\n"
482
483        self.print_lineno(ln)
484        self.lineprefix = "   "
485
486        self.output_highlight(args.get('purpose', ''))
487
488        self.data += "\n"
489
490        self.lineprefix = oldprefix
491        self.out_section(args)
492
493    def out_struct(self, fname, name, args):
494
495        name = args.get('struct', "")
496        purpose = args.get('purpose', "")
497        declaration = args.get('definition', "")
498        dtype = args.get('type', "struct")
499        ln = args.get('declaration_start_line', 0)
500
501        self.data += f"\n\n.. c:{dtype}:: {name}\n\n"
502
503        self.print_lineno(ln)
504
505        oldprefix = self.lineprefix
506        self.lineprefix += "  "
507
508        self.output_highlight(purpose)
509        self.data += "\n"
510
511        self.data += ".. container:: kernelindent\n\n"
512        self.data += f"{self.lineprefix}**Definition**::\n\n"
513
514        self.lineprefix = self.lineprefix + "  "
515
516        declaration = declaration.replace("\t", self.lineprefix)
517
518        self.data += f"{self.lineprefix}{dtype} {name}" + ' {' + "\n"
519        self.data += f"{declaration}{self.lineprefix}" + "};\n\n"
520
521        self.lineprefix = "  "
522        self.data += f"{self.lineprefix}**Members**\n\n"
523        for parameter in args.parameterlist:
524            if not parameter or parameter.startswith("#"):
525                continue
526
527            parameter_name = parameter.split("[", maxsplit=1)[0]
528
529            if args.parameterdescs.get(parameter_name) == KernelDoc.undescribed:
530                continue
531
532            self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
533
534            self.data += f"{self.lineprefix}``{parameter}``\n"
535
536            self.lineprefix = "    "
537            self.output_highlight(args.parameterdescs[parameter_name])
538            self.lineprefix = "  "
539
540            self.data += "\n"
541
542        self.data += "\n"
543
544        self.lineprefix = oldprefix
545        self.out_section(args)
546
547
548class ManFormat(OutputFormat):
549    """Consts and functions used by man pages output"""
550
551    highlights = (
552        (type_constant, r"\1"),
553        (type_constant2, r"\1"),
554        (type_func, r"\\fB\1\\fP"),
555        (type_enum, r"\\fI\1\\fP"),
556        (type_struct, r"\\fI\1\\fP"),
557        (type_typedef, r"\\fI\1\\fP"),
558        (type_union, r"\\fI\1\\fP"),
559        (type_param, r"\\fI\1\\fP"),
560        (type_param_ref, r"\\fI\1\2\\fP"),
561        (type_member, r"\\fI\1\2\3\\fP"),
562        (type_fallback, r"\\fI\1\\fP")
563    )
564    blankline = ""
565
566    date_formats = [
567        "%a %b %d %H:%M:%S %Z %Y",
568        "%a %b %d %H:%M:%S %Y",
569        "%Y-%m-%d",
570        "%b %d %Y",
571        "%B %d %Y",
572        "%m %d %Y",
573    ]
574
575    def __init__(self, modulename):
576        """
577        Creates class variables.
578
579        Not really mandatory, but it is a good coding style and makes
580        pylint happy.
581        """
582
583        super().__init__()
584        self.modulename = modulename
585
586        dt = None
587        tstamp = os.environ.get("KBUILD_BUILD_TIMESTAMP")
588        if tstamp:
589            for fmt in self.date_formats:
590                try:
591                    dt = datetime.strptime(tstamp, fmt)
592                    break
593                except ValueError:
594                    pass
595
596        if not dt:
597            dt = datetime.now()
598
599        self.man_date = dt.strftime("%B %Y")
600
601    def output_highlight(self, block):
602        """
603        Outputs a C symbol that may require being highlighted with
604        self.highlights variable using troff syntax
605        """
606
607        contents = self.highlight_block(block)
608
609        if isinstance(contents, list):
610            contents = "\n".join(contents)
611
612        for line in contents.strip("\n").split("\n"):
613            line = KernRe(r"^\s*").sub("", line)
614            if not line:
615                continue
616
617            if line[0] == ".":
618                self.data += "\\&" + line + "\n"
619            else:
620                self.data += line + "\n"
621
622    def out_doc(self, fname, name, args):
623        if not self.check_doc(name, args):
624            return
625
626        self.data += f'.TH "{self.modulename}" 9 "{self.modulename}" "{self.man_date}" "API Manual" LINUX' + "\n"
627
628        for section, text in args.sections.items():
629            self.data += f'.SH "{section}"' + "\n"
630            self.output_highlight(text)
631
632    def out_function(self, fname, name, args):
633        """output function in man"""
634
635        self.data += f'.TH "{args["function"]}" 9 "{args["function"]}" "{self.man_date}" "Kernel Hacker\'s Manual" LINUX' + "\n"
636
637        self.data += ".SH NAME\n"
638        self.data += f"{args['function']} \\- {args['purpose']}\n"
639
640        self.data += ".SH SYNOPSIS\n"
641        if args.get('functiontype', ''):
642            self.data += f'.B "{args["functiontype"]}" {args["function"]}' + "\n"
643        else:
644            self.data += f'.B "{args["function"]}' + "\n"
645
646        count = 0
647        parenth = "("
648        post = ","
649
650        for parameter in args.parameterlist:
651            if count == len(args.parameterlist) - 1:
652                post = ");"
653
654            dtype = args.parametertypes.get(parameter, "")
655            if function_pointer.match(dtype):
656                # Pointer-to-function
657                self.data += f'".BI "{parenth}{function_pointer.group(1)}" " ") ({function_pointer.group(2)}){post}"' + "\n"
658            else:
659                dtype = KernRe(r'([^\*])$').sub(r'\1 ', dtype)
660
661                self.data += f'.BI "{parenth}{dtype}"  "{post}"' + "\n"
662            count += 1
663            parenth = ""
664
665        if args.parameterlist:
666            self.data += ".SH ARGUMENTS\n"
667
668        for parameter in args.parameterlist:
669            parameter_name = re.sub(r'\[.*', '', parameter)
670
671            self.data += f'.IP "{parameter}" 12' + "\n"
672            self.output_highlight(args.parameterdescs.get(parameter_name, ""))
673
674        for section, text in args.sections.items():
675            self.data += f'.SH "{section.upper()}"' + "\n"
676            self.output_highlight(text)
677
678    def out_enum(self, fname, name, args):
679
680        name = args.get('enum', '')
681
682        self.data += f'.TH "{self.modulename}" 9 "enum {args["enum"]}" "{self.man_date}" "API Manual" LINUX' + "\n"
683
684        self.data += ".SH NAME\n"
685        self.data += f"enum {args['enum']} \\- {args['purpose']}\n"
686
687        self.data += ".SH SYNOPSIS\n"
688        self.data += f"enum {args['enum']}" + " {\n"
689
690        count = 0
691        for parameter in args.parameterlist:
692            self.data += f'.br\n.BI "    {parameter}"' + "\n"
693            if count == len(args.parameterlist) - 1:
694                self.data += "\n};\n"
695            else:
696                self.data += ", \n.br\n"
697
698            count += 1
699
700        self.data += ".SH Constants\n"
701
702        for parameter in args.parameterlist:
703            parameter_name = KernRe(r'\[.*').sub('', parameter)
704            self.data += f'.IP "{parameter}" 12' + "\n"
705            self.output_highlight(args.parameterdescs.get(parameter_name, ""))
706
707        for section, text in args.sections.items():
708            self.data += f'.SH "{section}"' + "\n"
709            self.output_highlight(text)
710
711    def out_typedef(self, fname, name, args):
712        module = self.modulename
713        typedef = args.get('typedef')
714        purpose = args.get('purpose')
715
716        self.data += f'.TH "{module}" 9 "{typedef}" "{self.man_date}" "API Manual" LINUX' + "\n"
717
718        self.data += ".SH NAME\n"
719        self.data += f"typedef {typedef} \\- {purpose}\n"
720
721        for section, text in args.sections.items():
722            self.data += f'.SH "{section}"' + "\n"
723            self.output_highlight(text)
724
725    def out_struct(self, fname, name, args):
726        module = self.modulename
727        struct_type = args.get('type')
728        struct_name = args.get('struct')
729        purpose = args.get('purpose')
730        definition = args.get('definition')
731
732        self.data += f'.TH "{module}" 9 "{struct_type} {struct_name}" "{self.man_date}" "API Manual" LINUX' + "\n"
733
734        self.data += ".SH NAME\n"
735        self.data += f"{struct_type} {struct_name} \\- {purpose}\n"
736
737        # Replace tabs with two spaces and handle newlines
738        declaration = definition.replace("\t", "  ")
739        declaration = KernRe(r"\n").sub('"\n.br\n.BI "', declaration)
740
741        self.data += ".SH SYNOPSIS\n"
742        self.data += f"{struct_type} {struct_name} " + "{" + "\n.br\n"
743        self.data += f'.BI "{declaration}\n' + "};\n.br\n\n"
744
745        self.data += ".SH Members\n"
746        for parameter in args.parameterlist:
747            if parameter.startswith("#"):
748                continue
749
750            parameter_name = re.sub(r"\[.*", "", parameter)
751
752            if args.parameterdescs.get(parameter_name) == KernelDoc.undescribed:
753                continue
754
755            self.data += f'.IP "{parameter}" 12' + "\n"
756            self.output_highlight(args.parameterdescs.get(parameter_name))
757
758        for section, text in args.sections.items():
759            self.data += f'.SH "{section}"' + "\n"
760            self.output_highlight(text)
761