diff --git a/gtkblueprinttool/ast.py b/gtkblueprinttool/ast.py index 26edc53..2ef4960 100644 --- a/gtkblueprinttool/ast.py +++ b/gtkblueprinttool/ast.py @@ -57,7 +57,7 @@ class UI(AstNode): @lazy_prop def objects_by_id(self): - return { obj.id: obj for obj in self.iterate_children_recursive() if hasattr(obj, "id") } + return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None } @validate() @@ -137,12 +137,12 @@ class Import(AstNode): class Template(AstNode): @validate("namespace", "class_name") - def gir_parent_exists(self): + def gir_class_exists(self): if not self.tokens["ignore_gir"]: self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"]) @property - def gir_parent(self): + def gir_class(self): return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"]) @@ -152,14 +152,14 @@ class Template(AstNode): @docs("class_name") def class_docs(self): - if self.gir_parent: - return self.gir_parent.doc + if self.gir_class: + return self.gir_class.doc def emit_xml(self, xml: XmlEmitter): xml.start_tag("template", **{ "class": self.tokens["name"], - "parent": self.gir_parent.glib_type_name if self.gir_parent else self.tokens["class_name"], + "parent": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"], }) for child in self.children: child.emit_xml(xml) @@ -210,12 +210,7 @@ class Child(AstNode): class ObjectContent(AstNode): @property def gir_class(self): - if isinstance(self.parent, Template): - return self.parent.gir_parent - elif isinstance(self.parent, Object): - return self.parent.gir_class - else: - raise CompilerBugError() + return self.parent.gir_class # @validate() # def only_one_style_class(self): @@ -233,13 +228,7 @@ class ObjectContent(AstNode): class Property(AstNode): @property def gir_class(self): - parent = self.parent.parent - if isinstance(parent, Template): - return parent.gir_parent - elif isinstance(parent, Object): - return parent.gir_class - else: - raise CompilerBugError() + return self.parent.parent.gir_class @property @@ -273,6 +262,19 @@ class Property(AstNode): ) + @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: @@ -323,13 +325,7 @@ class Signal(AstNode): @property def gir_class(self): - parent = self.parent.parent - if isinstance(parent, Template): - return parent.gir_parent - elif isinstance(parent, Object): - return parent.gir_class - else: - raise CompilerBugError() + return self.parent.parent.gir_class @validate("name") @@ -397,12 +393,14 @@ class IdentValue(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=type.members.keys(), ) + elif isinstance(type, gir.BoolType): # would have been parsed as a LiteralValue if it was correct raise CompileError( @@ -410,6 +408,18 @@ class IdentValue(Value): did_you_mean=["true", "false"], ) + else: + 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): diff --git a/gtkblueprinttool/gir.py b/gtkblueprinttool/gir.py index b88dc86..dfde6ab 100644 --- a/gtkblueprinttool/gir.py +++ b/gtkblueprinttool/gir.py @@ -55,23 +55,47 @@ def get_namespace(namespace, version): return _namespace_cache[filename] -class BasicType: +class GirType: pass +class BasicType: + name: str = "unknown type" + + def assignable_to(self, other) -> bool: + raise NotImplementedError() + + @property + def full_name(self) -> str: + return self.name + class BoolType(BasicType): - pass + name = "bool" + def assignable_to(self, other) -> bool: + return isinstance(other, BoolType) class IntType(BasicType): - pass + name = "int" + def assignable_to(self, other) -> bool: + return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) class UIntType(BasicType): - pass + name = "uint" + def assignable_to(self, other) -> bool: + return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) class FloatType(BasicType): - pass + name = "float" + def assignable_to(self, other) -> bool: + return isinstance(other, FloatType) + +class StringType(BasicType): + name = "string" + def assignable_to(self, other) -> bool: + return isinstance(other, StringType) _BASIC_TYPES = { "gboolean": BoolType, + "int": IntType, "gint": IntType, "gint64": IntType, "guint": UIntType, @@ -80,6 +104,7 @@ _BASIC_TYPES = { "gdouble": FloatType, "float": FloatType, "double": FloatType, + "utf8": StringType, } class GirNode: @@ -172,14 +197,23 @@ class Signal(GirNode): return f"signal {self.container.name}.{self.name} ({args})" -class Interface(GirNode): +class Interface(GirNode, GirType): def __init__(self, ns, xml: xml_reader.Element): super().__init__(ns, xml) self.properties = {child["name"]: Property(self, child) for child in xml.get_elements("property")} self.signals = {child["name"]: Signal(self, child) for child in xml.get_elements("glib:signal")} + self.prerequisites = [child["name"] for child in xml.get_elements("prerequisite")] + + def assignable_to(self, other) -> bool: + if self == other: + return True + for pre in self.prerequisites: + if self.get_containing(Namespace).lookup_type(pre).assignable_to(other): + return True + return False -class Class(GirNode): +class Class(GirNode, GirType): def __init__(self, ns, xml: xml_reader.Element): super().__init__(ns, xml) self._parent = xml["parent"] @@ -211,6 +245,17 @@ class Class(GirNode): return self.get_containing(Namespace).lookup_type(self._parent) + def assignable_to(self, other) -> bool: + if self == other: + return True + elif self.parent and self.parent.assignable_to(other): + return True + elif other in self.implements: + return True + else: + return False + + def _enum_properties(self): yield from self.own_properties.values() diff --git a/tests/sample_errors/class_assign.blp b/tests/sample_errors/class_assign.blp new file mode 100644 index 0000000..a0965ab --- /dev/null +++ b/tests/sample_errors/class_assign.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; + +Box box {} +Label { + label: box; +} diff --git a/tests/sample_errors/class_assign.err b/tests/sample_errors/class_assign.err new file mode 100644 index 0000000..e096816 --- /dev/null +++ b/tests/sample_errors/class_assign.err @@ -0,0 +1 @@ +5,10,3,Cannot assign Gtk.Box to string diff --git a/tests/sample_errors/obj_prop_type.blp b/tests/sample_errors/obj_prop_type.blp new file mode 100644 index 0000000..3600b68 --- /dev/null +++ b/tests/sample_errors/obj_prop_type.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Scale { + adjustment: Label {}; +} diff --git a/tests/sample_errors/obj_prop_type.err b/tests/sample_errors/obj_prop_type.err new file mode 100644 index 0000000..01f1202 --- /dev/null +++ b/tests/sample_errors/obj_prop_type.err @@ -0,0 +1 @@ +4,3,21,Cannot assign Gtk.Label to Gtk.Adjustment diff --git a/tests/sample_errors/object_dne.blp b/tests/sample_errors/object_dne.blp new file mode 100644 index 0000000..5e05f1b --- /dev/null +++ b/tests/sample_errors/object_dne.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: my_label; +} diff --git a/tests/sample_errors/object_dne.err b/tests/sample_errors/object_dne.err new file mode 100644 index 0000000..239dbe1 --- /dev/null +++ b/tests/sample_errors/object_dne.err @@ -0,0 +1 @@ +4,10,8,Could not find object with ID my_label diff --git a/tests/samples/id_prop.blp b/tests/samples/id_prop.blp new file mode 100644 index 0000000..8200868 --- /dev/null +++ b/tests/samples/id_prop.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Scale { + adjustment: adj; +} + +Adjustment adj {} diff --git a/tests/samples/id_prop.ui b/tests/samples/id_prop.ui new file mode 100644 index 0000000..4940824 --- /dev/null +++ b/tests/samples/id_prop.ui @@ -0,0 +1,8 @@ + + + + + adj + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 9e58e88..e0813be 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -92,6 +92,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("binding") self.assert_sample("child_type") self.assert_sample("flags") + self.assert_sample("id_prop") self.assert_sample("layout") self.assert_sample("menu") self.assert_sample("object_prop") @@ -105,12 +106,15 @@ class TestSamples(unittest.TestCase): def test_sample_errors(self): + self.assert_sample_error("class_assign") self.assert_sample_error("class_dne") self.assert_sample_error("duplicate_obj_id") self.assert_sample_error("enum_member_dne") self.assert_sample_error("invalid_bool") self.assert_sample_error("ns_not_imported") self.assert_sample_error("not_a_class") + self.assert_sample_error("object_dne") + self.assert_sample_error("obj_prop_type") self.assert_sample_error("property_dne") self.assert_sample_error("signal_dne") self.assert_sample_error("two_templates")