aboutsummaryrefslogtreecommitdiff
path: root/contrib/scripts/doc/warningfilter.py
blob: 31ac5865c8b3bb2cf9fc54e854f5bdd35ff5a690 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/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 <willow@howhill.com>
"""

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<path>/{filepath}|\[generated\]):(?P<linenumber>\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<name>{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<arg_name>\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<group_id>\w+): ignoring title \"(?P<new_title>{verbose_name})\" that does not match old title \"(?P<old_title>{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 </(?P<tag>[^>]+)> 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<tag>[^>]+)> found"),
    "expected whitespace after command": re.compile(rf"{main_match} expected whitespace after \\(?P<command>\w+) command"),
    "illegal command": re.compile(rf"{main_match} Illegal command (?P<illegal_cmd>(?:@|\\)\w+) as part of a \\(?P<command>\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("")