xref: /linux/Documentation/sphinx/kerneldoc.py (revision 6f7e6393d1ce636bb7ec77a7fe7b77458fddf701)
1# coding=utf-8
2# SPDX-License-Identifier: MIT
3#
4# Copyright © 2016 Intel Corporation
5#
6# Permission is hereby granted, free of charge, to any person obtaining a
7# copy of this software and associated documentation files (the "Software"),
8# to deal in the Software without restriction, including without limitation
9# the rights to use, copy, modify, merge, publish, distribute, sublicense,
10# and/or sell copies of the Software, and to permit persons to whom the
11# Software is furnished to do so, subject to the following conditions:
12#
13# The above copyright notice and this permission notice (including the next
14# paragraph) shall be included in all copies or substantial portions of the
15# Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
20# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23# IN THE SOFTWARE.
24#
25# Authors:
26#    Jani Nikula <jani.nikula@intel.com>
27#
28
29import codecs
30import os
31import subprocess
32import sys
33import re
34import glob
35
36from docutils import nodes, statemachine
37from docutils.statemachine import ViewList
38from docutils.parsers.rst import directives, Directive
39import sphinx
40from sphinx.util.docutils import switch_source_input
41from sphinx.util import logging
42from pprint import pformat
43
44srctree = os.path.abspath(os.environ["srctree"])
45sys.path.insert(0, os.path.join(srctree, "tools/lib/python"))
46
47from kdoc.kdoc_files import KernelFiles
48from kdoc.kdoc_output import RestFormat
49
50# Used when verbose is active to show how to reproduce kernel-doc
51# issues via command line
52kerneldoc_bin = "tools/docs/kernel-doc"
53
54__version__  = '1.0'
55kfiles = None
56logger = logging.getLogger(__name__)
57
58def cmd_str(cmd):
59    """
60    Helper function to output a command line that can be used to produce
61    the same records via command line. Helpful to debug troubles at the
62    script.
63    """
64
65    cmd_line = ""
66
67    for w in cmd:
68        if w == "" or " " in w:
69            esc_cmd = "'" + w + "'"
70        else:
71            esc_cmd = w
72
73        if cmd_line:
74            cmd_line += " " + esc_cmd
75            continue
76        else:
77            cmd_line = esc_cmd
78
79    return cmd_line
80
81class KernelDocDirective(Directive):
82    """Extract kernel-doc comments from the specified file"""
83    required_argument = 1
84    optional_arguments = 4
85    option_spec = {
86        'doc': directives.unchanged_required,
87        'export': directives.unchanged,
88        'internal': directives.unchanged,
89        'identifiers': directives.unchanged,
90        'no-identifiers': directives.unchanged,
91        'functions': directives.unchanged,
92    }
93    has_content = False
94    verbose = 0
95
96    parse_args = {}
97    msg_args = {}
98
99    def handle_args(self):
100
101        env = self.state.document.settings.env
102        cmd = [kerneldoc_bin, '-rst', '-enable-lineno']
103
104        filename = env.config.kerneldoc_srctree + '/' + self.arguments[0]
105
106        # Arguments used by KernelFiles.parse() function
107        self.parse_args = {
108            "file_list": [filename],
109            "export_file": []
110        }
111
112        # Arguments used by KernelFiles.msg() function
113        self.msg_args = {
114            "enable_lineno": True,
115            "export": False,
116            "internal": False,
117            "symbol": [],
118            "nosymbol": [],
119            "no_doc_sections": False
120        }
121
122        export_file_patterns = []
123
124        verbose = os.environ.get("V")
125        if verbose:
126            try:
127                self.verbose = int(verbose)
128            except ValueError:
129                pass
130
131        # Tell sphinx of the dependency
132        env.note_dependency(os.path.abspath(filename))
133
134        self.tab_width = self.options.get('tab-width',
135                                          self.state.document.settings.tab_width)
136
137        # 'function' is an alias of 'identifiers'
138        if 'functions' in self.options:
139            self.options['identifiers'] = self.options.get('functions')
140
141        # FIXME: make this nicer and more robust against errors
142        if 'export' in self.options:
143            cmd += ['-export']
144            self.msg_args["export"] = True
145            export_file_patterns = str(self.options.get('export')).split()
146        elif 'internal' in self.options:
147            cmd += ['-internal']
148            self.msg_args["internal"] = True
149            export_file_patterns = str(self.options.get('internal')).split()
150        elif 'doc' in self.options:
151            func = str(self.options.get('doc'))
152            cmd += ['-function', func]
153            self.msg_args["symbol"].append(func)
154        elif 'identifiers' in self.options:
155            identifiers = self.options.get('identifiers').split()
156            if identifiers:
157                for i in identifiers:
158                    i = i.rstrip("\\").strip()
159                    if not i:
160                        continue
161
162                    cmd += ['-function', i]
163                    self.msg_args["symbol"].append(i)
164            else:
165                cmd += ['-no-doc-sections']
166                self.msg_args["no_doc_sections"] = True
167
168        if 'no-identifiers' in self.options:
169            no_identifiers = self.options.get('no-identifiers').split()
170            if no_identifiers:
171                for i in no_identifiers:
172                    i = i.rstrip("\\").strip()
173                    if not i:
174                        continue
175
176                    cmd += ['-nosymbol', i]
177                    self.msg_args["nosymbol"].append(i)
178
179        for pattern in export_file_patterns:
180            pattern = pattern.rstrip("\\").strip()
181            if not pattern:
182                continue
183
184            for f in glob.glob(env.config.kerneldoc_srctree + '/' + pattern):
185                env.note_dependency(os.path.abspath(f))
186                cmd += ['-export-file', f]
187                self.parse_args["export_file"].append(f)
188
189            # Export file is needed by both parse and msg, as kernel-doc
190            # cache exports.
191            self.msg_args["export_file"] = self.parse_args["export_file"]
192
193        cmd += [filename]
194
195        return cmd
196
197    def parse_msg(self, filename, node, out):
198        """
199        Handles a kernel-doc output for a given file
200        """
201
202        env = self.state.document.settings.env
203
204        lines = statemachine.string2lines(out, self.tab_width,
205                                            convert_whitespace=True)
206        result = ViewList()
207
208        lineoffset = 0;
209        line_regex = re.compile(r"^\.\. LINENO ([0-9]+)$")
210        for line in lines:
211            match = line_regex.search(line)
212            if match:
213                # sphinx counts lines from 0
214                lineoffset = int(match.group(1)) - 1
215                # we must eat our comments since the upset the markup
216            else:
217                doc = str(env.srcdir) + "/" + env.docname + ":" + str(self.lineno)
218                result.append(line, doc + ": " + filename, lineoffset)
219                lineoffset += 1
220
221        self.do_parse(result, node)
222
223    def run_kdoc(self, kfiles):
224        """
225        Execute kernel-doc classes directly instead of running as a separate
226        command.
227        """
228
229        env = self.state.document.settings.env
230
231        node = nodes.section()
232
233        kfiles.parse(**self.parse_args)
234        filenames = self.parse_args["file_list"]
235
236        for filename, out in kfiles.msg(**self.msg_args, filenames=filenames):
237            self.parse_msg(filename, node, out)
238
239        return node.children
240
241    def run(self):
242        cmd = self.handle_args()
243        if self.verbose >= 1:
244            logger.info(cmd_str(cmd))
245
246        try:
247            return self.run_kdoc(kfiles)
248        except Exception as e:  # pylint: disable=W0703
249            logger.warning("kernel-doc '%s' processing failed with: %s" %
250                           (cmd_str(cmd), pformat(e)))
251            return [nodes.error(None, nodes.paragraph(text = "kernel-doc missing"))]
252
253    def do_parse(self, result, node):
254        with switch_source_input(self.state, result):
255            self.state.nested_parse(result, 0, node, match_titles=1)
256
257def setup_kfiles(app):
258    global kfiles
259    out_style = RestFormat()
260    kfiles = KernelFiles(out_style=out_style, logger=logger)
261
262
263def setup(app):
264    app.add_config_value('kerneldoc_srctree', None, 'env')
265    app.add_config_value('kerneldoc_verbosity', 1, 'env')
266
267    app.add_directive('kernel-doc', KernelDocDirective)
268
269    app.connect('builder-inited', setup_kfiles)
270
271    return dict(
272        version = __version__,
273        parallel_read_safe = True,
274        parallel_write_safe = True
275    )
276