#!/usr/bin/env python3 """ Filters and processes warnings generated by Doxygen, which are annoyingly inconsistent and verbose, for greater readability. (Neo)vim commands to go to the file and linenumber listed on a line, in the reports this program generates: :exe "let linenumber =" split(getline("."))[1] :exe "edit" fnameescape(split(getline("."))[0]) "|" linenumber It's easy to put a workflow together to clear up redundant doc comments (which generate "multiple @param docs" warnings), using simple vim commands to move the cursor and close buffers, Neovim's support for the Language Server Protocol or related tooling, and the command shown above. A useful sequence, for rapidly deleting a doc comment from its last line, is, in normal mode, `$v%ddd`. For setting up LSP integration in Neovim, refer to the lsp_config plugin. You may additionally need to generate compile_commands.json in the repository root, to allow the language server to find everything. This can be done using Bear (found at https://github.com/rizsotto/Bear). @author: willow """ import argparse as ap import re # Regular expression construction def sep_re(field, separator): "Constructs regex for a list" return rf"{field}(?:{separator}{field})*" # File names and paths fileclass = r"[\w-]" filename = rf"{fileclass}+" # filename = rf"(/{fileclass}+)+\.\w" filepath = rf"{sep_re(filename, '/')}\.(?:\w+)" main_match = rf"(?P/{filepath}|\[generated\]):(?P\d+): warning:" # Symbols type_name = rf"(?:const )?(?:unsigned (?:long )?|struct |enum )?(?:\w+)(?: \*?const)? \*{{0,3}}" var_def = rf"{type_name}\w+(?:\[(?:\(\d+/\d+\))?\])?" func_params = rf"\({sep_re(var_def, ', ')}(?:,\.\.\.)?\)" simple_name = r"\w+" func_name = simple_name verbose_name = rf"{sep_re(simple_name, ' ')}" command_re = "(?:]+>|\\\w+)" macro_params = rf"\({sep_re(simple_name, ', ')}(?:,\.\.\.)?\)" matches = { "not an input @file": re.compile(rf"{main_match} the name '(?P{filepath}|{simple_name})' supplied as the argument in the \\file statement is not an input file"), "multiple @param docs": re.compile(rf"{main_match} argument '(?P\w+)' from the argument list of ({func_name}) has multiple @param documentation sections"), "undocumented param": re.compile(rf"{main_match} The following parameters? of ({func_name})(?:{func_params}|{macro_params}) (?:is|are) not documented:"), "undocumented param (name)": re.compile(r" parameter '([\w.]+)'"), "explicit link not resolved": re.compile(rf"{main_match} explicit link request to '(\w+(?:\(\))?)' could not be resolved"), "unknown command": re.compile(rf"{main_match} Found unknown command '(\\\w+)'"), "missing argument": re.compile(rf"{main_match} argument '(\w+)' of command @param is not found in the argument list of {func_name}(?:{func_params}|{macro_params})"), "eof inside group": re.compile(rf"{main_match} end of file while inside a group"), "eof inside comment": re.compile(rf"{main_match} Reached end of file while still inside a \(nested\) comment. Nesting level \d+ \(probable line reference: \d+\)"), "eof inside code block": re.compile(rf"{main_match} reached end of file while inside a 'code' block!"), "eof inside code block (line 2)": re.compile(rf"The command that should end the block seems to be missing!"), "title mismatch": re.compile(rf"{main_match} group (?P\w+): ignoring title \"(?P{verbose_name})\" that does not match old title \"(?P{verbose_name})\""), "end of comment expecting command": re.compile(rf"{main_match} end of comment block while expecting command {command_re}"), "no matching tag": re.compile(rf"{main_match} found [^>]+)> tag without matching <(?P=tag)>"), "documented empty return type": re.compile(rf"{main_match} documented empty return type of {func_name}"), "unsupported tag": re.compile(rf"{main_match} Unsupported xml/html tag <(?P[^>]+)> found"), "expected whitespace after command": re.compile(rf"{main_match} expected whitespace after \\(?P\w+) command"), "illegal command": re.compile(rf"{main_match} Illegal command (?P(?:@|\\)\w+) as part of a (?P\\\w+) command"), "undeclared symbol": re.compile(rf"{main_match} documented symbol '(\w+)' was not declared or defined\."), "nameless member": re.compile(rf"{main_match} member with no name found."), "end of empty list": re.compile(rf"{main_match} End of list marker found without any preceding list items"), "blank": re.compile(rf"^\s*$"), # "": re.compile(rf"{main_match} "), } parser_choices = set(matches.keys()) - {"blank", "eof inside code block (line 2)", "undocumented param (name)"} parser = ap.ArgumentParser() parser.add_argument("filename") parser.add_argument("--summary", "-s", action="store_true") parser.add_argument("--key", "-k", choices=parser_choices, action="append", dest="keys") args = parser.parse_args() sorted_lines = {k:[] for k in matches.keys()} unsorted_lines = [] with open(args.filename, "r") as file: for line in file.readlines(): for key, value in matches.items(): if match := value.match(line): sorted_lines[key].append(match) break else: unsorted_lines.append(line.strip("\n")) processed_lines = {k: [" ".join(g for g in match.groups()) for match in matches] for k, matches in sorted_lines.items()} # Combining multiline warnings processed_lines["undocumented param"] = [ l1+" "+l2 for l1, l2 in zip(processed_lines["undocumented param"], processed_lines["undocumented param (name)"]) ] # Removing chaff del processed_lines["blank"] del processed_lines["eof inside code block (line 2)"] del processed_lines["undocumented param (name)"] # Preparing count dictionary and summarising the results counts = {k: len(v) for k, v in processed_lines.items()} if args.summary: for k, v in counts.items(): print(k+":", v) print("") if args.keys is not None: for key in args.keys: print(f"{key}: {counts[key]}") for line in processed_lines[key]: print(line) print("")