diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index f849c75..39d8336 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -38,7 +38,6 @@ from .gtk_styles import ExtStyles from .gtkbuilder_child import Child, ChildExtension, ChildInternal, ChildType from .gtkbuilder_template import Template from .imports import GtkDirective, Import -from .property_binding import PropertyBinding from .types import ClassName from .ui import UI from .values import ( diff --git a/blueprintcompiler/language/binding.py b/blueprintcompiler/language/binding.py index e6bffd0..86beec0 100644 --- a/blueprintcompiler/language/binding.py +++ b/blueprintcompiler/language/binding.py @@ -23,16 +23,57 @@ from .common import * from .expression import Expression, LiteralExpr, LookupOp +class BindingFlag(AstNode): + grammar = [ + AnyOf( + UseExact("flag", "inverted"), + UseExact("flag", "bidirectional"), + UseExact("flag", "no-sync-create"), + UseExact("flag", "sync-create"), + ) + ] + + @property + def flag(self) -> str: + return self.tokens["flag"] + + @validate() + def sync_create(self): + if self.flag == "sync-create": + raise UpgradeWarning( + "'sync-create' is now the default. Use 'no-sync-create' if this is not wanted.", + actions=[CodeAction("remove 'sync-create'", "")], + ) + + @validate() + def unique(self): + self.validate_unique_in_parent( + f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag + ) + + @validate() + def flags_only_if_simple(self): + if self.parent.simple_binding is None: + raise CompileError( + "Only bindings with a single lookup can have flags", + ) + + class Binding(AstNode): grammar = [ - Keyword("bind"), + AnyOf(Keyword("bind"), UseExact("bind", "bind-property")), Expression, + ZeroOrMore(BindingFlag), ] @property def expression(self) -> Expression: return self.children[Expression][0] + @property + def flags(self) -> T.List[BindingFlag]: + return self.children[BindingFlag] + @property def simple_binding(self) -> T.Optional["SimpleBinding"]: if isinstance(self.expression.last, LookupOp): @@ -40,14 +81,29 @@ class Binding(AstNode): from .values import IdentLiteral if isinstance(self.expression.last.lhs.literal.value, IdentLiteral): + flags = [x.flag for x in self.flags] return SimpleBinding( self.expression.last.lhs.literal.value.ident, self.expression.last.property_name, + no_sync_create="no-sync-create" in flags, + bidirectional="bidirectional" in flags, + inverted="inverted" in flags, ) return None + @validate("bind") + def bind_property(self): + if self.tokens["bind"] == "bind-property": + raise UpgradeWarning( + "'bind-property' is no longer needed. Use 'bind' instead. (blueprint 0.8.2)", + actions=[CodeAction("use 'bind'", "bind")], + ) + @dataclass class SimpleBinding: source: str property_name: str + no_sync_create: bool = False + bidirectional: bool = False + inverted: bool = False diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index fb24f5a..2f8e22e 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -30,6 +30,7 @@ from .gtkbuilder_template import Template class ValueTypeCtx: value_type: T.Optional[GirType] allow_null: bool = False + must_infer_type: bool = False @dataclass diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index a2185a3..10426fa 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -114,7 +114,7 @@ class LookupOp(InfixExpr): @context(ValueTypeCtx) def value_type(self) -> ValueTypeCtx: - return ValueTypeCtx(None) + return ValueTypeCtx(None, must_infer_type=True) @property def property_name(self) -> str: @@ -133,6 +133,10 @@ class LookupOp(InfixExpr): @validate("property") def property_exists(self): if self.lhs.type is None: + # Literal values throw their own errors if the type isn't known + if isinstance(self.lhs, LiteralExpr): + return + raise CompileError( f"Could not determine the type of the preceding expression", hints=[ diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index f65c5ca..b24cd07 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -22,21 +22,18 @@ from .binding import Binding from .common import * from .contexts import ValueTypeCtx from .gtkbuilder_template import Template -from .property_binding import PropertyBinding from .values import ObjectValue, Value class Property(AstNode): - grammar = Statement( - UseIdent("name"), ":", AnyOf(PropertyBinding, Binding, ObjectValue, Value) - ) + grammar = Statement(UseIdent("name"), ":", AnyOf(Binding, ObjectValue, Value)) @property def name(self) -> str: return self.tokens["name"] @property - def value(self) -> T.Union[PropertyBinding, Binding, ObjectValue, Value]: + def value(self) -> T.Union[Binding, ObjectValue, Value]: return self.children[0] @property @@ -51,7 +48,7 @@ class Property(AstNode): @validate() def binding_valid(self): if ( - (isinstance(self.value, PropertyBinding) or isinstance(self.value, Binding)) + isinstance(self.value, Binding) and self.gir_property is not None and self.gir_property.construct_only ): diff --git a/blueprintcompiler/language/property_binding.py b/blueprintcompiler/language/property_binding.py deleted file mode 100644 index 686d1e8..0000000 --- a/blueprintcompiler/language/property_binding.py +++ /dev/null @@ -1,145 +0,0 @@ -# property_binding.py -# -# Copyright 2023 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 * -from .contexts import ScopeCtx -from .gobject_object import Object - - -class PropertyBindingFlag(AstNode): - grammar = [ - AnyOf( - UseExact("flag", "inverted"), - UseExact("flag", "bidirectional"), - UseExact("flag", "no-sync-create"), - UseExact("flag", "sync-create"), - ) - ] - - @property - def flag(self) -> str: - return self.tokens["flag"] - - @validate() - def sync_create(self): - if self.flag == "sync-create": - raise UpgradeWarning( - "'sync-create' is now the default. Use 'no-sync-create' if this is not wanted.", - actions=[CodeAction("remove 'sync-create'", "")], - ) - - @validate() - def unique(self): - self.validate_unique_in_parent( - f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag - ) - - -class PropertyBinding(AstNode): - grammar = AnyOf( - [ - Keyword("bind-property"), - UseIdent("source"), - ".", - UseIdent("property"), - ZeroOrMore(PropertyBindingFlag), - ], - [ - Keyword("bind"), - UseIdent("source"), - ".", - UseIdent("property"), - PropertyBindingFlag, - ZeroOrMore(PropertyBindingFlag), - ], - ) - - @property - def source(self) -> str: - return self.tokens["source"] - - @property - def source_obj(self) -> T.Optional[Object]: - if self.root.is_legacy_template(self.source): - return self.root.template - return self.context[ScopeCtx].objects.get(self.source) - - @property - def property_name(self) -> str: - return self.tokens["property"] - - @property - def flags(self) -> T.List[PropertyBindingFlag]: - return self.children[PropertyBindingFlag] - - @property - def inverted(self) -> bool: - return any([f.flag == "inverted" for f in self.flags]) - - @property - def bidirectional(self) -> bool: - return any([f.flag == "bidirectional" for f in self.flags]) - - @property - def no_sync_create(self) -> bool: - return any([f.flag == "no-sync-create" for f in self.flags]) - - @validate("source") - def source_object_exists(self) -> None: - if self.source_obj is None: - raise CompileError( - f"Could not find object with ID {self.source}", - did_you_mean=(self.source, self.context[ScopeCtx].objects.keys()), - ) - - @validate("property") - def property_exists(self) -> None: - if self.source_obj is None: - return - - gir_class = self.source_obj.gir_class - - if gir_class is None or gir_class.incomplete: - # Objects that we have no gir data on should not be validated - # This happens for classes defined by the app itself - return - - if ( - isinstance(gir_class, gir.Class) - and gir_class.properties.get(self.property_name) is None - ): - raise CompileError( - f"{gir_class.full_name} does not have a property called {self.property_name}" - ) - - @validate("bind") - def old_bind(self): - if self.tokens["bind"]: - raise UpgradeWarning( - "Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags", - actions=[CodeAction("Use 'bind-property'", "bind-property")], - ) - - @validate("source") - def legacy_template(self): - if self.root.is_legacy_template(self.source): - raise UpgradeWarning( - "Use 'template' instead of the class name (introduced in 0.8.0)", - actions=[CodeAction("Use 'template'", "template")], - ) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index fd1765b..888d6a1 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -291,7 +291,7 @@ class IdentLiteral(AstNode): actions=[CodeAction("Use 'template'", "template")], ) - elif expected_type is not None: + elif expected_type is not None or self.context[ValueTypeCtx].must_infer_type: object = self.context[ScopeCtx].objects.get(self.ident) if object is None: if self.ident == "null": @@ -305,7 +305,11 @@ class IdentLiteral(AstNode): self.context[ScopeCtx].objects.keys(), ), ) - elif object.gir_class and not object.gir_class.assignable_to(expected_type): + elif ( + expected_type is not None + and object.gir_class is not None + and not object.gir_class.assignable_to(expected_type) + ): raise CompileError( f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}" ) diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 3ef7375..e9f091f 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -116,27 +116,21 @@ class XmlOutput(OutputFormat): if simple := value.simple_binding: props["bind-source"] = self._object_id(value, simple.source) props["bind-property"] = simple.property_name - props["bind-flags"] = "sync-create" + flags = [] + if not simple.no_sync_create: + flags.append("sync-create") + if simple.inverted: + flags.append("invert-boolean") + if simple.bidirectional: + flags.append("bidirectional") + props["bind-flags"] = "|".join(flags) or None + xml.put_self_closing("property", **props) else: xml.start_tag("binding", **props) self._emit_expression(value.expression, xml) xml.end_tag() - elif isinstance(value, PropertyBinding): - bind_flags = [] - if not value.no_sync_create: - bind_flags.append("sync-create") - if value.inverted: - bind_flags.append("invert-boolean") - if value.bidirectional: - bind_flags.append("bidirectional") - - props["bind-source"] = self._object_id(value, value.source) - props["bind-property"] = value.property_name - props["bind-flags"] = "|".join(bind_flags) or None - xml.put_self_closing("property", **props) - elif isinstance(value, ObjectValue): xml.start_tag("property", **props) self._emit_object(value.object, xml) diff --git a/tests/sample_errors/binding_flags.blp b/tests/sample_errors/binding_flags.blp new file mode 100644 index 0000000..7e94058 --- /dev/null +++ b/tests/sample_errors/binding_flags.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: bind $my_closure() as