From c79d8dc39612728d3c2dd66dc416c033a2298244 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 30 Oct 2021 20:31:52 -0500 Subject: [PATCH] Better error handling for incomplete syntax --- gtkblueprinttool/ast.py | 5 +++-- gtkblueprinttool/ast_utils.py | 12 ++++++++++++ gtkblueprinttool/parse_tree.py | 15 +++++++++++++++ gtkblueprinttool/parser.py | 6 +++--- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/gtkblueprinttool/ast.py b/gtkblueprinttool/ast.py index b0b699a..c9a6ea5 100644 --- a/gtkblueprinttool/ast.py +++ b/gtkblueprinttool/ast.py @@ -37,6 +37,7 @@ class AstNode: self.group = None self.parent = None self.child_nodes = None + self.incomplete = False def __init_subclass__(cls): cls.completers = [] @@ -148,9 +149,9 @@ class GtkDirective(AstNode): else: err = CompileError("Only GTK 4 is supported") 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: - err.hint("Expected `@gtk \"4.0\";`") + err.hint("Expected `using Gtk 4.0;`") raise err def emit_xml(self, xml: XmlEmitter): diff --git a/gtkblueprinttool/ast_utils.py b/gtkblueprinttool/ast_utils.py index c240d00..7946681 100644 --- a/gtkblueprinttool/ast_utils.py +++ b/gtkblueprinttool/ast_utils.py @@ -45,6 +45,11 @@ class Validator: # same message again 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 # from the tokens passed to the decorator, if they have not # already been set @@ -58,6 +63,13 @@ class Validator: # Re-raise the exception 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 # generation phase, might depend on) diff --git a/gtkblueprinttool/parse_tree.py b/gtkblueprinttool/parse_tree.py index 2017777..305db2b 100644 --- a/gtkblueprinttool/parse_tree.py +++ b/gtkblueprinttool/parse_tree.py @@ -66,6 +66,7 @@ class ParseGroup: self.tokens: T.Dict[str, Token] = {} self.start = start self.end = None + self.incomplete = False def add_child(self, child): child_type = child.ast_type.child_type @@ -85,12 +86,16 @@ class ParseGroup: child_type: [child.to_ast() for child in children] for child_type, children in self.children.items() } + try: ast = self.ast_type(**children, **self.keys) + ast.incomplete = self.incomplete ast.group = self ast.child_nodes = [c for child_type in children.values() for c in child_type] + for child in ast.child_nodes: child.parent = ast + return ast except TypeError as e: 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_children = [] self.last_group = None + self.group_incomplete = True self.errors = [] self.warnings = [] @@ -140,12 +146,14 @@ class ParseContext: for child in other.group_children: other.group.add_child(child) other.group.end = other.tokens[other.index - 1].end + other.group.incomplete = other.group_incomplete self.group_children.append(other.group) else: # If the other context had no match group of its own, collect all # its matched values self.group_keys = {**self.group_keys, **other.group_keys} self.group_children += other.group_children + self.group_incomplete |= other.group_incomplete self.index = other.index # 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) 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): """ Skips whitespace and comments. """ @@ -295,6 +309,7 @@ class Statement(ParseNode): return False except CompileError as e: ctx.errors.append(e) + ctx.group return True token = ctx.peek_token() diff --git a/gtkblueprinttool/parser.py b/gtkblueprinttool/parser.py index 3855bfe..8be20b6 100644 --- a/gtkblueprinttool/parser.py +++ b/gtkblueprinttool/parser.py @@ -30,8 +30,8 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]: gtk_directive = Group( ast.GtkDirective, Statement( - 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("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;`)"), UseNumberText("version").expected("a version number for GTK"), ) ) @@ -298,7 +298,7 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]: ui = Group( ast.UI, Sequence( - gtk_directive.err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), + gtk_directive, ZeroOrMore(import_statement), Until(AnyOf( template,