| #!/usr/bin/env python3 |
| # SPDX-License-Identifier: GPL-2.0 |
| # Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>. |
| |
| """ |
| Regular expression ancillary classes. |
| |
| Those help caching regular expressions and do matching for kernel-doc. |
| """ |
| |
| import re |
| |
| # Local cache for regular expressions |
| re_cache = {} |
| |
| |
| class KernRe: |
| """ |
| Helper class to simplify regex declaration and usage, |
| |
| It calls re.compile for a given pattern. It also allows adding |
| regular expressions and define sub at class init time. |
| |
| Regular expressions can be cached via an argument, helping to speedup |
| searches. |
| """ |
| |
| def _add_regex(self, string, flags): |
| """ |
| Adds a new regex or re-use it from the cache. |
| """ |
| self.regex = re_cache.get(string, None) |
| if not self.regex: |
| self.regex = re.compile(string, flags=flags) |
| if self.cache: |
| re_cache[string] = self.regex |
| |
| def __init__(self, string, cache=True, flags=0): |
| """ |
| Compile a regular expression and initialize internal vars. |
| """ |
| |
| self.cache = cache |
| self.last_match = None |
| |
| self._add_regex(string, flags) |
| |
| def __str__(self): |
| """ |
| Return the regular expression pattern. |
| """ |
| return self.regex.pattern |
| |
| def __add__(self, other): |
| """ |
| Allows adding two regular expressions into one. |
| """ |
| |
| return KernRe(str(self) + str(other), cache=self.cache or other.cache, |
| flags=self.regex.flags | other.regex.flags) |
| |
| def match(self, string): |
| """ |
| Handles a re.match storing its results |
| """ |
| |
| self.last_match = self.regex.match(string) |
| return self.last_match |
| |
| def search(self, string): |
| """ |
| Handles a re.search storing its results |
| """ |
| |
| self.last_match = self.regex.search(string) |
| return self.last_match |
| |
| def findall(self, string): |
| """ |
| Alias to re.findall |
| """ |
| |
| return self.regex.findall(string) |
| |
| def split(self, string): |
| """ |
| Alias to re.split |
| """ |
| |
| return self.regex.split(string) |
| |
| def sub(self, sub, string, count=0): |
| """ |
| Alias to re.sub |
| """ |
| |
| return self.regex.sub(sub, string, count=count) |
| |
| def group(self, num): |
| """ |
| Returns the group results of the last match |
| """ |
| |
| return self.last_match.group(num) |
| |
| |
| class NestedMatch: |
| """ |
| Finding nested delimiters is hard with regular expressions. It is |
| even harder on Python with its normal re module, as there are several |
| advanced regular expressions that are missing. |
| |
| This is the case of this pattern: |
| |
| '\\bSTRUCT_GROUP(\\(((?:(?>[^)(]+)|(?1))*)\\))[^;]*;' |
| |
| which is used to properly match open/close parenthesis of the |
| string search STRUCT_GROUP(), |
| |
| Add a class that counts pairs of delimiters, using it to match and |
| replace nested expressions. |
| |
| The original approach was suggested by: |
| https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex |
| |
| Although I re-implemented it to make it more generic and match 3 types |
| of delimiters. The logic checks if delimiters are paired. If not, it |
| will ignore the search string. |
| """ |
| |
| # TODO: make NestedMatch handle multiple match groups |
| # |
| # Right now, regular expressions to match it are defined only up to |
| # the start delimiter, e.g.: |
| # |
| # \bSTRUCT_GROUP\( |
| # |
| # is similar to: STRUCT_GROUP\((.*)\) |
| # except that the content inside the match group is delimiter's aligned. |
| # |
| # The content inside parenthesis are converted into a single replace |
| # group (e.g. r`\1'). |
| # |
| # It would be nice to change such definition to support multiple |
| # match groups, allowing a regex equivalent to. |
| # |
| # FOO\((.*), (.*), (.*)\) |
| # |
| # it is probably easier to define it not as a regular expression, but |
| # with some lexical definition like: |
| # |
| # FOO(arg1, arg2, arg3) |
| |
| DELIMITER_PAIRS = { |
| '{': '}', |
| '(': ')', |
| '[': ']', |
| } |
| |
| RE_DELIM = re.compile(r'[\{\}\[\]\(\)]') |
| |
| def _search(self, regex, line): |
| """ |
| Finds paired blocks for a regex that ends with a delimiter. |
| |
| The suggestion of using finditer to match pairs came from: |
| https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex |
| but I ended using a different implementation to align all three types |
| of delimiters and seek for an initial regular expression. |
| |
| The algorithm seeks for open/close paired delimiters and place them |
| into a stack, yielding a start/stop position of each match when the |
| stack is zeroed. |
| |
| The algorithm shoud work fine for properly paired lines, but will |
| silently ignore end delimiters that preceeds an start delimiter. |
| This should be OK for kernel-doc parser, as unaligned delimiters |
| would cause compilation errors. So, we don't need to rise exceptions |
| to cover such issues. |
| """ |
| |
| stack = [] |
| |
| for match_re in regex.finditer(line): |
| start = match_re.start() |
| offset = match_re.end() |
| |
| d = line[offset - 1] |
| if d not in self.DELIMITER_PAIRS: |
| continue |
| |
| end = self.DELIMITER_PAIRS[d] |
| stack.append(end) |
| |
| for match in self.RE_DELIM.finditer(line[offset:]): |
| pos = match.start() + offset |
| |
| d = line[pos] |
| |
| if d in self.DELIMITER_PAIRS: |
| end = self.DELIMITER_PAIRS[d] |
| |
| stack.append(end) |
| continue |
| |
| # Does the end delimiter match what it is expected? |
| if stack and d == stack[-1]: |
| stack.pop() |
| |
| if not stack: |
| yield start, offset, pos + 1 |
| break |
| |
| def search(self, regex, line): |
| """ |
| This is similar to re.search: |
| |
| It matches a regex that it is followed by a delimiter, |
| returning occurrences only if all delimiters are paired. |
| """ |
| |
| for t in self._search(regex, line): |
| |
| yield line[t[0]:t[2]] |
| |
| def sub(self, regex, sub, line, count=0): |
| """ |
| This is similar to re.sub: |
| |
| It matches a regex that it is followed by a delimiter, |
| replacing occurrences only if all delimiters are paired. |
| |
| if r'\1' is used, it works just like re: it places there the |
| matched paired data with the delimiter stripped. |
| |
| If count is different than zero, it will replace at most count |
| items. |
| """ |
| out = "" |
| |
| cur_pos = 0 |
| n = 0 |
| |
| for start, end, pos in self._search(regex, line): |
| out += line[cur_pos:start] |
| |
| # Value, ignoring start/end delimiters |
| value = line[end:pos - 1] |
| |
| # replaces \1 at the sub string, if \1 is used there |
| new_sub = sub |
| new_sub = new_sub.replace(r'\1', value) |
| |
| out += new_sub |
| |
| # Drop end ';' if any |
| if line[pos] == ';': |
| pos += 1 |
| |
| cur_pos = pos |
| n += 1 |
| |
| if count and count >= n: |
| break |
| |
| # Append the remaining string |
| l = len(line) |
| out += line[cur_pos:l] |
| |
| return out |