kdoc_re.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. #!/usr/bin/env python3
  2. # SPDX-License-Identifier: GPL-2.0
  3. # Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.
  4. """
  5. Regular expression ancillary classes.
  6. Those help caching regular expressions and do matching for kernel-doc.
  7. """
  8. import re
  9. # Local cache for regular expressions
  10. re_cache = {}
  11. class KernRe:
  12. """
  13. Helper class to simplify regex declaration and usage.
  14. It calls re.compile for a given pattern. It also allows adding
  15. regular expressions and define sub at class init time.
  16. Regular expressions can be cached via an argument, helping to speedup
  17. searches.
  18. """
  19. def _add_regex(self, string, flags):
  20. """
  21. Adds a new regex or reuses it from the cache.
  22. """
  23. self.regex = re_cache.get(string, None)
  24. if not self.regex:
  25. self.regex = re.compile(string, flags=flags)
  26. if self.cache:
  27. re_cache[string] = self.regex
  28. def __init__(self, string, cache=True, flags=0):
  29. """
  30. Compile a regular expression and initialize internal vars.
  31. """
  32. self.cache = cache
  33. self.last_match = None
  34. self._add_regex(string, flags)
  35. def __str__(self):
  36. """
  37. Return the regular expression pattern.
  38. """
  39. return self.regex.pattern
  40. def __repr__(self):
  41. return f're.compile("{self.regex.pattern}")'
  42. def __add__(self, other):
  43. """
  44. Allows adding two regular expressions into one.
  45. """
  46. return KernRe(str(self) + str(other), cache=self.cache or other.cache,
  47. flags=self.regex.flags | other.regex.flags)
  48. def match(self, string):
  49. """
  50. Handles a re.match storing its results.
  51. """
  52. self.last_match = self.regex.match(string)
  53. return self.last_match
  54. def search(self, string):
  55. """
  56. Handles a re.search storing its results.
  57. """
  58. self.last_match = self.regex.search(string)
  59. return self.last_match
  60. def findall(self, string):
  61. """
  62. Alias to re.findall.
  63. """
  64. return self.regex.findall(string)
  65. def split(self, string):
  66. """
  67. Alias to re.split.
  68. """
  69. return self.regex.split(string)
  70. def sub(self, sub, string, count=0):
  71. """
  72. Alias to re.sub.
  73. """
  74. return self.regex.sub(sub, string, count=count)
  75. def group(self, num):
  76. """
  77. Returns the group results of the last match.
  78. """
  79. return self.last_match.group(num)
  80. class NestedMatch:
  81. """
  82. Finding nested delimiters is hard with regular expressions. It is
  83. even harder on Python with its normal re module, as there are several
  84. advanced regular expressions that are missing.
  85. This is the case of this pattern::
  86. '\\bSTRUCT_GROUP(\\(((?:(?>[^)(]+)|(?1))*)\\))[^;]*;'
  87. which is used to properly match open/close parentheses of the
  88. string search STRUCT_GROUP(),
  89. Add a class that counts pairs of delimiters, using it to match and
  90. replace nested expressions.
  91. The original approach was suggested by:
  92. https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex
  93. Although I re-implemented it to make it more generic and match 3 types
  94. of delimiters. The logic checks if delimiters are paired. If not, it
  95. will ignore the search string.
  96. """
  97. # TODO: make NestedMatch handle multiple match groups
  98. #
  99. # Right now, regular expressions to match it are defined only up to
  100. # the start delimiter, e.g.:
  101. #
  102. # \bSTRUCT_GROUP\(
  103. #
  104. # is similar to: STRUCT_GROUP\((.*)\)
  105. # except that the content inside the match group is delimiter-aligned.
  106. #
  107. # The content inside parentheses is converted into a single replace
  108. # group (e.g. r`\1').
  109. #
  110. # It would be nice to change such definition to support multiple
  111. # match groups, allowing a regex equivalent to:
  112. #
  113. # FOO\((.*), (.*), (.*)\)
  114. #
  115. # it is probably easier to define it not as a regular expression, but
  116. # with some lexical definition like:
  117. #
  118. # FOO(arg1, arg2, arg3)
  119. DELIMITER_PAIRS = {
  120. '{': '}',
  121. '(': ')',
  122. '[': ']',
  123. }
  124. RE_DELIM = re.compile(r'[\{\}\[\]\(\)]')
  125. def _search(self, regex, line):
  126. """
  127. Finds paired blocks for a regex that ends with a delimiter.
  128. The suggestion of using finditer to match pairs came from:
  129. https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex
  130. but I ended using a different implementation to align all three types
  131. of delimiters and seek for an initial regular expression.
  132. The algorithm seeks for open/close paired delimiters and places them
  133. into a stack, yielding a start/stop position of each match when the
  134. stack is zeroed.
  135. The algorithm should work fine for properly paired lines, but will
  136. silently ignore end delimiters that precede a start delimiter.
  137. This should be OK for kernel-doc parser, as unaligned delimiters
  138. would cause compilation errors. So, we don't need to raise exceptions
  139. to cover such issues.
  140. """
  141. stack = []
  142. for match_re in regex.finditer(line):
  143. start = match_re.start()
  144. offset = match_re.end()
  145. d = line[offset - 1]
  146. if d not in self.DELIMITER_PAIRS:
  147. continue
  148. end = self.DELIMITER_PAIRS[d]
  149. stack.append(end)
  150. for match in self.RE_DELIM.finditer(line[offset:]):
  151. pos = match.start() + offset
  152. d = line[pos]
  153. if d in self.DELIMITER_PAIRS:
  154. end = self.DELIMITER_PAIRS[d]
  155. stack.append(end)
  156. continue
  157. # Does the end delimiter match what is expected?
  158. if stack and d == stack[-1]:
  159. stack.pop()
  160. if not stack:
  161. yield start, offset, pos + 1
  162. break
  163. def search(self, regex, line):
  164. """
  165. This is similar to re.search:
  166. It matches a regex that it is followed by a delimiter,
  167. returning occurrences only if all delimiters are paired.
  168. """
  169. for t in self._search(regex, line):
  170. yield line[t[0]:t[2]]
  171. def sub(self, regex, sub, line, count=0):
  172. r"""
  173. This is similar to re.sub:
  174. It matches a regex that it is followed by a delimiter,
  175. replacing occurrences only if all delimiters are paired.
  176. if the sub argument contains::
  177. r'\1'
  178. it will work just like re: it places there the matched paired data
  179. with the delimiter stripped.
  180. If count is different than zero, it will replace at most count
  181. items.
  182. """
  183. out = ""
  184. cur_pos = 0
  185. n = 0
  186. for start, end, pos in self._search(regex, line):
  187. out += line[cur_pos:start]
  188. # Value, ignoring start/end delimiters
  189. value = line[end:pos - 1]
  190. # replaces \1 at the sub string, if \1 is used there
  191. new_sub = sub
  192. new_sub = new_sub.replace(r'\1', value)
  193. out += new_sub
  194. # Drop end ';' if any
  195. if line[pos] == ';':
  196. pos += 1
  197. cur_pos = pos
  198. n += 1
  199. if count and count >= n:
  200. break
  201. # Append the remaining string
  202. l = len(line)
  203. out += line[cur_pos:l]
  204. return out