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