From 08369056d3d06a1d769123bb4f1274264bb7df4d Mon Sep 17 00:00:00 2001 From: baldurk Date: Thu, 2 Apr 2026 13:21:14 +0100 Subject: [PATCH] Add python script for autocompletion parsing * This tries to ast parse source that may contain an error. * We don't try to fully error recover from unlimited problems, but a single statement that is being edited can be tolerated by commenting out lines around it --- .../Code/pyrenderdoc/parse_reflection.py | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 qrenderdoc/Code/pyrenderdoc/parse_reflection.py diff --git a/qrenderdoc/Code/pyrenderdoc/parse_reflection.py b/qrenderdoc/Code/pyrenderdoc/parse_reflection.py new file mode 100644 index 000000000..27007cbe3 --- /dev/null +++ b/qrenderdoc/Code/pyrenderdoc/parse_reflection.py @@ -0,0 +1,140 @@ +import ast +from typing import List, Dict, Any, Tuple, Callable, TypeVar, Optional + + +# comment out all lines starting from a given point, +# to try and make things compile. Stops when it hits +# an indent that looks like the end of the statement +def _commentlines(text, first_comment_line): + lines = text.splitlines() + + indent = len(lines[first_comment_line]) - len(lines[first_comment_line].lstrip()) + lines[first_comment_line] = "#" + lines[first_comment_line] + for i in range(first_comment_line + 1, len(lines)): + if lines[i].strip() == "": + continue + if lines[i].startswith(" " * (indent + 1)): + lines[i] = "#" + lines[i] + continue + break + + return "\n".join(lines) + + +# Main class, reflects a given source text (if it can) and allows +# lookups of the types of expressions as well as auto-completion +# of partial expressions +class PyReflector: + def __init__(self, text: str, starting_globals: Dict[str, Any], debug_types: bool): + # the parsed module, or None if parsing completely failed + self.module: Optional[ast.Module] + + # the text that was actually parsed, including any truncation/commenting needed + # to get it to compile + self.parsed_text: str + + # an error if parsing completely failed + self.parse_error: Optional[str] + + # the starting set of globals to consider the module populated with + self.starting_globals = starting_globals + if self.starting_globals is None: + self.starting_globals = globals() + + # whether or not type-processing should be debugged. Instead of falling back + # to `typing.Any` for unknown types, instead a string bounded by "@@" is returned. + # Mostly for internal use + self.debug_types = debug_types + + # try to parse the text + self._parse_text(text) + + def _parse_text(self, text: str): + # try a simple parse. If there are no syntax errors this will + # succeed. + try: + self.module = ast.parse(text) + self.parsed_text = text + self.parse_error = None + return + except SyntaxError as err: + if err.lineno is None: + raise err + first_comment_line = err.lineno - 1 + + # when encountering an error, comment everything + # from the error line to the next line with same or + # less indent (excluding blank lines) and try again + + mod = _commentlines(text, first_comment_line) + + try: + self.module = ast.parse(mod) + self.parsed_text = mod + self.parse_error = None + return + except SyntaxError as err2: + if err2.lineno is None: + raise err2 + + # if the error has moved to a later line, that suggests the + # original error was reported from some previous line, so we + # should try from an earlier point + # if not, we can't recover this + if err2.lineno <= first_comment_line + 1: + self.module = None + self.parsed_text = mod + self.parse_error = "Error remained after comments" + + # first see where we can truncate to and successfully parse + # (up to 10 lines of non-blank lines truncated, to limit scope) + + lines = text.splitlines() + + trunc_lines = lines[0 : first_comment_line + 1] + removed = 0 + while len(trunc_lines) > 0: + if trunc_lines[-1].strip() == "": + del trunc_lines[-1] + continue + + del trunc_lines[-1] + removed += 1 + + if len(trunc_lines) == 0: + break + + if trunc_lines[-1].rstrip()[-1] == ":": + trunc_lines[-1] += " pass" + lines[len(trunc_lines) - 1] += " pass" + + try: + parsed = ast.parse("\n".join(trunc_lines)) + text = "\n".join(lines) + break + except Exception: + if removed >= 10: + self.module = None + self.parsed_text = "\n".join(trunc_lines) + self.parse_error = "Couldn't backtrack" + + # now we know that trunc_lines parses, + # retry commenting starting from there + mod = _commentlines(text, len(trunc_lines)) + + try: + self.module = ast.parse(mod) + self.parsed_text = mod + self.parse_error = None + return + except Exception: + self.module = None + self.parsed_text = mod + self.parse_error = "Error remained after comments" + + def valid(self): + return self.module is not None + + # get the source string for a given line + def get_line_source(self, line: int): + return self.parsed_text.splitlines()[line - 1]