diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 436e7ac42ffc..be8e566e0363 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -1,30 +1,25 @@ #!/usr/bin/env python # SPDX-License-Identifier: GPL-2.0 # -*- coding: utf-8; mode: python -*- -# pylint: disable=R0903, C0330, R0914, R0912, E0401 +# pylint: disable=C0209, C0301, E0401, R0022, R0902, R0903, R0912, R0914 """ - maintainers-include - ~~~~~~~~~~~~~~~~~~~ +Implementation of the ``maintainers-include`` reST-directive. - Implementation of the ``maintainers-include`` reST-directive. +:copyright: Copyright (C) 2019 Kees Cook +:license: GPL Version 2, June 1991 see linux/COPYING for details. - :copyright: Copyright (C) 2019 Kees Cook - :license: GPL Version 2, June 1991 see linux/COPYING for details. - - The ``maintainers-include`` reST-directive performs extensive parsing - specific to the Linux kernel's standard "MAINTAINERS" file, in an - effort to avoid needing to heavily mark up the original plain text. +The ``maintainers-include`` reST-directive performs extensive parsing +specific to the Linux kernel's standard "MAINTAINERS" file, in an +effort to avoid needing to heavily mark up the original plain text. """ -import sys -import re import os.path +import re from glob import glob from docutils import statemachine -from docutils.parsers.rst import Directive from docutils.parsers.rst.directives.misc import Include # @@ -32,193 +27,274 @@ from docutils.parsers.rst.directives.misc import Include # KERNELDOC_URL = "https://docs.kernel.org/" -def ErrorString(exc): # Shamelessly stolen from docutils - return f'{exc.__class__.__name}: {exc}' +__version__ = "1.0" -__version__ = '1.0' +maint_parser = None # pylint: disable=C0103 -maint_parser = None +JS_FILTER = """ +(function() { + function filterTable(table) { + const filter = document.getElementById("filter-table").value.trim(); + const rows = table.querySelectorAll("tbody tr"); + for (let i = 0; i < rows.length; i++) { + const tds = rows[i].getElementsByTagName("td"); + let match = false; + for (let j = 0; j < tds.length; j++) { + const cellText = (tds[j].textContent || tds[j].innerText); + if (cellText.includes(filter)) { + match = true; + break; + } + } + rows[i].style.display = match ? "table-row" : "none"; + } + } + function addInput() { + const table = document.getElementById("maintainers-table"); + if (!table) return; + let input = document.getElementById("filter-table"); + if (!input) { + const filt_div = document.createElement('div'); + filt_div.innerHTML = ` +

Filter: + + subsystem or property (case-sensitive) +

