2018-06-09 15:32:25 -03:00
|
|
|
"""
|
|
|
|
pep384_macrocheck.py
|
|
|
|
|
2021-09-10 11:44:03 -03:00
|
|
|
This program tries to locate errors in the relevant Python header
|
2018-06-09 15:32:25 -03:00
|
|
|
files where macros access type fields when they are reachable from
|
2021-09-10 11:44:03 -03:00
|
|
|
the limited API.
|
2018-06-09 15:32:25 -03:00
|
|
|
|
|
|
|
The idea is to search macros with the string "->tp_" in it.
|
|
|
|
When the macro name does not begin with an underscore,
|
|
|
|
then we have found a dormant error.
|
|
|
|
|
|
|
|
Christian Tismer
|
|
|
|
2018-06-02
|
|
|
|
"""
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
DEBUG = False
|
|
|
|
|
|
|
|
def dprint(*args, **kw):
|
|
|
|
if DEBUG:
|
|
|
|
print(*args, **kw)
|
|
|
|
|
|
|
|
def parse_headerfiles(startpath):
|
|
|
|
"""
|
|
|
|
Scan all header files which are reachable fronm Python.h
|
|
|
|
"""
|
|
|
|
search = "Python.h"
|
|
|
|
name = os.path.join(startpath, search)
|
|
|
|
if not os.path.exists(name):
|
|
|
|
raise ValueError("file {} was not found in {}\n"
|
|
|
|
"Please give the path to Python's include directory."
|
|
|
|
.format(search, startpath))
|
|
|
|
errors = 0
|
|
|
|
with open(name) as python_h:
|
|
|
|
while True:
|
|
|
|
line = python_h.readline()
|
|
|
|
if not line:
|
|
|
|
break
|
|
|
|
found = re.match(r'^\s*#\s*include\s*"(\w+\.h)"', line)
|
|
|
|
if not found:
|
|
|
|
continue
|
|
|
|
include = found.group(1)
|
|
|
|
dprint("Scanning", include)
|
|
|
|
name = os.path.join(startpath, include)
|
|
|
|
if not os.path.exists(name):
|
|
|
|
name = os.path.join(startpath, "../PC", include)
|
|
|
|
errors += parse_file(name)
|
|
|
|
return errors
|
|
|
|
|
|
|
|
def ifdef_level_gen():
|
|
|
|
"""
|
|
|
|
Scan lines for #ifdef and track the level.
|
|
|
|
"""
|
|
|
|
level = 0
|
|
|
|
ifdef_pattern = r"^\s*#\s*if" # covers ifdef and ifndef as well
|
|
|
|
endif_pattern = r"^\s*#\s*endif"
|
|
|
|
while True:
|
|
|
|
line = yield level
|
|
|
|
if re.match(ifdef_pattern, line):
|
|
|
|
level += 1
|
|
|
|
elif re.match(endif_pattern, line):
|
|
|
|
level -= 1
|
|
|
|
|
|
|
|
def limited_gen():
|
|
|
|
"""
|
|
|
|
Scan lines for Py_LIMITED_API yes(1) no(-1) or nothing (0)
|
|
|
|
"""
|
|
|
|
limited = [0] # nothing
|
|
|
|
unlimited_pattern = r"^\s*#\s*ifndef\s+Py_LIMITED_API"
|
|
|
|
limited_pattern = "|".join([
|
|
|
|
r"^\s*#\s*ifdef\s+Py_LIMITED_API",
|
|
|
|
r"^\s*#\s*(el)?if\s+!\s*defined\s*\(\s*Py_LIMITED_API\s*\)\s*\|\|",
|
|
|
|
r"^\s*#\s*(el)?if\s+defined\s*\(\s*Py_LIMITED_API"
|
|
|
|
])
|
|
|
|
else_pattern = r"^\s*#\s*else"
|
|
|
|
ifdef_level = ifdef_level_gen()
|
|
|
|
status = next(ifdef_level)
|
|
|
|
wait_for = -1
|
|
|
|
while True:
|
|
|
|
line = yield limited[-1]
|
|
|
|
new_status = ifdef_level.send(line)
|
|
|
|
dir = new_status - status
|
|
|
|
status = new_status
|
|
|
|
if dir == 1:
|
|
|
|
if re.match(unlimited_pattern, line):
|
|
|
|
limited.append(-1)
|
|
|
|
wait_for = status - 1
|
|
|
|
elif re.match(limited_pattern, line):
|
|
|
|
limited.append(1)
|
|
|
|
wait_for = status - 1
|
|
|
|
elif dir == -1:
|
|
|
|
# this must have been an endif
|
|
|
|
if status == wait_for:
|
|
|
|
limited.pop()
|
|
|
|
wait_for = -1
|
|
|
|
else:
|
|
|
|
# it could be that we have an elif
|
|
|
|
if re.match(limited_pattern, line):
|
|
|
|
limited.append(1)
|
|
|
|
wait_for = status - 1
|
|
|
|
elif re.match(else_pattern, line):
|
|
|
|
limited.append(-limited.pop()) # negate top
|
|
|
|
|
|
|
|
def parse_file(fname):
|
|
|
|
errors = 0
|
|
|
|
with open(fname) as f:
|
|
|
|
lines = f.readlines()
|
|
|
|
type_pattern = r"^.*?->\s*tp_"
|
|
|
|
define_pattern = r"^\s*#\s*define\s+(\w+)"
|
|
|
|
limited = limited_gen()
|
|
|
|
status = next(limited)
|
|
|
|
for nr, line in enumerate(lines):
|
|
|
|
status = limited.send(line)
|
|
|
|
line = line.rstrip()
|
|
|
|
dprint(fname, nr, status, line)
|
|
|
|
if status != -1:
|
|
|
|
if re.match(define_pattern, line):
|
|
|
|
name = re.match(define_pattern, line).group(1)
|
|
|
|
if not name.startswith("_"):
|
|
|
|
# found a candidate, check it!
|
|
|
|
macro = line + "\n"
|
|
|
|
idx = nr
|
|
|
|
while line.endswith("\\"):
|
|
|
|
idx += 1
|
|
|
|
line = lines[idx].rstrip()
|
|
|
|
macro += line + "\n"
|
|
|
|
if re.match(type_pattern, macro, re.DOTALL):
|
|
|
|
# this type field can reach the limited API
|
|
|
|
report(fname, nr + 1, macro)
|
|
|
|
errors += 1
|
|
|
|
return errors
|
|
|
|
|
|
|
|
def report(fname, nr, macro):
|
|
|
|
f = sys.stderr
|
|
|
|
print(fname + ":" + str(nr), file=f)
|
|
|
|
print(macro, file=f)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
p = sys.argv[1] if sys.argv[1:] else "../../Include"
|
|
|
|
errors = parse_headerfiles(p)
|
|
|
|
if errors:
|
|
|
|
# somehow it makes sense to raise a TypeError :-)
|
|
|
|
raise TypeError("These {} locations contradict the limited API."
|
|
|
|
.format(errors))
|