mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 11:49:12 +00:00 
			
		
		
		
	Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com>
		
			
				
	
	
		
			148 lines
		
	
	
	
		
			4.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			148 lines
		
	
	
	
		
			4.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""
 | 
						|
pep384_macrocheck.py
 | 
						|
 | 
						|
This program tries to locate errors in the relevant Python header
 | 
						|
files where macros access type fields when they are reachable from
 | 
						|
the limited API.
 | 
						|
 | 
						|
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))
 |