17db96d56Sopenharmony_ci"""
27db96d56Sopenharmony_cipep384_macrocheck.py
37db96d56Sopenharmony_ci
47db96d56Sopenharmony_ciThis program tries to locate errors in the relevant Python header
57db96d56Sopenharmony_cifiles where macros access type fields when they are reachable from
67db96d56Sopenharmony_cithe limited API.
77db96d56Sopenharmony_ci
87db96d56Sopenharmony_ciThe idea is to search macros with the string "->tp_" in it.
97db96d56Sopenharmony_ciWhen the macro name does not begin with an underscore,
107db96d56Sopenharmony_cithen we have found a dormant error.
117db96d56Sopenharmony_ci
127db96d56Sopenharmony_ciChristian Tismer
137db96d56Sopenharmony_ci2018-06-02
147db96d56Sopenharmony_ci"""
157db96d56Sopenharmony_ci
167db96d56Sopenharmony_ciimport sys
177db96d56Sopenharmony_ciimport os
187db96d56Sopenharmony_ciimport re
197db96d56Sopenharmony_ci
207db96d56Sopenharmony_ci
217db96d56Sopenharmony_ciDEBUG = False
227db96d56Sopenharmony_ci
237db96d56Sopenharmony_cidef dprint(*args, **kw):
247db96d56Sopenharmony_ci    if DEBUG:
257db96d56Sopenharmony_ci        print(*args, **kw)
267db96d56Sopenharmony_ci
277db96d56Sopenharmony_cidef parse_headerfiles(startpath):
287db96d56Sopenharmony_ci    """
297db96d56Sopenharmony_ci    Scan all header files which are reachable fronm Python.h
307db96d56Sopenharmony_ci    """
317db96d56Sopenharmony_ci    search = "Python.h"
327db96d56Sopenharmony_ci    name = os.path.join(startpath, search)
337db96d56Sopenharmony_ci    if not os.path.exists(name):
347db96d56Sopenharmony_ci        raise ValueError("file {} was not found in {}\n"
357db96d56Sopenharmony_ci            "Please give the path to Python's include directory."
367db96d56Sopenharmony_ci            .format(search, startpath))
377db96d56Sopenharmony_ci    errors = 0
387db96d56Sopenharmony_ci    with open(name) as python_h:
397db96d56Sopenharmony_ci        while True:
407db96d56Sopenharmony_ci            line = python_h.readline()
417db96d56Sopenharmony_ci            if not line:
427db96d56Sopenharmony_ci                break
437db96d56Sopenharmony_ci            found = re.match(r'^\s*#\s*include\s*"(\w+\.h)"', line)
447db96d56Sopenharmony_ci            if not found:
457db96d56Sopenharmony_ci                continue
467db96d56Sopenharmony_ci            include = found.group(1)
477db96d56Sopenharmony_ci            dprint("Scanning", include)
487db96d56Sopenharmony_ci            name = os.path.join(startpath, include)
497db96d56Sopenharmony_ci            if not os.path.exists(name):
507db96d56Sopenharmony_ci                name = os.path.join(startpath, "../PC", include)
517db96d56Sopenharmony_ci            errors += parse_file(name)
527db96d56Sopenharmony_ci    return errors
537db96d56Sopenharmony_ci
547db96d56Sopenharmony_cidef ifdef_level_gen():
557db96d56Sopenharmony_ci    """
567db96d56Sopenharmony_ci    Scan lines for #ifdef and track the level.
577db96d56Sopenharmony_ci    """
587db96d56Sopenharmony_ci    level = 0
597db96d56Sopenharmony_ci    ifdef_pattern = r"^\s*#\s*if"  # covers ifdef and ifndef as well
607db96d56Sopenharmony_ci    endif_pattern = r"^\s*#\s*endif"
617db96d56Sopenharmony_ci    while True:
627db96d56Sopenharmony_ci        line = yield level
637db96d56Sopenharmony_ci        if re.match(ifdef_pattern, line):
647db96d56Sopenharmony_ci            level += 1
657db96d56Sopenharmony_ci        elif re.match(endif_pattern, line):
667db96d56Sopenharmony_ci            level -= 1
677db96d56Sopenharmony_ci
687db96d56Sopenharmony_cidef limited_gen():
697db96d56Sopenharmony_ci    """
707db96d56Sopenharmony_ci    Scan lines for Py_LIMITED_API yes(1) no(-1) or nothing (0)
717db96d56Sopenharmony_ci    """
727db96d56Sopenharmony_ci    limited = [0]   # nothing
737db96d56Sopenharmony_ci    unlimited_pattern = r"^\s*#\s*ifndef\s+Py_LIMITED_API"
747db96d56Sopenharmony_ci    limited_pattern = "|".join([
757db96d56Sopenharmony_ci        r"^\s*#\s*ifdef\s+Py_LIMITED_API",
767db96d56Sopenharmony_ci        r"^\s*#\s*(el)?if\s+!\s*defined\s*\(\s*Py_LIMITED_API\s*\)\s*\|\|",
777db96d56Sopenharmony_ci        r"^\s*#\s*(el)?if\s+defined\s*\(\s*Py_LIMITED_API"
787db96d56Sopenharmony_ci        ])
797db96d56Sopenharmony_ci    else_pattern =      r"^\s*#\s*else"
807db96d56Sopenharmony_ci    ifdef_level = ifdef_level_gen()
817db96d56Sopenharmony_ci    status = next(ifdef_level)
827db96d56Sopenharmony_ci    wait_for = -1
837db96d56Sopenharmony_ci    while True:
847db96d56Sopenharmony_ci        line = yield limited[-1]
857db96d56Sopenharmony_ci        new_status = ifdef_level.send(line)
867db96d56Sopenharmony_ci        dir = new_status - status
877db96d56Sopenharmony_ci        status = new_status
887db96d56Sopenharmony_ci        if dir == 1:
897db96d56Sopenharmony_ci            if re.match(unlimited_pattern, line):
907db96d56Sopenharmony_ci                limited.append(-1)
917db96d56Sopenharmony_ci                wait_for = status - 1
927db96d56Sopenharmony_ci            elif re.match(limited_pattern, line):
937db96d56Sopenharmony_ci                limited.append(1)
947db96d56Sopenharmony_ci                wait_for = status - 1
957db96d56Sopenharmony_ci        elif dir == -1:
967db96d56Sopenharmony_ci            # this must have been an endif
977db96d56Sopenharmony_ci            if status == wait_for:
987db96d56Sopenharmony_ci                limited.pop()
997db96d56Sopenharmony_ci                wait_for = -1
1007db96d56Sopenharmony_ci        else:
1017db96d56Sopenharmony_ci            # it could be that we have an elif
1027db96d56Sopenharmony_ci            if re.match(limited_pattern, line):
1037db96d56Sopenharmony_ci                limited.append(1)
1047db96d56Sopenharmony_ci                wait_for = status - 1
1057db96d56Sopenharmony_ci            elif re.match(else_pattern, line):
1067db96d56Sopenharmony_ci                limited.append(-limited.pop())  # negate top
1077db96d56Sopenharmony_ci
1087db96d56Sopenharmony_cidef parse_file(fname):
1097db96d56Sopenharmony_ci    errors = 0
1107db96d56Sopenharmony_ci    with open(fname) as f:
1117db96d56Sopenharmony_ci        lines = f.readlines()
1127db96d56Sopenharmony_ci    type_pattern = r"^.*?->\s*tp_"
1137db96d56Sopenharmony_ci    define_pattern = r"^\s*#\s*define\s+(\w+)"
1147db96d56Sopenharmony_ci    limited = limited_gen()
1157db96d56Sopenharmony_ci    status = next(limited)
1167db96d56Sopenharmony_ci    for nr, line in enumerate(lines):
1177db96d56Sopenharmony_ci        status = limited.send(line)
1187db96d56Sopenharmony_ci        line = line.rstrip()
1197db96d56Sopenharmony_ci        dprint(fname, nr, status, line)
1207db96d56Sopenharmony_ci        if status != -1:
1217db96d56Sopenharmony_ci            if re.match(define_pattern, line):
1227db96d56Sopenharmony_ci                name = re.match(define_pattern, line).group(1)
1237db96d56Sopenharmony_ci                if not name.startswith("_"):
1247db96d56Sopenharmony_ci                    # found a candidate, check it!
1257db96d56Sopenharmony_ci                    macro = line + "\n"
1267db96d56Sopenharmony_ci                    idx = nr
1277db96d56Sopenharmony_ci                    while line.endswith("\\"):
1287db96d56Sopenharmony_ci                        idx += 1
1297db96d56Sopenharmony_ci                        line = lines[idx].rstrip()
1307db96d56Sopenharmony_ci                        macro += line + "\n"
1317db96d56Sopenharmony_ci                    if re.match(type_pattern, macro, re.DOTALL):
1327db96d56Sopenharmony_ci                        # this type field can reach the limited API
1337db96d56Sopenharmony_ci                        report(fname, nr + 1, macro)
1347db96d56Sopenharmony_ci                        errors += 1
1357db96d56Sopenharmony_ci    return errors
1367db96d56Sopenharmony_ci
1377db96d56Sopenharmony_cidef report(fname, nr, macro):
1387db96d56Sopenharmony_ci    f = sys.stderr
1397db96d56Sopenharmony_ci    print(fname + ":" + str(nr), file=f)
1407db96d56Sopenharmony_ci    print(macro, file=f)
1417db96d56Sopenharmony_ci
1427db96d56Sopenharmony_ciif __name__ == "__main__":
1437db96d56Sopenharmony_ci    p = sys.argv[1] if sys.argv[1:] else "../../Include"
1447db96d56Sopenharmony_ci    errors = parse_headerfiles(p)
1457db96d56Sopenharmony_ci    if errors:
1467db96d56Sopenharmony_ci        # somehow it makes sense to raise a TypeError :-)
1477db96d56Sopenharmony_ci        raise TypeError("These {} locations contradict the limited API."
1487db96d56Sopenharmony_ci                        .format(errors))
149