diff --git a/blueprintcompiler/ast.py b/blueprintcompiler/ast.py deleted file mode 100644 index 555e7bf..0000000 --- a/blueprintcompiler/ast.py +++ /dev/null @@ -1,477 +0,0 @@ -# ast.py -# -# Copyright 2021 James Westman -# -# This file is free software; you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This file is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program. If not, see . -# -# SPDX-License-Identifier: LGPL-3.0-or-later - -import typing as T - -from .ast_utils import * -from .errors import CompileError, CompilerBugError, MultipleErrors -from . import gir -from .lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType -from .parse_tree import * -from .tokenizer import Token -from .utils import lazy_prop -from .xml_emitter import XmlEmitter - - -class UI(AstNode): - """ The AST node for the entire file """ - - @property - def gir(self): - gir_ctx = gir.GirContext() - self._gir_errors = [] - - try: - gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace) - except CompileError as e: - e.start = self.children[GtkDirective][0].group.start - e.end = self.children[GtkDirective][0].group.end - self._gir_errors.append(e) - - for i in self.children[Import]: - try: - if i.gir_namespace is not None: - gir_ctx.add_namespace(i.gir_namespace) - except CompileError as e: - e.start = i.group.tokens["namespace"].start - e.end = i.group.tokens["version"].end - self._gir_errors.append(e) - - return gir_ctx - - - @lazy_prop - def objects_by_id(self): - return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None } - - - @validate() - def gir_errors(self): - # make sure gir is loaded - self.gir - if len(self._gir_errors): - raise MultipleErrors(self._gir_errors) - - - @validate() - def at_most_one_template(self): - if len(self.children[Template]) > 1: - for template in self.children[Template][1:]: - raise CompileError( - f"Only one template may be defined per file, but this file contains {len(self.children[Template])}", - template.group.tokens["name"].start, template.group.tokens["name"].end, - ) - - - @validate() - def unique_ids(self): - passed = {} - for obj in self.iterate_children_recursive(): - if obj.tokens["id"] is None: - continue - - if obj.tokens["id"] in passed: - token = obj.group.tokens["id"] - raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end) - passed[obj.tokens["id"]] = obj - - - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("interface") - for x in self.children: - x.emit_xml(xml) - xml.end_tag() - - -class GtkDirective(AstNode): - grammar = Statement( - Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), - Match("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), - UseNumberText("version").expected("a version number for GTK"), - ) - - @validate("version") - def gtk_version(self): - if self.tokens["version"] not in ["4.0"]: - err = CompileError("Only GTK 4 is supported") - if self.tokens["version"].startswith("4"): - err.hint("Expected the GIR version, not an exact version number. Use `using Gtk 4.0;`.") - else: - err.hint("Expected `using Gtk 4.0;`") - raise err - - - @property - def gir_namespace(self): - return gir.get_namespace("Gtk", self.tokens["version"]) - - - def emit_xml(self, xml: XmlEmitter): - xml.put_self_closing("requires", lib="gtk", version=self.tokens["version"]) - - -class Import(AstNode): - grammar = Statement( - "using", - UseIdent("namespace").expected("a GIR namespace"), - UseNumberText("version").expected("a version number"), - ) - - @validate("namespace", "version") - def namespace_exists(self): - gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) - - @property - def gir_namespace(self): - try: - return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) - except CompileError: - return None - - def emit_xml(self, xml): - pass - - -class Object(AstNode): - @validate("namespace") - def gir_ns_exists(self): - if not self.tokens["ignore_gir"]: - self.root.gir.validate_ns(self.tokens["namespace"]) - - @validate("class_name") - def gir_class_exists(self): - if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None: - self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"]) - - @property - def gir_ns(self): - if not self.tokens["ignore_gir"]: - return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") - - @property - def gir_class(self): - if self.tokens["class_name"] and not self.tokens["ignore_gir"]: - return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"]) - - - @docs("namespace") - def namespace_docs(self): - if ns := self.root.gir.namespaces.get(self.tokens["namespace"]): - return ns.doc - - - @docs("class_name") - def class_docs(self): - if self.gir_class: - return self.gir_class.doc - - - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("object", **{ - "class": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"], - "id": self.tokens["id"], - }) - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - - -class Template(Object): - def emit_xml(self, xml: XmlEmitter): - if self.gir_class: - parent = self.gir_class.glib_type_name - elif self.tokens["class_name"]: - parent = self.tokens["class_name"] - else: - parent = None - xml.start_tag("template", **{"class": self.tokens["name"]}, parent=parent) - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - - -class Child(AstNode): - def emit_xml(self, xml: XmlEmitter): - child_type = internal_child = None - if self.tokens["internal_child"]: - internal_child = self.tokens["child_type"] - else: - child_type = self.tokens["child_type"] - xml.start_tag("child", type=child_type, internal_child=internal_child) - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - - -class ObjectContent(AstNode): - @property - def gir_class(self): - return self.parent.gir_class - - # @validate() - # def only_one_style_class(self): - # if len(self.children[Style]) > 1: - # raise CompileError( - # f"Only one style directive allowed per object, but this object contains {len(self.children[Style])}", - # start=self.children[Style][1].group.start, - # ) - - def emit_xml(self, xml: XmlEmitter): - for x in self.children: - x.emit_xml(xml) - - -class Property(AstNode): - @property - def gir_class(self): - return self.parent.parent.gir_class - - - @property - def gir_property(self): - if self.gir_class is not None: - return self.gir_class.properties.get(self.tokens["name"]) - - - @property - def value_type(self): - if self.gir_property is not None: - return self.gir_property.type - - - @validate("name") - def property_exists(self): - if self.gir_class is None: - # Objects that we have no gir data on should not be validated - # This happens for classes defined by the app itself - return - - if isinstance(self.parent.parent, Template): - # If the property is part of a template, it might be defined by - # the application and thus not in gir - return - - if self.gir_property is None: - raise CompileError( - f"Class {self.gir_class.full_name} does not contain a property called {self.tokens['name']}", - did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()) - ) - - - @validate() - def obj_property_type(self): - if len(self.children[Object]) == 0: - return - - object = self.children[Object][0] - type = self.value_type - if object and type and object.gir_class and not object.gir_class.assignable_to(type): - raise CompileError( - f"Cannot assign {object.gir_class.full_name} to {type.full_name}" - ) - - - @docs("name") - def property_docs(self): - if self.gir_property is not None: - return self.gir_property.doc - - - def emit_xml(self, xml: XmlEmitter): - values = self.children[Value] - value = values[0] if len(values) == 1 else None - - bind_flags = [] - if self.tokens["sync_create"]: - bind_flags.append("sync-create") - if self.tokens["inverted"]: - bind_flags.append("invert-boolean") - if self.tokens["bidirectional"]: - bind_flags.append("bidirectional") - bind_flags_str = "|".join(bind_flags) or None - - props = { - "name": self.tokens["name"], - "bind-source": self.tokens["bind_source"], - "bind-property": self.tokens["bind_property"], - "bind-flags": bind_flags_str, - } - - if isinstance(value, TranslatedStringValue): - props = { **props, **value.attrs } - - if len(self.children[Object]) == 1: - xml.start_tag("property", **props) - self.children[Object][0].emit_xml(xml) - xml.end_tag() - elif value is None: - xml.put_self_closing("property", **props) - else: - xml.start_tag("property", **props) - value.emit_xml(xml) - xml.end_tag() - - -class Value(ast.AstNode): - pass - - -class TranslatedStringValue(Value): - @property - def attrs(self): - attrs = { "translatable": "true" } - if "context" in self.tokens: - attrs["context"] = self.tokens["context"] - return attrs - - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.tokens["value"]) - - -class LiteralValue(Value): - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.tokens["value"]) - - @validate() - def validate_for_type(self): - type = self.parent.value_type - if isinstance(type, gir.IntType): - try: - int(self.tokens["value"]) - except: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer") - - elif isinstance(type, gir.UIntType): - try: - int(self.tokens["value"]) - if int(self.tokens["value"]) < 0: - raise Exception() - except: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer") - - elif isinstance(type, gir.FloatType): - try: - float(self.tokens["value"]) - except: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to float") - - elif isinstance(type, gir.StringType): - pass - - elif isinstance(type, gir.Class) or isinstance(type, gir.Interface): - parseable_types = [ - "Gdk.Paintable", - "Gdk.Texture", - "Gdk.Pixbuf", - "GLib.File", - "Gtk.ShortcutTrigger", - "Gtk.ShortcutAction", - ] - if type.full_name not in parseable_types: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}") - - elif type is not None: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}") - - -class Flag(AstNode): - pass - -class FlagsValue(Value): - def emit_xml(self, xml: XmlEmitter): - xml.put_text("|".join([flag.tokens["value"] for flag in self.children[Flag]])) - - -class IdentValue(Value): - def emit_xml(self, xml: XmlEmitter): - if isinstance(self.parent.value_type, gir.Enumeration): - xml.put_text(self.parent.value_type.members[self.tokens["value"]].nick) - else: - xml.put_text(self.tokens["value"]) - - @validate() - def validate_for_type(self): - type = self.parent.value_type - - if isinstance(type, gir.Enumeration): - if self.tokens["value"] not in type.members: - raise CompileError( - f"{self.tokens['value']} is not a member of {type.full_name}", - did_you_mean=(self.tokens['value'], type.members.keys()), - ) - - elif isinstance(type, gir.BoolType): - if self.tokens["value"] not in ["true", "false"]: - raise CompileError( - f"Expected 'true' or 'false' for boolean value", - did_you_mean=(self.tokens['value'], ["true", "false"]), - ) - - elif type is not None: - object = self.root.objects_by_id.get(self.tokens["value"]) - if object is None: - raise CompileError( - f"Could not find object with ID {self.tokens['value']}", - did_you_mean=(self.tokens['value'], self.root.objects_by_id.keys()), - ) - elif object.gir_class and not object.gir_class.assignable_to(type): - raise CompileError( - f"Cannot assign {object.gir_class.full_name} to {type.full_name}" - ) - - - @docs() - def docs(self): - type = self.parent.value_type - if isinstance(type, gir.Enumeration): - if member := type.members.get(self.tokens["value"]): - return member.doc - else: - return type.doc - elif isinstance(type, gir.GirNode): - return type.doc - - - def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: - if isinstance(self.parent.value_type, gir.Enumeration): - token = self.group.tokens["value"] - yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) - - -class BaseAttribute(AstNode): - """ A helper class for attribute syntax of the form `name: literal_value;`""" - - tag_name: str = "" - attr_name: str = "name" - - def emit_xml(self, xml: XmlEmitter): - value = self.children[Value][0] - attrs = { self.attr_name: self.tokens["name"] } - - if isinstance(value, TranslatedStringValue): - attrs = { **attrs, **value.attrs } - - xml.start_tag(self.tag_name, **attrs) - value.emit_xml(xml) - xml.end_tag() - - -class BaseTypedAttribute(BaseAttribute): - """ A BaseAttribute whose parent has a value_type property that can assist - in validation. """ diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 33ccf61..62f0823 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -20,7 +20,6 @@ import typing as T from collections import ChainMap, defaultdict -from . import ast from .errors import * from .lsp_utils import SemanticToken from .utils import lazy_prop @@ -72,12 +71,6 @@ class AstNode: else: return self.parent.parent_by_type(type) - def validate_parent_type(self, ns: str, name: str, err_msg: str): - parent = self.root.gir.get_type(name, ns) - container_type = self.parent_by_type(ast.Object).gir_class - if container_type and not container_type.assignable_to(parent): - raise CompileError(f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}") - @lazy_prop def errors(self): return list(self._get_errors()) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index facbfd4..25f0301 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -19,7 +19,6 @@ import typing as T -from . import ast from . import gir from .completions_utils import * from .lsp_utils import Completion, CompletionItemKind diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 5666984..b3c787a 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -20,7 +20,6 @@ import typing as T -from . import ast from .tokenizer import Token, TokenType from .lsp_utils import Completion diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 91407b5..61af8e7 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,6 +1,8 @@ """ Contains all the syntax beyond basic objects, properties, signal, and templates. """ +from .gobject_object import Object +from .gobject_property import Property from .gobject_signal import Signal from .gtk_a11y import A11y from .gtk_combo_box_text import Items @@ -10,12 +12,36 @@ from .gtk_menu import menu from .gtk_size_group import Widgets from .gtk_string_list import Strings from .gtk_styles import Styles +from .gtkbuilder_child import Child +from .gtkbuilder_template import Template +from .ui import UI +from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue from .common import * -OBJECT_HOOKS.children = [menu] +OBJECT_HOOKS.children = [ + menu, + Object, +] OBJECT_CONTENT_HOOKS.children = [ - Signal, A11y, Styles, Layout, mime_types, patterns, suffixes, Widgets, Items, + Signal, + Property, + A11y, + Styles, + Layout, + mime_types, + patterns, + suffixes, + Widgets, + Items, Strings, + Child, +] + +VALUE_HOOKS.children = [ + TranslatedStringValue, + FlagsValue, + IdentValue, + LiteralValue, ] diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py new file mode 100644 index 0000000..49707ab --- /dev/null +++ b/blueprintcompiler/language/attributes.py @@ -0,0 +1,45 @@ +# attributes.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .values import Value, TranslatedStringValue +from .common import * + + +class BaseAttribute(AstNode): + """ A helper class for attribute syntax of the form `name: literal_value;`""" + + tag_name: str = "" + attr_name: str = "name" + + def emit_xml(self, xml: XmlEmitter): + value = self.children[Value][0] + attrs = { self.attr_name: self.tokens["name"] } + + if isinstance(value, TranslatedStringValue): + attrs = { **attrs, **value.attrs } + + xml.start_tag(self.tag_name, **attrs) + value.emit_xml(xml) + xml.end_tag() + + +class BaseTypedAttribute(BaseAttribute): + """ A BaseAttribute whose parent has a value_type property that can assist + in validation. """ diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index e6f9cca..ae6ffe1 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -18,11 +18,11 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from ..ast import BaseTypedAttribute, Value, Template +from .. import gir from ..ast_utils import AstNode, validate, docs from ..completions_utils import * from ..gir import StringType, BoolType, IntType, FloatType, GirType -from ..lsp_utils import Completion, CompletionItemKind +from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * from ..parser_utils import * from ..xml_emitter import XmlEmitter @@ -30,3 +30,4 @@ from ..xml_emitter import XmlEmitter OBJECT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf() +VALUE_HOOKS = AnyOf() diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py new file mode 100644 index 0000000..e7cf83e --- /dev/null +++ b/blueprintcompiler/language/gobject_object.py @@ -0,0 +1,97 @@ +# gobject_object.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * + + +class ObjectContent(AstNode): + grammar = ["{", Until(OBJECT_CONTENT_HOOKS, "}")] + + @property + def gir_class(self): + return self.parent.gir_class + + # @validate() + # def only_one_style_class(self): + # if len(self.children[Style]) > 1: + # raise CompileError( + # f"Only one style directive allowed per object, but this object contains {len(self.children[Style])}", + # start=self.children[Style][1].group.start, + # ) + + def emit_xml(self, xml: XmlEmitter): + for x in self.children: + x.emit_xml(xml) + +class Object(AstNode): + grammar = Sequence( + class_name, + Optional(UseIdent("id")), + ObjectContent, + ) + + @validate("namespace") + def gir_ns_exists(self): + if not self.tokens["ignore_gir"]: + self.root.gir.validate_ns(self.tokens["namespace"]) + + @validate("class_name") + def gir_class_exists(self): + if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None: + self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"]) + + @property + def gir_ns(self): + if not self.tokens["ignore_gir"]: + return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") + + @property + def gir_class(self): + if self.tokens["class_name"] and not self.tokens["ignore_gir"]: + return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"]) + + + @docs("namespace") + def namespace_docs(self): + if ns := self.root.gir.namespaces.get(self.tokens["namespace"]): + return ns.doc + + + @docs("class_name") + def class_docs(self): + if self.gir_class: + return self.gir_class.doc + + + def emit_xml(self, xml: XmlEmitter): + xml.start_tag("object", **{ + "class": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"], + "id": self.tokens["id"], + }) + for child in self.children: + child.emit_xml(xml) + xml.end_tag() + + +def validate_parent_type(node, ns: str, name: str, err_msg: str): + parent = node.root.gir.get_type(name, ns) + container_type = node.parent_by_type(Object).gir_class + if container_type and not container_type.assignable_to(parent): + raise CompileError(f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}") diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py new file mode 100644 index 0000000..bf63820 --- /dev/null +++ b/blueprintcompiler/language/gobject_property.py @@ -0,0 +1,139 @@ +# gobject_property.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .gobject_object import Object +from .gtkbuilder_template import Template +from .values import Value, TranslatedStringValue +from .common import * + + +class Property(AstNode): + grammar = AnyOf( + Statement( + UseIdent("name"), + ":", + "bind", + UseIdent("bind_source").expected("the ID of a source object to bind from"), + ".", + UseIdent("bind_property").expected("a property name to bind from"), + ZeroOrMore(AnyOf( + ["sync-create", UseLiteral("sync_create", True)], + ["inverted", UseLiteral("inverted", True)], + ["bidirectional", UseLiteral("bidirectional", True)], + )), + ), + Statement( + UseIdent("name"), + ":", + AnyOf( + OBJECT_HOOKS, + VALUE_HOOKS, + ).expected("a value"), + ), + ) + + @property + def gir_class(self): + return self.parent.parent.gir_class + + + @property + def gir_property(self): + if self.gir_class is not None: + return self.gir_class.properties.get(self.tokens["name"]) + + + @property + def value_type(self): + if self.gir_property is not None: + return self.gir_property.type + + + @validate("name") + def property_exists(self): + if self.gir_class is None: + # Objects that we have no gir data on should not be validated + # This happens for classes defined by the app itself + return + + if isinstance(self.parent.parent, Template): + # If the property is part of a template, it might be defined by + # the application and thus not in gir + return + + if self.gir_property is None: + raise CompileError( + f"Class {self.gir_class.full_name} does not contain a property called {self.tokens['name']}", + did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()) + ) + + + @validate() + def obj_property_type(self): + if len(self.children[Object]) == 0: + return + + object = self.children[Object][0] + type = self.value_type + if object and type and object.gir_class and not object.gir_class.assignable_to(type): + raise CompileError( + f"Cannot assign {object.gir_class.full_name} to {type.full_name}" + ) + + + @docs("name") + def property_docs(self): + if self.gir_property is not None: + return self.gir_property.doc + + + def emit_xml(self, xml: XmlEmitter): + values = self.children[Value] + value = values[0] if len(values) == 1 else None + + bind_flags = [] + if self.tokens["sync_create"]: + bind_flags.append("sync-create") + if self.tokens["inverted"]: + bind_flags.append("invert-boolean") + if self.tokens["bidirectional"]: + bind_flags.append("bidirectional") + bind_flags_str = "|".join(bind_flags) or None + + props = { + "name": self.tokens["name"], + "bind-source": self.tokens["bind_source"], + "bind-property": self.tokens["bind_property"], + "bind-flags": bind_flags_str, + } + + if isinstance(value, TranslatedStringValue): + props = { **props, **value.attrs } + + if len(self.children[Object]) == 1: + xml.start_tag("property", **props) + self.children[Object][0].emit_xml(xml) + xml.end_tag() + elif value is None: + xml.put_self_closing("property", **props) + else: + xml.start_tag("property", **props) + value.emit_xml(xml) + xml.end_tag() diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index b2ca164..453fb1f 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from .gtkbuilder_template import Template from .common import * diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index cbf69fa..b676126 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -17,14 +17,10 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from ..ast import BaseTypedAttribute, Value -from ..ast_utils import AstNode, validate, docs -from ..completions_utils import * -from ..gir import StringType, BoolType, IntType, FloatType, GirType -from ..lsp_utils import Completion, CompletionItemKind -from ..parse_tree import * -from ..parser_utils import * -from ..xml_emitter import XmlEmitter +from .gobject_object import ObjectContent, validate_parent_type +from .attributes import BaseTypedAttribute +from .values import Value +from .common import * def get_property_types(gir): @@ -109,7 +105,7 @@ class A11yProperty(BaseTypedAttribute): grammar = Statement( UseIdent("name"), ":", - value.expected("a value"), + VALUE_HOOKS.expected("a value"), ) @property @@ -153,7 +149,7 @@ class A11y(AstNode): @validate("accessibility") def container_is_widget(self): - self.validate_parent_type("Gtk", "Widget", "accessibility properties") + validate_parent_type(self, "Gtk", "Widget", "accessibility properties") def emit_xml(self, xml: XmlEmitter): @@ -164,7 +160,7 @@ class A11y(AstNode): @completer( - applies_in=[ast.ObjectContent], + applies_in=[ObjectContent], matches=new_statement_patterns, ) def a11y_completer(ast_node, match_variables): diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index d77bc5c..512a245 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -18,14 +18,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from ..ast import BaseTypedAttribute -from ..ast_utils import AstNode, validate -from ..completions_utils import * -from ..gir import StringType -from ..lsp_utils import Completion, CompletionItemKind -from ..parse_tree import * -from ..parser_utils import * -from ..xml_emitter import XmlEmitter +from .attributes import BaseTypedAttribute +from .gobject_object import ObjectContent, validate_parent_type +from .common import * class Item(BaseTypedAttribute): @@ -44,7 +39,7 @@ item = Group( UseIdent("name"), ":", ]), - value, + VALUE_HOOKS, ] ) @@ -59,7 +54,7 @@ class Items(AstNode): @validate("items") def container_is_combo_box_text(self): - self.validate_parent_type("Gtk", "ComboBoxText", "combo box items") + validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items") def emit_xml(self, xml: XmlEmitter): @@ -70,7 +65,7 @@ class Items(AstNode): @completer( - applies_in=[ast.ObjectContent], + applies_in=[ObjectContent], applies_in_subclass=("Gtk", "ComboBoxText"), matches=new_statement_patterns, ) diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 116431d..a943573 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -18,19 +18,14 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .. import ast -from ..ast_utils import AstNode, validate -from ..completions_utils import * -from ..lsp_utils import Completion, CompletionItemKind -from ..parse_tree import * -from ..parser_utils import * -from ..xml_emitter import XmlEmitter +from .gobject_object import ObjectContent, validate_parent_type +from .common import * class Filters(AstNode): @validate() def container_is_file_filter(self): - self.validate_parent_type("Gtk", "FileFilter", "file filter properties") + validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") def emit_xml(self, xml: XmlEmitter): xml.start_tag(self.tokens["tag_name"]) @@ -74,7 +69,7 @@ suffixes = create_node("suffixes", "suffix") @completer( - applies_in=[ast.ObjectContent], + applies_in=[ObjectContent], applies_in_subclass=("Gtk", "FileFilter"), matches=new_statement_patterns, ) diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 8289a29..f56b8a1 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -18,13 +18,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from ..ast import BaseAttribute -from ..ast_utils import AstNode, validate -from ..completions_utils import * -from ..lsp_utils import Completion, CompletionItemKind -from ..parse_tree import * -from ..parser_utils import * -from ..xml_emitter import XmlEmitter +from .attributes import BaseAttribute +from .gobject_object import ObjectContent, validate_parent_type +from .common import * class LayoutProperty(BaseAttribute): @@ -41,7 +37,7 @@ layout_prop = Group( Statement( UseIdent("name"), ":", - value.expected("a value"), + VALUE_HOOKS.expected("a value"), ) ) @@ -55,7 +51,7 @@ class Layout(AstNode): @validate("layout") def container_is_widget(self): - self.validate_parent_type("Gtk", "Widget", "layout properties") + validate_parent_type(self, "Gtk", "Widget", "layout properties") def emit_xml(self, xml: XmlEmitter): @@ -66,7 +62,7 @@ class Layout(AstNode): @completer( - applies_in=[ast.ObjectContent], + applies_in=[ObjectContent], applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index bbb8361..919b860 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -18,13 +18,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from ..ast import BaseAttribute -from ..ast_utils import AstNode -from ..completions_utils import * -from ..lsp_utils import Completion, CompletionItemKind -from ..parse_tree import * -from ..parser_utils import * -from ..xml_emitter import XmlEmitter +from .attributes import BaseAttribute +from .gobject_object import ObjectContent +from .ui import UI +from .common import * class Menu(AstNode): @@ -74,7 +71,7 @@ menu_attribute = Group( [ UseIdent("name"), ":", - value.expected("a value"), + VALUE_HOOKS.expected("a value"), Match(";").expected(), ] ) @@ -98,20 +95,20 @@ menu_item_shorthand = Group( "(", Group( MenuAttribute, - [UseLiteral("name", "label"), value], + [UseLiteral("name", "label"), VALUE_HOOKS], ), Optional([ ",", Optional([ Group( MenuAttribute, - [UseLiteral("name", "action"), value], + [UseLiteral("name", "action"), VALUE_HOOKS], ), Optional([ ",", Group( MenuAttribute, - [UseLiteral("name", "icon"), value], + [UseLiteral("name", "icon"), VALUE_HOOKS], ), ]) ]) @@ -143,7 +140,7 @@ menu = Group( @completer( - applies_in=[ast.UI], + applies_in=[UI], matches=new_statement_patterns, ) def menu_completer(ast_node, match_variables): diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index ea9a95c..54e3d89 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -18,13 +18,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .. import ast -from ..ast_utils import AstNode, validate -from ..completions_utils import * -from ..lsp_utils import Completion, CompletionItemKind -from ..parse_tree import * -from ..parser_utils import * -from ..xml_emitter import XmlEmitter +from .gobject_object import ObjectContent, validate_parent_type +from .common import * class Widget(AstNode): @@ -58,7 +53,7 @@ class Widgets(AstNode): @validate("widgets") def container_is_size_group(self): - self.validate_parent_type("Gtk", "SizeGroup", "size group properties") + validate_parent_type(self, "Gtk", "SizeGroup", "size group properties") def emit_xml(self, xml: XmlEmitter): xml.start_tag("widgets") @@ -68,7 +63,7 @@ class Widgets(AstNode): @completer( - applies_in=[ast.ObjectContent], + applies_in=[ObjectContent], applies_in_subclass=("Gtk", "SizeGroup"), matches=new_statement_patterns, ) diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index db19a8c..d79b4ed 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -18,18 +18,14 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from ..ast import BaseTypedAttribute, Value, TranslatedStringValue -from ..ast_utils import AstNode, validate -from ..completions_utils import * -from ..gir import StringType -from ..lsp_utils import Completion, CompletionItemKind -from ..parse_tree import * -from ..parser_utils import * -from ..xml_emitter import XmlEmitter +from .attributes import BaseTypedAttribute +from .gobject_object import ObjectContent, validate_parent_type +from .values import Value, TranslatedStringValue +from .common import * class Item(AstNode): - grammar = value + grammar = VALUE_HOOKS @property def value_type(self): @@ -53,7 +49,7 @@ class Strings(AstNode): @validate("items") def container_is_string_list(self): - self.validate_parent_type("Gtk", "StringList", "StringList items") + validate_parent_type(self, "Gtk", "StringList", "StringList items") def emit_xml(self, xml: XmlEmitter): @@ -64,7 +60,7 @@ class Strings(AstNode): @completer( - applies_in=[ast.ObjectContent], + applies_in=[ObjectContent], applies_in_subclass=("Gtk", "StringList"), matches=new_statement_patterns, ) diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 193f01f..23c086f 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -18,13 +18,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .. import ast -from ..ast_utils import AstNode, validate -from ..completions_utils import * -from ..lsp_utils import Completion, CompletionItemKind -from ..parse_tree import * -from ..parser_utils import * -from ..xml_emitter import XmlEmitter +from .gobject_object import ObjectContent, validate_parent_type +from .common import * class StyleClass(AstNode): @@ -44,7 +39,7 @@ class Styles(AstNode): @validate("styles") def container_is_widget(self): - self.validate_parent_type("Gtk", "Widget", "style classes") + validate_parent_type(self, "Gtk", "Widget", "style classes") def emit_xml(self, xml: XmlEmitter): xml.start_tag("style") @@ -54,7 +49,7 @@ class Styles(AstNode): @completer( - applies_in=[ast.ObjectContent], + applies_in=[ObjectContent], applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py new file mode 100644 index 0000000..eb69a1d --- /dev/null +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -0,0 +1,45 @@ +# gtkbuilder_child.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .gobject_object import Object +from .common import * + + +class Child(AstNode): + grammar = [ + Optional([ + "[", + Optional(["internal-child", UseLiteral("internal_child", True)]), + UseIdent("child_type").expected("a child type"), + "]", + ]), + Object, + ] + + def emit_xml(self, xml: XmlEmitter): + child_type = internal_child = None + if self.tokens["internal_child"]: + internal_child = self.tokens["child_type"] + else: + child_type = self.tokens["child_type"] + xml.start_tag("child", type=child_type, internal_child=internal_child) + for child in self.children: + child.emit_xml(xml) + xml.end_tag() diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py new file mode 100644 index 0000000..5a28fa9 --- /dev/null +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -0,0 +1,46 @@ +# gtkbuilder_template.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .gobject_object import Object, ObjectContent +from .common import * + + +class Template(Object): + grammar = [ + "template", + UseIdent("name").expected("template class name"), + Optional([ + Match(":"), + class_name.expected("parent class"), + ]), + ObjectContent, + ] + + def emit_xml(self, xml: XmlEmitter): + if self.gir_class: + parent = self.gir_class.glib_type_name + elif self.tokens["class_name"]: + parent = self.tokens["class_name"] + else: + parent = None + xml.start_tag("template", **{"class": self.tokens["name"]}, parent=parent) + for child in self.children: + child.emit_xml(xml) + xml.end_tag() diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index f3ddab7..15ba8e4 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from .. import gir from .common import * diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py new file mode 100644 index 0000000..e7020c9 --- /dev/null +++ b/blueprintcompiler/language/ui.py @@ -0,0 +1,103 @@ +# ui.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .. import gir +from .imports import GtkDirective, Import +from .gtkbuilder_template import Template +from .common import * + + +class UI(AstNode): + """ The AST node for the entire file """ + + grammar = [ + GtkDirective, + ZeroOrMore(Import), + Until(AnyOf( + Template, + OBJECT_HOOKS, + ), Eof()), + ] + + @property + def gir(self): + gir_ctx = gir.GirContext() + self._gir_errors = [] + + try: + gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace) + except CompileError as e: + e.start = self.children[GtkDirective][0].group.start + e.end = self.children[GtkDirective][0].group.end + self._gir_errors.append(e) + + for i in self.children[Import]: + try: + if i.gir_namespace is not None: + gir_ctx.add_namespace(i.gir_namespace) + except CompileError as e: + e.start = i.group.tokens["namespace"].start + e.end = i.group.tokens["version"].end + self._gir_errors.append(e) + + return gir_ctx + + + @property + def objects_by_id(self): + return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None } + + + @validate() + def gir_errors(self): + # make sure gir is loaded + self.gir + if len(self._gir_errors): + raise MultipleErrors(self._gir_errors) + + + @validate() + def at_most_one_template(self): + if len(self.children[Template]) > 1: + for template in self.children[Template][1:]: + raise CompileError( + f"Only one template may be defined per file, but this file contains {len(self.children[Template])}", + template.group.tokens["name"].start, template.group.tokens["name"].end, + ) + + + @validate() + def unique_ids(self): + passed = {} + for obj in self.iterate_children_recursive(): + if obj.tokens["id"] is None: + continue + + if obj.tokens["id"] in passed: + token = obj.group.tokens["id"] + raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end) + passed[obj.tokens["id"]] = obj + + + def emit_xml(self, xml: XmlEmitter): + xml.start_tag("interface") + for x in self.children: + x.emit_xml(xml) + xml.end_tag() diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py new file mode 100644 index 0000000..74d06f1 --- /dev/null +++ b/blueprintcompiler/language/values.py @@ -0,0 +1,175 @@ +# values.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * + + +class Value(AstNode): + pass + + +class TranslatedStringValue(Value): + grammar = AnyOf( + [ + "_", + "(", + UseQuoted("value").expected("a quoted string"), + Match(")").expected(), + ], + [ + "C_", + "(", + UseQuoted("context").expected("a quoted string"), + ",", + UseQuoted("value").expected("a quoted string"), + Optional(","), + Match(")").expected(), + ], + ) + + @property + def attrs(self): + attrs = { "translatable": "true" } + if "context" in self.tokens: + attrs["context"] = self.tokens["context"] + return attrs + + def emit_xml(self, xml: XmlEmitter): + xml.put_text(self.tokens["value"]) + + +class LiteralValue(Value): + grammar = AnyOf( + UseNumber("value"), + UseQuoted("value"), + ) + + def emit_xml(self, xml: XmlEmitter): + xml.put_text(self.tokens["value"]) + + @validate() + def validate_for_type(self): + type = self.parent.value_type + if isinstance(type, gir.IntType): + try: + int(self.tokens["value"]) + except: + raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer") + + elif isinstance(type, gir.UIntType): + try: + int(self.tokens["value"]) + if int(self.tokens["value"]) < 0: + raise Exception() + except: + raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer") + + elif isinstance(type, gir.FloatType): + try: + float(self.tokens["value"]) + except: + raise CompileError(f"Cannot convert {self.group.tokens['value']} to float") + + elif isinstance(type, gir.StringType): + pass + + elif isinstance(type, gir.Class) or isinstance(type, gir.Interface): + parseable_types = [ + "Gdk.Paintable", + "Gdk.Texture", + "Gdk.Pixbuf", + "GLib.File", + "Gtk.ShortcutTrigger", + "Gtk.ShortcutAction", + ] + if type.full_name not in parseable_types: + raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}") + + elif type is not None: + raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}") + + +class Flag(AstNode): + grammar = UseIdent("value") + +class FlagsValue(Value): + grammar = [Flag, "|", Delimited(Flag, "|")] + + def emit_xml(self, xml: XmlEmitter): + xml.put_text("|".join([flag.tokens["value"] for flag in self.children[Flag]])) + + +class IdentValue(Value): + grammar = UseIdent("value") + + def emit_xml(self, xml: XmlEmitter): + if isinstance(self.parent.value_type, gir.Enumeration): + xml.put_text(self.parent.value_type.members[self.tokens["value"]].nick) + else: + xml.put_text(self.tokens["value"]) + + @validate() + def validate_for_type(self): + type = self.parent.value_type + + if isinstance(type, gir.Enumeration): + if self.tokens["value"] not in type.members: + raise CompileError( + f"{self.tokens['value']} is not a member of {type.full_name}", + did_you_mean=(self.tokens['value'], type.members.keys()), + ) + + elif isinstance(type, gir.BoolType): + if self.tokens["value"] not in ["true", "false"]: + raise CompileError( + f"Expected 'true' or 'false' for boolean value", + did_you_mean=(self.tokens['value'], ["true", "false"]), + ) + + elif type is not None: + object = self.root.objects_by_id.get(self.tokens["value"]) + if object is None: + raise CompileError( + f"Could not find object with ID {self.tokens['value']}", + did_you_mean=(self.tokens['value'], self.root.objects_by_id.keys()), + ) + elif object.gir_class and not object.gir_class.assignable_to(type): + raise CompileError( + f"Cannot assign {object.gir_class.full_name} to {type.full_name}" + ) + + + @docs() + def docs(self): + type = self.parent.value_type + if isinstance(type, gir.Enumeration): + if member := type.members.get(self.tokens["value"]): + return member.doc + else: + return type.doc + elif isinstance(type, gir.GirNode): + return type.doc + + + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + if isinstance(self.parent.value_type, gir.Enumeration): + token = self.group.tokens["value"] + yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) + diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 10fac2d..9096f65 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -530,5 +530,7 @@ def to_parse_node(value) -> ParseNode: return Sequence(*value) elif isinstance(value, type) and hasattr(value, "grammar"): return Group(value, getattr(value, "grammar")) - else: + elif isinstance(value, ParseNode): return value + else: + raise CompilerBugError() diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index ede452c..1b40141 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -18,113 +18,18 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from . import ast from .errors import MultipleErrors from .parse_tree import * from .parser_utils import * from .tokenizer import TokenType -from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS +from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI -def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]: +def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors]]: """ Parses a list of tokens into an abstract syntax tree. """ - object = Group( - ast.Object, - None - ) - - property = Group( - ast.Property, - Statement( - UseIdent("name"), - ":", - AnyOf( - OBJECT_HOOKS, - object, - value, - ).expected("a value"), - ) - ) - - binding = Group( - ast.Property, - Statement( - UseIdent("name"), - ":", - "bind", - UseIdent("bind_source").expected("the ID of a source object to bind from"), - ".", - UseIdent("bind_property").expected("a property name to bind from"), - ZeroOrMore(AnyOf( - ["sync-create", UseLiteral("sync_create", True)], - ["inverted", UseLiteral("inverted", True)], - ["bidirectional", UseLiteral("bidirectional", True)], - )), - ) - ) - - child = Group( - ast.Child, - [ - Optional([ - "[", - Optional(["internal-child", UseLiteral("internal_child", True)]), - UseIdent("child_type").expected("a child type"), - "]", - ]), - object, - ] - ) - - object_content = Group( - ast.ObjectContent, - [ - "{", - Until(AnyOf( - OBJECT_CONTENT_HOOKS, - binding, - property, - child, - ), "}"), - ] - ) - - # work around the recursive reference - object.child = Sequence( - class_name, - Optional(UseIdent("id")), - object_content, - ) - - template = Group( - ast.Template, - [ - "template", - UseIdent("name").expected("template class name"), - Optional([ - Match(":"), - class_name.expected("parent class"), - ]), - object_content.expected("block"), - ] - ) - - ui = Group( - ast.UI, - [ - ast.GtkDirective, - ZeroOrMore(ast.Import), - Until(AnyOf( - OBJECT_HOOKS, - template, - object, - ), Eof()), - ] - ) - ctx = ParseContext(tokens) - ui.parse(ctx) + AnyOf(UI).parse(ctx) ast_node = ctx.last_group.to_ast() if ctx.last_group else None errors = MultipleErrors(ctx.errors) if len(ctx.errors) else None diff --git a/blueprintcompiler/parser_utils.py b/blueprintcompiler/parser_utils.py index 13086a6..af951fe 100644 --- a/blueprintcompiler/parser_utils.py +++ b/blueprintcompiler/parser_utils.py @@ -18,7 +18,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from . import ast from .parse_tree import * @@ -35,48 +34,3 @@ class_name = AnyOf( ], UseIdent("class_name"), ) - -literal = Group( - ast.LiteralValue, - AnyOf( - UseNumber("value"), - UseQuoted("value"), - ) -) - -ident_value = Group( - ast.IdentValue, - UseIdent("value"), -) - -flags_value = Group( - ast.FlagsValue, - [ - Group(ast.Flag, UseIdent("value")), - "|", - Delimited(Group(ast.Flag, UseIdent("value")), "|"), - ], -) - -translated_string = Group( - ast.TranslatedStringValue, - AnyOf( - [ - "_", - "(", - UseQuoted("value").expected("a quoted string"), - Match(")").expected(), - ], - [ - "C_", - "(", - UseQuoted("context").expected("a quoted string"), - ",", - UseQuoted("value").expected("a quoted string"), - Optional(","), - Match(")").expected(), - ], - ), -) - -value = AnyOf(translated_string, literal, flags_value, ident_value) diff --git a/tests/test_samples.py b/tests/test_samples.py index e8fa143..d0524ef 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -87,8 +87,7 @@ class TestSamples(unittest.TestCase): print("\n".join(diff)) raise AssertionError() else: # pragma: no cover - # Expected a compiler error but there wasn't one - raise AssertionError() + raise AssertionError("Expected a compiler error, but none was emitted") def assert_decompile(self, name):