| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: GPL-2.0 | |
| # Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>. | |
| # | |
| # pylint: disable=C0301,C0302,R0904,R0912,R0913,R0914,R0915,R0917,R1702 | |
| """ | |
| Classes and functions related to reading a C language source or header FILE | |
| and extract embedded documentation comments from it. | |
| """ | |
| import sys | |
| import re | |
| from pprint import pformat | |
| from kdoc.c_lex import CTokenizer, tokenizer_set_log | |
| from kdoc.kdoc_re import KernRe | |
| from kdoc.kdoc_item import KdocItem | |
| # | |
| # Regular expressions used to parse kernel-doc markups at KernelDoc class. | |
| # | |
| # Let's declare them in lowercase outside any class to make it easier to | |
| # convert from the Perl script. | |
| # | |
| # As those are evaluated at the beginning, no need to cache them | |
| # | |
| # Allow whitespace at end of comment start. | |
| doc_start = KernRe(r'^/\*\*\s*$', cache=False) | |
| doc_end = KernRe(r'\*/', cache=False) | |
| doc_com = KernRe(r'\s*\*\s*', cache=False) | |
| doc_com_body = KernRe(r'\s*\* ?', cache=False) | |
| doc_decl = doc_com + KernRe(r'(\w+)', cache=False) | |
| # @params and a strictly limited set of supported section names | |
| # Specifically: | |
| # Match @word: | |
| # @...: | |
| # @{section-name}: | |
| # while trying to not match literal block starts like "example::" | |
| # | |
| known_section_names = 'description|context|returns?|notes?|examples?' | |
| known_sections = KernRe(known_section_names, flags = re.I) | |
| doc_sect = doc_com + \ | |
| KernRe(r'\s*(@[.\w]+|@\.\.\.|' + known_section_names + r')\s*:([^:].*)?$', | |
| flags=re.I, cache=False) | |
| doc_content = doc_com_body + KernRe(r'(.*)', cache=False) | |
| doc_inline_start = KernRe(r'^\s*/\*\*\s*$', cache=False) | |
| doc_inline_sect = KernRe(r'\s*\*\s*(@\s*[\w][\w\.]*\s*):(.*)', cache=False) | |
| doc_inline_end = KernRe(r'^\s*\*/\s*$', cache=False) | |
| doc_inline_oneline = KernRe(r'^\s*/\*\*\s*(@\s*[\w][\w\.]*\s*):\s*(.*)\s*\*/\s*$', cache=False) | |
| export_symbol = KernRe(r'^\s*EXPORT_SYMBOL(_GPL)?\s*\(\s*(\w+)\s*\)\s*', cache=False) | |
| export_symbol_ns = KernRe(r'^\s*EXPORT_SYMBOL_NS(_GPL)?\s*\(\s*(\w+)\s*,\s*"\S+"\)\s*', cache=False) | |
| type_param = KernRe(r"@(\w*((\.\w+)|(->\w+))*(\.\.\.)?)", cache=False) | |
| # | |
| # Tests for the beginning of a kerneldoc block in its various forms. | |
| # | |
| doc_block = doc_com + KernRe(r'DOC:\s*(.*)?', cache=False) | |
| doc_begin_data = KernRe(r"^\s*\*?\s*(struct|union|enum|typedef|var)\b\s*(\w*)", cache = False) | |
| doc_begin_func = KernRe(str(doc_com) + # initial " * ' | |
| r"(?:\w+\s*\*\s*)?" + # type (not captured) | |
| r'(?:define\s+)?' + # possible "define" (not captured) | |
| r'(\w+)\s*(?:\(\w*\))?\s*' + # name and optional "(...)" | |
| r'(?:[-:].*)?$', # description (not captured) | |
| cache = False) | |
| # | |
| # Ancillary functions | |
| # | |
| multi_space = KernRe(r'\s\s+') | |
| def trim_whitespace(s): | |
| """ | |
| A little helper to get rid of excess white space. | |
| """ | |
| return multi_space.sub(' ', s.strip()) | |
| def trim_private_members(text): | |
| """ | |
| Remove ``struct``/``enum`` members that have been marked "private". | |
| """ | |
| tokens = CTokenizer(text) | |
| return str(tokens) | |
| class state: | |
| """ | |
| States used by the parser's state machine. | |
| """ | |
| # Parser states | |
| NORMAL = 0 #: Normal code. | |
| NAME = 1 #: Looking for function name. | |
| DECLARATION = 2 #: We have seen a declaration which might not be done. | |
| BODY = 3 #: The body of the comment. | |
| SPECIAL_SECTION = 4 #: Doc section ending with a blank line. | |
| PROTO = 5 #: Scanning prototype. | |
| DOCBLOCK = 6 #: Documentation block. | |
| INLINE_NAME = 7 #: Gathering doc outside main block. | |
| INLINE_TEXT = 8 #: Reading the body of inline docs. | |
| #: Names for each parser state. | |
| name = [ | |
| "NORMAL", | |
| "NAME", | |
| "DECLARATION", | |
| "BODY", | |
| "SPECIAL_SECTION", | |
| "PROTO", | |
| "DOCBLOCK", | |
| "INLINE_NAME", | |
| "INLINE_TEXT", | |
| ] | |
| SECTION_DEFAULT = "Description" #: Default section. | |
| class KernelEntry: | |
| """ | |
| Encapsulates a Kernel documentation entry. | |
| """ | |
| def __init__(self, config, fname, ln): | |
| self.config = config | |
| self.fname = fname | |
| self._contents = [] | |
| self.prototype = "" | |
| self.warnings = [] | |
| self.parameterlist = [] | |
| self.parameterdescs = {} | |
| self.parametertypes = {} | |
| self.parameterdesc_start_lines = {} | |
| self.sections_start_lines = {} | |
| self.sections = {} | |
| self.anon_struct_union = False | |
| self.leading_space = None | |
| self.fname = fname | |
| # State flags | |
| self.brcount = 0 | |
| self.declaration_start_line = ln + 1 | |
| # | |
| # Management of section contents | |
| # | |
| def add_text(self, text): | |
| """Add a new text to the entry contents list.""" | |
| self._contents.append(text) | |
| def contents(self): | |
| """Returns a string with all content texts that were added.""" | |
| return '\n'.join(self._contents) + '\n' | |
| # TODO: rename to emit_message after removal of kernel-doc.pl | |
| def emit_msg(self, ln, msg, *, warning=True): | |
| """Emit a message.""" | |
| log_msg = f"{self.fname}:{ln} {msg}" | |
| if not warning: | |
| self.config.log.info(log_msg) | |
| return | |
| # Delegate warning output to output logic, as this way it | |
| # will report warnings/info only for symbols that are output | |
| self.warnings.append(log_msg) | |
| return | |
| def begin_section(self, line_no, title = SECTION_DEFAULT, dump = False): | |
| """ | |
| Begin a new section. | |
| """ | |
| if dump: | |
| self.dump_section(start_new = True) | |
| self.section = title | |
| self.new_start_line = line_no | |
| def dump_section(self, start_new=True): | |
| """ | |
| Dumps section contents to arrays/hashes intended for that purpose. | |
| """ | |
| # | |
| # If we have accumulated no contents in the default ("description") | |
| # section, don't bother. | |
| # | |
| if self.section == SECTION_DEFAULT and not self._contents: | |
| return | |
| name = self.section | |
| contents = self.contents() | |
| if type_param.match(name): | |
| name = type_param.group(1) | |
| self.parameterdescs[name] = contents | |
| self.parameterdesc_start_lines[name] = self.new_start_line | |
| self.new_start_line = 0 | |
| else: | |
| if name in self.sections and self.sections[name] != "": | |
| # Only warn on user-specified duplicate section names | |
| if name != SECTION_DEFAULT: | |
| self.emit_msg(self.new_start_line, | |
| f"duplicate section name '{name}'") | |
| # Treat as a new paragraph - add a blank line | |
| self.sections[name] += '\n' + contents | |
| else: | |
| self.sections[name] = contents | |
| self.sections_start_lines[name] = self.new_start_line | |
| self.new_start_line = 0 | |
| # self.config.log.debug("Section: %s : %s", name, pformat(vars(self))) | |
| if start_new: | |
| self.section = SECTION_DEFAULT | |
| self._contents = [] | |
| python_warning = False | |
| class KernelDoc: | |
| """ | |
| Read a C language source or header FILE and extract embedded | |
| documentation comments. | |
| """ | |
| #: Name of context section. | |
| section_context = "Context" | |
| #: Name of return section. | |
| section_return = "Return" | |
| #: String to write when a parameter is not described. | |
| undescribed = "-- undescribed --" | |
| def __init__(self, config, fname, xforms, store_src=False): | |
| """Initialize internal variables""" | |
| self.fname = fname | |
| self.config = config | |
| self.xforms = xforms | |
| self.store_src = store_src | |
| tokenizer_set_log(self.config.log, f"{self.fname}: CMatch: ") | |
| # Initial state for the state machines | |
| self.state = state.NORMAL | |
| # Store entry currently being processed | |
| self.entry = None | |
| # Place all potential outputs into an array | |
| self.entries = [] | |
| # | |
| # We need Python 3.7 for its "dicts remember the insertion | |
| # order" guarantee | |
| # | |
| global python_warning | |
| if (not python_warning and | |
| sys.version_info.major == 3 and sys.version_info.minor < 7): | |
| self.emit_msg(0, | |
| 'Python 3.7 or later is required for correct results') | |
| python_warning = True | |
| def emit_msg(self, ln, msg, *, warning=True): | |
| """Emit a message""" | |
| if self.entry: | |
| self.entry.emit_msg(ln, msg, warning=warning) | |
| return | |
| log_msg = f"{self.fname}:{ln} {msg}" | |
| if warning: | |
| self.config.log.warning(log_msg) | |
| else: | |
| self.config.log.info(log_msg) | |
| def dump_section(self, start_new=True): | |
| """ | |
| Dump section contents to arrays/hashes intended for that purpose. | |
| """ | |
| if self.entry: | |
| self.entry.dump_section(start_new) | |
| # TODO: rename it to store_declaration after removal of kernel-doc.pl | |
| def output_declaration(self, dtype, name, **args): | |
| """ | |
| Store the entry into an entry array. | |
| The actual output and output filters will be handled elsewhere. | |
| """ | |
| item = KdocItem(name, self.fname, dtype, | |
| self.entry.declaration_start_line, **args) | |
| item.warnings = self.entry.warnings | |
| # Drop empty sections | |
| # TODO: improve empty sections logic to emit warnings | |
| sections = self.entry.sections | |
| for section in ["Description", "Return"]: | |
| if section in sections and not sections[section].rstrip(): | |
| del sections[section] | |
| item.set_sections(sections, self.entry.sections_start_lines) | |
| item.set_params(self.entry.parameterlist, self.entry.parameterdescs, | |
| self.entry.parametertypes, | |
| self.entry.parameterdesc_start_lines) | |
| self.entries.append(item) | |
| self.config.log.debug("Output: %s:%s = %s", dtype, name, pformat(args)) | |
| def emit_unused_warnings(self): | |
| """ | |
| When the parser fails to produce a valid entry, it places some | |
| warnings under `entry.warnings` that will be discarded when resetting | |
| the state. | |
| Ensure that those warnings are not lost. | |
| .. note:: | |
| Because we are calling `config.warning()` here, those | |
| warnings are not filtered by the `-W` parameters: they will all | |
| be produced even when `-Wreturn`, `-Wshort-desc`, and/or | |
| `-Wcontents-before-sections` are used. | |
| Allowing those warnings to be filtered is complex, because it | |
| would require storing them in a buffer and then filtering them | |
| during the output step of the code, depending on the | |
| selected symbols. | |
| """ | |
| if self.entry and self.entry not in self.entries: | |
| for log_msg in self.entry.warnings: | |
| self.config.warning(log_msg) | |
| def reset_state(self, ln): | |
| """ | |
| Ancillary routine to create a new entry. It initializes all | |
| variables used by the state machine. | |
| """ | |
| self.emit_unused_warnings() | |
| self.entry = KernelEntry(self.config, self.fname, ln) | |
| # State flags | |
| self.state = state.NORMAL | |
| def push_parameter(self, ln, decl_type, param, dtype, | |
| org_arg, declaration_name): | |
| """ | |
| Store parameters and their descriptions at self.entry. | |
| """ | |
| if self.entry.anon_struct_union and dtype == "" and param == "}": | |
| return # Ignore the ending }; from anonymous struct/union | |
| self.entry.anon_struct_union = False | |
| param = KernRe(r'[\[\)].*').sub('', param, count=1) | |
| # | |
| # Look at various "anonymous type" cases. | |
| # | |
| if dtype == '': | |
| if param.endswith("..."): | |
| named_variadic = len(param) > 3 | |
| if named_variadic: # there is a name provided, use that | |
| # | |
| # If the user documented the parameter using the | |
| # ``@name...:`` form, the description is stored in | |
| # parameterdescs under the unstripped key. Migrate | |
| # it to the stripped key so the user's text is not | |
| # silently dropped during output, and so the new | |
| # excess-parameter check in check_sections() does | |
| # not flag the unstripped key as orphaned. | |
| # | |
| orig = self.entry.parameterdescs.pop(param, None) | |
| param = param[:-3] | |
| if orig is not None and \ | |
| not self.entry.parameterdescs.get(param): | |
| self.entry.parameterdescs[param] = orig | |
| if not self.entry.parameterdescs.get(param): | |
| # | |
| # For a named variadic (e.g. ``args...``), emit the | |
| # standard "not described" warning before auto-filling | |
| # so a missing or mistyped ``@<name>:`` doc tag does | |
| # not go undetected. The bare ``...`` form has no | |
| # natural name for the user to document and so always | |
| # gets the auto-generated text. | |
| # | |
| if named_variadic and decl_type == 'function': | |
| self.emit_msg(ln, | |
| f"function parameter '{param}' " | |
| f"not described in " | |
| f"'{declaration_name}'") | |
| self.entry.parameterdescs[param] = "variable arguments" | |
| elif (not param) or param == "void": | |
| param = "void" | |
| self.entry.parameterdescs[param] = "no arguments" | |
| elif param in ["struct", "union"]: | |
| # Handle unnamed (anonymous) union or struct | |
| dtype = param | |
| param = "{unnamed_" + param + "}" | |
| self.entry.parameterdescs[param] = "anonymous\n" | |
| self.entry.anon_struct_union = True | |
| # Warn if parameter has no description | |
| # (but ignore ones starting with # as these are not parameters | |
| # but inline preprocessor statements) | |
| if param not in self.entry.parameterdescs and not param.startswith("#"): | |
| self.entry.parameterdescs[param] = self.undescribed | |
| if "." not in param: | |
| if decl_type == 'function': | |
| dname = f"{decl_type} parameter" | |
| else: | |
| dname = f"{decl_type} member" | |
| self.emit_msg(ln, | |
| f"{dname} '{param}' not described in '{declaration_name}'") | |
| # Strip spaces from param so that it is one continuous string on | |
| # parameterlist. This fixes a problem where check_sections() | |
| # cannot find a parameter like "addr[6 + 2]" because it actually | |
| # appears as "addr[6", "+", "2]" on the parameter list. | |
| # However, it's better to maintain the param string unchanged for | |
| # output, so just weaken the string compare in check_sections() | |
| # to ignore "[blah" in a parameter string. | |
| self.entry.parameterlist.append(param) | |
| org_arg = KernRe(r'\s\s+').sub(' ', org_arg) | |
| self.entry.parametertypes[param] = org_arg | |
| def create_parameter_list(self, ln, decl_type, args, | |
| splitter, declaration_name): | |
| """ | |
| Creates a list of parameters, storing them at self.entry. | |
| """ | |
| # temporarily replace all commas inside function pointer definition | |
| arg_expr = KernRe(r'(\([^\),]+),') | |
| while arg_expr.search(args): | |
| args = arg_expr.sub(r"\1#", args) | |
| for arg in args.split(splitter): | |
| # Ignore argument attributes | |
| arg = KernRe(r'\sPOS0?\s').sub(' ', arg) | |
| # Replace '[at_least ' with '[static '. This allows sphinx to parse | |
| # array parameter declarations like 'char A[at_least 4]', where | |
| # 'at_least' is #defined to 'static' by the kernel headers. | |
| arg = arg.replace('[at_least ', '[static ') | |
| # Strip leading/trailing spaces | |
| arg = arg.strip() | |
| arg = KernRe(r'\s+').sub(' ', arg, count=1) | |
| if arg.startswith('#'): | |
| # Treat preprocessor directive as a typeless variable just to fill | |
| # corresponding data structures "correctly". Catch it later in | |
| # output_* subs. | |
| # Treat preprocessor directive as a typeless variable | |
| self.push_parameter(ln, decl_type, arg, "", | |
| "", declaration_name) | |
| # | |
| # The pointer-to-function case. | |
| # | |
| elif KernRe(r'\(.+\)\s*\(').search(arg): | |
| arg = arg.replace('#', ',') | |
| r = KernRe(r'[^\(]+\(\*?\s*' # Everything up to "(*" | |
| r'([\w\[\].]*)' # Capture the name and possible [array] | |
| r'\s*\)') # Make sure the trailing ")" is there | |
| if r.match(arg): | |
| param = r.group(1) | |
| else: | |
| self.emit_msg(ln, f"Invalid param: {arg}") | |
| param = arg | |
| dtype = arg.replace(param, '') | |
| self.push_parameter(ln, decl_type, param, dtype, arg, declaration_name) | |
| # | |
| # The array-of-pointers case. Dig the parameter name out from the middle | |
| # of the declaration. | |
| # | |
| elif KernRe(r'\(.+\)\s*\[').search(arg): | |
| r = KernRe(r'[^\(]+\(\s*\*\s*' # Up to "(" and maybe "*" | |
| r'([\w.]*?)' # The actual pointer name | |
| r'\s*(\[\s*\w+\s*\]\s*)*\)') # The [array portion] | |
| if r.match(arg): | |
| param = r.group(1) | |
| else: | |
| self.emit_msg(ln, f"Invalid param: {arg}") | |
| param = arg | |
| dtype = arg.replace(param, '') | |
| self.push_parameter(ln, decl_type, param, dtype, arg, declaration_name) | |
| elif arg: | |
| # | |
| # Clean up extraneous spaces and split the string at commas; the first | |
| # element of the resulting list will also include the type information. | |
| # | |
| arg = KernRe(r'\s*:\s*').sub(":", arg) | |
| arg = KernRe(r'\s*\[').sub('[', arg) | |
| args = KernRe(r'\s*,\s*').split(arg) | |
| args[0] = re.sub(r'(\*+)\s*', r' \1', args[0]) | |
| # | |
| # args[0] has a string of "type a". If "a" includes an [array] | |
| # declaration, we want to not be fooled by any white space inside | |
| # the brackets, so detect and handle that case specially. | |
| # | |
| r = KernRe(r'^([^[\]]*\s+)(.*)$') | |
| if r.match(args[0]): | |
| args[0] = r.group(2) | |
| dtype = r.group(1) | |
| else: | |
| # No space in args[0]; this seems wrong but preserves previous behavior | |
| dtype = '' | |
| bitfield_re = KernRe(r'(.*?):(\w+)') | |
| for param in args: | |
| # | |
| # For pointers, shift the star(s) from the variable name to the | |
| # type declaration. | |
| # | |
| r = KernRe(r'^(\*+)\s*(.*)') | |
| if r.match(param): | |
| self.push_parameter(ln, decl_type, r.group(2), | |
| f"{dtype} {r.group(1)}", | |
| arg, declaration_name) | |
| # | |
| # Perform a similar shift for bitfields. | |
| # | |
| elif bitfield_re.search(param): | |
| if dtype != "": # Skip unnamed bit-fields | |
| self.push_parameter(ln, decl_type, bitfield_re.group(1), | |
| f"{dtype}:{bitfield_re.group(2)}", | |
| arg, declaration_name) | |
| else: | |
| self.push_parameter(ln, decl_type, param, dtype, | |
| arg, declaration_name) | |
| def check_sections(self, ln, decl_name, decl_type): | |
| """ | |
| Check for errors inside sections, emitting warnings if not found | |
| parameters are described. | |
| """ | |
| for section in self.entry.sections: | |
| if section not in self.entry.parameterlist and \ | |
| not known_sections.search(section): | |
| if decl_type == 'function': | |
| dname = f"{decl_type} parameter" | |
| else: | |
| dname = f"{decl_type} member" | |
| self.emit_msg(ln, | |
| f"Excess {dname} '{section}' description in '{decl_name}'") | |
| # | |
| # Check that documented parameter names (from doc comments, including | |
| # inline ``/** @member: */`` tags) actually match real members in | |
| # the declaration. This catches mismatched or stale kernel-doc | |
| # member tags that don't correspond to any actual struct/union | |
| # member or function parameter. | |
| # | |
| for param_name, desc in self.entry.parameterdescs.items(): | |
| # Skip auto-generated entries from push_parameter() | |
| if desc == self.undescribed: | |
| continue | |
| if desc in ("no arguments", "anonymous\n", "variable arguments"): | |
| continue | |
| if param_name.startswith("{unnamed_"): | |
| continue | |
| if param_name in self.entry.parameterlist: | |
| continue | |
| if decl_type == 'function': | |
| dname = f"{decl_type} parameter" | |
| else: | |
| dname = f"{decl_type} member" | |
| self.emit_msg(ln, | |
| f"Excess {dname} '{param_name}' description in '{decl_name}'") | |
| def check_return_section(self, ln, declaration_name, return_type): | |
| """ | |
| If the function doesn't return void, warns about the lack of a | |
| return description. | |
| """ | |
| if not self.config.wreturn: | |
| return | |
| # Ignore an empty return type (It's a macro) | |
| # Ignore functions with a "void" return type (but not "void *") | |
| if not return_type or KernRe(r'void\s*\w*\s*$').search(return_type): | |
| return | |
| if not self.entry.sections.get("Return", None): | |
| self.emit_msg(ln, | |
| f"No description found for return value of '{declaration_name}'") | |
| def split_struct_proto(self, proto): | |
| """ | |
| Split apart a structure prototype; returns (struct|union, name, | |
| members) or ``None``. | |
| """ | |
| type_pattern = r'(struct|union)' | |
| qualifiers = [ | |
| "__attribute__", | |
| "__packed", | |
| "__aligned", | |
| "____cacheline_aligned_in_smp", | |
| "____cacheline_aligned", | |
| ] | |
| definition_body = r'\{(.*)\}\s*' + "(?:" + '|'.join(qualifiers) + ")?" | |
| r = KernRe(type_pattern + r'\s+(\w+)\s*' + definition_body) | |
| if r.search(proto): | |
| return (r.group(1), r.group(2), r.group(3)) | |
| else: | |
| r = KernRe(r'typedef\s+' + type_pattern + r'\s*' + definition_body + r'\s*(\w+)\s*;') | |
| if r.search(proto): | |
| return (r.group(1), r.group(3), r.group(2)) | |
| return None | |
| def rewrite_struct_members(self, members): | |
| """ | |
| Process ``struct``/``union`` members from the most deeply nested | |
| outward. | |
| Rewrite the members of a ``struct`` or ``union`` for easier formatting | |
| later on. Among other things, this function will turn a member like:: | |
| struct { inner_members; } foo; | |
| into:: | |
| struct foo; inner_members; | |
| """ | |
| # | |
| # The trick is in the ``^{`` below - it prevents a match of an outer | |
| # ``struct``/``union`` until the inner one has been munged | |
| # (removing the ``{`` in the process). | |
| # | |
| struct_members = KernRe(r'(struct|union)' # 0: declaration type | |
| r'([^\{\};]+)' # 1: possible name | |
| r'(\{)' | |
| r'([^\{\}]*)' # 3: Contents of declaration | |
| r'(\})' | |
| r'([^\{\};]*)(;)') # 5: Remaining stuff after declaration | |
| tuples = struct_members.findall(members) | |
| while tuples: | |
| for t in tuples: | |
| newmember = "" | |
| oldmember = "".join(t) # Reconstruct the original formatting | |
| dtype, name, lbr, content, rbr, rest, semi = t | |
| # | |
| # Pass through each field name, normalizing the form and formatting. | |
| # | |
| for s_id in rest.split(','): | |
| s_id = s_id.strip() | |
| newmember += f"{dtype} {s_id}; " | |
| # | |
| # Remove bitfield/array/pointer info, getting the bare name. | |
| # | |
| s_id = KernRe(r'[:\[].*').sub('', s_id) | |
| s_id = KernRe(r'^\s*\**(\S+)\s*').sub(r'\1', s_id) | |
| # | |
| # Pass through the members of this inner structure/union. | |
| # | |
| for arg in content.split(';'): | |
| arg = arg.strip() | |
| # | |
| # Look for (type)(*name)(args) - pointer to function | |
| # | |
| r = KernRe(r'^([^\(]+\(\*?\s*)([\w.]*)(\s*\).*)') | |
| if r.match(arg): | |
| dtype, name, extra = r.group(1), r.group(2), r.group(3) | |
| # Pointer-to-function | |
| if not s_id: | |
| # Anonymous struct/union | |
| newmember += f"{dtype}{name}{extra}; " | |
| else: | |
| newmember += f"{dtype}{s_id}.{name}{extra}; " | |
| # | |
| # Otherwise a non-function member. | |
| # | |
| else: | |
| # | |
| # Remove bitmap and array portions and spaces around commas | |
| # | |
| arg = KernRe(r':\s*\d+\s*').sub('', arg) | |
| arg = KernRe(r'\[.*\]').sub('', arg) | |
| arg = KernRe(r'\s*,\s*').sub(',', arg) | |
| # | |
| # Look for a normal decl - "type name[,name...]" | |
| # | |
| r = KernRe(r'(.*)\s+([\S+,]+)') | |
| if r.search(arg): | |
| for name in r.group(2).split(','): | |
| name = KernRe(r'^\s*\**(\S+)\s*').sub(r'\1', name) | |
| if not s_id: | |
| # Anonymous struct/union | |
| newmember += f"{r.group(1)} {name}; " | |
| else: | |
| newmember += f"{r.group(1)} {s_id}.{name}; " | |
| else: | |
| newmember += f"{arg}; " | |
| # | |
| # At the end of the s_id loop, replace the original declaration with | |
| # the munged version. | |
| # | |
| members = members.replace(oldmember, newmember) | |
| # | |
| # End of the tuple loop - search again and see if there are outer members | |
| # that now turn up. | |
| # | |
| tuples = struct_members.findall(members) | |
| return members | |
| def format_struct_decl(self, declaration): | |
| """ | |
| Format the ``struct`` declaration into a standard form for inclusion | |
| in the resulting docs. | |
| """ | |
| # | |
| # Insert newlines, get rid of extra spaces. | |
| # | |
| declaration = KernRe(r'([\{;])').sub(r'\1\n', declaration) | |
| declaration = KernRe(r'\}\s+;').sub('};', declaration) | |
| # | |
| # Format inline enums with each member on its own line. | |
| # | |
| r = KernRe(r'(enum\s+\{[^\}]+),([^\n])') | |
| while r.search(declaration): | |
| declaration = r.sub(r'\1,\n\2', declaration) | |
| # | |
| # Now go through and supply the right number of tabs | |
| # for each line. | |
| # | |
| def_args = declaration.split('\n') | |
| level = 1 | |
| declaration = "" | |
| for clause in def_args: | |
| clause = KernRe(r'\s+').sub(' ', clause.strip(), count=1) | |
| if clause: | |
| if '}' in clause and level > 1: | |
| level -= 1 | |
| if not clause.startswith('#'): | |
| declaration += "\t" * level | |
| declaration += "\t" + clause + "\n" | |
| if "{" in clause and "}" not in clause: | |
| level += 1 | |
| return declaration | |
| def dump_struct(self, ln, proto, source): | |
| """ | |
| Store an entry for a ``struct`` or ``union`` | |
| """ | |
| # | |
| # Do the basic parse to get the pieces of the declaration. | |
| # | |
| source = source | |
| proto = trim_private_members(proto) | |
| struct_parts = self.split_struct_proto(proto) | |
| if not struct_parts: | |
| self.emit_msg(ln, f"{proto} error: Cannot parse struct or union!") | |
| return | |
| decl_type, declaration_name, members = struct_parts | |
| if self.entry.identifier != declaration_name: | |
| self.emit_msg(ln, f"expecting prototype for {decl_type} {self.entry.identifier}. " | |
| f"Prototype was for {decl_type} {declaration_name} instead\n") | |
| return | |
| # | |
| # Go through the list of members applying all of our transformations. | |
| # | |
| members = self.xforms.apply("struct", members) | |
| # | |
| # Deal with embedded struct and union members, and drop enums entirely. | |
| # | |
| declaration = members | |
| members = self.rewrite_struct_members(members) | |
| members = re.sub(r'(\{[^\{\}]*\})', '', members) | |
| # | |
| # Output the result and we are done. | |
| # | |
| self.create_parameter_list(ln, decl_type, members, ';', | |
| declaration_name) | |
| self.check_sections(ln, declaration_name, decl_type) | |
| self.output_declaration(decl_type, declaration_name, | |
| source=source, | |
| definition=self.format_struct_decl(declaration), | |
| purpose=self.entry.declaration_purpose) | |
| def dump_enum(self, ln, proto, source): | |
| """ | |
| Store an ``enum`` inside self.entries array. | |
| """ | |
| # | |
| # Strip preprocessor directives. Note that this depends on the | |
| # trailing semicolon we added in process_proto_type(). | |
| # | |
| source = source | |
| proto = trim_private_members(proto) | |
| proto = KernRe(r'#\s*((define|ifdef|if)\s+|endif)[^;]*;', flags=re.S).sub('', proto) | |
| # | |
| # Parse out the name and members of the enum. Typedef form first. | |
| # | |
| r = KernRe(r'typedef\s+enum\s*\{(.*)\}\s*(\w*)\s*;') | |
| if r.search(proto): | |
| declaration_name = r.group(2) | |
| members = r.group(1) | |
| # | |
| # Failing that, look for a straight enum | |
| # | |
| else: | |
| r = KernRe(r'enum\s+(\w*)\s*\{(.*)\}') | |
| if r.match(proto): | |
| declaration_name = r.group(1) | |
| members = r.group(2) | |
| # | |
| # OK, this isn't going to work. | |
| # | |
| else: | |
| self.emit_msg(ln, f"{proto}: error: Cannot parse enum!") | |
| return | |
| # | |
| # Make sure we found what we were expecting. | |
| # | |
| if self.entry.identifier != declaration_name: | |
| if self.entry.identifier == "": | |
| self.emit_msg(ln, | |
| f"{proto}: wrong kernel-doc identifier on prototype") | |
| else: | |
| self.emit_msg(ln, | |
| f"expecting prototype for enum {self.entry.identifier}. " | |
| f"Prototype was for enum {declaration_name} instead") | |
| return | |
| if not declaration_name: | |
| declaration_name = "(anonymous)" | |
| # | |
| # Parse out the name of each enum member, and verify that we | |
| # have a description for it. | |
| # | |
| member_set = set() | |
| members = KernRe(r'\([^;)]*\)').sub('', members) | |
| for arg in members.split(','): | |
| arg = KernRe(r'^\s*(\w+).*').sub(r'\1', arg) | |
| if not arg.strip(): | |
| continue | |
| self.entry.parameterlist.append(arg) | |
| if arg not in self.entry.parameterdescs: | |
| self.entry.parameterdescs[arg] = self.undescribed | |
| self.emit_msg(ln, | |
| f"Enum value '{arg}' not described in enum '{declaration_name}'") | |
| member_set.add(arg) | |
| # | |
| # Ensure that every described member actually exists in the enum. | |
| # | |
| for k in self.entry.parameterdescs: | |
| if k not in member_set: | |
| self.emit_msg(ln, | |
| f"Excess enum value '@{k}' description in '{declaration_name}'") | |
| self.output_declaration('enum', declaration_name, | |
| source=source, | |
| purpose=self.entry.declaration_purpose) | |
| def dump_var(self, ln, proto, source): | |
| """ | |
| Store variables that are part of kAPI. | |
| """ | |
| VAR_ATTRIBS = [ | |
| "extern", | |
| "const", | |
| ] | |
| OPTIONAL_VAR_ATTR = r"^(?:\b(?:" +"|".join(VAR_ATTRIBS) +r")\b\s*)*" | |
| # | |
| # Store the full prototype before modifying it | |
| # | |
| source = source | |
| full_proto = proto | |
| declaration_name = None | |
| # | |
| # Handle macro definitions | |
| # | |
| macro_prefixes = [ | |
| KernRe(r"DEFINE_[\w_]+\s*\(([\w_]+)\)"), | |
| ] | |
| for r in macro_prefixes: | |
| match = r.search(proto) | |
| if match: | |
| declaration_name = match.group(1) | |
| break | |
| # | |
| # Drop comments and macros to have a pure C prototype | |
| # | |
| if not declaration_name: | |
| proto = self.xforms.apply("var", proto) | |
| proto = proto.rstrip() | |
| # | |
| # Variable name is at the end of the declaration | |
| # | |
| default_val = None | |
| r= KernRe(OPTIONAL_VAR_ATTR + r"\s*[\w_\s]*\s+(?:\*+)?([\w_]+)\s*[\d\]\[]*\s*(=.*)?") | |
| if r.match(proto): | |
| if not declaration_name: | |
| declaration_name = r.group(1) | |
| default_val = r.group(2) | |
| else: | |
| r= KernRe(OPTIONAL_VAR_ATTR + r"(?:[\w_\s]*)?\s+(?:\*+)?(?:[\w_]+)\s*[\d\]\[]*\s*(=.*)?") | |
| if r.match(proto): | |
| default_val = r.group(1) | |
| if not declaration_name: | |
| self.emit_msg(ln,f"{proto}: can't parse variable") | |
| return | |
| if default_val: | |
| default_val = default_val.lstrip("=").strip() | |
| self.output_declaration("var", declaration_name, | |
| source=source, | |
| full_proto=full_proto, | |
| default_val=default_val, | |
| purpose=self.entry.declaration_purpose) | |
| def dump_declaration(self, ln, prototype, source): | |
| """ | |
| Store a data declaration inside self.entries array. | |
| """ | |
| if self.entry.decl_type == "enum": | |
| self.dump_enum(ln, prototype, source) | |
| elif self.entry.decl_type == "typedef": | |
| self.dump_typedef(ln, prototype, source) | |
| elif self.entry.decl_type in ["union", "struct"]: | |
| self.dump_struct(ln, prototype, source) | |
| elif self.entry.decl_type == "var": | |
| self.dump_var(ln, prototype, source) | |
| else: | |
| # This would be a bug | |
| self.emit_message(ln, f'Unknown declaration type: {self.entry.decl_type}') | |
| def dump_function(self, ln, prototype, source): | |
| """ | |
| Store a function or function macro inside self.entries array. | |
| """ | |
| source = source | |
| found = func_macro = False | |
| return_type = '' | |
| decl_type = 'function' | |
| # | |
| # If we have a macro, remove the "#define" at the front. | |
| # | |
| new_proto = KernRe(r"^#\s*define\s+").sub("", prototype) | |
| if new_proto != prototype: | |
| prototype = new_proto | |
| # | |
| # Dispense with the simple "#define A B" case here; the key | |
| # is the space after the name of the symbol being defined. | |
| # NOTE that the seemingly misnamed "func_macro" indicates a | |
| # macro *without* arguments. | |
| # | |
| r = KernRe(r'^(\w+)\s+') | |
| if r.search(prototype): | |
| return_type = '' | |
| declaration_name = r.group(1) | |
| func_macro = True | |
| found = True | |
| else: | |
| # | |
| # Apply the initial transformations. | |
| # | |
| prototype = self.xforms.apply("func", prototype) | |
| # Yes, this truly is vile. We are looking for: | |
| # 1. Return type (may be nothing if we're looking at a macro) | |
| # 2. Function name | |
| # 3. Function parameters. | |
| # | |
| # All the while we have to watch out for function pointer parameters | |
| # (which IIRC is what the two sections are for), C types (these | |
| # regexps don't even start to express all the possibilities), and | |
| # so on. | |
| # | |
| # If you mess with these regexps, it's a good idea to check that | |
| # the following functions' documentation still comes out right: | |
| # - parport_register_device (function pointer parameters) | |
| # - atomic_set (macro) | |
| # - pci_match_device, __copy_to_user (long return type) | |
| name = r'\w+' | |
| type1 = r'(?:[\w\s]+)?' | |
| type2 = r'(?:[\w\s]+\*+)+' | |
| # | |
| # Attempt to match first on (args) with no internal parentheses; this | |
| # lets us easily filter out __acquires() and other post-args stuff. If | |
| # that fails, just grab the rest of the line to the last closing | |
| # parenthesis. | |
| # | |
| proto_args = r'\(([^\(]*|.*)\)' | |
| # | |
| # (Except for the simple macro case) attempt to split up the prototype | |
| # in the various ways we understand. | |
| # | |
| if not found: | |
| patterns = [ | |
| rf'^()({name})\s*{proto_args}', | |
| rf'^({type1})\s+({name})\s*{proto_args}', | |
| rf'^({type2})\s*({name})\s*{proto_args}', | |
| ] | |
| for p in patterns: | |
| r = KernRe(p) | |
| if r.match(prototype): | |
| return_type = r.group(1) | |
| declaration_name = r.group(2) | |
| args = r.group(3) | |
| self.create_parameter_list(ln, decl_type, args, ',', | |
| declaration_name) | |
| found = True | |
| break | |
| # | |
| # Parsing done; make sure that things are as we expect. | |
| # | |
| if not found: | |
| self.emit_msg(ln, | |
| f"cannot understand function prototype: '{prototype}'") | |
| return | |
| if self.entry.identifier != declaration_name: | |
| self.emit_msg(ln, f"expecting prototype for {self.entry.identifier}(). " | |
| f"Prototype was for {declaration_name}() instead") | |
| return | |
| self.check_sections(ln, declaration_name, "function") | |
| self.check_return_section(ln, declaration_name, return_type) | |
| # | |
| # Store the result. | |
| # | |
| self.output_declaration(decl_type, declaration_name, | |
| source=source, | |
| typedef=('typedef' in return_type), | |
| functiontype=return_type, | |
| purpose=self.entry.declaration_purpose, | |
| func_macro=func_macro) | |
| def dump_typedef(self, ln, proto, source): | |
| """ | |
| Store a ``typedef`` inside self.entries array. | |
| """ | |
| # | |
| # We start by looking for function typedefs. | |
| # | |
| typedef_type = r'typedef((?:\s+[\w*]+\b){0,7}\s+(?:\w+\b|\*+))\s*' | |
| typedef_ident = r'\*?\s*(\w\S+)\s*' | |
| typedef_args = r'\s*\((.*)\);' | |
| source = source | |
| typedef1 = KernRe(typedef_type + r'\(' + typedef_ident + r'\)' + typedef_args) | |
| typedef2 = KernRe(typedef_type + typedef_ident + typedef_args) | |
| # Parse function typedef prototypes | |
| for r in [typedef1, typedef2]: | |
| if not r.match(proto): | |
| continue | |
| return_type = r.group(1).strip() | |
| declaration_name = r.group(2) | |
| args = r.group(3) | |
| if self.entry.identifier != declaration_name: | |
| self.emit_msg(ln, | |
| f"expecting prototype for typedef {self.entry.identifier}. Prototype was for typedef {declaration_name} instead\n") | |
| return | |
| self.create_parameter_list(ln, 'function', args, ',', declaration_name) | |
| self.output_declaration('function', declaration_name, | |
| source=source, | |
| typedef=True, | |
| functiontype=return_type, | |
| purpose=self.entry.declaration_purpose) | |
| return | |
| # | |
| # Not a function, try to parse a simple typedef. | |
| # | |
| r = KernRe(r'typedef.*\s+(\w+)\s*;') | |
| if r.match(proto): | |
| declaration_name = r.group(1) | |
| if self.entry.identifier != declaration_name: | |
| self.emit_msg(ln, | |
| f"expecting prototype for typedef {self.entry.identifier}. Prototype was for typedef {declaration_name} instead\n") | |
| return | |
| self.output_declaration('typedef', declaration_name, | |
| source=source, | |
| purpose=self.entry.declaration_purpose) | |
| return | |
| self.emit_msg(ln, "error: Cannot parse typedef!") | |
| @staticmethod | |
| def process_export(function_set, line): | |
| """ | |
| process ``EXPORT_SYMBOL*`` tags | |
| This method doesn't use any variable from the class, so declare it | |
| with a staticmethod decorator. | |
| """ | |
| # We support documenting some exported symbols with different | |
| # names. A horrible hack. | |
| suffixes = [ '_noprof' ] | |
| # Note: it accepts only one EXPORT_SYMBOL* per line, as having | |
| # multiple export lines would violate Kernel coding style. | |
| if export_symbol.search(line): | |
| symbol = export_symbol.group(2) | |
| elif export_symbol_ns.search(line): | |
| symbol = export_symbol_ns.group(2) | |
| else: | |
| return False | |
| # | |
| # Found an export, trim out any special suffixes | |
| # | |
| for suffix in suffixes: | |
| # Be backward compatible with Python < 3.9 | |
| if symbol.endswith(suffix): | |
| symbol = symbol[:-len(suffix)] | |
| function_set.add(symbol) | |
| return True | |
| def process_normal(self, ln, line, source): | |
| """ | |
| STATE_NORMAL: looking for the ``/**`` to begin everything. | |
| """ | |
| if not doc_start.match(line): | |
| return | |
| # start a new entry | |
| self.reset_state(ln) | |
| # next line is always the function name | |
| self.state = state.NAME | |
| def process_name(self, ln, line, source): | |
| """ | |
| STATE_NAME: Looking for the "name - description" line | |
| """ | |
| # | |
| # Check for a DOC: block and handle them specially. | |
| # | |
| if doc_block.search(line): | |
| if not doc_block.group(1): | |
| self.entry.begin_section(ln, "Introduction") | |
| else: | |
| self.entry.begin_section(ln, doc_block.group(1)) | |
| self.entry.identifier = self.entry.section | |
| self.state = state.DOCBLOCK | |
| # | |
| # Otherwise we're looking for a normal kerneldoc declaration line. | |
| # | |
| elif doc_decl.search(line): | |
| self.entry.identifier = doc_decl.group(1) | |
| # Test for data declaration | |
| if doc_begin_data.search(line): | |
| self.entry.decl_type = doc_begin_data.group(1) | |
| self.entry.identifier = doc_begin_data.group(2) | |
| # | |
| # Look for a function description | |
| # | |
| elif doc_begin_func.search(line): | |
| self.entry.identifier = doc_begin_func.group(1) | |
| self.entry.decl_type = "function" | |
| # | |
| # We struck out. | |
| # | |
| else: | |
| self.emit_msg(ln, | |
| f"This comment starts with '/**', but isn't a kernel-doc comment. Refer to Documentation/doc-guide/kernel-doc.rst\n{line}") | |
| self.state = state.NORMAL | |
| return | |
| # | |
| # OK, set up for a new kerneldoc entry. | |
| # | |
| self.state = state.BODY | |
| self.entry.identifier = self.entry.identifier.strip(" ") | |
| # if there's no @param blocks need to set up default section here | |
| self.entry.begin_section(ln + 1) | |
| # | |
| # Find the description portion, which *should* be there but | |
| # isn't always. | |
| # (We should be able to capture this from the previous parsing - someday) | |
| # | |
| r = KernRe("[-:](.*)") | |
| if r.search(line): | |
| self.entry.declaration_purpose = trim_whitespace(r.group(1)) | |
| self.state = state.DECLARATION | |
| else: | |
| self.entry.declaration_purpose = "" | |
| if not self.entry.declaration_purpose and self.config.wshort_desc: | |
| self.emit_msg(ln, | |
| f"missing initial short description on line:\n{line}") | |
| if not self.entry.identifier and self.entry.decl_type != "enum": | |
| self.emit_msg(ln, | |
| f"wrong kernel-doc identifier on line:\n{line}") | |
| self.state = state.NORMAL | |
| if self.config.verbose: | |
| self.emit_msg(ln, | |
| f"Scanning doc for {self.entry.decl_type} {self.entry.identifier}", | |
| warning=False) | |
| # | |
| # Failed to find an identifier. Emit a warning | |
| # | |
| else: | |
| self.emit_msg(ln, f"Cannot find identifier on line:\n{line}") | |
| def is_new_section(self, ln, line): | |
| """ | |
| Helper function to determine if a new section is being started. | |
| """ | |
| if doc_sect.search(line): | |
| self.state = state.BODY | |
| # | |
| # Pick out the name of our new section, tweaking it if need be. | |
| # | |
| newsection = doc_sect.group(1) | |
| if newsection.lower() == 'description': | |
| newsection = 'Description' | |
| elif newsection.lower() == 'context': | |
| newsection = 'Context' | |
| self.state = state.SPECIAL_SECTION | |
| elif newsection.lower() in ["@return", "@returns", | |
| "return", "returns"]: | |
| newsection = "Return" | |
| self.state = state.SPECIAL_SECTION | |
| elif newsection[0] == '@': | |
| self.state = state.SPECIAL_SECTION | |
| # | |
| # Initialize the contents, and get the new section going. | |
| # | |
| newcontents = doc_sect.group(2) | |
| if not newcontents: | |
| newcontents = "" | |
| self.dump_section() | |
| self.entry.begin_section(ln, newsection) | |
| self.entry.leading_space = None | |
| self.entry.add_text(newcontents.lstrip()) | |
| return True | |
| return False | |
| def is_comment_end(self, ln, line): | |
| """ | |
| Helper function to detect (and effect) the end of a kerneldoc comment. | |
| """ | |
| if doc_end.search(line): | |
| self.dump_section() | |
| # Look for doc_com + <text> + doc_end: | |
| r = KernRe(r'\s*\*\s*[a-zA-Z_0-9:.]+\*/') | |
| if r.match(line): | |
| self.emit_msg(ln, f"suspicious ending line: {line}") | |
| self.entry.prototype = "" | |
| self.entry.new_start_line = ln + 1 | |
| self.state = state.PROTO | |
| return True | |
| return False | |
| def process_decl(self, ln, line, source): | |
| """ | |
| STATE_DECLARATION: We've seen the beginning of a declaration. | |
| """ | |
| if self.is_new_section(ln, line) or self.is_comment_end(ln, line): | |
| return | |
| # | |
| # Look for anything with the " * " line beginning. | |
| # | |
| if doc_content.search(line): | |
| cont = doc_content.group(1) | |
| # | |
| # A blank line means that we have moved out of the declaration | |
| # part of the comment (without any "special section" parameter | |
| # descriptions). | |
| # | |
| if cont == "": | |
| self.state = state.BODY | |
| # | |
| # Otherwise we have more of the declaration section to soak up. | |
| # | |
| else: | |
| self.entry.declaration_purpose = \ | |
| trim_whitespace(self.entry.declaration_purpose + ' ' + cont) | |
| else: | |
| # Unknown line, ignore | |
| self.emit_msg(ln, f"bad line: {line}") | |
| def process_special(self, ln, line, source): | |
| """ | |
| STATE_SPECIAL_SECTION: a section ending with a blank line. | |
| """ | |
| # | |
| # If we have hit a blank line (only the " * " marker), then this | |
| # section is done. | |
| # | |
| if KernRe(r"\s*\*\s*$").match(line): | |
| self.entry.begin_section(ln, dump = True) | |
| self.state = state.BODY | |
| return | |
| # | |
| # Not a blank line, look for the other ways to end the section. | |
| # | |
| if self.is_new_section(ln, line) or self.is_comment_end(ln, line): | |
| return | |
| # | |
| # OK, we should have a continuation of the text for this section. | |
| # | |
| if doc_content.search(line): | |
| cont = doc_content.group(1) | |
| # | |
| # If the lines of text after the first in a special section have | |
| # leading white space, we need to trim it out or Sphinx will get | |
| # confused. For the second line (the None case), see what we | |
| # find there and remember it. | |
| # | |
| if self.entry.leading_space is None: | |
| r = KernRe(r'^(\s+)') | |
| if r.match(cont): | |
| self.entry.leading_space = len(r.group(1)) | |
| else: | |
| self.entry.leading_space = 0 | |
| # | |
| # Otherwise, before trimming any leading chars, be *sure* | |
| # that they are white space. We should maybe warn if this | |
| # isn't the case. | |
| # | |
| for i in range(0, self.entry.leading_space): | |
| if cont[i] != " ": | |
| self.entry.leading_space = i | |
| break | |
| # | |
| # Add the trimmed result to the section and we're done. | |
| # | |
| self.entry.add_text(cont[self.entry.leading_space:]) | |
| else: | |
| # Unknown line, ignore | |
| self.emit_msg(ln, f"bad line: {line}") | |
| def process_body(self, ln, line, source): | |
| """ | |
| STATE_BODY: the bulk of a kerneldoc comment. | |
| """ | |
| if self.is_new_section(ln, line) or self.is_comment_end(ln, line): | |
| return | |
| if doc_content.search(line): | |
| cont = doc_content.group(1) | |
| self.entry.add_text(cont) | |
| else: | |
| # Unknown line, ignore | |
| self.emit_msg(ln, f"bad line: {line}") | |
| def process_inline_name(self, ln, line, source): | |
| """STATE_INLINE_NAME: beginning of docbook comments within a prototype.""" | |
| if doc_inline_sect.search(line): | |
| self.entry.begin_section(ln, doc_inline_sect.group(1)) | |
| self.entry.add_text(doc_inline_sect.group(2).lstrip()) | |
| self.state = state.INLINE_TEXT | |
| elif doc_inline_end.search(line): | |
| self.dump_section() | |
| self.state = state.PROTO | |
| elif doc_content.search(line): | |
| self.emit_msg(ln, f"Incorrect use of kernel-doc format: {line}") | |
| self.state = state.PROTO | |
| # | |
| # Don't let it add partial comments at the code, as breaks the | |
| # logic meant to remove comments from prototypes. | |
| # | |
| self.process_proto_type(ln, "/**\n" + line, source) | |
| # else ... ?? | |
| def process_inline_text(self, ln, line, source): | |
| """STATE_INLINE_TEXT: docbook comments within a prototype.""" | |
| if doc_inline_end.search(line): | |
| self.dump_section() | |
| self.state = state.PROTO | |
| elif doc_content.search(line): | |
| self.entry.add_text(doc_content.group(1)) | |
| # else ... ?? | |
| def syscall_munge(self, ln, proto): # pylint: disable=W0613 | |
| """ | |
| Handle syscall definitions. | |
| """ | |
| is_void = False | |
| # Strip newlines/CR's | |
| proto = re.sub(r'[\r\n]+', ' ', proto) | |
| # Check if it's a SYSCALL_DEFINE0 | |
| if 'SYSCALL_DEFINE0' in proto: | |
| is_void = True | |
| # Replace SYSCALL_DEFINE with correct return type & function name | |
| proto = KernRe(r'SYSCALL_DEFINE.*\(').sub('long sys_', proto) | |
| r = KernRe(r'long\s+(sys_.*?),') | |
| if r.search(proto): | |
| proto = KernRe(',').sub('(', proto, count=1) | |
| elif is_void: | |
| proto = KernRe(r'\)').sub('(void)', proto, count=1) | |
| # Now delete all of the odd-numbered commas in the proto | |
| # so that argument types & names don't have a comma between them | |
| count = 0 | |
| length = len(proto) | |
| if is_void: | |
| length = 0 # skip the loop if is_void | |
| for ix in range(length): | |
| if proto[ix] == ',': | |
| count += 1 | |
| if count % 2 == 1: | |
| proto = proto[:ix] + ' ' + proto[ix + 1:] | |
| return proto | |
| def tracepoint_munge(self, ln, proto): | |
| """ | |
| Handle tracepoint definitions. | |
| """ | |
| tracepointname = None | |
| tracepointargs = None | |
| # Match tracepoint name based on different patterns | |
| r = KernRe(r'TRACE_EVENT\((.*?),') | |
| if r.search(proto): | |
| tracepointname = r.group(1) | |
| r = KernRe(r'DEFINE_SINGLE_EVENT\((.*?),') | |
| if r.search(proto): | |
| tracepointname = r.group(1) | |
| r = KernRe(r'DEFINE_EVENT\((.*?),(.*?),') | |
| if r.search(proto): | |
| tracepointname = r.group(2) | |
| if tracepointname: | |
| tracepointname = tracepointname.lstrip() | |
| r = KernRe(r'TP_PROTO\((.*?)\)') | |
| if r.search(proto): | |
| tracepointargs = r.group(1) | |
| if not tracepointname or not tracepointargs: | |
| self.emit_msg(ln, | |
| f"Unrecognized tracepoint format:\n{proto}\n") | |
| else: | |
| proto = f"static inline void trace_{tracepointname}({tracepointargs})" | |
| self.entry.identifier = f"trace_{self.entry.identifier}" | |
| return proto | |
| def process_proto_function(self, ln, line, source): | |
| """Ancillary routine to process a function prototype.""" | |
| # strip C99-style comments to end of line | |
| line = KernRe(r"//.*$", re.S).sub('', line) | |
| # | |
| # Soak up the line's worth of prototype text, stopping at { or ; if present. | |
| # | |
| if KernRe(r'\s*#\s*define').match(line): | |
| self.entry.prototype = line | |
| elif not line.startswith('#'): # skip other preprocessor stuff | |
| r = KernRe(r'([^\{]*)') | |
| if r.match(line): | |
| self.entry.prototype += r.group(1) + " " | |
| # | |
| # If we now have the whole prototype, clean it up and declare victory. | |
| # | |
| if '{' in line or ';' in line or KernRe(r'\s*#\s*define').match(line): | |
| # strip comments and surrounding spaces | |
| self.entry.prototype = KernRe(r'/\*.*\*/').sub('', self.entry.prototype).strip() | |
| # | |
| # Handle self.entry.prototypes for function pointers like: | |
| # int (*pcs_config)(struct foo) | |
| # by turning it into | |
| # int pcs_config(struct foo) | |
| # | |
| r = KernRe(r'^(\S+\s+)\(\s*\*(\S+)\)') | |
| self.entry.prototype = r.sub(r'\1\2', self.entry.prototype) | |
| # | |
| # Handle special declaration syntaxes | |
| # | |
| if 'SYSCALL_DEFINE' in self.entry.prototype: | |
| self.entry.prototype = self.syscall_munge(ln, | |
| self.entry.prototype) | |
| else: | |
| r = KernRe(r'TRACE_EVENT|DEFINE_EVENT|DEFINE_SINGLE_EVENT') | |
| if r.search(self.entry.prototype): | |
| self.entry.prototype = self.tracepoint_munge(ln, | |
| self.entry.prototype) | |
| # | |
| # ... and we're done | |
| # | |
| self.dump_function(ln, self.entry.prototype, source) | |
| self.reset_state(ln) | |
| def process_proto_type(self, ln, line, source): | |
| """ | |
| Ancillary routine to process a type. | |
| """ | |
| # Strip C99-style comments and surrounding whitespace | |
| line = KernRe(r"//.*$", re.S).sub('', line).strip() | |
| if not line: | |
| return # nothing to see here | |
| # To distinguish preprocessor directive from regular declaration later. | |
| if line.startswith('#'): | |
| line += ";" | |
| # | |
| # Split the declaration on any of { } or ;, and accumulate pieces | |
| # until we hit a semicolon while not inside {brackets} | |
| # | |
| r = KernRe(r'(.*?)([{};])') | |
| for chunk in r.split(line): | |
| if chunk: # Ignore empty matches | |
| self.entry.prototype += chunk | |
| # | |
| # This cries out for a match statement ... someday after we can | |
| # drop Python 3.9 ... | |
| # | |
| if chunk == '{': | |
| self.entry.brcount += 1 | |
| elif chunk == '}': | |
| self.entry.brcount -= 1 | |
| elif chunk == ';' and self.entry.brcount <= 0: | |
| self.dump_declaration(ln, self.entry.prototype, source) | |
| self.reset_state(ln) | |
| return | |
| # | |
| # We hit the end of the line while still in the declaration; put | |
| # in a space to represent the newline. | |
| # | |
| self.entry.prototype += ' ' | |
| def process_proto(self, ln, line, source): | |
| """STATE_PROTO: reading a function/whatever prototype.""" | |
| if doc_inline_oneline.search(line): | |
| self.entry.begin_section(ln, doc_inline_oneline.group(1)) | |
| self.entry.add_text(doc_inline_oneline.group(2)) | |
| self.dump_section() | |
| elif doc_inline_start.search(line): | |
| self.state = state.INLINE_NAME | |
| elif self.entry.decl_type == 'function': | |
| self.process_proto_function(ln, line, source) | |
| else: | |
| self.process_proto_type(ln, line, source) | |
| def process_docblock(self, ln, line, source): | |
| """STATE_DOCBLOCK: within a ``DOC:`` block.""" | |
| if doc_end.search(line): | |
| self.dump_section() | |
| self.output_declaration("doc", self.entry.identifier, | |
| source=source) | |
| self.reset_state(ln) | |
| elif doc_content.search(line): | |
| self.entry.add_text(doc_content.group(1)) | |
| def parse_export(self): | |
| """ | |
| Parses ``EXPORT_SYMBOL*`` macros from a single Kernel source file. | |
| """ | |
| export_table = set() | |
| try: | |
| with open(self.fname, "r", encoding="utf8", | |
| errors="backslashreplace") as fp: | |
| for line in fp: | |
| self.process_export(export_table, line) | |
| except IOError: | |
| return None | |
| return export_table | |
| #: The state/action table telling us which function to invoke in each state. | |
| state_actions = { | |
| state.NORMAL: process_normal, | |
| state.NAME: process_name, | |
| state.BODY: process_body, | |
| state.DECLARATION: process_decl, | |
| state.SPECIAL_SECTION: process_special, | |
| state.INLINE_NAME: process_inline_name, | |
| state.INLINE_TEXT: process_inline_text, | |
| state.PROTO: process_proto, | |
| state.DOCBLOCK: process_docblock, | |
| } | |
| def parse_kdoc(self): | |
| """ | |
| Open and process each line of a C source file. | |
| The parsing is controlled via a state machine, and the line is passed | |
| to a different process function depending on the state. The process | |
| function may update the state as needed. | |
| Besides parsing kernel-doc tags, it also parses export symbols. | |
| """ | |
| prev = "" | |
| prev_ln = None | |
| export_table = set() | |
| self.state = state.NORMAL | |
| source = "" | |
| try: | |
| with open(self.fname, "r", encoding="utf8", | |
| errors="backslashreplace") as fp: | |
| for ln, line in enumerate(fp): | |
| line = line.expandtabs().strip("\n") | |
| # Group continuation lines on prototypes | |
| if self.state == state.PROTO: | |
| if line.endswith("\\"): | |
| prev += line.rstrip("\\") | |
| if not prev_ln: | |
| prev_ln = ln | |
| continue | |
| if prev: | |
| ln = prev_ln | |
| line = prev + line | |
| prev = "" | |
| prev_ln = None | |
| self.config.log.debug("%d %s: %s", | |
| ln, state.name[self.state], | |
| line) | |
| if self.store_src: | |
| if source and self.state == state.NORMAL: | |
| source = "" | |
| elif self.state != state.NORMAL: | |
| source += line + "\n" | |
| # This is an optimization over the original script. | |
| # There, when export_file was used for the same file, | |
| # it was read twice. Here, we use the already-existing | |
| # loop to parse exported symbols as well. | |
| # | |
| if (self.state != state.NORMAL) or \ | |
| not self.process_export(export_table, line): | |
| prev_state = self.state | |
| # Hand this line to the appropriate state handler | |
| self.state_actions[self.state](self, ln, line, source) | |
| if prev_state == state.NORMAL and self.state != state.NORMAL: | |
| source += line + "\n" | |
| self.emit_unused_warnings() | |
| except OSError: | |
| self.config.log.error(f"Error: Cannot open file {self.fname}") | |
| return export_table, self.entries |