diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 58662b2..0f1132b 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,5 +1,5 @@ from .attributes import BaseAttribute, BaseTypedAttribute -from .expression import IdentExpr, LookupOp, Expr +from .expression import CastExpr, IdentExpr, LookupOp, ExprChain from .gobject_object import Object, ObjectContent from .gobject_property import Property from .gobject_signal import Signal diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 29df93e..2d50984 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -19,29 +19,59 @@ from .common import * +from .types import TypeName expr = Pratt() -class Expr(AstNode): +class Expr: + @property + def type(self) -> T.Optional[GirType]: + raise NotImplementedError() + + +class ExprChain(Expr, AstNode): grammar = expr + @property + def last(self) -> Expr: + return self.children[-1] -class InfixExpr(AstNode): + @property + def type(self) -> T.Optional[GirType]: + return self.last.type + + +class InfixExpr(Expr, AstNode): @property def lhs(self): - children = list(self.parent_by_type(Expr).children) + children = list(self.parent_by_type(ExprChain).children) return children[children.index(self) - 1] -class IdentExpr(AstNode): +class IdentExpr(Expr, AstNode): grammar = UseIdent("ident") @property def ident(self) -> str: return self.tokens["ident"] + @validate() + def exists(self): + if self.root.objects_by_id.get(self.ident) is None: + raise CompileError( + f"Could not find object with ID {self.ident}", + did_you_mean=(self.ident, self.root.objects_by_id.keys()), + ) + + @property + def type(self) -> T.Optional[GirType]: + if object := self.root.objects_by_id.get(self.ident): + return object.gir_class + else: + return None + class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] @@ -50,9 +80,53 @@ class LookupOp(InfixExpr): def property_name(self) -> str: return self.tokens["property"] + @property + def type(self) -> T.Optional[GirType]: + if isinstance(self.lhs.type, gir.Class) or isinstance( + self.lhs.type, gir.Interface + ): + if property := self.lhs.type.properties.get(self.property_name): + return property.type + + return None + + @validate("property") + def property_exists(self): + if self.lhs.type is None or isinstance(self.lhs.type, UncheckedType): + return + elif not isinstance(self.lhs.type, gir.Class) and not isinstance( + self.lhs.type, gir.Interface + ): + raise CompileError( + f"Type {self.lhs.type.full_name} does not have properties" + ) + elif self.lhs.type.properties.get(self.property_name) is None: + raise CompileError( + f"{self.lhs.type.full_name} does not have a property called {self.property_name}" + ) + + +class CastExpr(InfixExpr): + grammar = ["as", "(", TypeName, ")"] + + @property + def type(self) -> T.Optional[GirType]: + return self.children[TypeName][0].gir_type + + @validate() + def cast_makes_sense(self): + if self.lhs.type is None: + return + + if not self.type.assignable_to(self.lhs.type): + raise CompileError( + f"Invalid cast. No instance of {self.lhs.type.full_name} can be an instance of {self.type.full_name}." + ) + expr.children = [ Prefix(IdentExpr), - Prefix(["(", Expr, ")"]), + Prefix(["(", ExprChain, ")"]), Infix(10, LookupOp), + Infix(10, CastExpr), ] diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 8e6fd23..edf8b2a 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -54,7 +54,9 @@ class Object(AstNode): return self.children[ObjectContent][0] @property - def gir_class(self): + def gir_class(self) -> GirType: + if self.class_name is None: + raise CompilerBugError() return self.class_name.gir_type @cached_property diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index c6db999..10770a8 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .expression import Expr +from .expression import ExprChain from .gobject_object import Object from .gtkbuilder_template import Template from .values import Value, TranslatedStringValue @@ -51,7 +51,7 @@ class Property(AstNode): UseLiteral("binding", True), ":", "bind", - Expr, + ExprChain, ), Statement( UseIdent("name"), @@ -91,7 +91,7 @@ class Property(AstNode): if self.gir_property is None: raise CompileError( - f"Class {self.gir_class.full_name} does not contain a property called {self.tokens['name']}", + f"Class {self.gir_class.full_name} does not have a property called {self.tokens['name']}", did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()), ) diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 79f24f3..69ce12f 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -114,7 +114,7 @@ class XmlOutput(OutputFormat): elif value is None: if property.tokens["binding"]: xml.start_tag("binding", **props) - self._emit_expression(property.children[Expr][0], xml) + self._emit_expression(property.children[ExprChain][0], xml) xml.end_tag() else: xml.put_self_closing("property", **props) @@ -176,7 +176,7 @@ class XmlOutput(OutputFormat): else: raise CompilerBugError() - def _emit_expression(self, expression: Expr, xml: XmlEmitter): + def _emit_expression(self, expression: ExprChain, xml: XmlEmitter): self._emit_expression_part(expression.children[-1], xml) def _emit_expression_part(self, expression, xml: XmlEmitter): @@ -184,8 +184,10 @@ class XmlOutput(OutputFormat): self._emit_ident_expr(expression, xml) elif isinstance(expression, LookupOp): self._emit_lookup_op(expression, xml) - elif isinstance(expression, Expr): + elif isinstance(expression, ExprChain): self._emit_expression(expression, xml) + elif isinstance(expression, CastExpr): + self._emit_cast_expr(expression, xml) else: raise CompilerBugError() @@ -195,10 +197,13 @@ class XmlOutput(OutputFormat): xml.end_tag() def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter): - xml.start_tag("lookup", name=expr.property_name) + xml.start_tag("lookup", name=expr.property_name, type=expr.lhs.type) self._emit_expression_part(expr.lhs, xml) xml.end_tag() + def _emit_cast_expr(self, expr: CastExpr, xml: XmlEmitter): + self._emit_expression_part(expr.lhs, xml) + def _emit_attribute( self, tag: str, attr: str, name: str, value: Value, xml: XmlEmitter ): diff --git a/tests/sample_errors/expr_cast_conversion.blp b/tests/sample_errors/expr_cast_conversion.blp new file mode 100644 index 0000000..0b485c4 --- /dev/null +++ b/tests/sample_errors/expr_cast_conversion.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Overlay overlay { + margin-bottom: bind overlay.child as (Adjustment).value; +} \ No newline at end of file diff --git a/tests/sample_errors/expr_cast_conversion.err b/tests/sample_errors/expr_cast_conversion.err new file mode 100644 index 0000000..e449a6c --- /dev/null +++ b/tests/sample_errors/expr_cast_conversion.err @@ -0,0 +1 @@ +4,37,15,Invalid cast. No instance of Gtk.Widget can be an instance of Gtk.Adjustment. \ No newline at end of file diff --git a/tests/sample_errors/expr_lookup_dne.blp b/tests/sample_errors/expr_lookup_dne.blp new file mode 100644 index 0000000..ca05bfc --- /dev/null +++ b/tests/sample_errors/expr_lookup_dne.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Overlay overlay { + margin-bottom: bind overlay.child as (Label).not-a-property; +} \ No newline at end of file diff --git a/tests/sample_errors/expr_lookup_dne.err b/tests/sample_errors/expr_lookup_dne.err new file mode 100644 index 0000000..9349c9d --- /dev/null +++ b/tests/sample_errors/expr_lookup_dne.err @@ -0,0 +1 @@ +4,48,14,Gtk.Label does not have a property called not-a-property \ No newline at end of file diff --git a/tests/sample_errors/expr_lookup_no_properties.blp b/tests/sample_errors/expr_lookup_no_properties.blp new file mode 100644 index 0000000..3c446ef --- /dev/null +++ b/tests/sample_errors/expr_lookup_no_properties.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Overlay overlay { + margin-bottom: bind overlay.margin-bottom.what; +} \ No newline at end of file diff --git a/tests/sample_errors/expr_lookup_no_properties.err b/tests/sample_errors/expr_lookup_no_properties.err new file mode 100644 index 0000000..02aa4a6 --- /dev/null +++ b/tests/sample_errors/expr_lookup_no_properties.err @@ -0,0 +1 @@ +4,45,4,Type int does not have properties \ No newline at end of file diff --git a/tests/sample_errors/property_dne.err b/tests/sample_errors/property_dne.err index 2b6ff40..12df579 100644 --- a/tests/sample_errors/property_dne.err +++ b/tests/sample_errors/property_dne.err @@ -1 +1 @@ -4,3,19,Class Gtk.Label does not contain a property called not-a-real-property +4,3,19,Class Gtk.Label does not have a property called not-a-real-property diff --git a/tests/samples/expr_lookup.blp b/tests/samples/expr_lookup.blp index d172f7e..2556f9a 100644 --- a/tests/samples/expr_lookup.blp +++ b/tests/samples/expr_lookup.blp @@ -5,5 +5,5 @@ Overlay { } Label { - label: bind (label.parent).child.label; + label: bind (label.parent) as (Overlay).child as (Label).label; } diff --git a/tests/samples/expr_lookup.ui b/tests/samples/expr_lookup.ui index 2137e9b..91d7590 100644 --- a/tests/samples/expr_lookup.ui +++ b/tests/samples/expr_lookup.ui @@ -8,9 +8,9 @@ - - - + + + label diff --git a/tests/test_samples.py b/tests/test_samples.py index f038b60..1195cdf 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -151,7 +151,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("combo_box_text") self.assert_sample("comments") self.assert_sample("enum") - self.assert_sample("expr_lookup", skip_run=True) # TODO: Fix + self.assert_sample("expr_lookup") self.assert_sample("file_filter") self.assert_sample("flags") self.assert_sample("id_prop") @@ -207,6 +207,9 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("duplicates") self.assert_sample_error("empty") self.assert_sample_error("enum_member_dne") + self.assert_sample_error("expr_cast_conversion") + self.assert_sample_error("expr_lookup_dne") + self.assert_sample_error("expr_lookup_no_properties") self.assert_sample_error("filters_in_non_file_filter") self.assert_sample_error("gtk_3") self.assert_sample_error("gtk_exact_version")