xref: /linux/scripts/lib/kdoc/kdoc_output.py (revision a0db2051d7e1fca9a63a8643f1f187ff0b5931f1)
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 = name
371        else:
372            if args.get('functiontype'):
373                signature = args['functiontype'] + " "
374            signature += name + " ("
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:: {name}\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        ln = args.get('declaration_start_line', 0)
449
450        self.data += f"\n\n.. c:enum:: {name}\n\n"
451
452        self.print_lineno(ln)
453        self.lineprefix = "  "
454        self.output_highlight(args.get('purpose', ''))
455        self.data += "\n"
456
457        self.data += ".. container:: kernelindent\n\n"
458        outer = self.lineprefix + "  "
459        self.lineprefix = outer + "  "
460        self.data += f"{outer}**Constants**\n\n"
461
462        for parameter in args.parameterlist:
463            self.data += f"{outer}``{parameter}``\n"
464
465            if args.parameterdescs.get(parameter, '') != KernelDoc.undescribed:
466                self.output_highlight(args.parameterdescs[parameter])
467            else:
468                self.data += f"{self.lineprefix}*undescribed*\n\n"
469            self.data += "\n"
470
471        self.lineprefix = oldprefix
472        self.out_section(args)
473
474    def out_typedef(self, fname, name, args):
475
476        oldprefix = self.lineprefix
477        ln = args.get('declaration_start_line', 0)
478
479        self.data += f"\n\n.. c:type:: {name}\n\n"
480
481        self.print_lineno(ln)
482        self.lineprefix = "   "
483
484        self.output_highlight(args.get('purpose', ''))
485
486        self.data += "\n"
487
488        self.lineprefix = oldprefix
489        self.out_section(args)
490
491    def out_struct(self, fname, name, args):
492
493        purpose = args.get('purpose', "")
494        declaration = args.get('definition', "")
495        dtype = args.get('type', "struct")
496        ln = args.get('declaration_start_line', 0)
497
498        self.data += f"\n\n.. c:{dtype}:: {name}\n\n"
499
500        self.print_lineno(ln)
501
502        oldprefix = self.lineprefix
503        self.lineprefix += "  "
504
505        self.output_highlight(purpose)
506        self.data += "\n"
507
508        self.data += ".. container:: kernelindent\n\n"
509        self.data += f"{self.lineprefix}**Definition**::\n\n"
510
511        self.lineprefix = self.lineprefix + "  "
512
513        declaration = declaration.replace("\t", self.lineprefix)
514
515        self.data += f"{self.lineprefix}{dtype} {name}" + ' {' + "\n"
516        self.data += f"{declaration}{self.lineprefix}" + "};\n\n"
517
518        self.lineprefix = "  "
519        self.data += f"{self.lineprefix}**Members**\n\n"
520        for parameter in args.parameterlist:
521            if not parameter or parameter.startswith("#"):
522                continue
523
524            parameter_name = parameter.split("[", maxsplit=1)[0]
525
526            if args.parameterdescs.get(parameter_name) == KernelDoc.undescribed:
527                continue
528
529            self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
530
531            self.data += f"{self.lineprefix}``{parameter}``\n"
532
533            self.lineprefix = "    "
534            self.output_highlight(args.parameterdescs[parameter_name])
535            self.lineprefix = "  "
536
537            self.data += "\n"
538
539        self.data += "\n"
540
541        self.lineprefix = oldprefix
542        self.out_section(args)
543
544
545class ManFormat(OutputFormat):
546    """Consts and functions used by man pages output"""
547
548    highlights = (
549        (type_constant, r"\1"),
550        (type_constant2, r"\1"),
551        (type_func, r"\\fB\1\\fP"),
552        (type_enum, r"\\fI\1\\fP"),
553        (type_struct, r"\\fI\1\\fP"),
554        (type_typedef, r"\\fI\1\\fP"),
555        (type_union, r"\\fI\1\\fP"),
556        (type_param, r"\\fI\1\\fP"),
557        (type_param_ref, r"\\fI\1\2\\fP"),
558        (type_member, r"\\fI\1\2\3\\fP"),
559        (type_fallback, r"\\fI\1\\fP")
560    )
561    blankline = ""
562
563    date_formats = [
564        "%a %b %d %H:%M:%S %Z %Y",
565        "%a %b %d %H:%M:%S %Y",
566        "%Y-%m-%d",
567        "%b %d %Y",
568        "%B %d %Y",
569        "%m %d %Y",
570    ]
571
572    def __init__(self, modulename):
573        """
574        Creates class variables.
575
576        Not really mandatory, but it is a good coding style and makes
577        pylint happy.
578        """
579
580        super().__init__()
581        self.modulename = modulename
582
583        dt = None
584        tstamp = os.environ.get("KBUILD_BUILD_TIMESTAMP")
585        if tstamp:
586            for fmt in self.date_formats:
587                try:
588                    dt = datetime.strptime(tstamp, fmt)
589                    break
590                except ValueError:
591                    pass
592
593        if not dt:
594            dt = datetime.now()
595
596        self.man_date = dt.strftime("%B %Y")
597
598    def output_highlight(self, block):
599        """
600        Outputs a C symbol that may require being highlighted with
601        self.highlights variable using troff syntax
602        """
603
604        contents = self.highlight_block(block)
605
606        if isinstance(contents, list):
607            contents = "\n".join(contents)
608
609        for line in contents.strip("\n").split("\n"):
610            line = KernRe(r"^\s*").sub("", line)
611            if not line:
612                continue
613
614            if line[0] == ".":
615                self.data += "\\&" + line + "\n"
616            else:
617                self.data += line + "\n"
618
619    def out_doc(self, fname, name, args):
620        if not self.check_doc(name, args):
621            return
622
623        self.data += f'.TH "{self.modulename}" 9 "{self.modulename}" "{self.man_date}" "API Manual" LINUX' + "\n"
624
625        for section, text in args.sections.items():
626            self.data += f'.SH "{section}"' + "\n"
627            self.output_highlight(text)
628
629    def out_function(self, fname, name, args):
630        """output function in man"""
631
632        self.data += f'.TH "{name}" 9 "{name}" "{self.man_date}" "Kernel Hacker\'s Manual" LINUX' + "\n"
633
634        self.data += ".SH NAME\n"
635        self.data += f"{name} \\- {args['purpose']}\n"
636
637        self.data += ".SH SYNOPSIS\n"
638        if args.get('functiontype', ''):
639            self.data += f'.B "{args["functiontype"]}" {name}' + "\n"
640        else:
641            self.data += f'.B "{name}' + "\n"
642
643        count = 0
644        parenth = "("
645        post = ","
646
647        for parameter in args.parameterlist:
648            if count == len(args.parameterlist) - 1:
649                post = ");"
650
651            dtype = args.parametertypes.get(parameter, "")
652            if function_pointer.match(dtype):
653                # Pointer-to-function
654                self.data += f'".BI "{parenth}{function_pointer.group(1)}" " ") ({function_pointer.group(2)}){post}"' + "\n"
655            else:
656                dtype = KernRe(r'([^\*])$').sub(r'\1 ', dtype)
657
658                self.data += f'.BI "{parenth}{dtype}"  "{post}"' + "\n"
659            count += 1
660            parenth = ""
661
662        if args.parameterlist:
663            self.data += ".SH ARGUMENTS\n"
664
665        for parameter in args.parameterlist:
666            parameter_name = re.sub(r'\[.*', '', parameter)
667
668            self.data += f'.IP "{parameter}" 12' + "\n"
669            self.output_highlight(args.parameterdescs.get(parameter_name, ""))
670
671        for section, text in args.sections.items():
672            self.data += f'.SH "{section.upper()}"' + "\n"
673            self.output_highlight(text)
674
675    def out_enum(self, fname, name, args):
676        self.data += f'.TH "{self.modulename}" 9 "enum {name}" "{self.man_date}" "API Manual" LINUX' + "\n"
677
678        self.data += ".SH NAME\n"
679        self.data += f"enum {name} \\- {args['purpose']}\n"
680
681        self.data += ".SH SYNOPSIS\n"
682        self.data += f"enum {name}" + " {\n"
683
684        count = 0
685        for parameter in args.parameterlist:
686            self.data += f'.br\n.BI "    {parameter}"' + "\n"
687            if count == len(args.parameterlist) - 1:
688                self.data += "\n};\n"
689            else:
690                self.data += ", \n.br\n"
691
692            count += 1
693
694        self.data += ".SH Constants\n"
695
696        for parameter in args.parameterlist:
697            parameter_name = KernRe(r'\[.*').sub('', parameter)
698            self.data += f'.IP "{parameter}" 12' + "\n"
699            self.output_highlight(args.parameterdescs.get(parameter_name, ""))
700
701        for section, text in args.sections.items():
702            self.data += f'.SH "{section}"' + "\n"
703            self.output_highlight(text)
704
705    def out_typedef(self, fname, name, args):
706        module = self.modulename
707        purpose = args.get('purpose')
708
709        self.data += f'.TH "{module}" 9 "{name}" "{self.man_date}" "API Manual" LINUX' + "\n"
710
711        self.data += ".SH NAME\n"
712        self.data += f"typedef {name} \\- {purpose}\n"
713
714        for section, text in args.sections.items():
715            self.data += f'.SH "{section}"' + "\n"
716            self.output_highlight(text)
717
718    def out_struct(self, fname, name, args):
719        module = self.modulename
720        purpose = args.get('purpose')
721        definition = args.get('definition')
722
723        self.data += f'.TH "{module}" 9 "{args.type} {name}" "{self.man_date}" "API Manual" LINUX' + "\n"
724
725        self.data += ".SH NAME\n"
726        self.data += f"{args.type} {name} \\- {purpose}\n"
727
728        # Replace tabs with two spaces and handle newlines
729        declaration = definition.replace("\t", "  ")
730        declaration = KernRe(r"\n").sub('"\n.br\n.BI "', declaration)
731
732        self.data += ".SH SYNOPSIS\n"
733        self.data += f"{args.type} {name} " + "{" + "\n.br\n"
734        self.data += f'.BI "{declaration}\n' + "};\n.br\n\n"
735
736        self.data += ".SH Members\n"
737        for parameter in args.parameterlist:
738            if parameter.startswith("#"):
739                continue
740
741            parameter_name = re.sub(r"\[.*", "", parameter)
742
743            if args.parameterdescs.get(parameter_name) == KernelDoc.undescribed:
744                continue
745
746            self.data += f'.IP "{parameter}" 12' + "\n"
747            self.output_highlight(args.parameterdescs.get(parameter_name))
748
749        for section, text in args.sections.items():
750            self.data += f'.SH "{section}"' + "\n"
751            self.output_highlight(text)
752