+ `; + table.parentNode.insertBefore(filt_div, table); + const input = document.getElementById("filter-table") + input.addEventListener('input', () => filterTable(table)); + } + } + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', addInput); + } else { + addInput(); + } +})(); +""" + + +# Shamelessly stolen from docutils +def ErrorString(exc): # pylint: disable=C0103, C0116 + return f"{exc.__class__.__name}: {exc}" # pylint: disable=W0212 class MaintainersParser: """Parse MAINTAINERS file(s) content""" - def __init__(self, app_dir, path): + def __init__(self, base_dir, app_dir, path): self.path = path - self.profile_toc = set() - self.profile_entries = {} - - result = list() - result.append(".. _maintainers:") - result.append("") # Poor man's state machine. - descriptions = False - maintainers = False - subsystems = False + self.descriptions = False + self.maintainers = False + self.subsystems = False - # Field letter to field name mapping. - field_letter = None - fields = dict() + self.subsystem_name = None + + self.base_dir = base_dir + self.app_dir = app_dir + + self.re_doc = re.compile(r'(Documentation/(\S*)\.rst)') + + # + # Output variables with maintainers content to be stored + # + self.profile_toc = set() + self.profile_entries = {} + self.header = "" + self.maint_entries = {} + self.fields = {} prev = None - field_prev = "" - field_content = "" - subsystem_name = None - - base_dir, doc_dir, sphinx_dir = app_dir.partition("Documentation") - - for line in open(path): - # Have we reached the end of the preformatted Descriptions text? - if descriptions and line.startswith('Maintainers'): - descriptions = False - # Ensure a blank line following the last "|"-prefixed line. - result.append("") - - # Start subsystem processing? This is to skip processing the text - # between the Maintainers heading and the first subsystem name. - if maintainers and not subsystems: - if re.search('^[A-Z0-9]', line): - subsystems = True - - # Drop needless input whitespace. - line = line.rstrip() - - # - # Handle profile entries - either as files or as https refs - # - match = re.match(rf"P:\s*({doc_dir})(/\S+)\.rst", line) - if match: - name = "".join(match.groups()) - entry = os.path.relpath(base_dir + name, app_dir) - - full_name = os.path.join(base_dir, name) - path = os.path.relpath(full_name, app_dir) - # - # When SPHINXDIRS is used, it will try to reference files - # outside srctree, causing warnings. To avoid that, point - # to the latest official documentation - # - if path.startswith("../"): - entry = KERNELDOC_URL + match.group(2) + ".html" + with open(path, "r", encoding="utf-8") as fp: + for line in fp: + if self.descriptions: + self.parse_descriptions(line) + elif self.maintainers and not self.subsystems: + if re.search('^[A-Z0-9]', line): + self.subsystems = True + self.parse_subsystems(line) + else: + self.header += line + elif self.subsystems: + self.parse_subsystems(line) else: - entry = "/" + entry + self.header += line - if "*" in entry: - for e in glob(entry): + # Update the state machine when we find heading separators. + if line.startswith("----------"): + if prev.startswith("Descriptions"): + self.descriptions = True + if prev.startswith("Maintainers"): + self.maintainers = True + + # Retain previous line for state machine transitions. + prev = line + + def get_entries(self, text): + """Generate refs to ReST files in Documentation/""" + + if "Documentation/" not in text: + return None + + if "*" in text or "?" in text: + m = self.re_doc.search(text) + if not m: + return None + + doc_list = glob(m.group(1), root_dir=self.base_dir) + else: + doc_list = [text] + + entries = {} + for doc in doc_list: + m = self.re_doc.search(doc) + if m: + fname = m.group(1) + ename = m.group(2) + + entry = os.path.relpath(self.base_dir + fname, self.app_dir) + entry = entry.removesuffix(".rst") + + if entry.startswith("../"): + html = KERNELDOC_URL + ename + ".html" + entries[entry] = f'`{ename} <{html}>`_' + else: + entries[entry] = f':doc:`{ename} `' + + return entries + + def linkify(self, text): + """Return a list of doc files converted to cross-references""" + + entries = self.get_entries(text) + if not entries: + return text + + return self.re_doc.sub(", ".join(entries.values()), text) + + def parse_descriptions(self, line): + """Handle contents of the descriptions section.""" + + # Have we reached the end of the preformatted Descriptions text? + if line.startswith("Maintainers"): + self.descriptions = False + self.header += "\n" + line + return + + # Look for and record field letter to field name mappings: + # R: Designated *reviewer*: FullName + m = re.match(r"\s+(\S):\s+(\S+)", line) + if m: + field = m.group(1) + details = m.group(2) + + if field not in self.fields: + m = re.search(r"\*([^\*]+)\*", line) + if m: + self.fields[field] = m.group(1) + elif field in ['F', 'N', 'X', 'K']: + line = line.replace(details, f'``{details}``') + + self.header += "| " + self.linkify(line) + + + def parse_subsystems(self, line): + """Handle contents of the per-subsystem sections.""" + + # Drop needless input whitespace. + line = line.rstrip() + + # Skip empty lines: subsystem parser adds them as needed. + if not line: + return + + if line[1] != ':': + self.subsystem_name = re.sub(r"\s+", " ", self.linkify(line)) + return + + # Render a subsystem field as: + # :Field: entry + # entry... + field, details = line.split(":", 1) + details = details.strip() + + # + # Handle profile entries - either as files or as https refs + # + if field == "P": + entries = self.get_entries(details) + if entries: + for e, link in entries.items(): + if "html" not in link: self.profile_toc.add(e) - self.profile_entries[subsystem_name] = e - else: - self.profile_toc.add(entry) - self.profile_entries[subsystem_name] = entry + + self.profile_entries[self.subsystem_name] = link + + details = ", ".join(entries.values()) else: - match = re.match(r"P:\s*(https?://.*)", line) + match = re.match(r"(https?://.*)", details) if match: entry = match.group(1).strip() - self.profile_entries[subsystem_name] = entry - - # Linkify all non-wildcard refs to ReST files in Documentation/. - pat = r'(Documentation/([^\s\?\*]*)\.rst)' - m = re.search(pat, line) - if m: - # maintainers.rst is in a subdirectory, so include "../". - line = re.sub(pat, ':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line) - - # Check state machine for output rendering behavior. - output = None - if descriptions: - # Escape the escapes in preformatted text. - output = "| %s" % (line.replace("\\", "\\\\")) - # Look for and record field letter to field name mappings: - # R: Designated *reviewer*: FullName - m = re.search(r"\s(\S):\s", line) - if m: - field_letter = m.group(1) - if field_letter and not field_letter in fields: - m = re.search(r"\*([^\*]+)\*", line) - if m: - fields[field_letter] = m.group(1) - elif subsystems: - # Skip empty lines: subsystem parser adds them as needed. - if len(line) == 0: - continue - # Subsystem fields are batched into "field_content" - if line[1] != ':': - # Render a subsystem entry as: - # SUBSYSTEM NAME - # ~~~~~~~~~~~~~~ - - # Flush pending field content. - output = field_content + "\n\n" - field_content = "" - - subsystem_name = line.title() - - # Collapse whitespace in subsystem name. - heading = re.sub(r"\s+", " ", line) - output = output + "%s\n%s" % (heading, "~" * len(heading)) - field_prev = "" + self.profile_entries[self.subsystem_name] = entry else: - # Render a subsystem field as: - # :Field: entry - # entry... - field, details = line.split(':', 1) - details = details.strip() + self.profile_entries[self.subsystem_name] = f"``{details}``" - # Mark paths (and regexes) as literal text for improved - # readability and to escape any escapes. - if field in ['F', 'N', 'X', 'K']: - # But only if not already marked :) - if not ':doc:' in details: - details = '``%s``' % (details) + details = self.linkify(details) + else: + details = self.linkify(details) - # Comma separate email field continuations. - if field == field_prev and field_prev in ['M', 'R', 'L']: - field_content = field_content + "," + # + # Mark paths (and regexes) as literal text for improved + # readability and to escape any escapes. + # + if field in ['F', 'N', 'X', 'K']: + # But only if not already marked :) + if ':doc:' not in details and "http" not in details: + details = '``%s``' % (details) - # Do not repeat field names, so that field entries - # will be collapsed together. - if field != field_prev: - output = field_content + "\n" - field_content = ":%s:" % (fields.get(field, field)) - field_content = field_content + "\n\t%s" % (details) - field_prev = field - else: - output = line + if self.subsystem_name not in self.maint_entries: + self.maint_entries[self.subsystem_name] = {} - # Re-split on any added newlines in any above parsing. - if output != None: - for separated in output.split('\n'): - result.append(separated) + if field not in self.maint_entries[self.subsystem_name]: + self.maint_entries[self.subsystem_name][field] = [] - # Update the state machine when we find heading separators. - if line.startswith('----------'): - if prev.startswith('Descriptions'): - descriptions = True - if prev.startswith('Maintainers'): - maintainers = True + self.maint_entries[self.subsystem_name][field].append(details) - # Retain previous line for state machine transitions. - prev = line + self.field_prev = field - # Flush pending field contents. - if field_content != "": - for separated in field_content.split('\n'): - result.append(separated) - - self.output = "\n".join(result) - - # Create a TOC class class MaintainersInclude(Include): """MaintainersInclude (``maintainers-include``) directive""" + required_arguments = 0 def emit(self): """Parse all the MAINTAINERS lines into ReST for human-readability""" - global maint_parser - path = maint_parser.path - output = maint_parser.output + output = ".. _maintainers:\n\n" + output += maint_parser.header + + output += ".. _maintainers_table:\n\n" + output += ".. flat-table::\n" + output += " :header-rows: 1\n\n" + output += " * - Subsystem\n" + output += " - Properties\n\n" + + self.state.document['maintainers_included'] = True + + for name, fields in sorted(maint_parser.maint_entries.items()): + output += f" * - {name}\n" + tag = "-" + for field, lines in fields.items(): + field_name = maint_parser.fields.get(field, field) + + output += f" {tag} :{field_name}:\n " + output += ",\n ".join(lines) + "\n" + tag = " " + + output += "\n" # For debugging the pre-rendered results... #print(output, file=open("/tmp/MAINTAINERS.rst", "w")) @@ -232,20 +308,23 @@ class MaintainersInclude(Include): raise self.warning('"%s" directive disabled.' % self.name) try: - lines = self.emit() + self.emit() except IOError as error: raise self.severe('Problems with "%s" directive path:\n%s.' % (self.name, ErrorString(error))) return [] + class MaintainersProfile(Include): + """Generate a list with all maintainer's profiles""" + required_arguments = 0 def emit(self): """Parse all the MAINTAINERS lines looking for profile entries""" - global maint_parser - + env = self.state.document.settings.env + docdir = os.path.dirname(os.path.join(env.srcdir, env.docname)) path = maint_parser.path # @@ -253,10 +332,15 @@ class MaintainersProfile(Include): # output = "" for profile, entry in sorted(maint_parser.profile_entries.items()): + name = profile.title() + if entry.startswith("http"): - output += f"- `{profile} <{entry}>`_\n" + output += f"- `{name} <{entry}>`_\n" + elif entry.startswith("`"): + output += f"- {name}: {entry}\n" + self.warning(f"{profile}: Invalid 'P' tag: {entry}\n") else: - output += f"- :doc:`{profile} <{entry}>`\n" + output += f"- {entry}\n" # # Create a hidden TOC table with all profiles. That allows adding @@ -265,11 +349,16 @@ class MaintainersProfile(Include): output += "\n.. toctree::\n" output += " :hidden:\n\n" - for fname in maint_parser.profile_toc: + for f in sorted(maint_parser.profile_toc): + fname = os.path.join(maint_parser.base_dir, "Documentation", f) + fname = os.path.relpath(fname, docdir) output += f" {fname}\n" output += "\n" + # For debugging the pre-rendered results... + #print(output, file=open("/tmp/profiles.rst", "w")) + self.state.document.settings.record_dependencies.add(path) self.state_machine.insert_input(statemachine.string2lines(output), path) @@ -279,30 +368,43 @@ class MaintainersProfile(Include): raise self.warning('"%s" directive disabled.' % self.name) try: - lines = self.emit() + self.emit() except IOError as error: raise self.severe('Problems with "%s" directive path:\n%s.' % (self.name, ErrorString(error))) return [] + +# pylint: disable=W0613 +def add_filter_script(app, pagename, templatename, context, doctree): + """Add Filter javascript only to maintainers page""" + + if doctree and doctree.get('maintainers_included'): + app.add_js_file(None, body=JS_FILTER) + + def setup(app): - global maint_parser + """Setup Sphinx extension""" + global maint_parser # pylint: disable=W0603 - # - # NOTE: we're using os.fspath() here because of a Sphinx warning: - # RemovedInSphinx90Warning: Sphinx 9 will drop support for representing paths as strings. Use "pathlib.Path" or "os.fspath" instead. - # - app_dir = os.fspath(app.srcdir) - srctree = os.path.abspath(os.environ["srctree"]) - path = os.path.join(srctree, "MAINTAINERS") + app_dir = os.path.abspath(app.srcdir) + match = re.match(r"(.*/)Documentation", app_dir) + if not match: + raise ValueError('Documentation directory not found.') - maint_parser = MaintainersParser(app_dir, path) + base_dir = match.group(1) + path = os.path.join(base_dir, "MAINTAINERS") + + maint_parser = MaintainersParser(base_dir, app_dir, path) app.add_directive("maintainers-include", MaintainersInclude) app.add_directive("maintainers-profile-toc", MaintainersProfile) - return dict( - version = __version__, - parallel_read_safe = True, - parallel_write_safe = True - ) + + app.connect("html-page-context", add_filter_script) + + return { + "version": __version__, + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/MAINTAINERS b/MAINTAINERS index 2fb1c75afd16..dd424a4f9f3b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -25,9 +25,9 @@ Descriptions of section entries and preferred order C: URI for *chat* protocol, server and channel where developers usually hang out, for example irc://server/channel. P: *Subsystem Profile* document for more details submitting - patches to the given subsystem. This is either an in-tree file, - or a URI. See Documentation/maintainer/maintainer-entry-profile.rst - for details. + patches to the given subsystem. This is either an in-tree .rst file + inside Documentation/, or a URI. + See Documentation/maintainer/maintainer-entry-profile.rst for details. T: *SCM* tree type and location. Type is one of: git, hg, quilt, stgit, topgit F: *Files* and directories wildcard patterns. @@ -23402,7 +23402,7 @@ S: Maintained W: https://rust-for-linux.com/pin-init B: https://github.com/Rust-for-Linux/pin-init/issues C: zulip://rust-for-linux.zulipchat.com -P: rust/pin-init/CONTRIBUTING.md +P: https://github.com/Rust-for-Linux/pin-init/blob/main/CONTRIBUTING.md T: git https://github.com/Rust-for-Linux/linux.git pin-init-next F: rust/kernel/init.rs F: rust/pin-init/