mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-08-23 23:16:31 +00:00
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
This commit is contained in:
@@ -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]
|
||||
Reference in New Issue
Block a user