1"""
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))
149