1#!/usr/bin/env python3
2import optparse
3import subprocess
4import sys
5import os
6import re
7import glob
8
9links = {}
10symbols = {}
11structs = {}
12types = {}
13anonymous_enums = {}
14functions = {}
15constants = {}
16sections = {}
17
18def check_references():
19    """
20    Check if each [link] in the reference manual actually exists. Also fills
21    in global variable "links".
22    """
23    print("Checking References...")
24
25    html_refs = os.path.join(options.build, "docs", "html_refs")
26    for line in open(html_refs):
27        mob = re.match(r"\[(.*?)\]", line)
28        if mob:
29            links[mob.group(1)] = True
30
31    docs = glob.glob("docs/src/refman/*.txt")
32    for doc in docs:
33        text = file(doc).read()
34        text = re.compile("<script.*?>.*?</script>", re.S).sub("", text)
35        # in case of [A][B], we will not see A but we do see B.
36        for link in re.findall(r" \[([^[]*?)\][^([]", text):
37            if not link in links:
38                print("Missing: %s: %s" % (doc, link))
39        for section in re.findall(r"^#+ (.*)", text, re.MULTILINE):
40           if not section.startswith("API:"):
41              sections[section] = 1
42
43    for link in sections.keys():
44        del links[link]
45
46def add_struct(line):
47    if options.protos:
48        kind = re.match("\s*(\w+)", line).group(1)
49        if kind in ["typedef", "struct", "enum", "union"]:
50            mob = None
51            if kind != "typedef":
52                mob = re.match(kind + "\s+(\w+)", line)
53            if not mob:
54                mob = re.match(".*?(\w+);$", line)
55            if not mob and kind == "typedef":
56                mob = re.match("typedef.*?\(\s*\*\s*(\w+)\)", line)
57            if not mob:
58                anonymous_enums[line] = 1
59            else:
60                sname = mob.group(1)
61                if sname.startswith("_ALLEGRO_gl"):
62                    return
63                if kind == "typedef":
64                    types[sname] = line
65                else:
66                    structs[sname] = line
67
68
69def parse_header(lines, filename):
70    """
71    Minimal C parser which extracts most symbols from a header. Fills
72    them into the global variable "symbols".
73    """
74    n = 0
75    ok = False
76    brace = 0
77    lines2 = []
78    cline = ""
79    for line in lines:
80        line = line.strip()
81        if not line:
82            continue
83
84        if line.startswith("#"):
85            if line.startswith("#define"):
86                if ok:
87                    name = line[8:]
88                    match = re.match("#define ([a-zA-Z_]+)", line)
89                    name = match.group(1)
90                    symbols[name] = "macro"
91                    simple_constant = line.split()
92
93                    if len(simple_constant) == 3 and\
94                        not "(" in simple_constant[1] and\
95                        simple_constant[2][0].isdigit():
96                        constants[name] = simple_constant[2]
97                    n += 1
98            elif line.startswith("#undef"):
99                pass
100            else:
101                ok = False
102                match = re.match(r'# \d+ "(.*?)"', line)
103                if match:
104                    name = match.group(1)
105                    if name == "<stdin>" or name.startswith(options.build) or \
106                            name.startswith("include") or \
107                            name.startswith("addons") or\
108                            name.startswith(options.source):
109                        ok = True
110            continue
111        if not ok:
112            continue
113
114        sublines = line.split(";")
115
116        for i, subline in enumerate(sublines):
117            if i < len(sublines) - 1:
118                subline += ";"
119
120            brace -= subline.count("}")
121            brace -= subline.count(")")
122            brace += subline.count("{")
123            brace += subline.count("(")
124
125            if cline:
126                if cline[-1].isalnum():
127                    cline += " "
128            cline += subline
129            if brace == 0 and subline.endswith(";") or subline.endswith("}"):
130
131                lines2.append(cline.strip())
132                cline = ""
133
134    for line in lines2:
135        line = line.replace("__attribute__((__stdcall__))", "")
136        if line.startswith("enum"):
137            add_struct(line)
138        elif line.startswith("typedef"):
139            match = None
140            if not match:
141                match = re.match(r".*?(\w+);$", line)
142            if not match:
143                match = re.match(r".*?(\w*)\[", line)
144            if not match:
145                match = re.match(r".*?\(\s*\*\s*(\w+)\s*\).*?", line)
146
147            if match:
148                name = match.group(1)
149                symbols[name] = "typedef"
150                n += 1
151            else:
152                print("? " + line)
153
154            add_struct(line)
155
156        elif line.startswith("struct"):
157            add_struct(line)
158        elif line.startswith("union"):
159            add_struct(line)
160        else:
161            try:
162                parenthesis = line.find("(")
163                if parenthesis < 0:
164                    match = re.match(r".*?(\w+)\s*=", line)
165                    if not match:
166                        match = re.match(r".*?(\w+)\s*;$", line)
167                    if not match:
168                        match = re.match(r".*?(\w+)", line)
169                    symbols[match.group(1)] = "variable"
170                    n += 1
171                else:
172                    match = re.match(r".*?(\w+)\s*\(", line)
173                    fname = match.group(1)
174                    symbols[fname] = "function"
175                    if not fname in functions:
176                        functions[fname] = line
177                    n += 1
178            except AttributeError as e:
179                print("Cannot parse in " + filename)
180                print("Line is: " + line)
181                print(e)
182    return n
183
184
185def parse_all_headers():
186    """
187    Call parse_header() on all of Allegro's public include files.
188    """
189    p = options.source
190    includes = " -I " + p + "/include -I " + os.path.join(options.build,
191        "include")
192    includes += " -I " + p + "/addons/acodec"
193    headers = [p + "/include/allegro5/allegro.h",
194        p + "/addons/acodec/allegro5/allegro_acodec.h",
195        p + "/include/allegro5/allegro_opengl.h"]
196    if options.windows:
197        headers += [p + "/include/allegro5/allegro_windows.h"]
198
199    for addon in glob.glob(p + "/addons/*"):
200        name = addon[len(p + "/addons/"):]
201        header = os.path.join(p, "addons", name, "allegro5",
202            "allegro_" + name + ".h")
203        if os.path.exists(header):
204            headers.append(header)
205            includes += " -I " + os.path.join(p, "addons", name)
206
207    for header in headers:
208        p = subprocess.Popen(options.compiler + " -E -dD - " + includes,
209            stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True)
210        filename = "#include <allegro5/allegro.h>\n" + open(header).read()
211        p.stdin.write(filename.encode('utf-8'))
212        p.stdin.close()
213        text = p.stdout.read().decode("utf-8")
214        parse_header(text.splitlines(), header)
215        #print("%d definitions in %s" % (n, header))
216
217
218def check_undocumented_functions():
219    """
220    Cross-compare the documentation links with public symbols found in headers.
221    """
222    print("Checking if each documented function exists...")
223
224    parse_all_headers()
225
226    for link in links:
227        if not link in symbols:
228            print("Missing: " + link)
229
230    print("")
231    print("Checking if each function is documented...")
232    others = []
233    for link in symbols:
234        if not link in links:
235            if symbols[link] == "function":
236                print("Missing: " + link)
237            else:
238                if link and not link.startswith("GL") and \
239                        not link.startswith("gl") and \
240                        not link.startswith("_al_gl") and \
241                        not link.startswith("_ALLEGRO_gl") and \
242                        not link.startswith("_ALLEGRO_GL") and \
243                        not link.startswith("ALLEGRO_"):
244                    others.append(link)
245
246    print("Also leaking:")
247    others.sort()
248    print(", ".join(others))
249
250
251def list_all_symbols():
252    parse_all_headers()
253    for name in sorted(symbols.keys()):
254        print(name)
255
256
257def main(argv):
258    global options
259    p = optparse.OptionParser()
260    p.description = """\
261When run from the toplevel A5 directory, this script will parse the include,
262addons and cmake build directory for global definitions and check against all
263references in the documentation - then report symbols which are not documented.
264"""
265    p.add_option("-b", "--build", help="Path to the build directory.")
266    p.add_option("-c", "--compiler", help="Path to gcc.")
267    p.add_option("-s", "--source", help="Path to the source directory.")
268    p.add_option("-l", "--list", action="store_true",
269        help="List all symbols.")
270    p.add_option("-p", "--protos",  help="Write all public " +
271        "prototypes to the given file.")
272    p.add_option("-w", "--windows", action="store_true",
273        help="Include windows specific symbols.")
274    options, args = p.parse_args()
275
276    if not options.source:
277        options.source = "."
278    if not options.compiler:
279        options.compiler = "gcc"
280
281    if not options.build:
282        sys.stderr.write("Build path required (-p).\n")
283        p.print_help()
284        sys.exit(-1)
285
286    if options.protos:
287        parse_all_headers()
288        f = open(options.protos, "w")
289        for name, s in structs.items():
290            f.write(name + ": " + s + "\n")
291        for name, s in types.items():
292            f.write(name + ": " + s + "\n")
293        for e in anonymous_enums.keys():
294            f.write(": " + e + "\n")
295        for fname, proto in functions.items():
296            f.write(fname + "(): " + proto + "\n")
297        for name, value in constants.items():
298            f.write(name + ": #define " + name + " " + value + "\n")
299    elif options.list:
300        list_all_symbols()
301    else:
302        check_references()
303        print("")
304        check_undocumented_functions()
305
306
307if __name__ == "__main__":
308    main(sys.argv)
309