diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 822c7dc..f4bc92d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ build: - ninja -C _build docs/en - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - cd blueprint-regression-tests - - git checkout c61f425d5df359261f1369722e791f947bc3ede1 + - git checkout d14b95b6c1fc0cddd4b0ad21d224b05edee2d01f - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' diff --git a/NEWS.md b/NEWS.md index bd533d6..0997afb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# v0.6.0 (unreleased) + +## Breaking Changes +- Quoted and numeric literals are no longer interchangeable (e.g. `"800"` is +no longer an accepted value for an integer type). +- Boxed types are now type checked. + # v0.4.0 ## Added diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 3786078..b1ecf08 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -129,6 +129,11 @@ class StringType(BasicType): def assignable_to(self, other) -> bool: return isinstance(other, StringType) +class TypeType(BasicType): + name = "GType" + def assignable_to(self, other) -> bool: + return isinstance(other, TypeType) + _BASIC_TYPES = { "gboolean": BoolType, "int": IntType, @@ -141,6 +146,8 @@ _BASIC_TYPES = { "float": FloatType, "double": FloatType, "utf8": StringType, + "gtype": TypeType, + "type": TypeType, } class GirNode: @@ -213,7 +220,7 @@ class GirNode: @property def type(self): - return self.get_containing(Namespace).lookup_type(self.type_name) + raise NotImplementedError() class Property(GirNode): @@ -464,6 +471,18 @@ class Enumeration(GirNode, GirType): return type == self +class Boxed(GirNode, GirType): + def __init__(self, ns, tl: typelib.Typelib): + super().__init__(ns, tl) + + @property + def signature(self): + return f"boxed {self.full_name}" + + def assignable_to(self, type): + return type == self + + class Bitfield(Enumeration): def __init__(self, ns, tl: typelib.Typelib): super().__init__(ns, tl) @@ -491,6 +510,8 @@ class Namespace(GirNode): self.entries[entry_name] = Class(self, entry_blob) elif entry_type == typelib.BLOB_TYPE_INTERFACE: self.entries[entry_name] = Interface(self, entry_blob) + elif entry_type == typelib.BLOB_TYPE_BOXED or entry_type == typelib.BLOB_TYPE_STRUCT: + self.entries[entry_name] = Boxed(self, entry_blob) @cached_property def xml(self): @@ -595,6 +616,8 @@ class Repository(GirNode): return UIntType() elif type_id == typelib.TYPE_UTF8: return StringType() + elif type_id == typelib.TYPE_GTYPE: + return TypeType() else: raise CompilerBugError("Unknown type ID", type_id) else: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index feeb301..1b27040 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -18,7 +18,7 @@ from .gtkbuilder_child import Child from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .ui import UI -from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue +from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, QuotedValue, NumberValue from .common import * @@ -43,8 +43,10 @@ OBJECT_CONTENT_HOOKS.children = [ ] VALUE_HOOKS.children = [ + TypeValue, TranslatedStringValue, FlagsValue, IdentValue, - LiteralValue, + QuotedValue, + NumberValue, ] diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index f39db53..44e28ba 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -19,6 +19,7 @@ from .common import * +from .types import TypeName class Value(AstNode): @@ -55,11 +56,68 @@ class TranslatedStringValue(Value): xml.put_text(self.tokens["value"]) -class LiteralValue(Value): - grammar = AnyOf( - UseNumber("value"), - UseQuoted("value"), - ) +class TypeValue(Value): + grammar = [ + "typeof", + "(", + to_parse_node(TypeName).expected("type name"), + Match(")").expected(), + ] + + @property + def type_name(self): + return self.children[TypeName][0] + + def emit_xml(self, xml: XmlEmitter): + xml.put_text(self.type_name.glib_type_name) + + @validate() + def validate_for_type(self): + type = self.parent.value_type + if type is not None and not isinstance(type, gir.TypeType): + raise CompileError(f"Cannot convert GType to {type.full_name}") + + +class QuotedValue(Value): + grammar = 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) or isinstance(type, gir.UIntType) or isinstance(type, gir.FloatType): + raise CompileError(f"Cannot convert string to number") + + elif isinstance(type, gir.StringType): + pass + + elif isinstance(type, gir.Class) or isinstance(type, gir.Interface) or isinstance(type, gir.Boxed): + parseable_types = [ + "Gdk.Paintable", + "Gdk.Texture", + "Gdk.Pixbuf", + "GLib.File", + "Gtk.ShortcutTrigger", + "Gtk.ShortcutAction", + "Gdk.RGBA", + "Gdk.ContentFormats", + "Gsk.Transform", + "GLib.Variant", + ] + if type.full_name not in parseable_types: + hints = [] + if isinstance(type, gir.TypeType): + hints.append(f"use the typeof operator: 'typeof({self.tokens('value')})'") + raise CompileError(f"Cannot convert string to {type.full_name}", hints=hints) + + elif type is not None: + raise CompileError(f"Cannot convert string to {type.full_name}") + + +class NumberValue(Value): + grammar = UseNumber("value") def emit_xml(self, xml: XmlEmitter): xml.put_text(self.tokens["value"]) @@ -87,23 +145,8 @@ class LiteralValue(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}") + raise CompileError(f"Cannot convert number to {type.full_name}") class Flag(AstNode): diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index be1f366..e8b9152 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -26,6 +26,8 @@ import mmap, os from .errors import CompilerBugError +BLOB_TYPE_STRUCT = 3 +BLOB_TYPE_BOXED = 4 BLOB_TYPE_ENUM = 5 BLOB_TYPE_FLAGS = 6 BLOB_TYPE_OBJECT = 7 diff --git a/tests/sample_errors/a11y_prop_type.err b/tests/sample_errors/a11y_prop_type.err index c8cb1d5..cc0b0db 100644 --- a/tests/sample_errors/a11y_prop_type.err +++ b/tests/sample_errors/a11y_prop_type.err @@ -1 +1 @@ -5,18,1,Cannot convert 1 to Gtk.Orientation +5,18,1,Cannot convert number to Gtk.Orientation diff --git a/tests/samples/parseable.blp b/tests/samples/parseable.blp index 3bc3584..f4e8c2f 100644 --- a/tests/samples/parseable.blp +++ b/tests/samples/parseable.blp @@ -1,5 +1,14 @@ using Gtk 4.0; +using Gio 2.0; Gtk.Shortcut { trigger: "Escape"; } + +Picture { + paintable: "/path/to/paintable"; +} + +ColorButton { + rgba: "rgb(0, 0, 0)"; +} \ No newline at end of file diff --git a/tests/samples/parseable.ui b/tests/samples/parseable.ui index 78ceacb..231a25c 100644 --- a/tests/samples/parseable.ui +++ b/tests/samples/parseable.ui @@ -4,4 +4,10 @@ Escape + + /path/to/paintable + + + rgb(0, 0, 0) + diff --git a/tests/samples/typeof.blp b/tests/samples/typeof.blp new file mode 100644 index 0000000..5c5e2e5 --- /dev/null +++ b/tests/samples/typeof.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; +using GObject 2.0; +using Gio 2.0; + +Gio.ListStore { + item-type: typeof(GObject.Object); +} + +Gio.ListStore { + item-type: typeof(.MyObject); +} \ No newline at end of file diff --git a/tests/samples/typeof.ui b/tests/samples/typeof.ui new file mode 100644 index 0000000..4c45869 --- /dev/null +++ b/tests/samples/typeof.ui @@ -0,0 +1,10 @@ + + + + + GObject + + + MyObject + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 4fdb5c0..ba39ada 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -155,6 +155,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("template") self.assert_sample("template_no_parent") self.assert_sample("translated") + self.assert_sample("typeof") self.assert_sample("uint") self.assert_sample("unchecked_class") self.assert_sample("using")