| 
									
										
										
										
											2018-06-09 20:32:25 +02:00
										 |  |  | """
 | 
					
						
							|  |  |  | pep384_macrocheck.py | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-10 08:11:46 -07:00
										 |  |  | This program tries to locate errors in the relevant Python header | 
					
						
							| 
									
										
										
										
											2018-06-09 20:32:25 +02:00
										 |  |  | files where macros access type fields when they are reachable from | 
					
						
							| 
									
										
										
										
											2021-09-10 08:11:46 -07:00
										 |  |  | the limited API. | 
					
						
							| 
									
										
										
										
											2018-06-09 20:32:25 +02: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)) |