mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
Better error handling for incomplete syntax
This commit is contained in:
parent
c155ba7b15
commit
c79d8dc396
4 changed files with 33 additions and 5 deletions
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue