blob: 142d248dd2fa9b3cfc41455e64ebe25a220b2568 [file] [log] [blame]
Christian Tismerea62ce72018-06-09 20:32:25 +02001"""
2pep384_macrocheck.py
3
4This programm tries to locate errors in the relevant Python header
5files where macros access type fields when they are reachable from
6the limided API.
7
8The idea is to search macros with the string "->tp_" in it.
9When the macro name does not begin with an underscore,
10then we have found a dormant error.
11
12Christian Tismer
132018-06-02
14"""
15
16import sys
17import os
18import re
19
20
21DEBUG = False
22
23def dprint(*args, **kw):
24 if DEBUG:
25 print(*args, **kw)
26
27def parse_headerfiles(startpath):
28 """
29 Scan all header files which are reachable fronm Python.h
30 """
31 search = "Python.h"
32 name = os.path.join(startpath, search)
33 if not os.path.exists(name):
34 raise ValueError("file {} was not found in {}\n"
35 "Please give the path to Python's include directory."
36 .format(search, startpath))
37 errors = 0
38 with open(name) as python_h:
39 while True:
40 line = python_h.readline()
41 if not line:
42 break
43 found = re.match(r'^\s*#\s*include\s*"(\w+\.h)"', line)
44 if not found:
45 continue
46 include = found.group(1)
47 dprint("Scanning", include)
48 name = os.path.join(startpath, include)
49 if not os.path.exists(name):
50 name = os.path.join(startpath, "../PC", include)
51 errors += parse_file(name)
52 return errors
53
54def ifdef_level_gen():
55 """
56 Scan lines for #ifdef and track the level.
57 """
58 level = 0
59 ifdef_pattern = r"^\s*#\s*if" # covers ifdef and ifndef as well
60 endif_pattern = r"^\s*#\s*endif"
61 while True:
62 line = yield level
63 if re.match(ifdef_pattern, line):
64 level += 1
65 elif re.match(endif_pattern, line):
66 level -= 1
67
68def limited_gen():
69 """
70 Scan lines for Py_LIMITED_API yes(1) no(-1) or nothing (0)
71 """
72 limited = [0] # nothing
73 unlimited_pattern = r"^\s*#\s*ifndef\s+Py_LIMITED_API"
74 limited_pattern = "|".join([
75 r"^\s*#\s*ifdef\s+Py_LIMITED_API",
76 r"^\s*#\s*(el)?if\s+!\s*defined\s*\(\s*Py_LIMITED_API\s*\)\s*\|\|",
77 r"^\s*#\s*(el)?if\s+defined\s*\(\s*Py_LIMITED_API"
78 ])
79 else_pattern = r"^\s*#\s*else"
80 ifdef_level = ifdef_level_gen()
81 status = next(ifdef_level)
82 wait_for = -1
83 while True:
84 line = yield limited[-1]
85 new_status = ifdef_level.send(line)
86 dir = new_status - status
87 status = new_status
88 if dir == 1:
89 if re.match(unlimited_pattern, line):
90 limited.append(-1)
91 wait_for = status - 1
92 elif re.match(limited_pattern, line):
93 limited.append(1)
94 wait_for = status - 1
95 elif dir == -1:
96 # this must have been an endif
97 if status == wait_for:
98 limited.pop()
99 wait_for = -1
100 else:
101 # it could be that we have an elif
102 if re.match(limited_pattern, line):
103 limited.append(1)
104 wait_for = status - 1
105 elif re.match(else_pattern, line):
106 limited.append(-limited.pop()) # negate top
107
108def parse_file(fname):
109 errors = 0
110 with open(fname) as f:
111 lines = f.readlines()
112 type_pattern = r"^.*?->\s*tp_"
113 define_pattern = r"^\s*#\s*define\s+(\w+)"
114 limited = limited_gen()
115 status = next(limited)
116 for nr, line in enumerate(lines):
117 status = limited.send(line)
118 line = line.rstrip()
119 dprint(fname, nr, status, line)
120 if status != -1:
121 if re.match(define_pattern, line):
122 name = re.match(define_pattern, line).group(1)
123 if not name.startswith("_"):
124 # found a candidate, check it!
125 macro = line + "\n"
126 idx = nr
127 while line.endswith("\\"):
128 idx += 1
129 line = lines[idx].rstrip()
130 macro += line + "\n"
131 if re.match(type_pattern, macro, re.DOTALL):
132 # this type field can reach the limited API
133 report(fname, nr + 1, macro)
134 errors += 1
135 return errors
136
137def report(fname, nr, macro):
138 f = sys.stderr
139 print(fname + ":" + str(nr), file=f)
140 print(macro, file=f)
141
142if __name__ == "__main__":
143 p = sys.argv[1] if sys.argv[1:] else "../../Include"
144 errors = parse_headerfiles(p)
145 if errors:
146 # somehow it makes sense to raise a TypeError :-)
147 raise TypeError("These {} locations contradict the limited API."
148 .format(errors))