Place the C tokenizer on a different module. Signed-off-by: Mauro Carvalho Chehab <[email protected]> --- tools/lib/python/kdoc/c_lex.py | 239 +++++++++++++++++++++++++++ tools/lib/python/kdoc/kdoc_parser.py | 3 +- tools/lib/python/kdoc/kdoc_re.py | 233 -------------------------- 3 files changed, 241 insertions(+), 234 deletions(-) create mode 100644 tools/lib/python/kdoc/c_lex.py
diff --git a/tools/lib/python/kdoc/c_lex.py b/tools/lib/python/kdoc/c_lex.py new file mode 100644 index 000000000000..a104c29b63fb --- /dev/null +++ b/tools/lib/python/kdoc/c_lex.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +# Copyright(c) 2025: Mauro Carvalho Chehab <[email protected]>. + +""" +Regular expression ancillary classes. + +Those help caching regular expressions and do matching for kernel-doc. +""" + +import re + +from .kdoc_re import KernRe + +class CToken(): + """ + Data class to define a C token. + """ + + # Tokens that can be used by the parser. Works like an C enum. + + COMMENT = 0 #: A standard C or C99 comment, including delimiter. + STRING = 1 #: A string, including quotation marks. + CHAR = 2 #: A character, including apostophes. + NUMBER = 3 #: A number. + PUNC = 4 #: A puntuation mark: ``;`` / ``,`` / ``.``. + BEGIN = 5 #: A begin character: ``{`` / ``[`` / ``(``. + END = 6 #: A end character: ``}`` / ``]`` / ``)``. + CPP = 7 #: A preprocessor macro. + HASH = 8 #: The hash character - useful to handle other macros. + OP = 9 #: A C operator (add, subtract, ...). + STRUCT = 10 #: A ``struct`` keyword. + UNION = 11 #: An ``union`` keyword. + ENUM = 12 #: A ``struct`` keyword. + TYPEDEF = 13 #: A ``typedef`` keyword. + NAME = 14 #: A name. Can be an ID or a type. + SPACE = 15 #: Any space characters, including new lines + + MISMATCH = 255 #: an error indicator: should never happen in practice. + + # Dict to convert from an enum interger into a string. + _name_by_val = {v: k for k, v in dict(vars()).items() if isinstance(v, int)} + + # Dict to convert from string to an enum-like integer value. + _name_to_val = {k: v for v, k in _name_by_val.items()} + + @staticmethod + def to_name(val): + """Convert from an integer value from CToken enum into a string""" + + return CToken._name_by_val.get(val, f"UNKNOWN({val})") + + @staticmethod + def from_name(name): + """Convert a string into a CToken enum value""" + if name in CToken._name_to_val: + return CToken._name_to_val[name] + + return CToken.MISMATCH + + def __init__(self, kind, value, pos, + brace_level, paren_level, bracket_level): + self.kind = kind + self.value = value + self.pos = pos + self.brace_level = brace_level + self.paren_level = paren_level + self.bracket_level = bracket_level + + def __repr__(self): + name = self.to_name(self.kind) + if isinstance(self.value, str): + value = '"' + self.value + '"' + else: + value = self.value + + return f"CToken({name}, {value}, {self.pos}, " \ + f"{self.brace_level}, {self.paren_level}, {self.bracket_level})" + +#: Tokens to parse C code. +TOKEN_LIST = [ + (CToken.COMMENT, r"//[^\n]*|/\*[\s\S]*?\*/"), + + (CToken.STRING, r'"(?:\\.|[^"\\])*"'), + (CToken.CHAR, r"'(?:\\.|[^'\\])'"), + + (CToken.NUMBER, r"0[xX][0-9a-fA-F]+[uUlL]*|0[0-7]+[uUlL]*|" + r"[0-9]+(\.[0-9]*)?([eE][+-]?[0-9]+)?[fFlL]*"), + + (CToken.PUNC, r"[;,\.]"), + + (CToken.BEGIN, r"[\[\(\{]"), + + (CToken.END, r"[\]\)\}]"), + + (CToken.CPP, r"#\s*(define|include|ifdef|ifndef|if|else|elif|endif|undef|pragma)\b"), + + (CToken.HASH, r"#"), + + (CToken.OP, r"\+\+|\-\-|\->|==|\!=|<=|>=|&&|\|\||<<|>>|\+=|\-=|\*=|/=|%=" + r"|&=|\|=|\^=|=|\+|\-|\*|/|%|<|>|&|\||\^|~|!|\?|\:"), + + (CToken.STRUCT, r"\bstruct\b"), + (CToken.UNION, r"\bunion\b"), + (CToken.ENUM, r"\benum\b"), + (CToken.TYPEDEF, r"\bkinddef\b"), + + (CToken.NAME, r"[A-Za-z_][A-Za-z0-9_]*"), + + (CToken.SPACE, r"[\s]+"), + + (CToken.MISMATCH,r"."), +] + +#: Handle C continuation lines. +RE_CONT = KernRe(r"\\\n") + +RE_COMMENT_START = KernRe(r'/\*\s*') + +#: tokenizer regex. Will be filled at the first CTokenizer usage. +re_scanner = None + +class CTokenizer(): + """ + Scan C statements and definitions and produce tokens. + + When converted to string, it drops comments and handle public/private + values, respecting depth. + """ + + # This class is inspired and follows the basic concepts of: + # https://docs.python.org/3/library/re.html#writing-a-tokenizer + + def _tokenize(self, source): + """ + Interactor that parses ``source``, splitting it into tokens, as defined + at ``self.TOKEN_LIST``. + + The interactor returns a CToken class object. + """ + + # Handle continuation lines. Note that kdoc_parser already has a + # logic to do that. Still, let's keep it for completeness, as we might + # end re-using this tokenizer outsize kernel-doc some day - or we may + # eventually remove from there as a future cleanup. + source = RE_CONT.sub("", source) + + brace_level = 0 + paren_level = 0 + bracket_level = 0 + + for match in re_scanner.finditer(source): + kind = CToken.from_name(match.lastgroup) + pos = match.start() + value = match.group() + + if kind == CToken.MISMATCH: + raise RuntimeError(f"Unexpected token '{value}' on {pos}:\n\t{source}") + elif kind == CToken.BEGIN: + if value == '(': + paren_level += 1 + elif value == '[': + bracket_level += 1 + else: # value == '{' + brace_level += 1 + + elif kind == CToken.END: + if value == ')' and paren_level > 0: + paren_level -= 1 + elif value == ']' and bracket_level > 0: + bracket_level -= 1 + elif brace_level > 0: # value == '}' + brace_level -= 1 + + yield CToken(kind, value, pos, + brace_level, paren_level, bracket_level) + + def __init__(self, source): + """ + Create a regular expression to handle TOKEN_LIST. + + While I generally don't like using regex group naming via: + (?P<name>...) + + in this particular case, it makes sense, as we can pick the name + when matching a code via re_scanner(). + """ + global re_scanner + + if not re_scanner: + re_tokens = [] + + for kind, pattern in TOKEN_LIST: + name = CToken.to_name(kind) + re_tokens.append(f"(?P<{name}>{pattern})") + + re_scanner = KernRe("|".join(re_tokens), re.MULTILINE | re.DOTALL) + + self.tokens = [] + for tok in self._tokenize(source): + self.tokens.append(tok) + + def __str__(self): + out="" + show_stack = [True] + + for tok in self.tokens: + if tok.kind == CToken.BEGIN: + show_stack.append(show_stack[-1]) + + elif tok.kind == CToken.END: + prev = show_stack[-1] + if len(show_stack) > 1: + show_stack.pop() + + if not prev and show_stack[-1]: + # + # Try to preserve indent + # + out += "\t" * (len(show_stack) - 1) + + out += str(tok.value) + continue + + elif tok.kind == CToken.COMMENT: + comment = RE_COMMENT_START.sub("", tok.value) + + if comment.startswith("private:"): + show_stack[-1] = False + show = False + elif comment.startswith("public:"): + show_stack[-1] = True + + continue + + if show_stack[-1]: + out += str(tok.value) + + return out diff --git a/tools/lib/python/kdoc/kdoc_parser.py b/tools/lib/python/kdoc/kdoc_parser.py index 6b181ead3175..e804e61b09c0 100644 --- a/tools/lib/python/kdoc/kdoc_parser.py +++ b/tools/lib/python/kdoc/kdoc_parser.py @@ -13,7 +13,8 @@ import sys import re from pprint import pformat -from kdoc.kdoc_re import NestedMatch, KernRe, CTokenizer +from kdoc.kdoc_re import NestedMatch, KernRe +from kdoc.c_lex import CTokenizer from kdoc.kdoc_item import KdocItem # diff --git a/tools/lib/python/kdoc/kdoc_re.py b/tools/lib/python/kdoc/kdoc_re.py index 7bed4e9a8810..ba601a4f5035 100644 --- a/tools/lib/python/kdoc/kdoc_re.py +++ b/tools/lib/python/kdoc/kdoc_re.py @@ -141,239 +141,6 @@ class KernRe: return self.last_match.groups() -class TokType(): - - @staticmethod - def __str__(val): - """Return the name of an enum value""" - return TokType._name_by_val.get(val, f"UNKNOWN({val})") - -class CToken(): - """ - Data class to define a C token. - """ - - # Tokens that can be used by the parser. Works like an C enum. - - COMMENT = 0 #: A standard C or C99 comment, including delimiter. - STRING = 1 #: A string, including quotation marks. - CHAR = 2 #: A character, including apostophes. - NUMBER = 3 #: A number. - PUNC = 4 #: A puntuation mark: ``;`` / ``,`` / ``.``. - BEGIN = 5 #: A begin character: ``{`` / ``[`` / ``(``. - END = 6 #: A end character: ``}`` / ``]`` / ``)``. - CPP = 7 #: A preprocessor macro. - HASH = 8 #: The hash character - useful to handle other macros. - OP = 9 #: A C operator (add, subtract, ...). - STRUCT = 10 #: A ``struct`` keyword. - UNION = 11 #: An ``union`` keyword. - ENUM = 12 #: A ``struct`` keyword. - TYPEDEF = 13 #: A ``typedef`` keyword. - NAME = 14 #: A name. Can be an ID or a type. - SPACE = 15 #: Any space characters, including new lines - - MISMATCH = 255 #: an error indicator: should never happen in practice. - - # Dict to convert from an enum interger into a string. - _name_by_val = {v: k for k, v in dict(vars()).items() if isinstance(v, int)} - - # Dict to convert from string to an enum-like integer value. - _name_to_val = {k: v for v, k in _name_by_val.items()} - - @staticmethod - def to_name(val): - """Convert from an integer value from CToken enum into a string""" - - return CToken._name_by_val.get(val, f"UNKNOWN({val})") - - @staticmethod - def from_name(name): - """Convert a string into a CToken enum value""" - if name in CToken._name_to_val: - return CToken._name_to_val[name] - - return CToken.MISMATCH - - def __init__(self, kind, value, pos, - brace_level, paren_level, bracket_level): - self.kind = kind - self.value = value - self.pos = pos - self.brace_level = brace_level - self.paren_level = paren_level - self.bracket_level = bracket_level - - def __repr__(self): - name = self.to_name(self.kind) - if isinstance(self.value, str): - value = '"' + self.value + '"' - else: - value = self.value - - return f"CToken({name}, {value}, {self.pos}, " \ - f"{self.brace_level}, {self.paren_level}, {self.bracket_level})" - -#: Tokens to parse C code. -TOKEN_LIST = [ - (CToken.COMMENT, r"//[^\n]*|/\*[\s\S]*?\*/"), - - (CToken.STRING, r'"(?:\\.|[^"\\])*"'), - (CToken.CHAR, r"'(?:\\.|[^'\\])'"), - - (CToken.NUMBER, r"0[xX][0-9a-fA-F]+[uUlL]*|0[0-7]+[uUlL]*|" - r"[0-9]+(\.[0-9]*)?([eE][+-]?[0-9]+)?[fFlL]*"), - - (CToken.PUNC, r"[;,\.]"), - - (CToken.BEGIN, r"[\[\(\{]"), - - (CToken.END, r"[\]\)\}]"), - - (CToken.CPP, r"#\s*(define|include|ifdef|ifndef|if|else|elif|endif|undef|pragma)\b"), - - (CToken.HASH, r"#"), - - (CToken.OP, r"\+\+|\-\-|\->|==|\!=|<=|>=|&&|\|\||<<|>>|\+=|\-=|\*=|/=|%=" - r"|&=|\|=|\^=|=|\+|\-|\*|/|%|<|>|&|\||\^|~|!|\?|\:"), - - (CToken.STRUCT, r"\bstruct\b"), - (CToken.UNION, r"\bunion\b"), - (CToken.ENUM, r"\benum\b"), - (CToken.TYPEDEF, r"\bkinddef\b"), - - (CToken.NAME, r"[A-Za-z_][A-Za-z0-9_]*"), - - (CToken.SPACE, r"[\s]+"), - - (CToken.MISMATCH,r"."), -] - -#: Handle C continuation lines. -RE_CONT = KernRe(r"\\\n") - -RE_COMMENT_START = KernRe(r'/\*\s*') - -#: tokenizer regex. Will be filled at the first CTokenizer usage. -re_scanner = None - -class CTokenizer(): - """ - Scan C statements and definitions and produce tokens. - - When converted to string, it drops comments and handle public/private - values, respecting depth. - """ - - # This class is inspired and follows the basic concepts of: - # https://docs.python.org/3/library/re.html#writing-a-tokenizer - - def _tokenize(self, source): - """ - Interactor that parses ``source``, splitting it into tokens, as defined - at ``self.TOKEN_LIST``. - - The interactor returns a CToken class object. - """ - - # Handle continuation lines. Note that kdoc_parser already has a - # logic to do that. Still, let's keep it for completeness, as we might - # end re-using this tokenizer outsize kernel-doc some day - or we may - # eventually remove from there as a future cleanup. - source = RE_CONT.sub("", source) - - brace_level = 0 - paren_level = 0 - bracket_level = 0 - - for match in re_scanner.finditer(source): - kind = CToken.from_name(match.lastgroup) - pos = match.start() - value = match.group() - - if kind == CToken.MISMATCH: - raise RuntimeError(f"Unexpected token '{value}' on {pos}:\n\t{source}") - elif kind == CToken.BEGIN: - if value == '(': - paren_level += 1 - elif value == '[': - bracket_level += 1 - else: # value == '{' - brace_level += 1 - - elif kind == CToken.END: - if value == ')' and paren_level > 0: - paren_level -= 1 - elif value == ']' and bracket_level > 0: - bracket_level -= 1 - elif brace_level > 0: # value == '}' - brace_level -= 1 - - yield CToken(kind, value, pos, - brace_level, paren_level, bracket_level) - - def __init__(self, source): - """ - Create a regular expression to handle TOKEN_LIST. - - While I generally don't like using regex group naming via: - (?P<name>...) - - in this particular case, it makes sense, as we can pick the name - when matching a code via re_scanner(). - """ - global re_scanner - - if not re_scanner: - re_tokens = [] - - for kind, pattern in TOKEN_LIST: - name = CToken.to_name(kind) - re_tokens.append(f"(?P<{name}>{pattern})") - - re_scanner = KernRe("|".join(re_tokens), re.MULTILINE | re.DOTALL) - - self.tokens = [] - for tok in self._tokenize(source): - self.tokens.append(tok) - - def __str__(self): - out="" - show_stack = [True] - - for tok in self.tokens: - if tok.kind == CToken.BEGIN: - show_stack.append(show_stack[-1]) - - elif tok.kind == CToken.END: - prev = show_stack[-1] - if len(show_stack) > 1: - show_stack.pop() - - if not prev and show_stack[-1]: - # - # Try to preserve indent - # - out += "\t" * (len(show_stack) - 1) - - out += str(tok.value) - continue - - elif tok.kind == CToken.COMMENT: - comment = RE_COMMENT_START.sub("", tok.value) - - if comment.startswith("private:"): - show_stack[-1] = False - show = False - elif comment.startswith("public:"): - show_stack[-1] = True - - continue - - if show_stack[-1]: - out += str(tok.value) - - return out - #: Nested delimited pairs (brackets and parenthesis) DELIMITER_PAIRS = { -- 2.52.0

