Add the basic infrastructure to parse MAINTAINERS and generate a list
of MaintainerSection objects we can use later.

Add a --validate argument so we can use the script to ensure
MAINTAINERS is always parse-able in our CI.

Signed-off-by: Alex Bennée <[email protected]>
---
 scripts/get_maintainer.py | 165 +++++++++++++++++++++++++++++++++++++-
 1 file changed, 164 insertions(+), 1 deletion(-)

diff --git a/scripts/get_maintainer.py b/scripts/get_maintainer.py
index c713f290cc7..7b8ce2b65e3 100755
--- a/scripts/get_maintainer.py
+++ b/scripts/get_maintainer.py
@@ -10,9 +10,156 @@
 #
 # SPDX-License-Identifier: GPL-2.0-or-later
 
-from argparse import ArgumentParser, ArgumentTypeError
+from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction
 from os import path
 from pathlib import Path
+from enum import StrEnum, auto
+from re import compile as re_compile
+
+#
+# Subsystem MAINTAINER entries
+#
+# The MAINTAINERS file is an unstructured text file where the
+# important information is in lines that follow the form:
+#
+# X: some data
+#
+# where X is a documented tag and the data is variously an email,
+# path, regex or link. Other lines should be ignored except the
+# preceding non-blank or underlined line which represents the name of
+# the "subsystem" or general area of the project.
+#
+# A blank line denominates the end of a section.
+#
+
+tag_re = re_compile(r"^([A-Z]):")
+
+
+class UnhandledTag(Exception):
+    "Exception for unhandled tags"
+
+
+class BadStatus(Exception):
+    "Exception for unknown status"
+
+
+class Status(StrEnum):
+    "Maintenance status"
+
+    UNKNOWN = auto()
+    SUPPORTED = 'Supported'
+    MAINTAINED = 'Maintained'
+    ODD_FIXES = 'Odd Fixes'
+    ORPHAN = 'Orphan'
+    OBSOLETE = 'Obsolete'
+
+    @classmethod
+    def _missing_(cls, value):
+        # _missing_ is only invoked by the enum machinery if 'value' does not
+        # match any existing enum member's value.
+        # So, if we reach this point, 'value' is inherently invalid for this 
enum.
+        raise BadStatus(f"'{value}' is not a valid maintenance status.")
+
+
+person_re = 
re_compile(r"^(?P<name>[^<]+?)\s*<(?P<email>[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>\s*(?:@(?P<handle>\w+))?$")
+
+
+class BadPerson(Exception):
+    "Exception for un-parsable person"
+
+
+class Person:
+    "Class representing a maintainer or reviewer and their details"
+
+    def __init__(self, info):
+        match = person_re.search(info)
+
+        if match is None:
+            raise BadPerson(f"Failed to parse {info}")
+
+        self.name = match.group('name')
+        self.email = match.group('email')
+
+
+class MaintainerSection:
+    "Class representing a section of MAINTAINERS"
+
+    def _expand(self, pattern):
+        if pattern.endswith("/"):
+            return f"{pattern}*"
+        return pattern
+
+    def __init__(self, section, entries):
+        self.section = section
+        self.status = Status.UNKNOWN
+        self.maintainers = []
+        self.reviewers = []
+        self.files = []
+        self.files_exclude = []
+        self.trees = []
+        self.lists = []
+        self.web = []
+        self.keywords = []
+
+        for e in entries:
+            (tag, data) = e.split(": ", 2)
+
+            if tag == "M":
+                person = Person(data)
+                self.maintainers.append(person)
+            elif tag == "R":
+                person = Person(data)
+                self.reviewers.append(person)
+            elif tag == "S":
+                self.status = Status(data)
+            elif tag == "L":
+                self.lists.append(data)
+            elif tag == 'F':
+                pat = self._expand(data)
+                self.files.append(pat)
+            elif tag == 'W':
+                self.web.append(data)
+            elif tag == 'K':
+                self.keywords.append(data)
+            elif tag == 'T':
+                self.trees.append(data)
+            elif tag == 'X':
+                pat = self._expand(data)
+                self.files_exclude.append(pat)
+            else:
+                raise UnhandledTag(f"'{tag}' is not understood.")
+
+
+
+def read_maintainers(src):
+    """
+    Read the MAINTAINERS file, return a list of MaintainerSection objects.
+    """
+
+    mfile = path.join(src, 'MAINTAINERS')
+    entries = []
+
+    section = None
+    fields = []
+
+    with open(mfile, 'r', encoding='utf-8') as f:
+        for line in f:
+            if not line.strip():  # Blank line found, potential end of a 
section
+                if section:
+                    new_section = MaintainerSection(section, fields)
+                    entries.append(new_section)
+                    # reset for next section
+                    section = None
+                    fields = []
+            elif tag_re.match(line):
+                fields.append(line.strip())
+            else:
+                if line.startswith("-") or line.startswith("="):
+                    continue
+
+                section = line.strip()
+
+    return entries
 
 
 #
@@ -103,6 +250,12 @@ def main():
     group.add_argument('-f', '--file', type=valid_file_path,
                        help='path to source file')
 
+    # Validate MAINTAINERS
+    parser.add_argument('--validate',
+                        action=BooleanOptionalAction,
+                        default=None,
+                        help="Just validate MAINTAINERS file")
+
     # We need to know or be told where the root of the source tree is
     src = find_src_root()
 
@@ -115,6 +268,16 @@ def main():
 
     args = parser.parse_args()
 
+    try:
+        # Now we start by reading the MAINTAINERS file
+        maint_sections = read_maintainers(args.src)
+    except Exception as e:
+        print(f"Error: {e}")
+        exit(-1)
+
+    if args.validate:
+        print(f"loaded {len(maint_sections)} from MAINTAINERS")
+        exit(0)
 
 
 if __name__ == '__main__':
-- 
2.47.3


Reply via email to