Better error handling for incomplete syntax

This commit is contained in:
James Westman 2021-10-30 20:31:52 -05:00
parent c155ba7b15
commit c79d8dc396
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
4 changed files with 33 additions and 5 deletions

View file

@ -37,6 +37,7 @@ class AstNode:
self.group = None self.group = None
self.parent = None self.parent = None
self.child_nodes = None self.child_nodes = None
self.incomplete = False
def __init_subclass__(cls): def __init_subclass__(cls):
cls.completers = [] cls.completers = []
@ -148,9 +149,9 @@ class GtkDirective(AstNode):
else: else:
err = CompileError("Only GTK 4 is supported") err = CompileError("Only GTK 4 is supported")
if self.version.startswith("4"): if self.version.startswith("4"):
err.hint("Expected the GIR version, not an exact version number. Use `@gtk \"4.0\";`.") err.hint("Expected the GIR version, not an exact version number. Use `using Gtk 4.0;`.")
else: else:
err.hint("Expected `@gtk \"4.0\";`") err.hint("Expected `using Gtk 4.0;`")
raise err raise err
def emit_xml(self, xml: XmlEmitter): def emit_xml(self, xml: XmlEmitter):

View file

@ -45,6 +45,11 @@ class Validator:
# same message again # same message again
instance.__dict__[key + "_err"] = True instance.__dict__[key + "_err"] = True
# If the node is only partially complete, then an error must
# have already been reported at the parsing stage
if instance.incomplete:
return None
# This mess of code sets the error's start and end positions # This mess of code sets the error's start and end positions
# from the tokens passed to the decorator, if they have not # from the tokens passed to the decorator, if they have not
# already been set # already been set
@ -58,6 +63,13 @@ class Validator:
# Re-raise the exception # Re-raise the exception
raise e raise e
except Exception as e:
# If the node is only partially complete, then an error must
# have already been reported at the parsing stage
if instance.incomplete:
return None
else:
raise e
# Return the validation result (which other validators, or the code # Return the validation result (which other validators, or the code
# generation phase, might depend on) # generation phase, might depend on)

View file

@ -66,6 +66,7 @@ class ParseGroup:
self.tokens: T.Dict[str, Token] = {} self.tokens: T.Dict[str, Token] = {}
self.start = start self.start = start
self.end = None self.end = None
self.incomplete = False
def add_child(self, child): def add_child(self, child):
child_type = child.ast_type.child_type child_type = child.ast_type.child_type
@ -85,12 +86,16 @@ class ParseGroup:
child_type: [child.to_ast() for child in children] child_type: [child.to_ast() for child in children]
for child_type, children in self.children.items() for child_type, children in self.children.items()
} }
try: try:
ast = self.ast_type(**children, **self.keys) ast = self.ast_type(**children, **self.keys)
ast.incomplete = self.incomplete
ast.group = self ast.group = self
ast.child_nodes = [c for child_type in children.values() for c in child_type] ast.child_nodes = [c for child_type in children.values() for c in child_type]
for child in ast.child_nodes: for child in ast.child_nodes:
child.parent = ast child.parent = ast
return ast return ast
except TypeError as e: except TypeError as e:
raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.") raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.")
@ -114,6 +119,7 @@ class ParseContext:
self.group_keys = {} self.group_keys = {}
self.group_children = [] self.group_children = []
self.last_group = None self.last_group = None
self.group_incomplete = True
self.errors = [] self.errors = []
self.warnings = [] self.warnings = []
@ -140,12 +146,14 @@ class ParseContext:
for child in other.group_children: for child in other.group_children:
other.group.add_child(child) other.group.add_child(child)
other.group.end = other.tokens[other.index - 1].end other.group.end = other.tokens[other.index - 1].end
other.group.incomplete = other.group_incomplete
self.group_children.append(other.group) self.group_children.append(other.group)
else: else:
# If the other context had no match group of its own, collect all # If the other context had no match group of its own, collect all
# its matched values # its matched values
self.group_keys = {**self.group_keys, **other.group_keys} self.group_keys = {**self.group_keys, **other.group_keys}
self.group_children += other.group_children self.group_children += other.group_children
self.group_incomplete |= other.group_incomplete
self.index = other.index self.index = other.index
# Propagate the last parsed group down the stack so it can be easily # Propagate the last parsed group down the stack so it can be easily
@ -166,6 +174,12 @@ class ParseContext:
assert_true(key not in self.group_keys) assert_true(key not in self.group_keys)
self.group_keys[key] = (value, token) self.group_keys[key] = (value, token)
def set_group_incomplete(self):
""" Marks the current match group as incomplete (it could not be fully
parsed, but the parser recovered). """
assert_true(key not in self.group_keys)
self.group_incomplete = True
def skip(self): def skip(self):
""" Skips whitespace and comments. """ """ Skips whitespace and comments. """
@ -295,6 +309,7 @@ class Statement(ParseNode):
return False return False
except CompileError as e: except CompileError as e:
ctx.errors.append(e) ctx.errors.append(e)
ctx.group
return True return True
token = ctx.peek_token() token = ctx.peek_token()

View file

@ -30,8 +30,8 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
gtk_directive = Group( gtk_directive = Group(
ast.GtkDirective, ast.GtkDirective,
Statement( Statement(
Keyword("using").err("File must start with a \"using gtk\" directive (e.g. `using Gtk 4.0;`)"), Keyword("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
Keyword("Gtk").err("File must start with a \"using gtk\" directive (e.g. `using Gtk 4.0;`)"), Keyword("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
UseNumberText("version").expected("a version number for GTK"), UseNumberText("version").expected("a version number for GTK"),
) )
) )
@ -298,7 +298,7 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
ui = Group( ui = Group(
ast.UI, ast.UI,
Sequence( Sequence(
gtk_directive.err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), gtk_directive,
ZeroOrMore(import_statement), ZeroOrMore(import_statement),
Until(AnyOf( Until(AnyOf(
template, template,