From 180b529650e21d72c115fd7aa5c5bcebc974fcad Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 9 Jun 2022 14:51:35 -0500 Subject: [PATCH 001/290] Post-release version bump --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 08d45cd..bd6fba2 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('blueprint-compiler', - version: '0.2.0', + version: '0.3.0', ) subdir('docs') From fac311d3c3c632e3dcd548096df0cd93d3a6482b Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 9 Jun 2022 13:57:13 -0500 Subject: [PATCH 002/290] Update regression tests --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 960c4e4..0820f5a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ build: - ninja -C _build install - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - cd blueprint-regression-tests - - git checkout dba20ee77b0ba711893726208a0523073fc697e3 + - git checkout 6f164f5e259f1602fd3bfae5e64852fed7687b9f - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' From 7eb0c1ae0d8680da498b034cfa0660f8034945ce Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 11 Jun 2022 02:15:56 -0500 Subject: [PATCH 003/290] decompiler: Fix Adwaita version --- blueprintcompiler/decompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index a8b1c70..a7363e5 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -39,7 +39,7 @@ _NAMESPACES = [ ("GLib", "2.0"), ("GObject", "2.0"), ("Gio", "2.0"), - ("Adw", "1.0"), + ("Adw", "1"), ] From c094743e840efcc844fe1241ba656c270f033e45 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 17 Jun 2022 11:10:43 -0500 Subject: [PATCH 004/290] Fix compiling empty file --- blueprintcompiler/utils.py | 2 +- tests/sample_errors/empty.blp | 0 tests/sample_errors/empty.err | 1 + tests/test_samples.py | 1 + 4 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 tests/sample_errors/empty.blp create mode 100644 tests/sample_errors/empty.err diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index 6befea8..2d5451d 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -68,7 +68,7 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]: - if idx == 0: + if idx == 0 or len(text) == 0: return (0, 0) sp = text[:idx].splitlines(keepends=True) line_num = len(sp) diff --git a/tests/sample_errors/empty.blp b/tests/sample_errors/empty.blp new file mode 100644 index 0000000..e69de29 diff --git a/tests/sample_errors/empty.err b/tests/sample_errors/empty.err new file mode 100644 index 0000000..854962f --- /dev/null +++ b/tests/sample_errors/empty.err @@ -0,0 +1 @@ +1,0,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`) \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 40e9d0b..51af836 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -180,6 +180,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("does_not_implement") self.assert_sample_error("duplicate_obj_id") self.assert_sample_error("duplicates") + self.assert_sample_error("empty") self.assert_sample_error("enum_member_dne") self.assert_sample_error("filters_in_non_file_filter") self.assert_sample_error("gtk_3") From 4fefa0bd7352f201470b4c9461f898b8573b289b Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 29 Jan 2022 21:21:04 -0600 Subject: [PATCH 005/290] Add lookup expressions --- blueprintcompiler/ast_utils.py | 5 +- blueprintcompiler/language/__init__.py | 1 + blueprintcompiler/language/expression.py | 63 +++++++++++++++++++ .../language/gobject_property.py | 23 +++++-- blueprintcompiler/parse_tree.py | 60 ++++++++++++++++++ tests/samples/expr_lookup.blp | 9 +++ tests/samples/expr_lookup.ui | 20 ++++++ tests/test_samples.py | 1 + 8 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 blueprintcompiler/language/expression.py create mode 100644 tests/samples/expr_lookup.blp create mode 100644 tests/samples/expr_lookup.ui diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 2e8bd97..1f3eb5b 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -33,7 +33,10 @@ class Children: def __iter__(self): return iter(self._children) def __getitem__(self, key): - return [child for child in self._children if isinstance(child, key)] + if isinstance(key, int): + return self._children[key] + else: + return [child for child in self._children if isinstance(child, key)] class AstNode: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 68c3d8a..feeb301 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -2,6 +2,7 @@ templates. """ from .attributes import BaseAttribute, BaseTypedAttribute +from .expression import Expr 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 new file mode 100644 index 0000000..a684b39 --- /dev/null +++ b/blueprintcompiler/language/expression.py @@ -0,0 +1,63 @@ +# expressions.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 * + + +expr = Pratt() + + +class Expr(AstNode): + grammar = expr + + def emit_xml(self, xml: XmlEmitter): + self.children[-1].emit_xml(xml) + + +class InfixExpr(AstNode): + @property + def lhs(self): + children = list(self.parent_by_type(Expr).children) + return children[children.index(self) - 1] + + +class IdentExpr(AstNode): + grammar = UseIdent("ident") + + def emit_xml(self, xml: XmlEmitter): + xml.start_tag("constant") + xml.put_text(self.tokens["ident"]) + xml.end_tag() + + +class LookupOp(InfixExpr): + grammar = [".", UseIdent("property")] + + def emit_xml(self, xml: XmlEmitter): + xml.start_tag("lookup", name=self.tokens["property"]) + self.lhs.emit_xml(xml) + xml.end_tag() + + +expr.children = [ + Prefix(IdentExpr), + Prefix(["(", Expr, ")"]), + Infix(10, LookupOp), +] diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index c623b9a..f0a2ef4 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from .expression import Expr from .gobject_object import Object from .gtkbuilder_template import Template from .values import Value, TranslatedStringValue @@ -26,19 +27,27 @@ from .common import * class Property(AstNode): grammar = AnyOf( - Statement( + [ UseIdent("name"), ":", Keyword("bind"), - UseIdent("bind_source").expected("the ID of a source object to bind from"), + UseIdent("bind_source"), ".", - UseIdent("bind_property").expected("a property name to bind from"), + UseIdent("bind_property"), ZeroOrMore(AnyOf( ["no-sync-create", UseLiteral("no_sync_create", True)], ["inverted", UseLiteral("inverted", True)], ["bidirectional", UseLiteral("bidirectional", True)], Match("sync-create").warn("sync-create is deprecated in favor of no-sync-create"), )), + ";", + ], + Statement( + UseIdent("name"), + UseLiteral("binding", True), + ":", + "bind", + Expr, ), Statement( UseIdent("name"), @@ -154,7 +163,13 @@ class Property(AstNode): self.children[Object][0].emit_xml(xml) xml.end_tag() elif value is None: - xml.put_self_closing("property", **props) + if self.tokens["binding"]: + xml.start_tag("binding", **props) + for x in self.children: + x.emit_xml(xml) + xml.end_tag() + else: + xml.put_self_closing("property", **props); else: xml.start_tag("property", **props) value.emit_xml(xml) diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index e56a220..bc36415 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -98,6 +98,7 @@ class ParseContext: def __init__(self, tokens, index=0): self.tokens = list(tokens) + self.binding_power = 0 self.index = index self.start = index self.group = None @@ -118,6 +119,7 @@ class ParseContext: ctx = ParseContext(self.tokens, self.index) ctx.errors = self.errors ctx.warnings = self.warnings + ctx.binding_power = self.binding_power return ctx def apply_child(self, other): @@ -544,6 +546,64 @@ class Keyword(ParseNode): return str(token) == self.kw +class Prefix(ParseNode): + def __init__(self, child): + self.child = to_parse_node(child) + + def _parse(self, ctx: ParseContext): + return self.child.parse(ctx).succeeded() + + +class Infix(ParseNode): + def __init__(self, binding_power: int, child): + self.binding_power = binding_power + self.child = to_parse_node(child) + + def _parse(self, ctx: ParseContext): + ctx.binding_power = self.binding_power + return self.child.parse(ctx).succeeded() + + def __lt__(self, other): + return self.binding_power < other.binding_power + def __eq__(self, other): + return self.binding_power == other.binding_power + + +class Pratt(ParseNode): + """ Basic Pratt parser implementation. """ + + def __init__(self, *children): + self.children = children + + @property + def children(self): + return self._children + @children.setter + def children(self, children): + self._children = children + self.prefixes = [child for child in children if isinstance(child, Prefix)] + self.infixes = sorted([child for child in children if isinstance(child, Infix)], reverse=True) + + def _parse(self, ctx: ParseContext) -> bool: + for prefix in self.prefixes: + if prefix.parse(ctx).succeeded(): + break + else: + # none of the prefixes could be parsed + return False + + while True: + succeeded = False + for infix in self.infixes: + if infix.binding_power <= ctx.binding_power: + break + if infix.parse(ctx).succeeded(): + succeeded = True + break + if not succeeded: + return True + + def to_parse_node(value) -> ParseNode: if isinstance(value, str): return Match(value) diff --git a/tests/samples/expr_lookup.blp b/tests/samples/expr_lookup.blp new file mode 100644 index 0000000..d172f7e --- /dev/null +++ b/tests/samples/expr_lookup.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; + +Overlay { + Label label {} +} + +Label { + label: bind (label.parent).child.label; +} diff --git a/tests/samples/expr_lookup.ui b/tests/samples/expr_lookup.ui new file mode 100644 index 0000000..2137e9b --- /dev/null +++ b/tests/samples/expr_lookup.ui @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + label + + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 51af836..84cdb4f 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -136,6 +136,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("combo_box_text") self.assert_sample("comments") self.assert_sample("enum") + self.assert_sample("expr_lookup") self.assert_sample("file_filter") self.assert_sample("flags") self.assert_sample("id_prop") From 75475d1a457dce5fc8d66a2156e4489fe4204420 Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 5 May 2022 13:55:36 -0500 Subject: [PATCH 006/290] tokenizer: Fix number parsing (again) --- blueprintcompiler/tokenizer.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index be59e2e..2e7fa1b 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -40,9 +40,9 @@ _tokens = [ (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), (TokenType.QUOTED, r'"(\\"|[^"\n])*"'), (TokenType.QUOTED, r"'(\\'|[^'\n])*'"), - (TokenType.NUMBER, r"0x[A-Fa-f0-9_]+"), - (TokenType.NUMBER, r"[-+]?[\d_]*\d(\.[\d_]*\d)?"), - (TokenType.NUMBER, r"[-+]?\.[\d_]*\d"), + (TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), + (TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"), + (TokenType.NUMBER, r"[-+]?\.[\d_]+"), (TokenType.WHITESPACE, r"\s+"), (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), (TokenType.COMMENT, r"\/\/[^\n]*"), @@ -67,10 +67,13 @@ class Token: return None string = str(self).replace("_", "") - if string.startswith("0x"): - return int(string, 16) - else: - return float(string.replace("_", "")) + try: + if string.startswith("0x"): + return int(string, 16) + else: + return float(string.replace("_", "")) + except: + raise CompileError(f"{str(self)} is not a valid number literal", self.start, self.end) def _tokenize(ui_ml: str): From e78fae4f120503d08e513190048c10d678272a7a Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 11 Jun 2022 00:22:19 -0500 Subject: [PATCH 007/290] build: Set the module path in the build Instead of trying to find the module by traversing from the executable, have meson hardcode the path. I *think* this is a little less fragile. --- blueprint-compiler.py | 19 ++++++++++--------- meson.build | 25 ++++++++++++++++--------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/blueprint-compiler.py b/blueprint-compiler.py index a3eb0e9..aa1dc8f 100755 --- a/blueprint-compiler.py +++ b/blueprint-compiler.py @@ -21,18 +21,19 @@ import os, sys -# Try to find the python module, assuming the current file is installed to (prefix)/bin -dirname = os.path.join(os.path.dirname(os.path.dirname(__file__)), "share", "blueprint-compiler") -if os.path.isdir(os.path.join(dirname, "blueprintcompiler")): - sys.path.insert(0, dirname) - -# Get the configured (or, if running from source, not configured) version number +# These variables should be set by meson. If they aren't, we're running +# uninstalled, and we might have to guess some values. version = "@VERSION@" +module_path = "@MODULE_PATH@" +libdir = "@LIBDIR@" -def literal(key): - return "@" + key + "@" +if version == "\u0040VERSION@": + version = "uninstalled" +else: + # If Meson set the configuration values, insert the module path it set + sys.path.insert(0, module_path) from blueprintcompiler import main if __name__ == "__main__": - main.main("uninstalled" if version == literal("VERSION") else version) + main.main(version) diff --git a/meson.build b/meson.build index bd6fba2..540c0ec 100644 --- a/meson.build +++ b/meson.build @@ -17,22 +17,29 @@ configure_file( install_dir: join_paths(datadir, 'pkgconfig'), ) +config = configuration_data({ + 'VERSION': meson.project_version(), + 'LIBDIR': get_option('prefix') / get_option('libdir'), +}) + +if meson.is_subproject() + config.set('MODULE_PATH', meson.current_source_dir()) +else + config.set('MODULE_PATH', py.get_install_dir()) +endif + blueprint_compiler = configure_file( input: 'blueprint-compiler.py', output: 'blueprint-compiler', - configuration: { - 'VERSION': meson.project_version(), - }, + configuration: config, install: not meson.is_subproject(), install_dir: get_option('bindir'), ) -# Don't use the output configure_file here--that file is in the build directory -# and won't be able to find the python modules in the source directory. -meson.override_find_program('blueprint-compiler', find_program('blueprint-compiler.py')) - -if not meson.is_subproject() - install_subdir('blueprintcompiler', install_dir: datadir / 'blueprint-compiler') +if meson.is_subproject() + meson.override_find_program('blueprint-compiler', blueprint_compiler) +else + install_subdir('blueprintcompiler', install_dir: py.get_install_dir()) endif subdir('tests') From 06f54c8ff8d5f7c78c1d21bcbc59fdd87ce14910 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 5 Mar 2022 17:54:27 -0600 Subject: [PATCH 008/290] Use typelib instead of XML For normal compilation, use .typelib files rather than .gir XML files. This is much faster. Rather than using libgirepository, which would try to actually load the libraries, we use a custom parser. The language server will still read XML because it needs to access documentation, which is not in the typelib, but that's generally fine because it's a long lived process and only has to do that once. --- .gitlab-ci.yml | 2 +- blueprint-compiler.py | 3 +- blueprintcompiler/decompiler.py | 25 +- blueprintcompiler/gir.py | 436 ++++++++++++++++++-------- blueprintcompiler/language/imports.py | 10 +- blueprintcompiler/language/values.py | 4 +- blueprintcompiler/main.py | 7 +- blueprintcompiler/typelib.py | 292 +++++++++++++++++ blueprintcompiler/xml_reader.py | 32 +- tests/fuzz.py | 6 +- 10 files changed, 647 insertions(+), 170 deletions(-) create mode 100644 blueprintcompiler/typelib.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0820f5a..bd40630 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,7 @@ build: - coverage report - coverage html - coverage xml - - meson _build -Ddocs=true + - meson _build -Ddocs=true --prefix=/usr - ninja -C _build - ninja -C _build test - ninja -C _build install diff --git a/blueprint-compiler.py b/blueprint-compiler.py index aa1dc8f..f6a542e 100755 --- a/blueprint-compiler.py +++ b/blueprint-compiler.py @@ -29,6 +29,7 @@ libdir = "@LIBDIR@" if version == "\u0040VERSION@": version = "uninstalled" + libdir = None else: # If Meson set the configuration values, insert the module path it set sys.path.insert(0, module_path) @@ -36,4 +37,4 @@ else: from blueprintcompiler import main if __name__ == "__main__": - main.main(version) + main.main(version, libdir) diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index a7363e5..cd66386 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -122,8 +122,13 @@ class DecompileCtx: self._blocks_need_end[-1] = _CLOSING[line[-1]] self._indent += 1 - def print_attribute(self, name, value, type): + def get_enum_name(value): + for member in type.members.values(): + if member.nick == value or member.c_ident == value: + return member.name + return value.replace('-', '_') + if type is None: self.print(f"{name}: \"{escape_quote(value)}\";") elif type.assignable_to(FloatType()): @@ -141,16 +146,11 @@ class DecompileCtx: self.print(f"{name}: \"{escape_quote(value)}\";") elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")): self.print(f"{name}: {value};") - elif isinstance(type, Enumeration): - for member in type.members.values(): - if member.nick == value or member.c_ident == value: - self.print(f"{name}: {member.name};") - break - else: - self.print(f"{name}: {value.replace('-', '_')};") elif isinstance(type, Bitfield): - flags = re.sub(r"\s*\|\s*", " | ", value).replace("-", "_") - self.print(f"{name}: {flags};") + flags = [get_enum_name(flag) for flag in value.split("|")] + self.print(f"{name}: {' | '.join(flags)};") + elif isinstance(type, Enumeration): + self.print(f"{name}: {get_enum_name(value)};") else: self.print(f"{name}: \"{escape_quote(value)}\";") @@ -171,9 +171,8 @@ def _decompile_element(ctx: DecompileCtx, gir, xml): ctx.start_block() gir = decompiler(ctx, gir, **args) - for child_type in xml.children.values(): - for child in child_type: - _decompile_element(ctx, gir, child) + for child in xml.children: + _decompile_element(ctx, gir, child) ctx.end_block() diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 6234077..1085222 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -22,39 +22,90 @@ import typing as T import os, sys from .errors import CompileError, CompilerBugError -from . import xml_reader +from . import typelib, xml_reader - -extra_search_paths: T.List[str] = [] _namespace_cache = {} - -_search_paths = [] -xdg_data_home = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) -_search_paths.append(os.path.join(xdg_data_home, "gir-1.0")) -xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":") -_search_paths += [os.path.join(dir, "gir-1.0") for dir in xdg_data_dirs] +_xml_cache = {} def get_namespace(namespace, version): - filename = f"{namespace}-{version}.gir" + from .main import LIBDIR, VERSION + search_paths = [] + if LIBDIR is not None: + search_paths.append(os.path.join(LIBDIR, "girepository-1.0")) + + # This is a fragile hack to make blueprint-compiler work uninstalled on + # most systems. + if VERSION == "uninstalled": + search_paths += [ + "/usr/lib/girepository-1.0", + "/usr/local/lib/girepository-1.0", + "/app/lib/girepository-1.0", + "/usr/lib64/girepository-1.0", + "/usr/local/lib64/girepository-1.0", + "/app/lib64/girepository-1.0", + ] + + if typelib_path := os.environ.get("GI_TYPELIB_PATH"): + search_paths.append(typelib_path) + + filename = f"{namespace}-{version}.typelib" if filename not in _namespace_cache: - for search_path in _search_paths: + for search_path in search_paths: path = os.path.join(search_path, filename) if os.path.exists(path) and os.path.isfile(path): - xml = xml_reader.parse(path, xml_reader.PARSE_GIR) - repository = Repository(xml) + tl = typelib.load_typelib(path) + repository = Repository(tl) - _namespace_cache[filename] = repository.namespaces.get(namespace) + _namespace_cache[filename] = repository.namespace break if filename not in _namespace_cache: - raise CompileError(f"Namespace {namespace}-{version} could not be found") + raise CompileError( + f"Namespace {namespace}-{version} could not be found", + hints=["search path: " + os.pathsep.join(search_paths)], + ) return _namespace_cache[filename] +def get_xml(namespace, version): + from .main import VERSION + from xml.etree import ElementTree + search_paths = [] + + # Same fragile hack as before + if VERSION == "uninstalled": + search_paths += [ + "/usr/share/gir-1.0", + "/usr/local/share/gir-1.0", + "/app/share/gir-1.0", + ] + + if data_paths := os.environ.get("XDG_DATA_DIRS"): + search_paths += [os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep)] + + filename = f"{namespace}-{version}.gir" + + if filename not in _xml_cache: + for search_path in search_paths: + path = os.path.join(search_path, filename) + + if os.path.exists(path) and os.path.isfile(path): + _xml_cache[filename] = xml_reader.parse(path) + break + + if filename not in _xml_cache: + raise CompileError( + f"GObject introspection file '{namespace}-{version}.gir' could not be found", + hints=["search path: " + os.pathsep.join(search_paths)], + ) + + return _xml_cache[filename] + + class GirType: @property def doc(self): @@ -115,9 +166,9 @@ _BASIC_TYPES = { } class GirNode: - def __init__(self, container, xml): + def __init__(self, container, tl): self.container = container - self.xml = xml + self.tl = tl def get_containing(self, container_type): if self.container is None: @@ -127,9 +178,15 @@ class GirNode: else: return self.container.get_containing(container_type) + @cached_property + def xml(self): + for el in self.container.xml.children: + if el.attrs.get("name") == self.name: + return el + @cached_property def glib_type_name(self): - return self.xml["glib:type-name"] + return self.tl.OBJ_GTYPE_NAME @cached_property def full_name(self): @@ -140,11 +197,11 @@ class GirNode: @cached_property def name(self) -> str: - return self.xml["name"] + return self.tl.BLOB_NAME @cached_property def cname(self) -> str: - return self.xml["c:type"] + return self.tl.OBJ_GTYPE_NAME @cached_property def available_in(self) -> str: @@ -169,7 +226,7 @@ class GirNode: @property def type_name(self): - return self.xml.get_elements('type')[0]['name'] + return self.type.name @property def type(self): @@ -177,76 +234,164 @@ class GirNode: class Property(GirNode): - def __init__(self, klass, xml: xml_reader.Element): - super().__init__(klass, xml) + def __init__(self, klass, tl: typelib.Typelib): + super().__init__(klass, tl) - @property + @cached_property + def name(self): + return self.tl.PROP_NAME + + @cached_property + def type(self): + return self.get_containing(Repository)._resolve_type_id(self.tl.PROP_TYPE) + + @cached_property def signature(self): return f"{self.type_name} {self.container.name}.{self.name}" @property def writable(self): - return self.xml["writable"] == "1" + return self.tl.PROP_WRITABLE == 1 @property def construct_only(self): - return self.xml["construct-only"] == "1" + return self.tl.PROP_CONSTRUCT_ONLY == 1 class Parameter(GirNode): - def __init__(self, container: GirNode, xml: xml_reader.Element): - super().__init__(container, xml) + def __init__(self, container: GirNode, tl: typelib.Typelib): + super().__init__(container, tl) class Signal(GirNode): - def __init__(self, klass, xml: xml_reader.Element): - super().__init__(klass, xml) - if parameters := xml.get_elements('parameters'): - self.params = [Parameter(self, child) for child in parameters[0].get_elements('parameter')] - else: - self.params = [] + def __init__(self, klass, tl: typelib.Typelib): + super().__init__(klass, tl) + # if parameters := xml.get_elements('parameters'): + # self.params = [Parameter(self, child) for child in parameters[0].get_elements('parameter')] + # else: + # self.params = [] @property def signature(self): - args = ", ".join([f"{p.type_name} {p.name}" for p in self.params]) + # TODO: fix + # args = ", ".join([f"{p.type_name} {p.name}" for p in self.params]) + args = "" return f"signal {self.container.name}.{self.name} ({args})" 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 __init__(self, ns, tl: typelib.Typelib): + super().__init__(ns, tl) + + @cached_property + def properties(self): + n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES + offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE + offset += (n_prerequisites + n_prerequisites % 2) * 2 + n_properties = self.tl.INTERFACE_N_PROPERTIES + property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE + result = {} + for i in range(n_properties): + property = Property(self, self.tl[offset + i * property_size]) + result[property.name] = property + return result + + @cached_property + def signals(self): + n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES + offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE + offset += (n_prerequisites + n_prerequisites % 2) * 2 + offset += self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE + offset += self.tl.INTERFACE_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE + n_signals = self.tl.INTERFACE_N_SIGNALS + property_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE + result = {} + for i in range(n_signals): + signal = Signal(self, self.tl[offset + i * property_size]) + result[signal.name] = signal + return result + + @cached_property + def prerequisites(self): + n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES + result = [] + for i in range(n_prerequisites): + entry = self.tl.INTERFACE_PREREQUISITES[i * 2].AS_DIR_ENTRY + result.append(self.get_containing(Repository)._resolve_dir_entry(entry)) + return result 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): + if pre.assignable_to(other): return True return False class Class(GirNode, GirType): - def __init__(self, ns, xml: xml_reader.Element): - super().__init__(ns, xml) - self._parent = xml["parent"] - self.implements = [impl["name"] for impl in xml.get_elements("implements")] - self.own_properties = {child["name"]: Property(self, child) for child in xml.get_elements("property")} - self.own_signals = {child["name"]: Signal(self, child) for child in xml.get_elements("glib:signal")} + def __init__(self, ns, tl: typelib.Typelib): + super().__init__(ns, tl) @property def abstract(self): - return self.xml["abstract"] == "1" + return self.tl.OBJ_ABSTRACT == 1 - @property + @cached_property + def implements(self): + n_interfaces = self.tl.OBJ_N_INTERFACES + result = [] + for i in range(n_interfaces): + entry = self.tl[self.tl.header.HEADER_OBJECT_BLOB_SIZE + i * 2].AS_DIR_ENTRY + result.append(self.get_containing(Repository)._resolve_dir_entry(entry)) + return result + + @cached_property + def own_properties(self): + n_interfaces = self.tl.OBJ_N_INTERFACES + offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE + offset += (n_interfaces + n_interfaces % 2) * 2 + offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE + offset += self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + n_properties = self.tl.OBJ_N_PROPERTIES + property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE + result = {} + for i in range(n_properties): + property = Property(self, self.tl[offset + i * property_size]) + result[property.name] = property + return result + + @cached_property + def own_signals(self): + n_interfaces = self.tl.OBJ_N_INTERFACES + offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE + offset += (n_interfaces + n_interfaces % 2) * 2 + offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE + offset += self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + offset += self.tl.OBJ_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE + offset += self.tl.OBJ_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE + n_signals = self.tl.OBJ_N_SIGNALS + signal_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE + result = {} + for i in range(n_signals): + signal = Signal(self, self.tl[offset][i * signal_size]) + result[signal.name] = signal + return result + + @cached_property + def parent(self): + if entry := self.tl.OBJ_PARENT: + return self.get_containing(Repository)._resolve_dir_entry(entry) + else: + return None + + @cached_property def signature(self): result = f"class {self.container.name}.{self.name}" if self.parent is not None: result += f" : {self.parent.container.name}.{self.parent.name}" if len(self.implements): - result += " implements " + ", ".join(self.implements) + result += " implements " + ", ".join([impl.full_name for impl in self.implements]) return result @cached_property @@ -257,13 +402,6 @@ class Class(GirNode, GirType): def signals(self): return { s.name: s for s in self._enum_signals() } - @cached_property - def parent(self): - if self._parent is None: - return None - return self.get_containing(Namespace).lookup_type(self._parent) - - def assignable_to(self, other) -> bool: if self == other: return True @@ -271,12 +409,11 @@ class Class(GirNode, GirType): return True else: for iface in self.implements: - if self.get_containing(Namespace).lookup_type(iface).assignable_to(other): + if iface.assignable_to(other): return True return False - def _enum_properties(self): yield from self.own_properties.values() @@ -284,7 +421,7 @@ class Class(GirNode, GirType): yield from self.parent.properties.values() for impl in self.implements: - yield from self.get_containing(Namespace).lookup_type(impl).properties.values() + yield from impl.properties.values() def _enum_signals(self): yield from self.own_signals.values() @@ -293,25 +430,28 @@ class Class(GirNode, GirType): yield from self.parent.signals.values() for impl in self.implements: - yield from self.get_containing(Namespace).lookup_type(impl).signals.values() + yield from impl.signals.values() class EnumMember(GirNode): - def __init__(self, ns, xml: xml_reader.Element): - super().__init__(ns, xml) - self._value = xml["value"] + def __init__(self, ns, tl: typelib.Typelib): + super().__init__(ns, tl) @property def value(self): - return self._value + return self.tl.VALUE_VALUE - @property + @cached_property + def name(self): + return self.tl.VALUE_NAME + + @cached_property def nick(self): - return self.xml["glib:nick"] + return self.name.replace("_", "-") @property def c_ident(self): - return self.xml["c:identifier"] + return self.tl.attr("c:identifier") @property def signature(self): @@ -319,9 +459,19 @@ class EnumMember(GirNode): class Enumeration(GirNode, GirType): - def __init__(self, ns, xml: xml_reader.Element): - super().__init__(ns, xml) - self.members = { child["name"]: EnumMember(self, child) for child in xml.get_elements("member") } + def __init__(self, ns, tl: typelib.Typelib): + super().__init__(ns, tl) + + @cached_property + def members(self): + members = {} + n_values = self.tl.ENUM_N_VALUES + values = self.tl.ENUM_VALUES + value_size = self.tl.header.HEADER_VALUE_BLOB_SIZE + for i in range(n_values): + member = EnumMember(self, values[i * value_size]) + members[member.name] = member + return members @property def signature(self): @@ -331,64 +481,68 @@ class Enumeration(GirNode, GirType): return type == self -class BitfieldMember(GirNode): - def __init__(self, ns, xml: xml_reader.Element): - super().__init__(ns, xml) - self._value = xml["value"] - - @property - def value(self): - return self._value - - @property - def signature(self): - return f"bitfield member {self.full_name} = {bin(self.value)}" - - -class Bitfield(GirNode, GirType): - def __init__(self, ns, xml: xml_reader.Element): - super().__init__(ns, xml) - self.members = { child["name"]: EnumMember(self, child) for child in xml.get_elements("member") } - - @property - def signature(self): - return f"bitfield {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) class Namespace(GirNode): - def __init__(self, repo, xml: xml_reader.Element): - super().__init__(repo, xml) - self.classes = { child["name"]: Class(self, child) for child in xml.get_elements("class") } - self.interfaces = { child["name"]: Interface(self, child) for child in xml.get_elements("interface") } - self.enumerations = { child["name"]: Enumeration(self, child) for child in xml.get_elements("enumeration") } - self.bitfields = { child["name"]: Bitfield(self, child) for child in xml.get_elements("bitfield") } - self.version = xml["version"] + def __init__(self, repo, tl: typelib.Typelib): + super().__init__(repo, tl) + + self.entries: T.Dict[str, GirNode] = {} + + n_local_entries = tl.HEADER_N_ENTRIES + directory = tl.HEADER_DIRECTORY + for i in range(n_local_entries): + entry = directory[i * tl.HEADER_ENTRY_BLOB_SIZE] + entry_name = entry.DIR_ENTRY_NAME + entry_type = entry.DIR_ENTRY_BLOB_TYPE + entry_blob = entry.DIR_ENTRY_OFFSET + + if entry_type == typelib.BLOB_TYPE_ENUM: + self.entries[entry_name] = Enumeration(self, entry_blob) + elif entry_type == typelib.BLOB_TYPE_FLAGS: + self.entries[entry_name] = Bitfield(self, entry_blob) + elif entry_type == typelib.BLOB_TYPE_OBJECT: + self.entries[entry_name] = Class(self, entry_blob) + elif entry_type == typelib.BLOB_TYPE_INTERFACE: + self.entries[entry_name] = Interface(self, entry_blob) + + @cached_property + def xml(self): + return get_xml(self.name, self.version).get_elements("namespace")[0] + + @cached_property + def name(self): + return self.tl.HEADER_NAMESPACE + + @cached_property + def version(self): + return self.tl.HEADER_NSVERSION @property def signature(self): return f"namespace {self.name} {self.version}" + @cached_property + def classes(self): + return { name: entry for name, entry in self.entries.items() if isinstance(entry, Class) } + + @cached_property + def interfaces(self): + return { name: entry for name, entry in self.entries.items() if isinstance(entry, Interface) } def get_type(self, name): """ Gets a type (class, interface, enum, etc.) from this namespace. """ - return ( - self.classes.get(name) - or self.interfaces.get(name) - or self.enumerations.get(name) - or self.bitfields.get(name) - ) - + return self.entries.get(name) def get_type_by_cname(self, cname: str): """ Gets a type from this namespace by its C name. """ - for item in [*self.classes.values(), *self.interfaces.values(), *self.enumerations.values()]: - if item.cname == cname: + for item in self.entries.values(): + if hasattr(item, "cname") and item.cname == cname: return item - def lookup_type(self, type_name: str): """ Looks up a type in the scope of this namespace (including in the namespace's dependencies). """ @@ -403,25 +557,26 @@ class Namespace(GirNode): class Repository(GirNode): - def __init__(self, xml: xml_reader.Element): - super().__init__(None, xml) - self.namespaces = { child["name"]: Namespace(self, child) for child in xml.get_elements("namespace") } + def __init__(self, tl: typelib.Typelib): + super().__init__(None, tl) - try: - self.includes = { include["name"]: get_namespace(include["name"], include["version"]) for include in xml.get_elements("include") } - except: - raise CompilerBugError(f"Failed to load dependencies.") + self.namespace = Namespace(self, tl) + if dependencies := tl[0x24].string: + deps = [tuple(dep.split("-", 1)) for dep in dependencies.split("|")] + try: + self.includes = { name: get_namespace(name, version) for name, version in deps } + except: + raise CompilerBugError(f"Failed to load dependencies.") + else: + self.includes = {} def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: - if namespace := self.namespaces.get(ns): - return namespace.get_type(name) - else: - return self.lookup_namespace(ns).get_type(name) + return self.lookup_namespace(ns).get_type(name) def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: - for ns in self.namespaces.values(): + for ns in [self.namespace, *self.includes.values()]: if type := ns.get_type_by_cname(name): return type return None @@ -429,13 +584,40 @@ class Repository(GirNode): def lookup_namespace(self, ns: str): """ Finds a namespace among this namespace's dependencies. """ - if namespace := self.namespaces.get(ns): - return namespace + if ns == self.namespace.name: + return self.namespace else: for include in self.includes.values(): if namespace := include.get_containing(Repository).lookup_namespace(ns): return namespace + def _resolve_dir_entry(self, dir_entry: typelib.Typelib): + if dir_entry.DIR_ENTRY_LOCAL: + return self.namespace.get_type(dir_entry.DIR_ENTRY_NAME) + else: + ns = dir_entry.DIR_ENTRY_NAMESPACE + return self.lookup_namespace(ns).get_type(dir_entry.DIR_ENTRY_NAME) + + def _resolve_type_id(self, type_id: int): + if type_id & 0xFFFFFF == 0: + type_id = (type_id >> 27) & 0x1F + # simple type + if type_id == typelib.TYPE_BOOLEAN: + return BoolType() + elif type_id in [typelib.TYPE_FLOAT, typelib.TYPE_DOUBLE]: + return FloatType() + elif type_id in [typelib.TYPE_INT8, typelib.TYPE_INT16, typelib.TYPE_INT32, typelib.TYPE_INT64]: + return IntType() + elif type_id in [typelib.TYPE_UINT8, typelib.TYPE_UINT16, typelib.TYPE_UINT32, typelib.TYPE_UINT64]: + return UIntType() + elif type_id == typelib.TYPE_UTF8: + return StringType() + else: + raise CompilerBugError("Unknown type ID", type_id) + else: + return self._resolve_dir_entry(self.tl.header[type_id].INTERFACE_TYPE_INTERFACE) + + class GirContext: def __init__(self): diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index cdef888..682f8cd 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -41,8 +41,14 @@ class GtkDirective(AstNode): try: gir.get_namespace("Gtk", self.tokens["version"]) - except: - raise CompileError("Could not find GTK 4 introspection files. Is gobject-introspection installed?", fatal=True) + except CompileError as e: + raise CompileError( + "Could not find GTK 4 introspection files. Is gobject-introspection installed?", + fatal=True, + # preserve the hints from the original error, because it contains + # useful debugging information + hints=e.hints, + ) @property diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 29e5d98..f39db53 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -153,7 +153,7 @@ class IdentValue(Value): def validate_for_type(self): type = self.parent.value_type - if isinstance(type, gir.Enumeration) or isinstance(type, gir.Bitfield): + 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}", @@ -183,7 +183,7 @@ class IdentValue(Value): @docs() def docs(self): type = self.parent.value_type - if isinstance(type, gir.Enumeration) or isinstance(type, gir.Bitfield): + if isinstance(type, gir.Enumeration): if member := type.members.get(self.tokens["value"]): return member.doc else: diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 1a5e92a..d528e7e 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -28,6 +28,7 @@ from .utils import Colors from .xml_emitter import XmlEmitter VERSION = "uninstalled" +LIBDIR = None class BlueprintApp: def main(self): @@ -144,7 +145,7 @@ class BlueprintApp: return ast.generate(), warnings -def main(version): - global VERSION - VERSION = version +def main(version, libdir): + global VERSION, LIBDIR + VERSION, LIBDIR = version, libdir BlueprintApp().main() diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py new file mode 100644 index 0000000..be1f366 --- /dev/null +++ b/blueprintcompiler/typelib.py @@ -0,0 +1,292 @@ +# typelib.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 + + +import typing as T +import math +from ctypes import * +import mmap, os + +from .errors import CompilerBugError + + +BLOB_TYPE_ENUM = 5 +BLOB_TYPE_FLAGS = 6 +BLOB_TYPE_OBJECT = 7 +BLOB_TYPE_INTERFACE = 8 + +TYPE_VOID = 0 +TYPE_BOOLEAN = 1 +TYPE_INT8 = 2 +TYPE_UINT8 = 3 +TYPE_INT16 = 4 +TYPE_UINT16 = 5 +TYPE_INT32 = 6 +TYPE_UINT32 = 7 +TYPE_INT64 = 8 +TYPE_UINT64 = 9 +TYPE_FLOAT = 10 +TYPE_DOUBLE = 11 +TYPE_GTYPE = 12 +TYPE_UTF8 = 13 +TYPE_FILENAME = 14 +TYPE_ARRAY = 15 +TYPE_INTERFACE = 16 +TYPE_GLIST = 17 +TYPE_GSLIST = 18 +TYPE_GHASH = 19 +TYPE_ERROR = 20 +TYPE_UNICHAR = 21 + + +class Field: + def __init__(self, offset, type, shift=0, mask=None): + self._offset = offset + self._type = type + self._shift = shift + self._mask = (1 << mask) - 1 if mask else None + self._name = f"{offset}__{type}__{shift}__{mask}" + + def __get__(self, typelib, _objtype=None): + if typelib is None: + return self + + def shift_mask(n): + n = n >> self._shift + if self._mask: + n = n & self._mask + return n + + tl = typelib[self._offset] + if self._type == "u8": + return shift_mask(tl.u8) + elif self._type == "u16": + return shift_mask(tl.u16) + elif self._type == "u32": + return shift_mask(tl.u32) + elif self._type == "i8": + return shift_mask(tl.i8) + elif self._type == "i16": + return shift_mask(tl.i16) + elif self._type == "i32": + return shift_mask(tl.i32) + elif self._type == "pointer": + return tl.header[tl.u32] + elif self._type == "offset": + return tl + elif self._type == "string": + return tl.string + elif self._type == "dir_entry": + return tl.header.dir_entry(tl.u16) + else: + raise CompilerBugError(self._type) + + +class Typelib: + AS_DIR_ENTRY = Field(0, "dir_entry") + + HEADER_N_ENTRIES = Field(0x14, "u16") + HEADER_N_LOCAL_ENTRIES = Field(0x16, "u16") + HEADER_DIRECTORY = Field(0x18, "pointer") + HEADER_N_ATTRIBUTES = Field(0x1C, "u32") + HEADER_ATTRIBUTES = Field(0x20, "pointer") + + HEADER_DEPENDENCIES = Field(0x24, "pointer") + + HEADER_NAMESPACE = Field(0x2C, "string") + HEADER_NSVERSION = Field(0x30, "string") + + HEADER_ENTRY_BLOB_SIZE = Field(0x3C, "u16") + HEADER_FUNCTION_BLOB_SIZE = Field(0x3E, "u16") + HEADER_CALLBACK_BLOB_SIZE = Field(0x40, "u16") + HEADER_SIGNAL_BLOB_SIZE = Field(0x42, "u16") + HEADER_PROPERTY_BLOB_SIZE = Field(0x48, "u16") + HEADER_FIELD_BLOB_SIZE = Field(0x4A, "u16") + HEADER_VALUE_BLOB_SIZE = Field(0x4C, "u16") + HEADER_ATTRIBUTE_BLOB_SIZE = Field(0x4E, "u16") + HEADER_ENUM_BLOB_SIZE = Field(0x56, "u16") + HEADER_OBJECT_BLOB_SIZE = Field(0x5A, "u16") + HEADER_INTERFACE_BLOB_SIZE = Field(0x5C, "u16") + + DIR_ENTRY_BLOB_TYPE = Field(0x0, "u16") + DIR_ENTRY_LOCAL = Field(0x2, "u16", 0, 1) + DIR_ENTRY_NAME = Field(0x4, "string") + DIR_ENTRY_OFFSET = Field(0x8, "pointer") + DIR_ENTRY_NAMESPACE = Field(0x8, "string") + + ATTR_OFFSET = Field(0x0, "u32") + ATTR_NAME = Field(0x0, "string") + ATTR_VALUE = Field(0x0, "string") + + INTERFACE_TYPE_INTERFACE = Field(0x2, "dir_entry") + + BLOB_NAME = Field(0x4, "string") + + ENUM_GTYPE_NAME = Field(0x8, "string") + ENUM_N_VALUES = Field(0x10, "u16") + ENUM_N_METHODS = Field(0x12, "u16") + ENUM_VALUES = Field(0x18, "offset") + + INTERFACE_GTYPE_NAME = Field(0x8, "string") + INTERFACE_N_PREREQUISITES = Field(0x12, "u16") + INTERFACE_N_PROPERTIES = Field(0x14, "u16") + INTERFACE_N_METHODS = Field(0x16, "u16") + INTERFACE_N_SIGNALS = Field(0x18, "u16") + INTERFACE_N_VFUNCS = Field(0x1A, "u16") + INTERFACE_N_CONSTANTS = Field(0x1C, "u16") + INTERFACE_PREREQUISITES = Field(0x28, "offset") + + OBJ_DEPRECATED = Field(0x02, "u16", 0, 1) + OBJ_ABSTRACT = Field(0x02, "u16", 1, 1) + OBJ_FUNDAMENTAL = Field(0x02, "u16", 2, 1) + OBJ_FINAL = Field(0x02, "u16", 3, 1) + OBJ_GTYPE_NAME = Field(0x08, "string") + OBJ_PARENT = Field(0x10, "dir_entry") + OBJ_GTYPE_STRUCT = Field(0x14, "string") + OBJ_N_INTERFACES = Field(0x14, "u16") + OBJ_N_FIELDS = Field(0x16, "u16") + OBJ_N_PROPERTIES = Field(0x18, "u16") + OBJ_N_METHODS = Field(0x1A, "u16") + OBJ_N_SIGNALS = Field(0x1C, "u16") + OBJ_N_VFUNCS = Field(0x1E, "u16") + OBJ_N_CONSTANTS = Field(0x20, "u16") + OBJ_N_FIELD_CALLBACKS = Field(0x22, "u16") + + PROP_NAME = Field(0x0, "string") + PROP_DEPRECATED = Field(0x4, "u32", 0, 1) + PROP_READABLE = Field(0x4, "u32", 1, 1) + PROP_WRITABLE = Field(0x4, "u32", 2, 1) + PROP_CONSTRUCT = Field(0x4, "u32", 3, 1) + PROP_CONSTRUCT_ONLY = Field(0x4, "u32", 4, 1) + PROP_TYPE = Field(0xC, "u32") + + VALUE_NAME = Field(0x4, "string") + VALUE_VALUE = Field(0x8, "i32") + + def __init__(self, typelib_file, offset): + self._typelib_file = typelib_file + self._offset = offset + + def __getitem__(self, index): + return Typelib(self._typelib_file, self._offset + index) + + def attr(self, name): + return self.header.attr(self._offset, name) + + @property + def header(self): + return TypelibHeader(self._typelib_file) + + @property + def u8(self): + """Gets the 8-bit unsigned int at this location.""" + return self._int(1, False) + + @property + def u16(self): + """Gets the 16-bit unsigned int at this location.""" + return self._int(2, False) + + @property + def u32(self): + """Gets the 32-bit unsigned int at this location.""" + return self._int(4, False) + + @property + def i8(self): + """Gets the 8-bit unsigned int at this location.""" + return self._int(1, True) + + @property + def i16(self): + """Gets the 16-bit unsigned int at this location.""" + return self._int(2, True) + + @property + def i32(self): + """Gets the 32-bit unsigned int at this location.""" + return self._int(4, True) + + @property + def string(self) -> T.Optional[str]: + """Interprets the 32-bit unsigned int at this location as a pointer + within the typelib file, and returns the null-terminated string at that + pointer.""" + + loc = self.u32 + if loc == 0: + return None + + end = loc + while self._typelib_file[end] != 0: + end += 1 + return self._typelib_file[loc:end].decode("utf-8") + + def _int(self, size, signed): + return int.from_bytes(self._typelib_file[self._offset:self._offset + size], 'little') + + +class TypelibHeader(Typelib): + def __init__(self, typelib_file): + super().__init__(typelib_file, 0) + + def dir_entry(self, index): + if index == 0: + return None + else: + return self.HEADER_DIRECTORY[(index - 1) * self.HEADER_ENTRY_BLOB_SIZE] + + def attr(self, offset, name): + lower = 0 + upper = self.HEADER_N_ATTRIBUTES + attr_size = self.HEADER_ATTRIBUTE_BLOB_SIZE + attrs = self.HEADER_ATTRIBUTES + mid = 0 + + while lower <= upper: + mid = math.floor((upper + lower) / 2) + attr = attrs[mid * attr_size] + if attr.ATTR_OFFSET < offset: + lower = mid + 1 + elif attr.ATTR_OFFSET > offset: + upper = mid - 1 + else: + while mid >= 0 and attrs[(mid - 1) * attr_size].ATTR_OFFSET == offset: + mid -= 1 + break + if attrs[mid * attr_size].ATTR_OFFSET != offset: + # no match found + return None + while attrs[mid * attr_size].ATTR_OFFSET == offset: + if attrs[mid * attr_size].ATTR_NAME == name: + return attrs[mid * attr_size].ATTR_VALUE + mid += 1 + return None + + def attr_by_index(self, index): + pass + + @property + def dir_entries(self): + return [self.dir_entry(i) for i in range(self[0x16].u16)] + + +def load_typelib(path: str) -> Typelib: + with open(path, "rb") as f: + return Typelib(f.read(), 0) diff --git a/blueprintcompiler/xml_reader.py b/blueprintcompiler/xml_reader.py index 877d20a..24ae5ff 100644 --- a/blueprintcompiler/xml_reader.py +++ b/blueprintcompiler/xml_reader.py @@ -36,7 +36,7 @@ class Element: def __init__(self, tag, attrs: T.Dict[str, str]): self.tag = tag self.attrs = attrs - self.children: T.Dict[str, T.List["Element"]] = defaultdict(list) + self.children: T.List["Element"] = [] self.cdata_chunks: T.List[str] = [] @cached_property @@ -44,50 +44,42 @@ class Element: return ''.join(self.cdata_chunks) def get_elements(self, name) -> T.List["Element"]: - return self.children.get(name, []) + return [ + child + for child in self.children + if child.tag == name + ] def __getitem__(self, key): return self.attrs.get(key) class Handler(sax.handler.ContentHandler): - def __init__(self, parse_type): + def __init__(self): self.root = None self.stack = [] - self.skipping = 0 - self._interesting_elements = parse_type def startElement(self, name, attrs): - if self._interesting_elements is not None and name not in self._interesting_elements: - self.skipping += 1 - if self.skipping > 0: - return - element = Element(name, attrs.copy()) if len(self.stack): last = self.stack[-1] - last.children[name].append(element) + last.children.append(element) else: self.root = element self.stack.append(element) - def endElement(self, name): - if self.skipping == 0: - self.stack.pop() - if self._interesting_elements is not None and name not in self._interesting_elements: - self.skipping -= 1 + self.stack.pop() def characters(self, content): - if not self.skipping: - self.stack[-1].cdata_chunks.append(content) + self.stack[-1].cdata_chunks.append(content) -def parse(filename, parse_type=None): +def parse(filename): parser = sax.make_parser() - handler = Handler(parse_type) + handler = Handler() parser.setContentHandler(handler) parser.parse(filename) return handler.root diff --git a/tests/fuzz.py b/tests/fuzz.py index 17f2eeb..4f7c879 100644 --- a/tests/fuzz.py +++ b/tests/fuzz.py @@ -3,7 +3,7 @@ from pythonfuzz.main import PythonFuzz sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -from blueprintcompiler import tokenizer, parser, decompiler +from blueprintcompiler import tokenizer, parser, decompiler, gir from blueprintcompiler.completions import complete from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError, CompilerBugError from blueprintcompiler.tokenizer import Token, TokenType, tokenize @@ -27,4 +27,8 @@ def fuzz(buf): pass if __name__ == "__main__": + # Make sure Gtk 4.0 is accessible, otherwise every test will fail on that + # and nothing interesting will be tested + gir.get_namespace("Gtk", "4.0") + fuzz() From c5f2e4ed4b06cdea568cb57f71968b27ada1705a Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 24 Jun 2022 17:16:51 -0500 Subject: [PATCH 009/290] gir: Gracefully handle missing .gir files They aren't required for compilation anymore, and not being able to show documentation on hover is probably not worth crashing the language server over. --- blueprintcompiler/gir.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 1085222..2515ab1 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -214,9 +214,14 @@ class GirNode: if self.signature: sections.append("```\n" + self.signature + "\n```") - el = self.xml.get_elements("doc") - if len(el) == 1: - sections.append(el[0].cdata.strip()) + try: + el = self.xml.get_elements("doc") + if len(el) == 1: + sections.append(el[0].cdata.strip()) + except: + # Not a huge deal, but if you want docs in the language server you + # should ensure .gir files are installed + pass return "\n\n---\n\n".join(sections) From cfae48a65acb92e3cc5679b75c1688e4e391c8c5 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 24 Jun 2022 17:25:56 -0500 Subject: [PATCH 010/290] docs: Add notes for distro packagers --- docs/index.rst | 1 + docs/packaging.rst | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 docs/packaging.rst diff --git a/docs/index.rst b/docs/index.rst index 1b2ec6e..8d560e5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces. translations flatpak examples + packaging .. code-block:: diff --git a/docs/packaging.rst b/docs/packaging.rst new file mode 100644 index 0000000..b0cd4e8 --- /dev/null +++ b/docs/packaging.rst @@ -0,0 +1,39 @@ +==================== +For Distro Packagers +==================== + +blueprint-compiler is a build tool that converts UI definitions written in +Blueprint into XML files that are installed with the app and that GTK can read. +So for most applications that use blueprint-compiler, it is a build dependency. +It is a Python program, but like most GNOME-related projects, it uses +`Meson `_ as its build system. + +GObject Introspection +~~~~~~~~~~~~~~~~~~~~~ + +Blueprint files can import GObject Introspection namespaces like this: + +.. code-block:: + + using Gtk 4.0; + using Adw 1; + +To compile a blueprint file, ``.typelib`` files for all of the imported +namespaces must be installed. All blueprint files must import Gtk 4.0, so +``Gtk-4.0.typelib`` is effectively a runtime dependency of blueprint-compiler. + +So, if a package uses blueprint-compiler, its build dependencies should include +the typelib files for any namespaces imported in its blueprint files. (Note +that many apps also have the same typelib files as runtime dependencies, +separately from blueprint). + +The search path for typelib files is defined by the +`libdir `_ that Meson +uses. Additional paths may be passed at runtime through the ``GI_TYPELIB_PATH`` +environment variable. + +In addition, the blueprint language server uses ``.gir`` files to provide +documentation on hover. Some distros package these files separately from the +main package (e.g. in a ``-devel`` package). The language server will not crash +if these files are not present, but for a good user experience you should make +sure they are installed. \ No newline at end of file From 68610a7dbabb30a788d99a1e9a1556aa578d1dcd Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 28 Jun 2022 23:43:01 -0500 Subject: [PATCH 011/290] typelib: Use GIRepository to find typelib path --- blueprintcompiler/gir.py | 32 +++++--------------------------- docs/packaging.rst | 7 ++----- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 2515ab1..2b09027 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -21,6 +21,10 @@ from functools import cached_property import typing as T import os, sys +import gi # type: ignore +gi.require_version("GIRepository", "2.0") +from gi.repository import GIRepository # type: ignore + from .errors import CompileError, CompilerBugError from . import typelib, xml_reader @@ -29,25 +33,7 @@ _xml_cache = {} def get_namespace(namespace, version): - from .main import LIBDIR, VERSION - search_paths = [] - if LIBDIR is not None: - search_paths.append(os.path.join(LIBDIR, "girepository-1.0")) - - # This is a fragile hack to make blueprint-compiler work uninstalled on - # most systems. - if VERSION == "uninstalled": - search_paths += [ - "/usr/lib/girepository-1.0", - "/usr/local/lib/girepository-1.0", - "/app/lib/girepository-1.0", - "/usr/lib64/girepository-1.0", - "/usr/local/lib64/girepository-1.0", - "/app/lib64/girepository-1.0", - ] - - if typelib_path := os.environ.get("GI_TYPELIB_PATH"): - search_paths.append(typelib_path) + search_paths = GIRepository.Repository.get_search_path() filename = f"{namespace}-{version}.typelib" @@ -76,14 +62,6 @@ def get_xml(namespace, version): from xml.etree import ElementTree search_paths = [] - # Same fragile hack as before - if VERSION == "uninstalled": - search_paths += [ - "/usr/share/gir-1.0", - "/usr/local/share/gir-1.0", - "/app/share/gir-1.0", - ] - if data_paths := os.environ.get("XDG_DATA_DIRS"): search_paths += [os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep)] diff --git a/docs/packaging.rst b/docs/packaging.rst index b0cd4e8..4f248e6 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -21,17 +21,14 @@ Blueprint files can import GObject Introspection namespaces like this: To compile a blueprint file, ``.typelib`` files for all of the imported namespaces must be installed. All blueprint files must import Gtk 4.0, so ``Gtk-4.0.typelib`` is effectively a runtime dependency of blueprint-compiler. +blueprint-compiler also depends on pygobject, because it uses GIRepository +to determine the search path for typelib files. So, if a package uses blueprint-compiler, its build dependencies should include the typelib files for any namespaces imported in its blueprint files. (Note that many apps also have the same typelib files as runtime dependencies, separately from blueprint). -The search path for typelib files is defined by the -`libdir `_ that Meson -uses. Additional paths may be passed at runtime through the ``GI_TYPELIB_PATH`` -environment variable. - In addition, the blueprint language server uses ``.gir`` files to provide documentation on hover. Some distros package these files separately from the main package (e.g. in a ``-devel`` package). The language server will not crash From 12c9a434f10912c395bd71dd093472e9951dab8b Mon Sep 17 00:00:00 2001 From: James Westman Date: Wed, 29 Jun 2022 02:24:06 -0500 Subject: [PATCH 012/290] ci: Install pygobject in CI image --- build-aux/Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build-aux/Dockerfile b/build-aux/Dockerfile index f2f168e..4b9e0af 100644 --- a/build-aux/Dockerfile +++ b/build-aux/Dockerfile @@ -1,9 +1,8 @@ FROM fedora:latest -RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel libadwaita-devel python3-devel +RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel \ + libadwaita-devel python3-devel python3-gobject git RUN pip3 install furo mypy sphinx coverage # The version on PyPI is very old and doesn't install. Use the upstream package registry instead. -RUN pip install pythonfuzz --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple - -RUN dnf install -y git +RUN pip install pythonfuzz --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple \ No newline at end of file From 9542f72ce2b515af6649e97463f5180c7c27fe32 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 8 Jul 2022 10:29:46 -0500 Subject: [PATCH 013/290] Ci: Update coverage configuration --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bd40630..79cac6a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,7 +26,9 @@ build: - _build - htmlcov reports: - cobertura: coverage.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml fuzz: image: registry.gitlab.gnome.org/jwestman/blueprint-compiler From f6eacaa3d904b3c5e43f18900b11a48022ab84ab Mon Sep 17 00:00:00 2001 From: Alexander Bisono Date: Sat, 25 Jun 2022 20:15:43 -0400 Subject: [PATCH 014/290] Add emacs major mode to 'Editor Plugins' --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ed0cb8f..ad1f204 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ template ShumateDemoWindow : Gtk.ApplicationWindow { - [Syntax highlighting by thetek42](https://github.com/thetek42/vim-blueprint-syntax) - [Syntax highlighting by gabmus](https://gitlab.com/gabmus/vim-blueprint) +### GNU Emacs + +- [Major mode by DrBluefall](https://github.com/DrBluefall/blueprint-mode) + ## License Copyright (C) 2021 James Westman From b9fdc5a5f1f7e00598e01e005d4d8c6ac0b9ccdb Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 9 Jul 2022 14:47:07 -0500 Subject: [PATCH 015/290] Fix action widgets in templates Fixes #69. --- blueprintcompiler/language/gobject_object.py | 8 +++++--- blueprintcompiler/language/gtkbuilder_template.py | 5 +---- tests/samples/action_widgets.blp | 2 +- tests/samples/action_widgets.ui | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index e36e4ce..e3960ab 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -97,13 +97,15 @@ class Object(AstNode): if child.response_id ] - def emit_xml(self, xml: XmlEmitter): - from .gtkbuilder_child import Child - + def emit_start_tag(self, xml: XmlEmitter): xml.start_tag("object", **{ "class": self.gir_class or self.tokens["class_name"], "id": self.tokens["id"], }) + + def emit_xml(self, xml: XmlEmitter): + self.emit_start_tag(xml) + for child in self.children: child.emit_xml(xml) diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 0de7381..7806285 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -41,15 +41,12 @@ class Template(Object): def unique_in_parent(self): self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",) - def emit_xml(self, xml: XmlEmitter): + def emit_start_tag(self, xml: XmlEmitter): xml.start_tag( "template", **{"class": self.tokens["name"]}, parent=self.gir_class or self.tokens["class_name"] ) - for child in self.children: - child.emit_xml(xml) - xml.end_tag() @decompiler("template") diff --git a/tests/samples/action_widgets.blp b/tests/samples/action_widgets.blp index 2d4d6ae..34293a0 100644 --- a/tests/samples/action_widgets.blp +++ b/tests/samples/action_widgets.blp @@ -1,6 +1,6 @@ using Gtk 4.0; -Dialog { +template MyDialog : Dialog { [action response=cancel] Button cancel_button { label: _("Cancel"); diff --git a/tests/samples/action_widgets.ui b/tests/samples/action_widgets.ui index 91b6e64..8c41bb2 100644 --- a/tests/samples/action_widgets.ui +++ b/tests/samples/action_widgets.ui @@ -1,7 +1,7 @@ - + From 2da6be7618f4052352479e3fb556774efd1e4eab Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sat, 9 Jul 2022 21:04:28 +0200 Subject: [PATCH 016/290] lsp: Fix crash when import version missing The issue is specific to the language server, since it's trying to use an AST that contains errors. The test would not fail but was added anyway. --- CONTRIBUTING.md | 9 +++++++++ blueprintcompiler/language/imports.py | 7 ++++--- tests/sample_errors/no_import_version.blp | 1 + tests/sample_errors/no_import_version.err | 1 + tests/test_samples.py | 1 + 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 tests/sample_errors/no_import_version.blp create mode 100644 tests/sample_errors/no_import_version.err diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b7690a2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +First of all, thank you for contributing to Blueprint. + +If you learn something useful, please add it to this file. + +# Run the test suite + +``` +python -m unittest +``` diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index 682f8cd..4fac192 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -31,16 +31,17 @@ class GtkDirective(AstNode): @validate("version") def gtk_version(self): - if self.tokens["version"] not in ["4.0"]: + version = self.tokens["version"] + if version not in ["4.0"]: err = CompileError("Only GTK 4 is supported") - if self.tokens["version"].startswith("4"): + if version and 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 try: - gir.get_namespace("Gtk", self.tokens["version"]) + gir.get_namespace("Gtk", version) except CompileError as e: raise CompileError( "Could not find GTK 4 introspection files. Is gobject-introspection installed?", diff --git a/tests/sample_errors/no_import_version.blp b/tests/sample_errors/no_import_version.blp new file mode 100644 index 0000000..665ce62 --- /dev/null +++ b/tests/sample_errors/no_import_version.blp @@ -0,0 +1 @@ +using Gtk \ No newline at end of file diff --git a/tests/sample_errors/no_import_version.err b/tests/sample_errors/no_import_version.err new file mode 100644 index 0000000..4ee792f --- /dev/null +++ b/tests/sample_errors/no_import_version.err @@ -0,0 +1 @@ +1,10,0,Expected a version number for GTK diff --git a/tests/test_samples.py b/tests/test_samples.py index 84cdb4f..ea6b2ea 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -188,6 +188,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("gtk_exact_version") self.assert_sample_error("invalid_bool") self.assert_sample_error("layout_in_non_widget") + self.assert_sample_error("no_import_version") self.assert_sample_error("ns_not_imported") self.assert_sample_error("not_a_class") self.assert_sample_error("object_dne") From 012fc6192671e835e3df7f5c0f82088da1ebf111 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sat, 9 Jul 2022 21:31:54 +0200 Subject: [PATCH 017/290] Update documentation --- README.md | 28 +++++++++++++++++----------- docs/index.rst | 21 +++++++++++++++------ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ad1f204..503d7a3 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,31 @@ template ShumateDemoWindow : Gtk.ApplicationWindow { } ``` -## Editor plugins +## Editors -### Vim +[Workbench](https://github.com/sonnyp/Workbench) and [GNOME Builder](https://apps.gnome.org/app/org.gnome.Builder/) have builtin support for Blueprint. + +Vim - [Syntax highlighting by thetek42](https://github.com/thetek42/vim-blueprint-syntax) - [Syntax highlighting by gabmus](https://gitlab.com/gabmus/vim-blueprint) -### GNU Emacs +GNU Emacs - [Major mode by DrBluefall](https://github.com/DrBluefall/blueprint-mode) +Visual Studio Code + +- [Blueprint Language Plugin by bodil](https://github.com/bodil/vscode-blueprint) + +## Donate + +You can support my work on GitHub Sponsors! + +## Getting in Touch + +Matrix room: [#blueprint-language:matrix.org](https://matrix.to/#/#blueprint-language:matrix.org) + ## License Copyright (C) 2021 James Westman @@ -92,11 +106,3 @@ 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 . - -## Donate - -You can support my work on GitHub Sponsors! - -## Getting in Touch - -Matrix room: [#blueprint-language:matrix.org](https://matrix.to/#/#blueprint-language:matrix.org) \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 8d560e5..f35e9fd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,25 +51,34 @@ Features - **Modern tooling.** IDE integration for `GNOME Builder `_ is in progress, and a VS Code extension is also planned. +Links +----- + +- `Source code `_ +- `Vim syntax highlighting plugin by thetek42 `_ +- `Vim syntax highlighting plugin by gabmus `_ +- `GNU Emacs major mode by DrBluefall `_ +- `Visual Studio Code plugin by bodil `_ + Built with Blueprint -------------------- +- `Dialect `_ - `Extension Manager `_ +- `favagtk `_ - `Feeds `_ - `Geopard `_ - `Giara `_ - `Health `_ - `HydraPaper `_ - `Identity `_ +- `Login Manager Settings `_ +- `Paper `_ +- `Passes `_ - `Plitki `_ - `Solanum `_ +- `Swatch `_ - `Text Pieces `_ - `Video Trimmer `_ - `WhatIP `_ - `Workbench `_ - -Links ------ - -- `Source code `_ -- `Vim syntax highlighting plugin `_ From 0a0389b1f8bdd918f6716cd7e9c9531f04c0da2c Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 25 Jun 2022 01:04:41 -0500 Subject: [PATCH 018/290] grammar: Create an AST node for type names --- blueprintcompiler/completions.py | 3 +- blueprintcompiler/gir.py | 17 +-- blueprintcompiler/language/common.py | 1 - blueprintcompiler/language/gobject_object.py | 43 +------- .../language/gtkbuilder_template.py | 18 +++- blueprintcompiler/language/types.py | 101 ++++++++++++++++++ blueprintcompiler/parser.py | 1 - blueprintcompiler/parser_utils.py | 36 ------- tests/sample_errors/class_dne.err | 2 +- tests/sample_errors/not_a_class.err | 2 +- 10 files changed, 127 insertions(+), 97 deletions(-) create mode 100644 blueprintcompiler/language/types.py delete mode 100644 blueprintcompiler/parser_utils.py diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index e030961..7566f18 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -20,6 +20,7 @@ import typing as T from . import gir, language +from .language.types import ClassName from .ast_utils import AstNode from .completions_utils import * from .lsp_utils import Completion, CompletionItemKind @@ -140,7 +141,7 @@ def signal_completer(ast_node, match_variables): if not isinstance(ast_node.parent, language.Object): name = "on" else: - name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.tokens["class_name"].lower()) + name = "on_" + (ast_node.parent.children[ClassName][0].tokens["id"] or ast_node.parent.children[ClassName][0].tokens["class_name"].lower()) yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;") diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 2b09027..3786078 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -651,24 +651,17 @@ class GirContext: did_you_mean=(ns, self.namespaces.keys()), ) + def validate_type(self, name: str, ns: str): + """ Raises an exception if there is a problem looking up the given type. """ - def validate_class(self, name: str, ns: str): - """ Raises an exception if there is a problem looking up the given - class (it doesn't exist, it isn't a class, etc.) """ - - ns = ns or "Gtk" self.validate_ns(ns) type = self.get_type(name, ns) + ns = ns or "Gtk" + if type is None: raise CompileError( - f"Namespace {ns} does not contain a class called {name}", + f"Namespace {ns} does not contain a type called {name}", did_you_mean=(name, self.namespaces[ns].classes.keys()), ) - elif not isinstance(type, Class): - raise CompileError( - f"{ns}.{name} is not a class", - did_you_mean=(name, self.namespaces[ns].classes.keys()), - ) - diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 03cb428..deeb5fc 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -27,7 +27,6 @@ from ..decompiler import DecompileCtx, decompiler from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * -from ..parser_utils import * from ..xml_emitter import XmlEmitter diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index e3960ab..9a78667 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -23,6 +23,7 @@ from functools import cached_property from .common import * from .response_id import ResponseId +from .types import ClassName, ConcreteClassName class ObjectContent(AstNode): @@ -38,50 +39,14 @@ class ObjectContent(AstNode): class Object(AstNode): grammar: T.Any = [ - class_name, + ConcreteClassName, 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"]) - - @validate("namespace", "class_name") - def not_abstract(self): - if self.gir_class is not None and self.gir_class.abstract: - raise CompileError( - f"{self.gir_class.full_name} can't be instantiated because it's abstract", - hints=[f"did you mean to use a subclass of {self.gir_class.full_name}?"] - ) - - @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 + return self.children[ClassName][0].gir_type @cached_property def action_widgets(self) -> T.List[ResponseId]: @@ -99,7 +64,7 @@ class Object(AstNode): def emit_start_tag(self, xml: XmlEmitter): xml.start_tag("object", **{ - "class": self.gir_class or self.tokens["class_name"], + "class": self.children[ClassName][0].glib_type_name, "id": self.tokens["id"], }) diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 7806285..ffee11c 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -20,6 +20,7 @@ from .gobject_object import Object, ObjectContent from .common import * +from .types import ClassName class Template(Object): @@ -28,24 +29,31 @@ class Template(Object): UseIdent("name").expected("template class name"), Optional([ Match(":"), - class_name.expected("parent class"), + to_parse_node(ClassName).expected("parent class"), ]), ObjectContent, ] - @validate() - def not_abstract(self): - pass # does not apply to templates + @property + def gir_class(self): + # Templates might not have a parent class defined + if len(self.children[ClassName]): + return self.children[ClassName][0].gir_type @validate("name") def unique_in_parent(self): self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",) def emit_start_tag(self, xml: XmlEmitter): + if len(self.children[ClassName]): + parent = self.children[ClassName][0].glib_type_name + else: + parent = None + xml.start_tag( "template", **{"class": self.tokens["name"]}, - parent=self.gir_class or self.tokens["class_name"] + parent=parent ) diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py new file mode 100644 index 0000000..0d6b38c --- /dev/null +++ b/blueprintcompiler/language/types.py @@ -0,0 +1,101 @@ +# types.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 + + +import typing as T +from .common import * +from ..gir import Class, Interface + + +class TypeName(AstNode): + grammar = AnyOf( + [ + UseIdent("namespace"), + ".", + UseIdent("class_name"), + ], + [ + ".", + UseIdent("class_name"), + UseLiteral("ignore_gir", True), + ], + UseIdent("class_name"), + ) + + @validate("class_name") + def type_exists(self): + if not self.tokens["ignore_gir"] and self.gir_ns is not None: + self.root.gir.validate_type(self.tokens["class_name"], self.tokens["namespace"]) + + @validate("namespace") + def gir_ns_exists(self): + if not self.tokens["ignore_gir"]: + self.root.gir.validate_ns(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_type(self) -> T.Optional[gir.Class]: + if self.tokens["class_name"] and not self.tokens["ignore_gir"]: + return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"]) + return None + + @property + def glib_type_name(self) -> str: + if gir_type := self.gir_type: + return gir_type.glib_type_name + else: + return self.tokens["class_name"] + + @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_type: + return self.gir_type.doc + + def emit_xml(self, xml: XmlEmitter): + pass + + +class ClassName(TypeName): + @validate("namespace", "class_name") + def gir_class_exists(self): + if self.gir_type is not None and not isinstance(self.gir_type, Class): + if isinstance(self.gir_type, Interface): + raise CompileError(f"{self.gir_type.full_name} is an interface, not a class") + else: + raise CompileError(f"{self.gir_type.full_name} is not a class") + + +class ConcreteClassName(ClassName): + @validate("namespace", "class_name") + def not_abstract(self): + if isinstance(self.gir_type, Class) and self.gir_type.abstract: + raise CompileError( + f"{self.gir_type.full_name} can't be instantiated because it's abstract", + hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"] + ) + diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 6064481..7c62ae6 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -20,7 +20,6 @@ from .errors import MultipleErrors, PrintableError from .parse_tree import * -from .parser_utils import * from .tokenizer import TokenType from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI diff --git a/blueprintcompiler/parser_utils.py b/blueprintcompiler/parser_utils.py deleted file mode 100644 index af951fe..0000000 --- a/blueprintcompiler/parser_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -# parser_utils.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 - - -from .parse_tree import * - - -class_name = AnyOf( - [ - UseIdent("namespace"), - ".", - UseIdent("class_name"), - ], - [ - ".", - UseIdent("class_name"), - UseLiteral("ignore_gir", True), - ], - UseIdent("class_name"), -) diff --git a/tests/sample_errors/class_dne.err b/tests/sample_errors/class_dne.err index 573baf3..57d74f8 100644 --- a/tests/sample_errors/class_dne.err +++ b/tests/sample_errors/class_dne.err @@ -1 +1 @@ -3,29,13,Namespace Gtk does not contain a class called NotARealClass +3,29,13,Namespace Gtk does not contain a type called NotARealClass diff --git a/tests/sample_errors/not_a_class.err b/tests/sample_errors/not_a_class.err index d0a7260..c69f602 100644 --- a/tests/sample_errors/not_a_class.err +++ b/tests/sample_errors/not_a_class.err @@ -1 +1 @@ -3,29,10,Gtk.Orientable is not a class +3,25,14,Gtk.Orientable is an interface, not a class From 664fa2250b02f64f2c1d8afd82801ab01e353c78 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 9 Jul 2022 16:40:02 -0500 Subject: [PATCH 019/290] Validate that an object can have children Fixes #32. --- .../language/gtkbuilder_child.py | 20 +++++++++++++++++++ tests/sample_errors/children.blp | 5 +++++ tests/sample_errors/children.err | 1 + tests/test_samples.py | 1 + 4 files changed, 27 insertions(+) create mode 100644 tests/sample_errors/children.blp create mode 100644 tests/sample_errors/children.err diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 83fadf7..72843a1 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -24,6 +24,10 @@ from .gobject_object import Object from .response_id import ResponseId from .common import * +ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ + ("Gtk", "Buildable"), + ("Gio", "ListStore") +] class Child(AstNode): grammar = [ @@ -37,6 +41,22 @@ class Child(AstNode): Object, ] + @validate() + def parent_can_have_child(self): + if gir_class := self.parent.gir_class: + for namespace, name in ALLOWED_PARENTS: + parent_type = self.root.gir.get_type(name, namespace) + if gir_class.assignable_to(parent_type): + break + else: + hints=["only Gio.ListStore or Gtk.Buildable implementors can have children"] + if "child" in gir_class.properties: + hints.append("did you mean to assign this object to the 'child' property?") + raise CompileError( + f"{gir_class.full_name} doesn't have children", + hints=hints, + ) + @cached_property def response_id(self) -> T.Optional[ResponseId]: """Get action widget's response ID. diff --git a/tests/sample_errors/children.blp b/tests/sample_errors/children.blp new file mode 100644 index 0000000..400fe2d --- /dev/null +++ b/tests/sample_errors/children.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +ListItem { + Label {} +} \ No newline at end of file diff --git a/tests/sample_errors/children.err b/tests/sample_errors/children.err new file mode 100644 index 0000000..d294464 --- /dev/null +++ b/tests/sample_errors/children.err @@ -0,0 +1 @@ +4,3,8,Gtk.ListItem doesn't have children \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index ea6b2ea..4fdb5c0 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -175,6 +175,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("action_widget_response_dne") self.assert_sample_error("action_widget_negative_response") self.assert_sample_error("bitfield_member_dne") + self.assert_sample_error("children") self.assert_sample_error("class_assign") self.assert_sample_error("class_dne") self.assert_sample_error("consecutive_unexpected_tokens") From 08da6f79c7a4fee9212ef82d8c6c8095fc7644ce Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 16 Jul 2022 21:16:45 -0500 Subject: [PATCH 020/290] Fix referencing template by ID --- blueprintcompiler/language/gtkbuilder_template.py | 6 +++--- tests/samples/template.blp | 4 ++++ tests/samples/template.ui | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index ffee11c..6ab6c5a 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -26,7 +26,7 @@ from .types import ClassName class Template(Object): grammar = [ "template", - UseIdent("name").expected("template class name"), + UseIdent("id").expected("template class name"), Optional([ Match(":"), to_parse_node(ClassName).expected("parent class"), @@ -40,7 +40,7 @@ class Template(Object): if len(self.children[ClassName]): return self.children[ClassName][0].gir_type - @validate("name") + @validate("id") def unique_in_parent(self): self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",) @@ -52,7 +52,7 @@ class Template(Object): xml.start_tag( "template", - **{"class": self.tokens["name"]}, + **{"class": self.tokens["id"]}, parent=parent ) diff --git a/tests/samples/template.blp b/tests/samples/template.blp index 570dca7..7773e25 100644 --- a/tests/samples/template.blp +++ b/tests/samples/template.blp @@ -4,3 +4,7 @@ template TestTemplate : ApplicationWindow { test-property: "Hello, world"; test-signal => on_test_signal(); } + +Dialog { + transient-for: TestTemplate; +} \ No newline at end of file diff --git a/tests/samples/template.ui b/tests/samples/template.ui index 095fbd8..aebec5e 100644 --- a/tests/samples/template.ui +++ b/tests/samples/template.ui @@ -5,4 +5,7 @@ Hello, world + + TestTemplate + From 30f0deea34851aa6fbb4f8a5dcd9216f5ac714f9 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 23 Jul 2022 15:05:40 -0500 Subject: [PATCH 021/290] Exit with error code when a bug is reported --- blueprintcompiler/errors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 8ee80b9..70aea2a 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -157,3 +157,5 @@ along with the input file(s) if possible, on GitLab: {Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue {Colors.CLEAR}""") + sys.exit(1) + From 50db59f2d2c88fe0ee2fc979e11e7c989eaa07da Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 24 Jul 2022 23:12:05 +0200 Subject: [PATCH 022/290] lsp: Report error hints --- blueprintcompiler/lsp.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 8c2a009..bbe4c1d 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -270,9 +270,14 @@ class LanguageServer: }) def _create_diagnostic(self, text, uri, err): + message = err.message + + for hint in err.hints: + message += '\nhint: ' + hint + result = { "range": utils.idxs_to_range(err.start, err.end, text), - "message": err.message, + "message": message, "severity": DiagnosticSeverity.Warning if isinstance(err, CompileWarning) else DiagnosticSeverity.Error, } From 59283a76adc8d270ff5f67b630b7dfa905ec34a9 Mon Sep 17 00:00:00 2001 From: Alan Beveridge Date: Tue, 9 Aug 2022 15:41:09 +0000 Subject: [PATCH 023/290] Update docs/index.rst --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index f35e9fd..373dcb6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -67,6 +67,7 @@ Built with Blueprint - `Extension Manager `_ - `favagtk `_ - `Feeds `_ +- `File Shredder `_ - `Geopard `_ - `Giara `_ - `Health `_ From ad71324ccb8d834e2ccebc5579776db8a495de76 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 4 Sep 2022 14:02:05 -0500 Subject: [PATCH 024/290] Add MAINTENANCE.md --- MAINTENANCE.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 MAINTENANCE.md diff --git a/MAINTENANCE.md b/MAINTENANCE.md new file mode 100644 index 0000000..a1eab84 --- /dev/null +++ b/MAINTENANCE.md @@ -0,0 +1,9 @@ +## Releasing a new version + +1. Look at the git log since the previous release. Note every significant change +in the NEWS file. +2. Update the version number at the top of meson.build according to semver. +3. Make a new commit with just these two changes. Use `Release v{version}` +as the commit message. Tag the commit as `v{version}` and push the tag. +4. Create a "Post-release version bump" commit. +5. Announce the release through relevant channels (Twitter, TWIG, etc.) From 75a6d95988736ec0471d22ceb07579c0cedac2ad Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 4 Sep 2022 14:04:03 -0500 Subject: [PATCH 025/290] Release v0.4.0 --- NEWS.md | 19 +++++++++++++++++++ meson.build | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 NEWS.md diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..bd533d6 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,19 @@ +# v0.4.0 + +## Added +- Lookup expressions +- With the language server, hovering over a diagnostic message now shows any + associated hints. + +## Changed +- The compiler now uses .typelib files rather than XML .gir files, which reduces + dependencies and should reduce compile times by about half a second. + +## Fixed +- Fix the decompiler/porting tool not importing the Adw namespace when needed +- Fix a crash when trying to compile an empty file +- Fix parsing of number tokens +- Fix a bug where action widgets did not work in templates +- Fix a crash in the language server that occurred when a `using` statement had +no version +- If a compiler bug is reported, the process now exits with a non-zero code diff --git a/meson.build b/meson.build index 540c0ec..1302995 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('blueprint-compiler', - version: '0.3.0', + version: '0.4.0', ) subdir('docs') From 6ad1433587fa487e0d72c545178a61f8961c3b21 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 4 Sep 2022 16:54:21 -0500 Subject: [PATCH 026/290] Post-release version bump --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 1302995..be517bc 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('blueprint-compiler', - version: '0.4.0', + version: '0.5.0', ) subdir('docs') From c998655af6e01d9f6c70b6af9bb5ed98eeabb945 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Tue, 4 Oct 2022 01:26:38 +0000 Subject: [PATCH 027/290] doc: Add more apps built with Blueprint --- docs/index.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 373dcb6..3439413 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,6 +63,8 @@ Links Built with Blueprint -------------------- +- `Bottles `_ +- `Commit `_ - `Dialect `_ - `Extension Manager `_ - `favagtk `_ @@ -70,15 +72,19 @@ Built with Blueprint - `File Shredder `_ - `Geopard `_ - `Giara `_ +- `Gradience `_ - `Health `_ - `HydraPaper `_ - `Identity `_ +- `Junction `_ - `Login Manager Settings `_ - `Paper `_ - `Passes `_ +- `Playhouse `_ - `Plitki `_ - `Solanum `_ - `Swatch `_ +- `Tangram `_ - `Text Pieces `_ - `Video Trimmer `_ - `WhatIP `_ From 12c1c7b8d6c2e950ee72a0c5963d03c09798ca5e Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 2 Oct 2022 14:42:06 -0500 Subject: [PATCH 028/290] docs: Fix build setting The docs need to be set to build_always_stale so you don't need to delete the directory to rebuild them. This also means we want to disable build_by_default. --- .gitlab-ci.yml | 1 + docs/meson.build | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 79cac6a..1315899 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,7 @@ build: - ninja -C _build - ninja -C _build test - ninja -C _build install + - ninja -C _build docs/en - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - cd blueprint-regression-tests - git checkout 6f164f5e259f1602fd3bfae5e64852fed7687b9f diff --git a/docs/meson.build b/docs/meson.build index 9b76852..95e545d 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -3,9 +3,9 @@ if get_option('docs') sphinx = find_program(['sphinx-build-3', 'sphinx-build'], required: true) custom_target('docs', - command: [sphinx, '-b', 'html', '-c', meson.current_source_dir(), meson.current_source_dir(), '@OUTPUT@'], - output: 'en', - build_by_default: true + command: [sphinx, '-b', 'html', '-c', meson.current_source_dir(), meson.current_source_dir(), '@OUTPUT@'], + output: 'en', + build_always_stale: true, ) endif From 82980a466b1c4d6e66199e9ec6c0f34aa759927e Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sat, 17 Sep 2022 22:36:24 +0200 Subject: [PATCH 029/290] doc: Add documentation for Gtk.Label attributes --- docs/examples.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/examples.rst b/docs/examples.rst index 4d9c1cd..9b123d2 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -418,3 +418,26 @@ Gtk.Dialog and Gtk.InfoBar [action response=9] Gtk.Button app_defined_response {} } + +Gtk.Label +~~~~~~~~~ + +Blueprint does not have syntax for `attributes` on `Gtk.Label`. + +Instead, you should use `platform classes `_, `CSS `_, `Pango Markup `_ or `Gtk.Label.set_attributes `_. + +Examples + +.. code-block:: + + Gtk.Label { + label: "Hello, World!"; + styles ["title-1"] + } + +.. code-block:: + + Gtk.Label { + label: "Hello, World!"; + use-markup: true; + } From 302903b89c2a5be6148a8b258387b437fa2da698 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sat, 17 Sep 2022 22:37:00 +0200 Subject: [PATCH 030/290] doc: Add documentation to contribute --- CONTRIBUTING.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7690a2..d817250 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,21 @@ If you learn something useful, please add it to this file. # Run the test suite -``` +```sh python -m unittest ``` + +# Build the docs + +```sh +pip install -U sphinx furo + +meson -Ddocs=true build +# or +meson --reconfigure -Ddocs=true build + +ninja -C build docs/en + +python -m http.server 2310 --bind 127.0.0.1 --directory build/docs/en/ +xdg-open http://127.0.0.1:2310/ +``` From 15496353dec101dc4ac5fba582231b5b9705b783 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 4 Oct 2022 12:08:43 -0500 Subject: [PATCH 031/290] ci: Update regression tests --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1315899..822c7dc 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 6f164f5e259f1602fd3bfae5e64852fed7687b9f + - git checkout c61f425d5df359261f1369722e791f947bc3ede1 - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' From ee2b9b2950ef9c542e668a02715295144ffb85f9 Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 6 Oct 2022 04:13:38 +0000 Subject: [PATCH 032/290] Update MAINTENANCE.md --- MAINTENANCE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MAINTENANCE.md b/MAINTENANCE.md index a1eab84..a487b88 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -6,4 +6,5 @@ in the NEWS file. 3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag. 4. Create a "Post-release version bump" commit. -5. Announce the release through relevant channels (Twitter, TWIG, etc.) +5. Go to the Releases page in GitLab and create a new release from the tag. +6. Announce the release through relevant channels (Twitter, TWIG, etc.) From c0c40b15771ad3e0edbf8f2f13408dfe62d0fc4f Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 16 Sep 2022 22:33:49 -0500 Subject: [PATCH 033/290] language: Support boxed types and GType - Add support for type checking boxed types - Remove support for converting string and number literals - Add the `typeof()` operator for GType literals --- .gitlab-ci.yml | 2 +- NEWS.md | 7 +++ blueprintcompiler/gir.py | 25 +++++++- blueprintcompiler/language/__init__.py | 6 +- blueprintcompiler/language/values.py | 85 +++++++++++++++++++------- blueprintcompiler/typelib.py | 2 + tests/sample_errors/a11y_prop_type.err | 2 +- tests/samples/parseable.blp | 9 +++ tests/samples/parseable.ui | 6 ++ tests/samples/typeof.blp | 11 ++++ tests/samples/typeof.ui | 10 +++ tests/test_samples.py | 1 + 12 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 tests/samples/typeof.blp create mode 100644 tests/samples/typeof.ui 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") From f1c3413dc1549061972a8b444a14441bd3e8524b Mon Sep 17 00:00:00 2001 From: William Roy Date: Fri, 7 Oct 2022 18:02:39 +0000 Subject: [PATCH 034/290] Escape backlash on Windows --- blueprint-compiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprint-compiler.py b/blueprint-compiler.py index f6a542e..61eae79 100755 --- a/blueprint-compiler.py +++ b/blueprint-compiler.py @@ -24,8 +24,8 @@ import os, sys # These variables should be set by meson. If they aren't, we're running # uninstalled, and we might have to guess some values. version = "@VERSION@" -module_path = "@MODULE_PATH@" -libdir = "@LIBDIR@" +module_path = r"@MODULE_PATH@" +libdir = r"@LIBDIR@" if version == "\u0040VERSION@": version = "uninstalled" From 447785ec8c2c8c1f9fc842a5bd73c43f17d09ee7 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 14 Oct 2022 21:16:18 -0500 Subject: [PATCH 035/290] language: Remove inline menus Inline menus didn't work anyway--menus have to be referenced by ID (though, curiously, you *can* put the within the tag and immediately reference it--but that's a hack, and not what blueprint-compiler was doing). --- blueprintcompiler/language/__init__.py | 5 ----- blueprintcompiler/language/common.py | 1 - blueprintcompiler/language/gobject_property.py | 2 +- blueprintcompiler/language/gtk_menu.py | 5 +++-- blueprintcompiler/language/ui.py | 5 ++++- blueprintcompiler/parser.py | 2 +- tests/sample_errors/assign_inline_menu.blp | 5 ----- tests/sample_errors/assign_inline_menu.err | 1 - tests/{samples => sample_errors}/inline_menu.blp | 0 tests/sample_errors/inline_menu.err | 1 + tests/samples/inline_menu.ui | 9 --------- tests/test_samples.py | 3 +-- 12 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 tests/sample_errors/assign_inline_menu.blp delete mode 100644 tests/sample_errors/assign_inline_menu.err rename tests/{samples => sample_errors}/inline_menu.blp (100%) create mode 100644 tests/sample_errors/inline_menu.err delete mode 100644 tests/samples/inline_menu.ui diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 1b27040..ce247c3 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -22,11 +22,6 @@ from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, Qu from .common import * -OBJECT_HOOKS.children = [ - menu, - Object, -] - OBJECT_CONTENT_HOOKS.children = [ Signal, Property, diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index deeb5fc..108c43a 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -30,6 +30,5 @@ from ..parse_tree import * from ..xml_emitter import XmlEmitter -OBJECT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf() VALUE_HOOKS = AnyOf() diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index f0a2ef4..e5d753b 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -53,7 +53,7 @@ class Property(AstNode): UseIdent("name"), ":", AnyOf( - OBJECT_HOOKS, + Object, VALUE_HOOKS, ).expected("a value"), ), diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index b63233c..2c4131a 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -17,10 +17,10 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import typing as T from .attributes import BaseAttribute from .gobject_object import Object, ObjectContent -from .ui import UI from .common import * @@ -128,7 +128,7 @@ menu_contents.children = [ ), "}"), ] -menu = Group( +menu: Group = Group( Menu, [ "menu", @@ -138,6 +138,7 @@ menu = Group( ], ) +from .ui import UI @completer( applies_in=[UI], diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index 9123bf8..c1adbde 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -21,6 +21,8 @@ from .. import gir from .imports import GtkDirective, Import from .gtkbuilder_template import Template +from .gobject_object import Object +from .gtk_menu import menu from .common import * @@ -32,7 +34,8 @@ class UI(AstNode): ZeroOrMore(Import), Until(AnyOf( Template, - OBJECT_HOOKS, + menu, + Object, ), Eof()), ] diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 7c62ae6..739a165 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -21,7 +21,7 @@ from .errors import MultipleErrors, PrintableError from .parse_tree import * from .tokenizer import TokenType -from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI +from .language import OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]: diff --git a/tests/sample_errors/assign_inline_menu.blp b/tests/sample_errors/assign_inline_menu.blp deleted file mode 100644 index 9e5a944..0000000 --- a/tests/sample_errors/assign_inline_menu.blp +++ /dev/null @@ -1,5 +0,0 @@ -using Gtk 4.0; - -Button { - label: menu {}; -} diff --git a/tests/sample_errors/assign_inline_menu.err b/tests/sample_errors/assign_inline_menu.err deleted file mode 100644 index 5200603..0000000 --- a/tests/sample_errors/assign_inline_menu.err +++ /dev/null @@ -1 +0,0 @@ -4,3,15,Cannot assign Gio.Menu to string diff --git a/tests/samples/inline_menu.blp b/tests/sample_errors/inline_menu.blp similarity index 100% rename from tests/samples/inline_menu.blp rename to tests/sample_errors/inline_menu.blp diff --git a/tests/sample_errors/inline_menu.err b/tests/sample_errors/inline_menu.err new file mode 100644 index 0000000..3115750 --- /dev/null +++ b/tests/sample_errors/inline_menu.err @@ -0,0 +1 @@ +4,15,4,Namespace Gtk does not contain a type called menu \ No newline at end of file diff --git a/tests/samples/inline_menu.ui b/tests/samples/inline_menu.ui deleted file mode 100644 index a6f2bcc..0000000 --- a/tests/samples/inline_menu.ui +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/tests/test_samples.py b/tests/test_samples.py index ba39ada..b5d3e3e 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -140,7 +140,6 @@ class TestSamples(unittest.TestCase): self.assert_sample("file_filter") self.assert_sample("flags") self.assert_sample("id_prop") - self.assert_sample("inline_menu") self.assert_sample("layout") self.assert_sample("menu") self.assert_sample("numbers") @@ -167,7 +166,6 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("a11y_prop_obj_dne") self.assert_sample_error("a11y_prop_type") self.assert_sample_error("abstract_class") - self.assert_sample_error("assign_inline_menu") self.assert_sample_error("action_widget_float_response") self.assert_sample_error("action_widget_have_no_id") self.assert_sample_error("action_widget_multiple_default") @@ -188,6 +186,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("filters_in_non_file_filter") self.assert_sample_error("gtk_3") self.assert_sample_error("gtk_exact_version") + self.assert_sample_error("inline_menu") self.assert_sample_error("invalid_bool") self.assert_sample_error("layout_in_non_widget") self.assert_sample_error("no_import_version") From b3783b9c6ac0de02ff3da1fb5226d1e7396ecd61 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 15 Oct 2022 11:26:18 -0500 Subject: [PATCH 036/290] lsp: Log to stderr rather than a file --- blueprintcompiler/lsp.py | 19 ++++++++----------- blueprintcompiler/main.py | 3 +-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index bbe4c1d..b1610d7 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -27,6 +27,10 @@ from .lsp_utils import * from . import tokenizer, parser, utils, xml_reader +def printerr(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + def command(json_method): def decorator(func): func._json_method = json_method @@ -89,10 +93,9 @@ class OpenFile: class LanguageServer: commands: T.Dict[str, T.Callable] = {} - def __init__(self, logfile=None): + def __init__(self): self.client_capabilities = {} self._open_files: {str: OpenFile} = {} - self.logfile = logfile def run(self): # Read tags from gir files. During normal compilation these are @@ -110,7 +113,7 @@ class LanguageServer: if line.startswith("Content-Length:"): content_len = int(line.split("Content-Length:")[1].strip()) line = sys.stdin.buffer.read(content_len).decode() - self._log("input: " + line) + printerr("input: " + line) data = json.loads(line) method = data.get("method") @@ -120,22 +123,16 @@ class LanguageServer: if method in self.commands: self.commands[method](self, id, params) except Exception as e: - self._log(traceback.format_exc()) + printerr(traceback.format_exc()) def _send(self, data): data["jsonrpc"] = "2.0" line = json.dumps(data, separators=(",", ":")) + "\r\n" - self._log("output: " + line) + printerr("output: " + line) sys.stdout.write(f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}") sys.stdout.flush() - def _log(self, msg): - if self.logfile is not None: - self.logfile.write(str(msg)) - self.logfile.write("\n") - self.logfile.flush() - def _send_response(self, id, result): self._send({ "id": id, diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index d528e7e..9f2f4c2 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -48,7 +48,6 @@ class BlueprintApp: port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port) lsp = self.add_subcommand("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp) - lsp.add_argument("--logfile", dest="logfile", default=None, type=argparse.FileType('a')) self.add_subcommand("help", "Show this message", self.cmd_help) @@ -125,7 +124,7 @@ class BlueprintApp: def cmd_lsp(self, opts): - langserv = LanguageServer(opts.logfile) + langserv = LanguageServer() langserv.run() From 8cf793023da75b479f365c5633da9873acd6915b Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 15 Oct 2022 11:47:42 -0500 Subject: [PATCH 037/290] Fix crash in language server --- blueprintcompiler/language/imports.py | 3 ++- blueprintcompiler/language/ui.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index 4fac192..823baec 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -57,7 +57,8 @@ class GtkDirective(AstNode): # validate the GTK version first to make sure the more specific error # message is emitted self.gtk_version() - return gir.get_namespace("Gtk", self.tokens["version"]) + if self.tokens["version"] is not None: + return gir.get_namespace("Gtk", self.tokens["version"]) def emit_xml(self, xml: XmlEmitter): diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index c1adbde..e0d5dcd 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -45,7 +45,8 @@ class UI(AstNode): self._gir_errors = [] try: - gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace) + if gtk := self.children[GtkDirective][0].gir_namespace: + gir_ctx.add_namespace(gtk) except CompileError as e: self._gir_errors.append(e) From a24f16109fdc06e2d1a220a0e84d5f4a1fb80189 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 14 Oct 2022 21:04:37 -0500 Subject: [PATCH 038/290] Separate output into its own module --- .gitlab-ci.yml | 2 +- blueprintcompiler/ast_utils.py | 11 - blueprintcompiler/gir.py | 8 +- blueprintcompiler/interactive_port.py | 4 +- blueprintcompiler/language/__init__.py | 12 +- blueprintcompiler/language/attributes.py | 11 - blueprintcompiler/language/common.py | 1 - blueprintcompiler/language/expression.py | 17 +- blueprintcompiler/language/gobject_object.py | 40 +-- .../language/gobject_property.py | 41 --- blueprintcompiler/language/gobject_signal.py | 37 ++- blueprintcompiler/language/gtk_a11y.py | 6 - .../language/gtk_combo_box_text.py | 6 - blueprintcompiler/language/gtk_file_filter.py | 11 +- blueprintcompiler/language/gtk_layout.py | 6 - blueprintcompiler/language/gtk_menu.py | 22 +- blueprintcompiler/language/gtk_size_group.py | 9 - blueprintcompiler/language/gtk_string_list.py | 13 - blueprintcompiler/language/gtk_styles.py | 9 - .../language/gtkbuilder_child.py | 15 +- .../language/gtkbuilder_template.py | 27 +- blueprintcompiler/language/imports.py | 7 - blueprintcompiler/language/response_id.py | 30 +- blueprintcompiler/language/types.py | 3 - blueprintcompiler/language/ui.py | 7 - blueprintcompiler/language/values.py | 34 +-- blueprintcompiler/main.py | 6 +- blueprintcompiler/outputs/__init__.py | 7 + blueprintcompiler/outputs/xml/__init__.py | 272 ++++++++++++++++++ .../{ => outputs/xml}/xml_emitter.py | 13 +- tests/fuzz.py | 5 +- tests/samples/signal.ui | 2 +- tests/test_samples.py | 4 +- 33 files changed, 407 insertions(+), 291 deletions(-) create mode 100644 blueprintcompiler/outputs/__init__.py create mode 100644 blueprintcompiler/outputs/xml/__init__.py rename blueprintcompiler/{ => outputs/xml}/xml_emitter.py (86%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f4bc92d..2790cd1 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 d14b95b6c1fc0cddd4b0ad21d224b05edee2d01f + - git checkout 94613f275efc810610768d5ee8b2aec28392c3e8 - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 1f3eb5b..cc5c44d 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -23,7 +23,6 @@ import typing as T from .errors import * from .lsp_utils import SemanticToken -from .xml_emitter import XmlEmitter class Children: @@ -96,16 +95,6 @@ class AstNode: if isinstance(item, attr_type): yield name, item - def generate(self) -> str: - """ Generates an XML string from the node. """ - xml = XmlEmitter() - self.emit_xml(xml) - return xml.result - - def emit_xml(self, xml: XmlEmitter): - """ Emits the XML representation of this AST node to the XmlEmitter. """ - raise NotImplementedError() - def get_docs(self, idx: int) -> T.Optional[str]: for name, attr in self._attrs_by_type(Docs): if attr.token_name: diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index b1ecf08..78335ee 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -28,11 +28,11 @@ from gi.repository import GIRepository # type: ignore from .errors import CompileError, CompilerBugError from . import typelib, xml_reader -_namespace_cache = {} +_namespace_cache: T.Dict[str, "Namespace"] = {} _xml_cache = {} -def get_namespace(namespace, version): +def get_namespace(namespace, version) -> "Namespace": search_paths = GIRepository.Repository.get_search_path() filename = f"{namespace}-{version}.typelib" @@ -518,11 +518,11 @@ class Namespace(GirNode): return get_xml(self.name, self.version).get_elements("namespace")[0] @cached_property - def name(self): + def name(self) -> str: return self.tl.HEADER_NAMESPACE @cached_property - def version(self): + def version(self) -> str: return self.tl.HEADER_NSVERSION @property diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index ff551df..10a9b56 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -23,6 +23,7 @@ import difflib import os from . import decompiler, tokenizer, parser +from .outputs.xml import XmlOutput from .errors import MultipleErrors, PrintableError from .utils import Colors @@ -57,7 +58,8 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: if len(ast.errors): raise MultipleErrors(ast.errors) - ast.generate() + output = XmlOutput() + output.emit(ast) except PrintableError as e: e.pretty_print(out_file, decompiled) diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index ce247c3..da5a44c 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,16 +1,13 @@ -""" Contains all the syntax beyond basic objects, properties, signal, and -templates. """ - from .attributes import BaseAttribute, BaseTypedAttribute -from .expression import Expr +from .expression import IdentExpr, LookupOp, Expr from .gobject_object import Object, ObjectContent from .gobject_property import Property from .gobject_signal import Signal from .gtk_a11y import A11y from .gtk_combo_box_text import Items -from .gtk_file_filter import mime_types, patterns, suffixes +from .gtk_file_filter import mime_types, patterns, suffixes, Filters from .gtk_layout import Layout -from .gtk_menu import menu +from .gtk_menu import menu, Menu, MenuAttribute from .gtk_size_group import Widgets from .gtk_string_list import Strings from .gtk_styles import Styles @@ -18,7 +15,8 @@ from .gtkbuilder_child import Child from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .ui import UI -from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, QuotedValue, NumberValue +from .types import ClassName +from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, Flag, QuotedValue, NumberValue, Value from .common import * diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py index a6db9f1..66faa60 100644 --- a/blueprintcompiler/language/attributes.py +++ b/blueprintcompiler/language/attributes.py @@ -32,17 +32,6 @@ class BaseAttribute(AstNode): def name(self): return self.tokens["name"] - def emit_xml(self, xml: XmlEmitter): - value = self.children[Value][0] - attrs = { self.attr_name: self.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 diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 108c43a..f6a8f8e 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -27,7 +27,6 @@ from ..decompiler import DecompileCtx, decompiler from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * -from ..xml_emitter import XmlEmitter OBJECT_CONTENT_HOOKS = AnyOf() diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index a684b39..29df93e 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -27,9 +27,6 @@ expr = Pratt() class Expr(AstNode): grammar = expr - def emit_xml(self, xml: XmlEmitter): - self.children[-1].emit_xml(xml) - class InfixExpr(AstNode): @property @@ -41,19 +38,17 @@ class InfixExpr(AstNode): class IdentExpr(AstNode): grammar = UseIdent("ident") - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("constant") - xml.put_text(self.tokens["ident"]) - xml.end_tag() + @property + def ident(self) -> str: + return self.tokens["ident"] class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("lookup", name=self.tokens["property"]) - self.lhs.emit_xml(xml) - xml.end_tag() + @property + def property_name(self) -> str: + return self.tokens["property"] expr.children = [ diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 9a78667..711795c 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -33,10 +33,6 @@ class ObjectContent(AstNode): def gir_class(self): return self.parent.gir_class - def emit_xml(self, xml: XmlEmitter): - for x in self.children: - x.emit_xml(xml) - class Object(AstNode): grammar: T.Any = [ ConcreteClassName, @@ -44,9 +40,21 @@ class Object(AstNode): ObjectContent, ] + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def class_name(self) -> ClassName | None: + return self.children[ClassName][0] + + @property + def content(self) -> ObjectContent: + return self.children[ObjectContent][0] + @property def gir_class(self): - return self.children[ClassName][0].gir_type + return self.class_name.gir_type @cached_property def action_widgets(self) -> T.List[ResponseId]: @@ -62,28 +70,6 @@ class Object(AstNode): if child.response_id ] - def emit_start_tag(self, xml: XmlEmitter): - xml.start_tag("object", **{ - "class": self.children[ClassName][0].glib_type_name, - "id": self.tokens["id"], - }) - - def emit_xml(self, xml: XmlEmitter): - self.emit_start_tag(xml) - - for child in self.children: - child.emit_xml(xml) - - # List action widgets - action_widgets = self.action_widgets - if action_widgets: - xml.start_tag("action-widgets") - for action_widget in action_widgets: - action_widget.emit_action_widget(xml) - xml.end_tag() - - xml.end_tag() - def validate_parent_type(node, ns: str, name: str, err_msg: str): parent = node.root.gir.get_type(name, ns) diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index e5d753b..4d47e80 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -133,44 +133,3 @@ class Property(AstNode): 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["bind_source"] and not self.tokens["no_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: - if self.tokens["binding"]: - xml.start_tag("binding", **props) - for x in self.children: - x.emit_xml(xml) - xml.end_tag() - else: - 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 a519dee..ba1b48b 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -40,6 +40,30 @@ class Signal(AstNode): )), ) + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def detail_name(self) -> str | None: + return self.tokens["detail_name"] + + @property + def handler(self) -> str: + return self.tokens["handler"] + + @property + def object_id(self) -> str | None: + return self.tokens["object"] + + @property + def is_swapped(self) -> bool: + return self.tokens["swapped"] or False + + @property + def is_after(self) -> bool: + return self.tokens["after"] or False + @property def gir_signal(self): @@ -89,19 +113,6 @@ class Signal(AstNode): return self.gir_signal.doc - def emit_xml(self, xml: XmlEmitter): - name = self.tokens["name"] - if self.tokens["detail_name"]: - name += "::" + self.tokens["detail_name"] - xml.put_self_closing( - "signal", - name=name, - handler=self.tokens["handler"], - swapped="true" if self.tokens["swapped"] else None, - object=self.tokens["object"] - ) - - @decompiler("signal") def decompile_signal(ctx, gir, name, handler, swapped="false", object=None): object_name = object or "" diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 57bd6af..cab72e0 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -167,12 +167,6 @@ class A11y(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate accessibility block") - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("accessibility") - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index ecee31f..31d2d08 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -60,12 +60,6 @@ class Items(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate items block") - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("items") - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 39e563e..4419311 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -39,18 +39,9 @@ class Filters(AstNode): ) wrapped_validator(self) - def emit_xml(self, xml: XmlEmitter): - xml.start_tag(self.tokens["tag_name"]) - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - class FilterString(AstNode): - def emit_xml(self, xml): - xml.start_tag(self.tokens["tag_name"]) - xml.put_text(self.tokens["name"]) - xml.end_tag() + pass def create_node(tag_name: str, singular: str): diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 8ccc136..b52f7bc 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -64,12 +64,6 @@ class Layout(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate layout block") - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("layout") - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 2c4131a..355997a 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -19,22 +19,26 @@ import typing as T +from blueprintcompiler.language.values import Value + from .attributes import BaseAttribute from .gobject_object import Object, ObjectContent from .common import * -class Menu(Object): - def emit_xml(self, xml: XmlEmitter): - xml.start_tag(self.tokens["tag"], id=self.tokens["id"]) - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - +class Menu(AstNode): @property def gir_class(self): return self.root.gir.namespaces["Gtk"].lookup_type("Gio.Menu") + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def tag(self) -> str: + return self.tokens["tag"] + class MenuAttribute(BaseAttribute): tag_name = "attribute" @@ -43,6 +47,10 @@ class MenuAttribute(BaseAttribute): def value_type(self): return None + @property + def value(self) -> Value: + return self.children[Value][0] + menu_contents = Sequence() diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index de766ed..eb56043 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -39,9 +39,6 @@ class Widget(AstNode): f"Cannot assign {object.gir_class.full_name} to {type.full_name}" ) - def emit_xml(self, xml: XmlEmitter): - xml.put_self_closing("widget", name=self.tokens["name"]) - class Widgets(AstNode): grammar = [ @@ -59,12 +56,6 @@ class Widgets(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate widgets block") - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("widgets") - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 4f19190..e7eb0f8 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -31,13 +31,6 @@ class Item(AstNode): def value_type(self): return StringType() - def emit_xml(self, xml: XmlEmitter): - value = self.children[Value][0] - attrs = value.attrs if isinstance(value, TranslatedStringValue) else {} - xml.start_tag("item", **attrs) - value.emit_xml(xml) - xml.end_tag() - class Strings(AstNode): grammar = [ @@ -55,12 +48,6 @@ class Strings(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate strings block") - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("items") - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index f93ddc0..644d8a3 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -25,9 +25,6 @@ from .common import * class StyleClass(AstNode): grammar = UseQuoted("name") - def emit_xml(self, xml): - xml.put_self_closing("class", name=self.tokens["name"]) - class Styles(AstNode): grammar = [ @@ -45,12 +42,6 @@ class Styles(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate styles block") - def emit_xml(self, xml: XmlEmitter): - xml.start_tag("style") - for child in self.children: - child.emit_xml(xml) - xml.end_tag() - @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 72843a1..71efe19 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -41,6 +41,10 @@ class Child(AstNode): Object, ] + @property + def object(self) -> Object: + return self.children[Object][0] + @validate() def parent_can_have_child(self): if gir_class := self.parent.gir_class: @@ -70,17 +74,6 @@ class Child(AstNode): else: return None - 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() - @decompiler("child") def decompile_child(ctx, gir, type=None, internal_child=None): diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 6ab6c5a..35ef4f2 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -34,28 +34,27 @@ class Template(Object): ObjectContent, ] + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def class_name(self) -> ClassName | None: + if len(self.children[ClassName]): + return self.children[ClassName][0] + else: + return None + @property def gir_class(self): # Templates might not have a parent class defined - if len(self.children[ClassName]): - return self.children[ClassName][0].gir_type + if class_name := self.class_name: + return class_name.gir_type @validate("id") def unique_in_parent(self): self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",) - def emit_start_tag(self, xml: XmlEmitter): - if len(self.children[ClassName]): - parent = self.children[ClassName][0].glib_type_name - else: - parent = None - - xml.start_tag( - "template", - **{"class": self.tokens["id"]}, - parent=parent - ) - @decompiler("template") def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"): diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index 823baec..f0fe3df 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -61,10 +61,6 @@ class GtkDirective(AstNode): 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", @@ -82,6 +78,3 @@ class Import(AstNode): return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) except CompileError: return None - - def emit_xml(self, xml): - pass diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index 7dce2f2..745c73f 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -120,6 +120,14 @@ class ResponseId(AstNode): if widget.tokens["is_default"]: raise CompileError("Default response is already set") + @property + def response_id(self) -> str: + return self.tokens["response_id"] + + @property + def is_default(self) -> bool: + return self.tokens["is_default"] or False + @property def widget_id(self) -> str: """Get action widget ID.""" @@ -128,25 +136,3 @@ class ResponseId(AstNode): _object: Object = self.parent.children[Object][0] return _object.tokens["id"] - def emit_xml(self, xml: XmlEmitter) -> None: - """Emit nothing. - - Response ID don't have to emit any XML in place, - but have to emit action-widget tag in separate - place (see `ResponseId.emit_action_widget`) - """ - - def emit_action_widget(self, xml: XmlEmitter) -> None: - """Emit action-widget XML. - - Must be called while tag is open. - - For more details see `GtkDialog` and `GtkInfoBar` docs. - """ - xml.start_tag( - "action-widget", - response=self.tokens["response_id"], - default=self.tokens["is_default"] - ) - xml.put_text(self.widget_id) - xml.end_tag() diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 0d6b38c..8f39cbf 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -76,9 +76,6 @@ class TypeName(AstNode): if self.gir_type: return self.gir_type.doc - def emit_xml(self, xml: XmlEmitter): - pass - class ClassName(TypeName): @validate("namespace", "class_name") diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index e0d5dcd..5a9c4fb 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -86,10 +86,3 @@ class UI(AstNode): 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 index 44e28ba..5030774 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -46,14 +46,12 @@ class TranslatedStringValue(Value): ) @property - def attrs(self): - attrs = { "translatable": "true" } - if "context" in self.tokens: - attrs["context"] = self.tokens["context"] - return attrs + def string(self) -> str: + return self.tokens["value"] - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.tokens["value"]) + @property + def context(self) -> str | None: + return self.tokens["context"] class TypeValue(Value): @@ -68,9 +66,6 @@ class TypeValue(Value): 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 @@ -81,8 +76,9 @@ class TypeValue(Value): class QuotedValue(Value): grammar = UseQuoted("value") - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.tokens["value"]) + @property + def value(self) -> str: + return self.tokens["value"] @validate() def validate_for_type(self): @@ -119,8 +115,9 @@ class QuotedValue(Value): class NumberValue(Value): grammar = UseNumber("value") - def emit_xml(self, xml: XmlEmitter): - xml.put_text(self.tokens["value"]) + @property + def value(self) -> int | float: + return self.tokens["value"] @validate() def validate_for_type(self): @@ -179,19 +176,10 @@ class FlagsValue(Value): if type is not None and not isinstance(type, gir.Bitfield): raise CompileError(f"{type.full_name} is not a bitfield type") - 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 diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 9f2f4c2..124654d 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -25,7 +25,7 @@ from .errors import PrintableError, report_bug, MultipleErrors from .lsp import LanguageServer from . import parser, tokenizer, decompiler, interactive_port from .utils import Colors -from .xml_emitter import XmlEmitter +from .outputs import XmlOutput VERSION = "uninstalled" LIBDIR = None @@ -141,7 +141,9 @@ class BlueprintApp: if len(ast.errors): raise MultipleErrors(ast.errors) - return ast.generate(), warnings + formatter = XmlOutput() + + return formatter.emit(ast), warnings def main(version, libdir): diff --git a/blueprintcompiler/outputs/__init__.py b/blueprintcompiler/outputs/__init__.py new file mode 100644 index 0000000..e3054a3 --- /dev/null +++ b/blueprintcompiler/outputs/__init__.py @@ -0,0 +1,7 @@ +from ..language import UI + +class OutputFormat: + def emit(self, ui: UI) -> str: + raise NotImplementedError() + +from .xml import XmlOutput diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py new file mode 100644 index 0000000..2cbb425 --- /dev/null +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -0,0 +1,272 @@ +from .. import OutputFormat +from ...language import * +from .xml_emitter import XmlEmitter + + +class XmlOutput(OutputFormat): + def emit(self, ui: UI) -> str: + xml = XmlEmitter() + self._emit_ui(ui, xml) + return xml.result + + def _emit_ui(self, ui: UI, xml: XmlEmitter): + xml.start_tag("interface") + + for x in ui.children: + if isinstance(x, GtkDirective): + self._emit_gtk_directive(x, xml) + elif isinstance(x, Import): + pass + elif isinstance(x, Template): + self._emit_template(x, xml) + elif isinstance(x, Object): + self._emit_object(x, xml) + elif isinstance(x, Menu): + self._emit_menu(x, xml) + else: + raise CompilerBugError() + + xml.end_tag() + + def _emit_gtk_directive(self, gtk: GtkDirective, xml: XmlEmitter): + xml.put_self_closing("requires", lib="gtk", version=gtk.gir_namespace.version) + + def _emit_template(self, template: Template, xml: XmlEmitter): + xml.start_tag("template", **{"class": template.id}, parent=template.class_name) + self._emit_object_or_template(template, xml) + xml.end_tag() + + def _emit_object(self, obj: Object, xml: XmlEmitter): + xml.start_tag( + "object", + **{"class": obj.class_name}, + id=obj.id, + ) + self._emit_object_or_template(obj, xml) + xml.end_tag() + + def _emit_object_or_template(self, obj: Object | Template, xml: XmlEmitter): + for child in obj.content.children: + if isinstance(child, Property): + self._emit_property(child, xml) + elif isinstance(child, Signal): + self._emit_signal(child, xml) + elif isinstance(child, Child): + self._emit_child(child, xml) + else: + self._emit_extensions(child, xml) + + # List action widgets + action_widgets = obj.action_widgets + if action_widgets: + xml.start_tag("action-widgets") + for action_widget in action_widgets: + xml.start_tag( + "action-widget", + response=action_widget.response_id, + default=action_widget.is_default or None, + ) + xml.put_text(action_widget.widget_id) + xml.end_tag() + xml.end_tag() + + def _emit_menu(self, menu: Menu, xml: XmlEmitter): + xml.start_tag(menu.tag, id=menu.id) + for child in menu.children: + if isinstance(child, Menu): + self._emit_menu(child, xml) + elif isinstance(child, MenuAttribute): + self._emit_attribute("attribute", "name", child.name, child.value, xml) + else: + raise CompilerBugError() + xml.end_tag() + + def _emit_property(self, property: Property, xml: XmlEmitter): + values = property.children[Value] + value = values[0] if len(values) == 1 else None + + bind_flags = [] + if property.tokens["bind_source"] and not property.tokens["no_sync_create"]: + bind_flags.append("sync-create") + if property.tokens["inverted"]: + bind_flags.append("invert-boolean") + if property.tokens["bidirectional"]: + bind_flags.append("bidirectional") + bind_flags_str = "|".join(bind_flags) or None + + props = { + "name": property.tokens["name"], + "bind-source": property.tokens["bind_source"], + "bind-property": property.tokens["bind_property"], + "bind-flags": bind_flags_str, + } + + if isinstance(value, TranslatedStringValue): + xml.start_tag("property", **props, **self._translated_string_attrs(value)) + xml.put_text(value.string) + xml.end_tag() + elif len(property.children[Object]) == 1: + xml.start_tag("property", **props) + self._emit_object(property.children[Object][0], xml) + xml.end_tag() + elif value is None: + if property.tokens["binding"]: + xml.start_tag("binding", **props) + self._emit_expression(property.children[Expr][0], xml) + xml.end_tag() + else: + xml.put_self_closing("property", **props) + else: + xml.start_tag("property", **props) + self._emit_value(value, xml) + xml.end_tag() + + def _translated_string_attrs( + self, translated: TranslatedStringValue + ) -> T.Dict[str, str | None]: + return { + "translatable": "true", + "context": translated.context, + } + + def _emit_signal(self, signal: Signal, xml: XmlEmitter): + name = signal.name + if signal.detail_name: + name += "::" + signal.detail_name + xml.put_self_closing( + "signal", + name=name, + handler=signal.handler, + swapped=signal.is_swapped or None, + object=signal.object_id, + ) + + def _emit_child(self, child: Child, xml: XmlEmitter): + child_type = internal_child = None + + if child.tokens["internal_child"]: + internal_child = child.tokens["child_type"] + else: + child_type = child.tokens["child_type"] + + xml.start_tag("child", type=child_type, internal_child=internal_child) + self._emit_object(child.object, xml) + xml.end_tag() + + def _emit_value(self, value: Value, xml: XmlEmitter): + if isinstance(value, IdentValue): + if isinstance(value.parent.value_type, gir.Enumeration): + xml.put_text( + value.parent.value_type.members[value.tokens["value"]].nick + ) + else: + xml.put_text(value.tokens["value"]) + elif isinstance(value, QuotedValue) or isinstance(value, NumberValue): + xml.put_text(value.value) + elif isinstance(value, FlagsValue): + xml.put_text("|".join([flag.tokens["value"] for flag in value.children])) + elif isinstance(value, TranslatedStringValue): + raise CompilerBugError("translated values must be handled in the parent") + elif isinstance(value, TypeValue): + xml.put_text(value.type_name.glib_type_name) + else: + raise CompilerBugError() + + def _emit_expression(self, expression: Expr, xml: XmlEmitter): + self._emit_expression_part(expression.children[-1], xml) + + def _emit_expression_part(self, expression, xml: XmlEmitter): + if isinstance(expression, IdentExpr): + self._emit_ident_expr(expression, xml) + elif isinstance(expression, LookupOp): + self._emit_lookup_op(expression, xml) + elif isinstance(expression, Expr): + self._emit_expression(expression, xml) + else: + raise CompilerBugError() + + def _emit_ident_expr(self, expr: IdentExpr, xml: XmlEmitter): + xml.start_tag("constant") + xml.put_text(expr.ident) + xml.end_tag() + + def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter): + xml.start_tag("lookup", name=expr.property_name) + self._emit_expression_part(expr.lhs, xml) + xml.end_tag() + + def _emit_attribute( + self, tag: str, attr: str, name: str, value: Value, xml: XmlEmitter + ): + attrs = {attr: name} + + if isinstance(value, TranslatedStringValue): + xml.start_tag(tag, **attrs, **self._translated_string_attrs(value)) + xml.put_text(value.string) + xml.end_tag() + else: + xml.start_tag(tag, **attrs) + self._emit_value(value, xml) + xml.end_tag() + + def _emit_extensions(self, extension, xml: XmlEmitter): + if isinstance(extension, A11y): + xml.start_tag("accessibility") + for child in extension.children: + self._emit_attribute( + child.tag_name, "name", child.name, child.children[Value][0], xml + ) + xml.end_tag() + + elif isinstance(extension, Filters): + xml.start_tag(extension.tokens["tag_name"]) + for child in extension.children: + xml.start_tag(child.tokens["tag_name"]) + xml.put_text(child.tokens["name"]) + xml.end_tag() + xml.end_tag() + + elif isinstance(extension, Items): + xml.start_tag("items") + for child in extension.children: + self._emit_attribute( + "item", "id", child.name, child.children[Value][0], xml + ) + xml.end_tag() + + elif isinstance(extension, Layout): + xml.start_tag("layout") + for child in extension.children: + self._emit_attribute( + "property", "name", child.name, child.children[Value][0], xml + ) + xml.end_tag() + + elif isinstance(extension, Strings): + xml.start_tag("items") + for child in extension.children: + value = child.children[Value][0] + if isinstance(value, TranslatedStringValue): + xml.start_tag("item", **self._translated_string_attrs(value)) + xml.put_text(value.string) + xml.end_tag() + else: + xml.start_tag("item") + self._emit_value(value, xml) + xml.end_tag() + xml.end_tag() + + elif isinstance(extension, Styles): + xml.start_tag("style") + for child in extension.children: + xml.put_self_closing("class", name=child.tokens["name"]) + xml.end_tag() + + elif isinstance(extension, Widgets): + xml.start_tag("widgets") + for child in extension.children: + xml.put_self_closing("widget", name=child.tokens["name"]) + xml.end_tag() + + else: + raise CompilerBugError() diff --git a/blueprintcompiler/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py similarity index 86% rename from blueprintcompiler/xml_emitter.py rename to blueprintcompiler/outputs/xml/xml_emitter.py index d92d1bd..def58b5 100644 --- a/blueprintcompiler/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -17,9 +17,10 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later - from xml.sax import saxutils -from . import gir + +from blueprintcompiler.gir import GirType +from blueprintcompiler.language.types import ClassName class XmlEmitter: @@ -29,7 +30,7 @@ class XmlEmitter: self._tag_stack = [] self._needs_newline = False - def start_tag(self, tag, **attrs): + def start_tag(self, tag, **attrs: str | GirType | ClassName | bool | None): self._indent() self.result += f"<{tag}" for key, val in attrs.items(): @@ -55,7 +56,7 @@ class XmlEmitter: self.result += f"" self._needs_newline = True - def put_text(self, text): + def put_text(self, text: str | int | float): self.result += saxutils.escape(str(text)) self._needs_newline = False @@ -64,7 +65,9 @@ class XmlEmitter: self.result += "\n" + " " * (self.indent * len(self._tag_stack)) def _to_string(self, val): - if isinstance(val, gir.GirType): + if isinstance(val, GirType): + return val.glib_type_name + elif isinstance(val, ClassName): return val.glib_type_name else: return str(val) diff --git a/tests/fuzz.py b/tests/fuzz.py index 4f7c879..1ebd02d 100644 --- a/tests/fuzz.py +++ b/tests/fuzz.py @@ -1,6 +1,8 @@ import os, sys from pythonfuzz.main import PythonFuzz +from blueprintcompiler.outputs.xml import XmlOutput + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from blueprintcompiler import tokenizer, parser, decompiler, gir @@ -17,8 +19,9 @@ def fuzz(buf): tokens = tokenizer.tokenize(blueprint) ast, errors, warnings = parser.parse(tokens) + xml = XmlOutput() if errors is None and len(ast.errors) == 0: - actual = ast.generate() + xml.emit(ast) except CompilerBugError as e: raise e except PrintableError: diff --git a/tests/samples/signal.ui b/tests/samples/signal.ui index 52740fe..b361d69 100644 --- a/tests/samples/signal.ui +++ b/tests/samples/signal.ui @@ -5,7 +5,7 @@ - + diff --git a/tests/test_samples.py b/tests/test_samples.py index b5d3e3e..5f0d9e5 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -28,6 +28,7 @@ from blueprintcompiler.completions import complete from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler import utils +from blueprintcompiler.outputs.xml import XmlOutput class TestSamples(unittest.TestCase): @@ -56,7 +57,8 @@ class TestSamples(unittest.TestCase): if len(warnings): raise MultipleErrors(warnings) - actual = ast.generate() + xml = XmlOutput() + actual = xml.emit(ast) if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) From a8a209550baebcdd58717fff8205576a05e8b888 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 18 Oct 2022 15:02:31 -0500 Subject: [PATCH 039/290] typelib: Fix big-endian architectures --- blueprintcompiler/typelib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index e8b9152..0946320 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -17,7 +17,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later - +import sys import typing as T import math from ctypes import * @@ -241,7 +241,7 @@ class Typelib: return self._typelib_file[loc:end].decode("utf-8") def _int(self, size, signed): - return int.from_bytes(self._typelib_file[self._offset:self._offset + size], 'little') + return int.from_bytes(self._typelib_file[self._offset:self._offset + size], sys.byteorder) class TypelibHeader(Typelib): From d6c6a66c15613c34cdbafb32ae5facd3bade6ddd Mon Sep 17 00:00:00 2001 From: "FeRD (Frank Dana)" Date: Tue, 18 Oct 2022 20:49:16 +0000 Subject: [PATCH 040/290] port: Fix directory recursion --- blueprintcompiler/interactive_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index 10a9b56..229f238 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -87,7 +87,7 @@ def listdir_recursive(subdir): full = os.path.join(subdir, file) if full == "./subprojects": # skip the subprojects directory - return + continue if os.path.isfile(full): yield full elif os.path.isdir(full): From 0b8012b8e1b985d8d5d15e163d5e25aa4a168970 Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 27 Oct 2022 13:15:58 -0500 Subject: [PATCH 041/290] Support Python 3.9 Remove the '|' syntax for type unions, since it's a 3.10 feature, and set mypy to check with version 3.9. --- .gitlab-ci.yml | 2 +- blueprintcompiler/language/gobject_object.py | 2 +- blueprintcompiler/language/gobject_signal.py | 5 +++-- blueprintcompiler/language/gtkbuilder_template.py | 3 ++- blueprintcompiler/language/values.py | 5 +++-- blueprintcompiler/outputs/xml/__init__.py | 6 ++++-- blueprintcompiler/outputs/xml/xml_emitter.py | 6 ++++-- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2790cd1..6982239 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ build: image: registry.gitlab.gnome.org/jwestman/blueprint-compiler stage: build script: - - mypy blueprintcompiler + - mypy --python-version=3.9 blueprintcompiler - coverage run -m unittest - coverage report - coverage html diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 711795c..2647a5a 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -45,7 +45,7 @@ class Object(AstNode): return self.tokens["id"] @property - def class_name(self) -> ClassName | None: + def class_name(self) -> T.Optional[ClassName]: return self.children[ClassName][0] @property diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index ba1b48b..1cba801 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -17,6 +17,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import typing as T from .gtkbuilder_template import Template from .common import * @@ -45,7 +46,7 @@ class Signal(AstNode): return self.tokens["name"] @property - def detail_name(self) -> str | None: + def detail_name(self) -> T.Optional[str]: return self.tokens["detail_name"] @property @@ -53,7 +54,7 @@ class Signal(AstNode): return self.tokens["handler"] @property - def object_id(self) -> str | None: + def object_id(self) -> T.Optional[str]: return self.tokens["object"] @property diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 35ef4f2..7392782 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -17,6 +17,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import typing as T from .gobject_object import Object, ObjectContent from .common import * @@ -39,7 +40,7 @@ class Template(Object): return self.tokens["id"] @property - def class_name(self) -> ClassName | None: + def class_name(self) -> T.Optional[ClassName]: if len(self.children[ClassName]): return self.children[ClassName][0] else: diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 5030774..2a889ec 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -17,6 +17,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import typing as T from .common import * from .types import TypeName @@ -50,7 +51,7 @@ class TranslatedStringValue(Value): return self.tokens["value"] @property - def context(self) -> str | None: + def context(self) -> T.Optional[str]: return self.tokens["context"] @@ -116,7 +117,7 @@ class NumberValue(Value): grammar = UseNumber("value") @property - def value(self) -> int | float: + def value(self) -> T.Union[int, float]: return self.tokens["value"] @validate() diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 2cbb425..e7e33d9 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -1,3 +1,5 @@ +import typing as T + from .. import OutputFormat from ...language import * from .xml_emitter import XmlEmitter @@ -45,7 +47,7 @@ class XmlOutput(OutputFormat): self._emit_object_or_template(obj, xml) xml.end_tag() - def _emit_object_or_template(self, obj: Object | Template, xml: XmlEmitter): + def _emit_object_or_template(self, obj: T.Union[Object, Template], xml: XmlEmitter): for child in obj.content.children: if isinstance(child, Property): self._emit_property(child, xml) @@ -123,7 +125,7 @@ class XmlOutput(OutputFormat): def _translated_string_attrs( self, translated: TranslatedStringValue - ) -> T.Dict[str, str | None]: + ) -> T.Dict[str, T.Optional[str]]: return { "translatable": "true", "context": translated.context, diff --git a/blueprintcompiler/outputs/xml/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py index def58b5..374b406 100644 --- a/blueprintcompiler/outputs/xml/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -17,6 +17,8 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import typing as T + from xml.sax import saxutils from blueprintcompiler.gir import GirType @@ -30,7 +32,7 @@ class XmlEmitter: self._tag_stack = [] self._needs_newline = False - def start_tag(self, tag, **attrs: str | GirType | ClassName | bool | None): + def start_tag(self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None]): self._indent() self.result += f"<{tag}" for key, val in attrs.items(): @@ -56,7 +58,7 @@ class XmlEmitter: self.result += f"" self._needs_newline = True - def put_text(self, text: str | int | float): + def put_text(self, text: T.Union[str, int, float]): self.result += saxutils.escape(str(text)) self._needs_newline = False From 8efc290e475507a709deb6568b909574cdf51be2 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 2 Nov 2022 15:43:41 +0000 Subject: [PATCH 042/290] doc: Mention history and Workbench --- docs/index.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 3439413..91b53a2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,21 +48,32 @@ Features - **Easy to learn.** The syntax should be very familiar to most people. Scroll through the :doc:`examples page ` for a quick overview of the whole language. -- **Modern tooling.** IDE integration for `GNOME Builder `_ - is in progress, and a VS Code extension is also planned. +- **Modern tooling.** Blueprint ships a `Language Server `_ for IDE integration. Links ----- - `Source code `_ +- `Workbench `_ lets you try, preview and export Blueprint +- `GNOME Builder `_ provides builtin support - `Vim syntax highlighting plugin by thetek42 `_ - `Vim syntax highlighting plugin by gabmus `_ - `GNU Emacs major mode by DrBluefall `_ - `Visual Studio Code plugin by bodil `_ +History +------- + +1. `Simplify our UI declarative language, a strawman proposal `_ +2. `A Markup Language for GTK `_ +3. `Introducing Blueprint: A New Way to Craft User Interfaces `_ +4. `Next Steps for Blueprint `_ + Built with Blueprint -------------------- +- `AdwSteamGtk `_ +- `Blurble `_ - `Bottles `_ - `Commit `_ - `Dialect `_ @@ -72,16 +83,21 @@ Built with Blueprint - `File Shredder `_ - `Geopard `_ - `Giara `_ +- `Girens `_ - `Gradience `_ - `Health `_ - `HydraPaper `_ - `Identity `_ - `Junction `_ - `Login Manager Settings `_ +- `Maniatic Launcher `_ +- `NewCaw `_ - `Paper `_ - `Passes `_ - `Playhouse `_ - `Plitki `_ +- `Raider `_ +- `Retro `_ - `Solanum `_ - `Swatch `_ - `Tangram `_ From bc15ac9efbb762e9e960badb5bf35655e5d8603b Mon Sep 17 00:00:00 2001 From: James Westman Date: Wed, 2 Nov 2022 10:18:30 -0500 Subject: [PATCH 043/290] docs: Add warning that blueprint is experimental --- docs/_static/styles.css | 8 ++ docs/conf.py | 2 + docs/experimental.svg | 163 ++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 8 ++ 4 files changed, 181 insertions(+) create mode 100644 docs/_static/styles.css create mode 100644 docs/experimental.svg diff --git a/docs/_static/styles.css b/docs/_static/styles.css new file mode 100644 index 0000000..914861d --- /dev/null +++ b/docs/_static/styles.css @@ -0,0 +1,8 @@ +.experimental-admonition { + display: flex; + align-items: center; +} + +.experimental-admonition img { + width: 64px; +} \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index fc413d9..b9d6d07 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,3 +50,5 @@ html_theme = 'furo' # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] + +html_css_files = ['styles.css'] \ No newline at end of file diff --git a/docs/experimental.svg b/docs/experimental.svg new file mode 100644 index 0000000..4ebac45 --- /dev/null +++ b/docs/experimental.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst index 91b53a2..dcde058 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,14 @@ Overview ======== +.. warning:: + .. container:: experimental-admonition + + .. image:: experimental.svg + + **Blueprint is still experimental.** Future versions may have breaking changes, and most GTK tutorials use XML syntax. + + Blueprint is a markup language and compiler for GTK 4 user interfaces. .. toctree:: From 86b07ef0aec07d2d5d1f85c861094be637fbf24d Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 22 Nov 2022 11:38:00 -0600 Subject: [PATCH 044/290] Fix new mypy errors --- blueprintcompiler/errors.py | 2 +- blueprintcompiler/parse_tree.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 70aea2a..e3bc787 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -141,7 +141,7 @@ class CompilerBugError(Exception): """ Emitted on assertion errors """ -def assert_true(truth: bool, message:str=None): +def assert_true(truth: bool, message: T.Optional[str]=None): if not truth: raise CompilerBugError(message) diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index bc36415..0e2644b 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -444,7 +444,7 @@ class Match(ParseNode): token = ctx.next_token() return str(token) == self.op - def expected(self, expect: str = None): + def expected(self, expect: T.Optional[str] = None): """ Convenience method for err(). """ if expect is None: return self.err(f"Expected '{self.op}'") From 46a06bb7b699cca896e248b4781a13282beda0f2 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 22 Nov 2022 11:43:19 -0600 Subject: [PATCH 045/290] docs: Fix version in flatpak docs --- docs/flatpak.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/flatpak.rst b/docs/flatpak.rst index 6c40a9b..7382b47 100644 --- a/docs/flatpak.rst +++ b/docs/flatpak.rst @@ -16,7 +16,7 @@ a module in your flatpak manifest: { "type": "git", "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", - "branch": "main" + "tag": "v0.4.0" } ] } From d8f1b41ef07057f35de135ef9e8a40ae51a9741d Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 26 Nov 2022 16:44:43 -0600 Subject: [PATCH 046/290] docs: Document the new typeof() operator --- docs/examples.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/examples.rst b/docs/examples.rst index 9b123d2..13b97a8 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -161,6 +161,26 @@ Defining object properties inline Note the semicolon after the closing brace of the ``Gtk.Adjustment``. It is required. +Type Literals +~~~~~~~~~~~~~ + +Some properties have the type `GObject.Type `_, that is, their value is +itself a description of a type in the GObject type system. An example is +`Gio.ListStore:item-type `_. Such properties can be +specified with the ``typeof()`` operator: + +.. code-block:: + + Gio.ListStore { + item-type: typeof(.MyDataModel); + } + +.. note:: + + Though it looks like a function, ``typeof()`` is actually a special operator. This is an important syntactic + distinction because the argument to ``typeof()`` is interpreted at the grammar level as a type, not a value or + expression. + Bindings ~~~~~~~~ From b915c227f899473d807b609b29dfd4a52416db10 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 26 Nov 2022 17:10:53 -0600 Subject: [PATCH 047/290] Fix type declaration --- blueprintcompiler/lsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index b1610d7..6e43e3b 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -95,7 +95,7 @@ class LanguageServer: def __init__(self): self.client_capabilities = {} - self._open_files: {str: OpenFile} = {} + self._open_files: T.Dict[str, OpenFile] = {} def run(self): # Read tags from gir files. During normal compilation these are From 97f0feaf7ea9ada0bd6ac51d6efd497a317009ca Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 26 Nov 2022 17:14:12 -0600 Subject: [PATCH 048/290] Update MAINTENANCE.md --- MAINTENANCE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MAINTENANCE.md b/MAINTENANCE.md index a487b88..3f62476 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -2,7 +2,9 @@ 1. Look at the git log since the previous release. Note every significant change in the NEWS file. -2. Update the version number at the top of meson.build according to semver. +2. Update the version number, according to semver: + - At the top of meson.build + - In docs/flatpak.rst 3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag. 4. Create a "Post-release version bump" commit. From 9adcab2d225fd6435edc85c72a0b67e33880e00b Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 26 Nov 2022 17:14:49 -0600 Subject: [PATCH 049/290] Release v0.6.0 --- NEWS.md | 27 ++++++++++++++++++++++++--- docs/flatpak.rst | 2 +- meson.build | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0997afb..276d879 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,10 +1,31 @@ -# v0.6.0 (unreleased) +# v0.6.0 ## Breaking Changes -- Quoted and numeric literals are no longer interchangeable (e.g. `"800"` is -no longer an accepted value for an integer type). +- 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. +## Added +- There is now syntax for `GType` literals: the `typeof()` pseudo-function. For example, list stores have an `item-type` + property which is now specifiable like this: `item-type: typeof(.MyDataModel)`. See the documentation for more details. + +## Changed +- The language server now logs to stderr. + +## Fixed +- Fix the build on Windows, where backslashes in paths were not escaped. (William Roy) +- Remove the syntax for specifying menu objects inline, since it does not work. +- Fix a crash in the language server that was triggered in files with incomplete `using Gtk 4.0;` statements. +- Fixed compilation on big-endian systems. +- Fix an issue in the interactive port tool that would lead to missed files. (Frank Dana) + +## Documentation +- Fix an issue for documentation contributors where changing the documentation files would not trigger a rebuild. +- Document the missing support for Gtk.Label ``, which is intentional, and recommend alternatives. (Sonny + Piers) +- Add a prominent warning that Blueprint is still experimental + + # v0.4.0 ## Added diff --git a/docs/flatpak.rst b/docs/flatpak.rst index 7382b47..f848671 100644 --- a/docs/flatpak.rst +++ b/docs/flatpak.rst @@ -16,7 +16,7 @@ a module in your flatpak manifest: { "type": "git", "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", - "tag": "v0.4.0" + "tag": "v0.6.0" } ] } diff --git a/meson.build b/meson.build index be517bc..2642084 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('blueprint-compiler', - version: '0.5.0', + version: '0.6.0', ) subdir('docs') From 00a31d87bbac71133a7080a027e02371b511bdef Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 26 Nov 2022 17:20:04 -0600 Subject: [PATCH 050/290] Post-release version bump --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 2642084..186c967 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('blueprint-compiler', - version: '0.6.0', + version: '0.7.0', ) subdir('docs') From 6a36d923800738dbec65fa6ad9a992e5de89801a Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 29 Nov 2022 09:19:31 -0600 Subject: [PATCH 051/290] ci: Update regression tests --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6982239..598481b 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 94613f275efc810610768d5ee8b2aec28392c3e8 + - git checkout e1a2b04ce13838794eec9678deff95802fa278d1 - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' From 8fee46ec686ca4232feffea6c6d1b51daff4f4c0 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 11:49:10 -0600 Subject: [PATCH 052/290] Format using black --- blueprintcompiler/ast_utils.py | 28 ++- blueprintcompiler/completions.py | 49 +++-- blueprintcompiler/completions_utils.py | 15 +- blueprintcompiler/decompiler.py | 103 +++++++--- blueprintcompiler/errors.py | 51 +++-- blueprintcompiler/gir.py | 127 ++++++++---- blueprintcompiler/interactive_port.py | 107 +++++++--- blueprintcompiler/language/__init__.py | 11 +- blueprintcompiler/language/attributes.py | 6 +- blueprintcompiler/language/gobject_object.py | 9 +- .../language/gobject_property.py | 41 ++-- blueprintcompiler/language/gobject_signal.py | 31 ++- blueprintcompiler/language/gtk_a11y.py | 13 +- .../language/gtk_combo_box_text.py | 17 +- blueprintcompiler/language/gtk_file_filter.py | 18 +- blueprintcompiler/language/gtk_layout.py | 7 +- blueprintcompiler/language/gtk_menu.py | 120 +++++------- blueprintcompiler/language/gtk_size_group.py | 2 +- blueprintcompiler/language/gtk_string_list.py | 5 +- blueprintcompiler/language/gtk_styles.py | 3 +- .../language/gtkbuilder_child.py | 27 ++- .../language/gtkbuilder_template.py | 14 +- blueprintcompiler/language/imports.py | 13 +- blueprintcompiler/language/response_id.py | 24 +-- blueprintcompiler/language/types.py | 15 +- blueprintcompiler/language/ui.py | 28 +-- blueprintcompiler/language/values.py | 43 ++-- blueprintcompiler/lsp.py | 185 +++++++++++------- blueprintcompiler/lsp_utils.py | 10 +- blueprintcompiler/main.py | 43 ++-- blueprintcompiler/outputs/__init__.py | 2 + blueprintcompiler/parse_tree.py | 182 ++++++++++------- blueprintcompiler/parser.py | 2 +- blueprintcompiler/tokenizer.py | 48 ++--- blueprintcompiler/typelib.py | 4 +- blueprintcompiler/utils.py | 30 +-- blueprintcompiler/xml_reader.py | 31 +-- tests/fuzz.py | 9 +- tests/test_samples.py | 30 +-- tests/test_tokenizer.py | 82 ++++---- 40 files changed, 975 insertions(+), 610 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index cc5c44d..e4f2efa 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -26,11 +26,14 @@ from .lsp_utils import SemanticToken class Children: - """ Allows accessing children by type using array syntax. """ + """Allows accessing children by type using array syntax.""" + def __init__(self, children): self._children = children + def __iter__(self): return iter(self._children) + def __getitem__(self, key): if isinstance(key, int): return self._children[key] @@ -39,7 +42,7 @@ class Children: class AstNode: - """ Base class for nodes in the abstract syntax tree. """ + """Base class for nodes in the abstract syntax tree.""" completers: T.List = [] @@ -55,8 +58,9 @@ class AstNode: def __init_subclass__(cls): cls.completers = [] - cls.validators = [getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")] - + cls.validators = [ + getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator") + ] @property def root(self): @@ -116,13 +120,11 @@ class AstNode: for child in self.children: yield from child.get_semantic_tokens() - def iterate_children_recursive(self) -> T.Iterator["AstNode"]: yield self for child in self.children: yield from child.iterate_children_recursive() - def validate_unique_in_parent(self, error, check=None): for child in self.parent.children: if child is self: @@ -132,13 +134,19 @@ class AstNode: if check is None or check(child): raise CompileError( error, - references=[ErrorReference(child.group.start, child.group.end, "previous declaration was here")] + references=[ + ErrorReference( + child.group.start, + child.group.end, + "previous declaration was here", + ) + ], ) def validate(token_name=None, end_token_name=None, skip_incomplete=False): - """ Decorator for functions that validate an AST node. Exceptions raised - during validation are marked with range information from the tokens. """ + """Decorator for functions that validate an AST node. Exceptions raised + during validation are marked with range information from the tokens.""" def decorator(func): def inner(self): @@ -191,7 +199,7 @@ class Docs: def docs(*args, **kwargs): - """ Decorator for functions that return documentation for tokens. """ + """Decorator for functions that return documentation for tokens.""" def decorator(func): return Docs(func, *args, **kwargs) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 7566f18..085baee 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -30,9 +30,13 @@ from .tokenizer import TokenType, Token Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] -def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int) -> T.Iterator[Completion]: +def _complete( + ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int +) -> T.Iterator[Completion]: for child in ast_node.children: - if child.group.start <= idx and (idx < child.group.end or (idx == child.group.end and child.incomplete)): + if child.group.start <= idx and ( + idx < child.group.end or (idx == child.group.end and child.incomplete) + ): yield from _complete(child, tokens, idx, token_idx) return @@ -49,7 +53,9 @@ def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int yield from completer(prev_tokens, ast_node) -def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[Completion]: +def complete( + ast_node: AstNode, tokens: T.List[Token], idx: int +) -> T.Iterator[Completion]: token_idx = 0 # find the current token for i, token in enumerate(tokens): @@ -71,13 +77,17 @@ def using_gtk(ast_node, match_variables): @completer( applies_in=[language.UI, language.ObjectContent, language.Template], - matches=new_statement_patterns + matches=new_statement_patterns, ) def namespace(ast_node, match_variables): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") for ns in ast_node.root.children[language.Import]: if ns.gir_namespace is not None: - yield Completion(ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".") + yield Completion( + ns.gir_namespace.name, + CompletionItemKind.Module, + text=ns.gir_namespace.name + ".", + ) @completer( @@ -85,7 +95,7 @@ def namespace(ast_node, match_variables): matches=[ [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, ".")], - ] + ], ) def object_completer(ast_node, match_variables): ns = ast_node.root.gir.namespaces.get(match_variables[0]) @@ -117,9 +127,7 @@ def property_completer(ast_node, match_variables): @completer( applies_in=[language.Property, language.BaseTypedAttribute], - matches=[ - [(TokenType.IDENT, None), (TokenType.OP, ":")] - ], + matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) def prop_value_completer(ast_node, match_variables): if isinstance(ast_node.value_type, gir.Enumeration): @@ -141,16 +149,23 @@ def signal_completer(ast_node, match_variables): if not isinstance(ast_node.parent, language.Object): name = "on" else: - name = "on_" + (ast_node.parent.children[ClassName][0].tokens["id"] or ast_node.parent.children[ClassName][0].tokens["class_name"].lower()) - yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;") + name = "on_" + ( + ast_node.parent.children[ClassName][0].tokens["id"] + or ast_node.parent.children[ClassName][0] + .tokens["class_name"] + .lower() + ) + yield Completion( + signal, + CompletionItemKind.Property, + snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;", + ) -@completer( - applies_in=[language.UI], - matches=new_statement_patterns -) +@completer(applies_in=[language.UI], matches=new_statement_patterns) def template_completer(ast_node, match_variables): yield Completion( - "template", CompletionItemKind.Snippet, - snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}" + "template", + CompletionItemKind.Snippet, + snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}", ) diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 85f5159..b9811b9 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -32,20 +32,25 @@ new_statement_patterns = [ def applies_to(*ast_types): - """ Decorator describing which AST nodes the completer should apply in. """ + """Decorator describing which AST nodes the completer should apply in.""" + def decorator(func): for c in ast_types: c.completers.append(func) return func + return decorator -def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None): + +def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def decorator(func): def inner(prev_tokens: T.List[Token], ast_node): # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: - type = ast_node.root.gir.get_type(applies_in_subclass[1], applies_in_subclass[0]) + type = ast_node.root.gir.get_type( + applies_in_subclass[1], applies_in_subclass[0] + ) if ast_node.gir_class and not ast_node.gir_class.assignable_to(type): return @@ -59,7 +64,9 @@ def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None): for i in range(0, len(pattern)): type, value = pattern[i] token = prev_tokens[i - len(pattern)] - if token.type != type or (value is not None and str(token) != value): + if token.type != type or ( + value is not None and str(token) != value + ): break if value is None: match_variables.append(str(token)) diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index cd66386..145e4be 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -60,16 +60,16 @@ class DecompileCtx: self.gir.add_namespace(get_namespace("Gtk", "4.0")) - @property def result(self): - imports = "\n".join([ - f"using {ns} {namespace.version};" - for ns, namespace in self.gir.namespaces.items() - ]) + imports = "\n".join( + [ + f"using {ns} {namespace.version};" + for ns, namespace in self.gir.namespaces.items() + ] + ) return imports + "\n" + self._result - def type_by_cname(self, cname): if type := self.gir.get_type_by_cname(cname): return type @@ -83,7 +83,6 @@ class DecompileCtx: except: pass - def start_block(self): self._blocks_need_end.append(None) @@ -94,7 +93,6 @@ class DecompileCtx: def end_block_with(self, text): self._blocks_need_end[-1] = text - def print(self, line, newline=True): if line == "}" or line == "]": self._indent -= 1 @@ -109,7 +107,11 @@ class DecompileCtx: line_type = LineType.STMT else: line_type = LineType.NONE - if line_type != self._last_line_type and self._last_line_type != LineType.BLOCK_START and line_type != LineType.BLOCK_END: + if ( + line_type != self._last_line_type + and self._last_line_type != LineType.BLOCK_START + and line_type != LineType.BLOCK_END + ): self._result += "\n" self._last_line_type = line_type @@ -127,10 +129,10 @@ class DecompileCtx: for member in type.members.values(): if member.nick == value or member.c_ident == value: return member.name - return value.replace('-', '_') + return value.replace("-", "_") if type is None: - self.print(f"{name}: \"{escape_quote(value)}\";") + self.print(f'{name}: "{escape_quote(value)}";') elif type.assignable_to(FloatType()): self.print(f"{name}: {value};") elif type.assignable_to(BoolType()): @@ -139,12 +141,20 @@ class DecompileCtx: elif ( type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture")) - or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable")) - or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction")) - or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable") + ) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction") + ) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger") + ) + ): + self.print(f'{name}: "{escape_quote(value)}";') + elif type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("GObject.Object") ): - self.print(f"{name}: \"{escape_quote(value)}\";") - elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")): self.print(f"{name}: {value};") elif isinstance(type, Bitfield): flags = [get_enum_name(flag) for flag in value.split("|")] @@ -152,7 +162,7 @@ class DecompileCtx: elif isinstance(type, Enumeration): self.print(f"{name}: {get_enum_name(value)};") else: - self.print(f"{name}: \"{escape_quote(value)}\";") + self.print(f'{name}: "{escape_quote(value)}";') def _decompile_element(ctx: DecompileCtx, gir, xml): @@ -191,19 +201,21 @@ def decompile(data): return ctx.result - def canon(string: str) -> str: if string == "class": return "klass" else: return string.replace("-", "_").lower() + def truthy(string: str) -> bool: return string.lower() in ["yes", "true", "t", "y", "1"] + def full_name(gir): return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name + def lookup_by_cname(gir, cname: str): if isinstance(gir, GirContext): return gir.get_type_by_cname(cname) @@ -216,15 +228,17 @@ def decompiler(tag, cdata=False): func._cdata = cdata _DECOMPILERS[tag] = func return func + return decorator def escape_quote(string: str) -> str: - return (string - .replace("\\", "\\\\") - .replace("\'", "\\'") - .replace("\"", "\\\"") - .replace("\n", "\\n")) + return ( + string.replace("\\", "\\\\") + .replace("'", "\\'") + .replace('"', '\\"') + .replace("\n", "\\n") + ) @decompiler("interface") @@ -243,7 +257,18 @@ def decompile_placeholder(ctx, gir): @decompiler("property", cdata=True) -def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None): +def decompile_property( + ctx, + gir, + name, + cdata, + bind_source=None, + bind_property=None, + bind_flags=None, + translatable="false", + comments=None, + context=None, +): name = name.replace("_", "-") if comments is not None: ctx.print(f"/* Translators: {comments} */") @@ -263,18 +288,32 @@ def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=No ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") elif truthy(translatable): if context is not None: - ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");") + ctx.print( + f'{name}: C_("{escape_quote(context)}", "{escape_quote(cdata)}");' + ) else: - ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");") + ctx.print(f'{name}: _("{escape_quote(cdata)}");') elif gir is None or gir.properties.get(name) is None: - ctx.print(f"{name}: \"{escape_quote(cdata)}\";") + ctx.print(f'{name}: "{escape_quote(cdata)}";') else: ctx.print_attribute(name, cdata, gir.properties.get(name).type) return gir + @decompiler("attribute", cdata=True) -def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None): - decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context) +def decompile_attribute( + ctx, gir, name, cdata, translatable="false", comments=None, context=None +): + decompile_property( + ctx, + gir, + name, + cdata, + translatable=translatable, + comments=comments, + context=context, + ) + @decompiler("attributes") def decompile_attributes(ctx, gir): @@ -291,5 +330,7 @@ class UnsupportedError(Exception): print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}") if self.tag: print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}") - print(f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You -probably need to port this file manually.{Colors.CLEAR}\n""") + print( + f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You +probably need to port this file manually.{Colors.CLEAR}\n""" + ) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index e3bc787..7ddaef0 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -23,9 +23,10 @@ import sys, traceback from . import utils from .utils import Colors + class PrintableError(Exception): - """ Parent class for errors that can be pretty-printed for the user, e.g. - compilation warnings and errors. """ + """Parent class for errors that can be pretty-printed for the user, e.g. + compilation warnings and errors.""" def pretty_print(self, filename, code): raise NotImplementedError() @@ -39,12 +40,22 @@ class ErrorReference: class CompileError(PrintableError): - """ A PrintableError with a start/end position and optional hints """ + """A PrintableError with a start/end position and optional hints""" category = "error" color = Colors.RED - def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None, actions=None, fatal=False, references=None): + def __init__( + self, + message, + start=None, + end=None, + did_you_mean=None, + hints=None, + actions=None, + fatal=False, + references=None, + ): super().__init__(message) self.message = message @@ -62,7 +73,6 @@ class CompileError(PrintableError): self.hints.append(hint) return self - def _did_you_mean(self, word: str, options: T.List[str]): if word.replace("_", "-") in options: self.hint(f"use '-', not '_': `{word.replace('_', '-')}`") @@ -86,9 +96,11 @@ class CompileError(PrintableError): # Display 1-based line numbers line_num += 1 - stream.write(f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} + stream.write( + f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""") +{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" + ) for hint in self.hints: stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n") @@ -98,9 +110,11 @@ at {filename} line {line_num} column {col_num}: line = code.splitlines(True)[line_num] line_num += 1 - stream.write(f"""{Colors.FAINT}note: {ref.message}: + stream.write( + f"""{Colors.FAINT}note: {ref.message}: at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""") +{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" + ) stream.write("\n") @@ -122,9 +136,9 @@ class CodeAction: class MultipleErrors(PrintableError): - """ If multiple errors occur during compilation, they can be collected into + """If multiple errors occur during compilation, they can be collected into a list and re-thrown using the MultipleErrors exception. It will - pretty-print all of the errors and a count of how many errors there are. """ + pretty-print all of the errors and a count of how many errors there are.""" def __init__(self, errors: T.List[CompileError]): super().__init__() @@ -138,24 +152,25 @@ class MultipleErrors(PrintableError): class CompilerBugError(Exception): - """ Emitted on assertion errors """ + """Emitted on assertion errors""" -def assert_true(truth: bool, message: T.Optional[str]=None): +def assert_true(truth: bool, message: T.Optional[str] = None): if not truth: raise CompilerBugError(message) -def report_bug(): # pragma: no cover - """ Report an error and ask people to report it. """ +def report_bug(): # pragma: no cover + """Report an error and ask people to report it.""" print(traceback.format_exc()) print(f"Arguments: {sys.argv}\n") - print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** + print( + f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** The blueprint-compiler program has crashed. Please report the above stacktrace, along with the input file(s) if possible, on GitLab: {Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue -{Colors.CLEAR}""") +{Colors.CLEAR}""" + ) sys.exit(1) - diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 78335ee..a1bb419 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -21,9 +21,10 @@ from functools import cached_property import typing as T import os, sys -import gi # type: ignore +import gi # type: ignore + gi.require_version("GIRepository", "2.0") -from gi.repository import GIRepository # type: ignore +from gi.repository import GIRepository # type: ignore from .errors import CompileError, CompilerBugError from . import typelib, xml_reader @@ -60,10 +61,13 @@ def get_namespace(namespace, version) -> "Namespace": def get_xml(namespace, version): from .main import VERSION from xml.etree import ElementTree + search_paths = [] if data_paths := os.environ.get("XDG_DATA_DIRS"): - search_paths += [os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep)] + search_paths += [ + os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep) + ] filename = f"{namespace}-{version}.gir" @@ -104,36 +108,57 @@ class BasicType(GirType): def full_name(self) -> str: return self.name + class BoolType(BasicType): name = "bool" + def assignable_to(self, other) -> bool: return isinstance(other, BoolType) + class IntType(BasicType): name = "int" + def assignable_to(self, other) -> bool: - return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) + return ( + isinstance(other, IntType) + or isinstance(other, UIntType) + or isinstance(other, FloatType) + ) + class UIntType(BasicType): name = "uint" + def assignable_to(self, other) -> bool: - return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) + return ( + isinstance(other, IntType) + or isinstance(other, UIntType) + or isinstance(other, FloatType) + ) + class FloatType(BasicType): 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) + class TypeType(BasicType): name = "GType" + def assignable_to(self, other) -> bool: return isinstance(other, TypeType) + _BASIC_TYPES = { "gboolean": BoolType, "int": IntType, @@ -150,6 +175,7 @@ _BASIC_TYPES = { "type": TypeType, } + class GirNode: def __init__(self, container, tl): self.container = container @@ -291,7 +317,9 @@ class Interface(GirNode, GirType): n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE offset += (n_prerequisites + n_prerequisites % 2) * 2 - offset += self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE + offset += ( + self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE + ) offset += self.tl.INTERFACE_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE n_signals = self.tl.INTERFACE_N_SIGNALS property_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE @@ -342,7 +370,9 @@ class Class(GirNode, GirType): offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE offset += (n_interfaces + n_interfaces % 2) * 2 offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE - offset += self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + offset += ( + self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + ) n_properties = self.tl.OBJ_N_PROPERTIES property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE result = {} @@ -357,7 +387,9 @@ class Class(GirNode, GirType): offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE offset += (n_interfaces + n_interfaces % 2) * 2 offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE - offset += self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + offset += ( + self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + ) offset += self.tl.OBJ_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE offset += self.tl.OBJ_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE n_signals = self.tl.OBJ_N_SIGNALS @@ -381,16 +413,18 @@ class Class(GirNode, GirType): if self.parent is not None: result += f" : {self.parent.container.name}.{self.parent.name}" if len(self.implements): - result += " implements " + ", ".join([impl.full_name for impl in self.implements]) + result += " implements " + ", ".join( + [impl.full_name for impl in self.implements] + ) return result @cached_property def properties(self): - return { p.name: p for p in self._enum_properties() } + return {p.name: p for p in self._enum_properties()} @cached_property def signals(self): - return { s.name: s for s in self._enum_signals() } + return {s.name: s for s in self._enum_signals()} def assignable_to(self, other) -> bool: if self == other: @@ -509,9 +543,12 @@ class Namespace(GirNode): elif entry_type == typelib.BLOB_TYPE_OBJECT: 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) + 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): @@ -531,25 +568,33 @@ class Namespace(GirNode): @cached_property def classes(self): - return { name: entry for name, entry in self.entries.items() if isinstance(entry, Class) } + return { + name: entry + for name, entry in self.entries.items() + if isinstance(entry, Class) + } @cached_property def interfaces(self): - return { name: entry for name, entry in self.entries.items() if isinstance(entry, Interface) } + return { + name: entry + for name, entry in self.entries.items() + if isinstance(entry, Interface) + } def get_type(self, name): - """ Gets a type (class, interface, enum, etc.) from this namespace. """ + """Gets a type (class, interface, enum, etc.) from this namespace.""" return self.entries.get(name) def get_type_by_cname(self, cname: str): - """ Gets a type from this namespace by its C name. """ + """Gets a type from this namespace by its C name.""" for item in self.entries.values(): if hasattr(item, "cname") and item.cname == cname: return item def lookup_type(self, type_name: str): - """ Looks up a type in the scope of this namespace (including in the - namespace's dependencies). """ + """Looks up a type in the scope of this namespace (including in the + namespace's dependencies).""" if type_name in _BASIC_TYPES: return _BASIC_TYPES[type_name]() @@ -569,7 +614,9 @@ class Repository(GirNode): if dependencies := tl[0x24].string: deps = [tuple(dep.split("-", 1)) for dep in dependencies.split("|")] try: - self.includes = { name: get_namespace(name, version) for name, version in deps } + self.includes = { + name: get_namespace(name, version) for name, version in deps + } except: raise CompilerBugError(f"Failed to load dependencies.") else: @@ -578,16 +625,14 @@ class Repository(GirNode): def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: return self.lookup_namespace(ns).get_type(name) - def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: for ns in [self.namespace, *self.includes.values()]: if type := ns.get_type_by_cname(name): return type return None - def lookup_namespace(self, ns: str): - """ Finds a namespace among this namespace's dependencies. """ + """Finds a namespace among this namespace's dependencies.""" if ns == self.namespace.name: return self.namespace else: @@ -610,9 +655,19 @@ class Repository(GirNode): return BoolType() elif type_id in [typelib.TYPE_FLOAT, typelib.TYPE_DOUBLE]: return FloatType() - elif type_id in [typelib.TYPE_INT8, typelib.TYPE_INT16, typelib.TYPE_INT32, typelib.TYPE_INT64]: + elif type_id in [ + typelib.TYPE_INT8, + typelib.TYPE_INT16, + typelib.TYPE_INT32, + typelib.TYPE_INT64, + ]: return IntType() - elif type_id in [typelib.TYPE_UINT8, typelib.TYPE_UINT16, typelib.TYPE_UINT32, typelib.TYPE_UINT64]: + elif type_id in [ + typelib.TYPE_UINT8, + typelib.TYPE_UINT16, + typelib.TYPE_UINT32, + typelib.TYPE_UINT64, + ]: return UIntType() elif type_id == typelib.TYPE_UTF8: return StringType() @@ -621,30 +676,30 @@ class Repository(GirNode): else: raise CompilerBugError("Unknown type ID", type_id) else: - return self._resolve_dir_entry(self.tl.header[type_id].INTERFACE_TYPE_INTERFACE) - + return self._resolve_dir_entry( + self.tl.header[type_id].INTERFACE_TYPE_INTERFACE + ) class GirContext: def __init__(self): self.namespaces = {} - def add_namespace(self, namespace: Namespace): other = self.namespaces.get(namespace.name) if other is not None and other.version != namespace.version: - raise CompileError(f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier") + raise CompileError( + f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier" + ) self.namespaces[namespace.name] = namespace - def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: for ns in self.namespaces.values(): if type := ns.get_type_by_cname(name): return type return None - def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: ns = ns or "Gtk" @@ -653,7 +708,6 @@ class GirContext: return self.namespaces[ns].get_type(name) - def get_class(self, name: str, ns: str) -> T.Optional[Class]: type = self.get_type(name, ns) if isinstance(type, Class): @@ -661,10 +715,9 @@ class GirContext: else: return None - def validate_ns(self, ns: str): - """ Raises an exception if there is a problem looking up the given - namespace. """ + """Raises an exception if there is a problem looking up the given + namespace.""" ns = ns or "Gtk" @@ -675,7 +728,7 @@ class GirContext: ) def validate_type(self, name: str, ns: str): - """ Raises an exception if there is a problem looking up the given type. """ + """Raises an exception if there is a problem looking up the given type.""" self.validate_ns(ns) diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index 229f238..dd00317 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -35,9 +35,11 @@ class CouldNotPort: def __init__(self, message): self.message = message + def change_suffix(f): return f.removesuffix(".ui") + ".blp" + def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: if os.path.exists(out_file): return CouldNotPort("already exists") @@ -63,12 +65,15 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: except PrintableError as e: e.pretty_print(out_file, decompiled) - print(f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}") + print( + f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}" + ) print(f"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}") print( -f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the + f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the porting tool. If you think it's a bug (which is likely), please file an issue on GitLab: -{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""") +{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" + ) return CouldNotPort("does not compile") @@ -108,7 +113,9 @@ def enter(): def step1(): - print(f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}") + print( + f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}" + ) if os.path.exists("subprojects/blueprint-compiler.wrap"): print("subprojects/blueprint-compiler.wrap already exists, skipping\n") @@ -121,17 +128,20 @@ def step1(): pass from .main import VERSION + VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION with open("subprojects/blueprint-compiler.wrap", "w") as wrap: - wrap.write(f"""[wrap-git] + wrap.write( + f"""[wrap-git] directory = blueprint-compiler url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git revision = {VERSION} depth = 1 [provide] -program_names = blueprint-compiler""") +program_names = blueprint-compiler""" + ) print() @@ -146,7 +156,9 @@ def step2(): if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"): gitignore.write("\n/subprojects/blueprint-compiler\n") else: - print("'/subprojects/blueprint-compiler' already in .gitignore, skipping") + print( + "'/subprojects/blueprint-compiler' already in .gitignore, skipping" + ) else: if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"): with open(".gitignore", "w") as gitignore: @@ -169,9 +181,13 @@ def step3(): if isinstance(result, CouldNotPort): if result.message == "already exists": print(Colors.FAINT, end="") - print(f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}") + print( + f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}" + ) else: - print(f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}") + print( + f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}" + ) success += 1 print() @@ -180,7 +196,9 @@ def step3(): elif success == len(files): print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}") elif success > 0: - print(f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}") + print( + f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}" + ) else: print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}") @@ -204,22 +222,33 @@ def step3(): def step4(ported): print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}") - print(f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}") + print( + f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}" + ) - meson_files = [file for file in listdir_recursive(".") if os.path.basename(file) == "meson.build"] + meson_files = [ + file + for file in listdir_recursive(".") + if os.path.basename(file) == "meson.build" + ] for meson_file in meson_files: with open(meson_file, "r") as f: if "gnome.compile_resources" in f.read(): parent = os.path.dirname(meson_file) - file_list = "\n ".join([ - f"'{os.path.relpath(file, parent)}'," - for file in ported - if file.startswith(parent) - ]) + file_list = "\n ".join( + [ + f"'{os.path.relpath(file, parent)}'," + for file in ported + if file.startswith(parent) + ] + ) if len(file_list): - print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}") - print(f""" + print( + f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}" + ) + print( + f""" blueprints = custom_target('blueprints', input: files( {file_list} @@ -227,14 +256,17 @@ blueprints = custom_target('blueprints', output: '.', command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], ) -""") +""" + ) enter() - print(f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()' + print( + f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()' arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR} dependencies: blueprints, - """) + """ + ) enter() print() @@ -244,7 +276,9 @@ def step5(in_files): print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}") if not os.path.exists("po/POTFILES.in"): - print(f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n") + print( + f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n" + ) return with open("po/POTFILES.in", "r") as potfiles: @@ -257,12 +291,24 @@ def step5(in_files): new_data = "".join(lines) - print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}") print( - "".join([ - (Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + Colors.CLEAR - for line in difflib.unified_diff(old_lines, lines) - ]) + f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}" + ) + print( + "".join( + [ + ( + Colors.GREEN + if line.startswith("+") + else Colors.RED + Colors.FAINT + if line.startswith("-") + else "" + ) + + line + + Colors.CLEAR + for line in difflib.unified_diff(old_lines, lines) + ] + ) ) if yesno("Is this ok?"): @@ -291,5 +337,6 @@ def run(opts): step5(in_files) step6(in_files) - print(f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}") - + print( + f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}" + ) diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index da5a44c..58662b2 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -16,7 +16,16 @@ from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .ui import UI from .types import ClassName -from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, Flag, QuotedValue, NumberValue, Value +from .values import ( + TypeValue, + IdentValue, + TranslatedStringValue, + FlagsValue, + Flag, + QuotedValue, + NumberValue, + Value, +) from .common import * diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py index 66faa60..77f01f2 100644 --- a/blueprintcompiler/language/attributes.py +++ b/blueprintcompiler/language/attributes.py @@ -23,7 +23,7 @@ from .common import * class BaseAttribute(AstNode): - """ A helper class for attribute syntax of the form `name: literal_value;`""" + """A helper class for attribute syntax of the form `name: literal_value;`""" tag_name: str = "" attr_name: str = "name" @@ -34,5 +34,5 @@ class BaseAttribute(AstNode): class BaseTypedAttribute(BaseAttribute): - """ A BaseAttribute whose parent has a value_type property that can assist - in validation. """ + """A BaseAttribute whose parent has a value_type property that can assist + in validation.""" diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 2647a5a..8e6fd23 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -33,6 +33,7 @@ class ObjectContent(AstNode): def gir_class(self): return self.parent.gir_class + class Object(AstNode): grammar: T.Any = [ ConcreteClassName, @@ -75,13 +76,17 @@ 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}") + raise CompileError( + f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}" + ) @decompiler("object") def decompile_object(ctx, gir, klass, id=None): gir_class = ctx.type_by_cname(klass) - klass_name = decompile.full_name(gir_class) if gir_class is not None else "." + klass + klass_name = ( + decompile.full_name(gir_class) if gir_class is not None else "." + klass + ) if id is None: ctx.print(f"{klass_name} {{") else: diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 4d47e80..2c1e2ae 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -34,12 +34,16 @@ class Property(AstNode): UseIdent("bind_source"), ".", UseIdent("bind_property"), - ZeroOrMore(AnyOf( - ["no-sync-create", UseLiteral("no_sync_create", True)], - ["inverted", UseLiteral("inverted", True)], - ["bidirectional", UseLiteral("bidirectional", True)], - Match("sync-create").warn("sync-create is deprecated in favor of no-sync-create"), - )), + ZeroOrMore( + AnyOf( + ["no-sync-create", UseLiteral("no_sync_create", True)], + ["inverted", UseLiteral("inverted", True)], + ["bidirectional", UseLiteral("bidirectional", True)], + Match("sync-create").warn( + "sync-create is deprecated in favor of no-sync-create" + ), + ) + ), ";", ], Statement( @@ -63,19 +67,16 @@ class Property(AstNode): 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: @@ -91,15 +92,19 @@ 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']}", - did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()) + did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()), ) @validate("bind") def property_bindable(self): - if self.tokens["bind"] and self.gir_property is not None and self.gir_property.construct_only: + if ( + self.tokens["bind"] + and self.gir_property is not None + and self.gir_property.construct_only + ): raise CompileError( f"{self.gir_property.full_name} can't be bound because it is construct-only", - hints=["construct-only properties may only be set to a static value"] + hints=["construct-only properties may only be set to a static value"], ) @validate("name") @@ -107,7 +112,6 @@ class Property(AstNode): if self.gir_property is not None and not self.gir_property.writable: raise CompileError(f"{self.gir_property.full_name} is not writable") - @validate() def obj_property_type(self): if len(self.children[Object]) == 0: @@ -115,20 +119,23 @@ class Property(AstNode): 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): + 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}" ) - @validate("name") def unique_in_parent(self): self.validate_unique_in_parent( f"Duplicate property '{self.tokens['name']}'", - check=lambda child: child.tokens["name"] == self.tokens["name"] + check=lambda child: child.tokens["name"] == self.tokens["name"], ) - @docs("name") def property_docs(self): if self.gir_property is not None: diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 1cba801..d50792e 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -26,19 +26,23 @@ from .common import * class Signal(AstNode): grammar = Statement( UseIdent("name"), - Optional([ - "::", - UseIdent("detail_name").expected("a signal detail name"), - ]), + Optional( + [ + "::", + UseIdent("detail_name").expected("a signal detail name"), + ] + ), "=>", UseIdent("handler").expected("the name of a function to handle the signal"), Match("(").expected("argument list"), Optional(UseIdent("object")).expected("object identifier"), Match(")").expected(), - ZeroOrMore(AnyOf( - [Keyword("swapped"), UseLiteral("swapped", True)], - [Keyword("after"), UseLiteral("after", True)], - )), + ZeroOrMore( + AnyOf( + [Keyword("swapped"), UseLiteral("swapped", True)], + [Keyword("after"), UseLiteral("after", True)], + ) + ), ) @property @@ -65,18 +69,15 @@ class Signal(AstNode): def is_after(self) -> bool: return self.tokens["after"] or False - @property def gir_signal(self): if self.gir_class is not None: return self.gir_class.signals.get(self.tokens["name"]) - @property def gir_class(self): return self.parent.parent.gir_class - @validate("name") def signal_exists(self): if self.gir_class is None: @@ -92,10 +93,9 @@ class Signal(AstNode): if self.gir_signal is None: raise CompileError( f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}", - did_you_mean=(self.tokens["name"], self.gir_class.signals.keys()) + did_you_mean=(self.tokens["name"], self.gir_class.signals.keys()), ) - @validate("object") def object_exists(self): object_id = self.tokens["object"] @@ -103,10 +103,7 @@ class Signal(AstNode): return if self.root.objects_by_id.get(object_id) is None: - raise CompileError( - f"Could not find object with ID '{object_id}'" - ) - + raise CompileError(f"Could not find object with ID '{object_id}'") @docs("name") def signal_docs(self): diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index cab72e0..f2066ff 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -86,6 +86,7 @@ def get_state_types(gir): "selected": BoolType(), } + def get_types(gir): return { **get_property_types(gir), @@ -93,6 +94,7 @@ def get_types(gir): **get_state_types(gir), } + def _get_docs(gir, name): if gir_type := ( gir.get_type("AccessibleProperty", "Gtk").members.get(name) @@ -174,8 +176,7 @@ class A11y(AstNode): ) def a11y_completer(ast_node, match_variables): yield Completion( - "accessibility", CompletionItemKind.Snippet, - snippet="accessibility {\n $0\n}" + "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" ) @@ -185,20 +186,24 @@ def a11y_completer(ast_node, match_variables): ) def a11y_name_completer(ast_node, match_variables): for name, type in get_types(ast_node.root.gir).items(): - yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type)) + yield Completion( + name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type) + ) @decompiler("relation", cdata=True) def decompile_relation(ctx, gir, name, cdata): ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name)) + @decompiler("state", cdata=True) def decompile_state(ctx, gir, name, cdata, translatable="false"): if decompile.truthy(translatable): - ctx.print(f"{name}: _(\"{_escape_quote(cdata)}\");") + ctx.print(f'{name}: _("{_escape_quote(cdata)}");') else: ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name)) + @decompiler("accessibility") def decompile_accessibility(ctx, gir): ctx.print("accessibility {") diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 31d2d08..f0f6f37 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -35,12 +35,14 @@ class Item(BaseTypedAttribute): item = Group( Item, [ - Optional([ - UseIdent("name"), - ":", - ]), + Optional( + [ + UseIdent("name"), + ":", + ] + ), VALUE_HOOKS, - ] + ], ) @@ -67,7 +69,4 @@ class Items(AstNode): matches=new_statement_patterns, ) def items_completer(ast_node, match_variables): - yield Completion( - "items", CompletionItemKind.Snippet, - snippet="items [$0]" - ) + yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 4419311..9625094 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -37,6 +37,7 @@ class Filters(AstNode): f"Duplicate {self.tokens['tag_name']} block", check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"], ) + wrapped_validator(self) @@ -57,12 +58,12 @@ def create_node(tag_name: str, singular: str): [ UseQuoted("name"), UseLiteral("tag_name", singular), - ] + ], ), ",", ), "]", - ] + ], ) @@ -77,31 +78,38 @@ suffixes = create_node("suffixes", "suffix") matches=new_statement_patterns, ) def file_filter_completer(ast_node, match_variables): - yield Completion("mime-types", CompletionItemKind.Snippet, snippet="mime-types [\"$0\"]") - yield Completion("patterns", CompletionItemKind.Snippet, snippet="patterns [\"$0\"]") - yield Completion("suffixes", CompletionItemKind.Snippet, snippet="suffixes [\"$0\"]") + yield Completion( + "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' + ) + yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]') + yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]') @decompiler("mime-types") def decompile_mime_types(ctx, gir): ctx.print("mime-types [") + @decompiler("mime-type", cdata=True) def decompile_mime_type(ctx, gir, cdata): ctx.print(f'"{cdata}",') + @decompiler("patterns") def decompile_patterns(ctx, gir): ctx.print("patterns [") + @decompiler("pattern", cdata=True) def decompile_pattern(ctx, gir, cdata): ctx.print(f'"{cdata}",') + @decompiler("suffixes") def decompile_suffixes(ctx, gir): ctx.print("suffixes [") + @decompiler("suffix", cdata=True) def decompile_suffix(ctx, gir, cdata): ctx.print(f'"{cdata}",') diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index b52f7bc..9af82fd 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -45,7 +45,7 @@ layout_prop = Group( UseIdent("name"), ":", VALUE_HOOKS.expected("a value"), - ) + ), ) @@ -71,10 +71,7 @@ class Layout(AstNode): matches=new_statement_patterns, ) def layout_completer(ast_node, match_variables): - yield Completion( - "layout", CompletionItemKind.Snippet, - snippet="layout {\n $0\n}" - ) + yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") @decompiler("layout") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 355997a..3d3e6ee 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -56,22 +56,12 @@ menu_contents = Sequence() menu_section = Group( Menu, - [ - "section", - UseLiteral("tag", "section"), - Optional(UseIdent("id")), - menu_contents - ] + ["section", UseLiteral("tag", "section"), Optional(UseIdent("id")), menu_contents], ) menu_submenu = Group( Menu, - [ - "submenu", - UseLiteral("tag", "submenu"), - Optional(UseIdent("id")), - menu_contents - ] + ["submenu", UseLiteral("tag", "submenu"), Optional(UseIdent("id")), menu_contents], ) menu_attribute = Group( @@ -81,7 +71,7 @@ menu_attribute = Group( ":", VALUE_HOOKS.expected("a value"), Match(";").expected(), - ] + ], ) menu_item = Group( @@ -92,7 +82,7 @@ menu_item = Group( Optional(UseIdent("id")), Match("{").expected(), Until(menu_attribute, "}"), - ] + ], ) menu_item_shorthand = Group( @@ -105,58 +95,60 @@ menu_item_shorthand = Group( MenuAttribute, [UseLiteral("name", "label"), VALUE_HOOKS], ), - Optional([ - ",", - Optional([ - Group( - MenuAttribute, - [UseLiteral("name", "action"), VALUE_HOOKS], + Optional( + [ + ",", + Optional( + [ + Group( + MenuAttribute, + [UseLiteral("name", "action"), VALUE_HOOKS], + ), + Optional( + [ + ",", + Group( + MenuAttribute, + [UseLiteral("name", "icon"), VALUE_HOOKS], + ), + ] + ), + ] ), - Optional([ - ",", - Group( - MenuAttribute, - [UseLiteral("name", "icon"), VALUE_HOOKS], - ), - ]) - ]) - ]), + ] + ), Match(")").expected(), - ] + ], ) menu_contents.children = [ Match("{"), - Until(AnyOf( - menu_section, - menu_submenu, - menu_item_shorthand, - menu_item, - menu_attribute, - ), "}"), + Until( + AnyOf( + menu_section, + menu_submenu, + menu_item_shorthand, + menu_item, + menu_attribute, + ), + "}", + ), ] menu: Group = Group( Menu, - [ - "menu", - UseLiteral("tag", "menu"), - Optional(UseIdent("id")), - menu_contents - ], + ["menu", UseLiteral("tag", "menu"), Optional(UseIdent("id")), menu_contents], ) from .ui import UI + @completer( applies_in=[UI], matches=new_statement_patterns, ) def menu_completer(ast_node, match_variables): - yield Completion( - "menu", CompletionItemKind.Snippet, - snippet="menu {\n $0\n}" - ) + yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") @completer( @@ -165,34 +157,21 @@ def menu_completer(ast_node, match_variables): ) def menu_content_completer(ast_node, match_variables): yield Completion( - "submenu", CompletionItemKind.Snippet, - snippet="submenu {\n $0\n}" + "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" ) yield Completion( - "section", CompletionItemKind.Snippet, - snippet="section {\n $0\n}" + "section", CompletionItemKind.Snippet, snippet="section {\n $0\n}" ) + yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}") yield Completion( - "item", CompletionItemKind.Snippet, - snippet="item {\n $0\n}" - ) - yield Completion( - "item (shorthand)", CompletionItemKind.Snippet, - snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")' + "item (shorthand)", + CompletionItemKind.Snippet, + snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")', ) - yield Completion( - "label", CompletionItemKind.Snippet, - snippet='label: $0;' - ) - yield Completion( - "action", CompletionItemKind.Snippet, - snippet='action: "$0";' - ) - yield Completion( - "icon", CompletionItemKind.Snippet, - snippet='icon: "$0";' - ) + yield Completion("label", CompletionItemKind.Snippet, snippet="label: $0;") + yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";') + yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";') @decompiler("menu") @@ -202,6 +181,7 @@ def decompile_menu(ctx, gir, id=None): else: ctx.print("menu {") + @decompiler("submenu") def decompile_submenu(ctx, gir, id=None): if id: @@ -209,6 +189,7 @@ def decompile_submenu(ctx, gir, id=None): else: ctx.print("submenu {") + @decompiler("item") def decompile_item(ctx, gir, id=None): if id: @@ -216,6 +197,7 @@ def decompile_item(ctx, gir, id=None): else: ctx.print("item {") + @decompiler("section") def decompile_section(ctx, gir, id=None): if id: diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index eb56043..80c47af 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -32,7 +32,7 @@ class Widget(AstNode): if object is None: raise CompileError( f"Could not find object with ID {self.tokens['name']}", - did_you_mean=(self.tokens['name'], self.root.objects_by_id.keys()), + did_you_mean=(self.tokens["name"], self.root.objects_by_id.keys()), ) elif object.gir_class and not object.gir_class.assignable_to(type): raise CompileError( diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index e7eb0f8..347b9e8 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -55,7 +55,4 @@ class Strings(AstNode): matches=new_statement_patterns, ) def strings_completer(ast_node, match_variables): - yield Completion( - "strings", CompletionItemKind.Snippet, - snippet="strings [$0]" - ) + yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 644d8a3..1005568 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -49,13 +49,14 @@ class Styles(AstNode): matches=new_statement_patterns, ) def style_completer(ast_node, match_variables): - yield Completion("styles", CompletionItemKind.Keyword, snippet="styles [\"$0\"]") + yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') @decompiler("style") def decompile_style(ctx, gir): ctx.print(f"styles [") + @decompiler("class") def decompile_style_class(ctx, gir, name): ctx.print(f'"{name}",') diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 71efe19..45b2074 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -26,18 +26,21 @@ from .common import * ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ ("Gtk", "Buildable"), - ("Gio", "ListStore") + ("Gio", "ListStore"), ] + class Child(AstNode): grammar = [ - Optional([ - "[", - Optional(["internal-child", UseLiteral("internal_child", True)]), - UseIdent("child_type").expected("a child type"), - Optional(ResponseId), - "]", - ]), + Optional( + [ + "[", + Optional(["internal-child", UseLiteral("internal_child", True)]), + UseIdent("child_type").expected("a child type"), + Optional(ResponseId), + "]", + ] + ), Object, ] @@ -53,9 +56,13 @@ class Child(AstNode): if gir_class.assignable_to(parent_type): break else: - hints=["only Gio.ListStore or Gtk.Buildable implementors can have children"] + hints = [ + "only Gio.ListStore or Gtk.Buildable implementors can have children" + ] if "child" in gir_class.properties: - hints.append("did you mean to assign this object to the 'child' property?") + hints.append( + "did you mean to assign this object to the 'child' property?" + ) raise CompileError( f"{gir_class.full_name} doesn't have children", hints=hints, diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 7392782..a215cdf 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -28,10 +28,12 @@ class Template(Object): grammar = [ "template", UseIdent("id").expected("template class name"), - Optional([ - Match(":"), - to_parse_node(ClassName).expected("parent class"), - ]), + Optional( + [ + Match(":"), + to_parse_node(ClassName).expected("parent class"), + ] + ), ObjectContent, ] @@ -54,7 +56,9 @@ class Template(Object): @validate("id") def unique_in_parent(self): - self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",) + self.validate_unique_in_parent( + f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}", + ) @decompiler("template") diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index f0fe3df..be6a003 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -24,8 +24,12 @@ from .common import * 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;`)"), + 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"), ) @@ -35,7 +39,9 @@ class GtkDirective(AstNode): if version not in ["4.0"]: err = CompileError("Only GTK 4 is supported") if version and version.startswith("4"): - err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.") + 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 @@ -51,7 +57,6 @@ class GtkDirective(AstNode): hints=e.hints, ) - @property def gir_namespace(self): # validate the GTK version first to make sure the more specific error diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index 745c73f..073173a 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -26,21 +26,13 @@ from .common import * class ResponseId(AstNode): """Response ID of action widget.""" - ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ - ("Gtk", "Dialog"), - ("Gtk", "InfoBar") - ] + ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")] grammar = [ Keyword("response"), "=", - AnyOf( - UseIdent("response_id"), - UseNumber("response_id") - ), - Optional([ - Keyword("default"), UseLiteral("is_default", True) - ]) + AnyOf(UseIdent("response_id"), UseNumber("response_id")), + Optional([Keyword("default"), UseLiteral("is_default", True)]), ] @validate() @@ -91,18 +83,15 @@ class ResponseId(AstNode): if isinstance(response, int): if response < 0: - raise CompileError( - "Numeric response type can't be negative") + raise CompileError("Numeric response type can't be negative") elif isinstance(response, float): raise CompileError( - "Response type must be GtkResponseType member or integer," - " not float" + "Response type must be GtkResponseType member or integer," " not float" ) else: responses = gir.get_type("ResponseType", "Gtk").members.keys() if response not in responses: - raise CompileError( - f"Response type \"{response}\" doesn't exist") + raise CompileError(f'Response type "{response}" doesn\'t exist') @validate("default") def no_multiple_default(self) -> None: @@ -135,4 +124,3 @@ class ResponseId(AstNode): _object: Object = self.parent.children[Object][0] return _object.tokens["id"] - diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 8f39cbf..0987262 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -41,7 +41,9 @@ class TypeName(AstNode): @validate("class_name") def type_exists(self): if not self.tokens["ignore_gir"] and self.gir_ns is not None: - self.root.gir.validate_type(self.tokens["class_name"], self.tokens["namespace"]) + self.root.gir.validate_type( + self.tokens["class_name"], self.tokens["namespace"] + ) @validate("namespace") def gir_ns_exists(self): @@ -56,7 +58,9 @@ class TypeName(AstNode): @property def gir_type(self) -> T.Optional[gir.Class]: if self.tokens["class_name"] and not self.tokens["ignore_gir"]: - return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"]) + return self.root.gir.get_type( + self.tokens["class_name"], self.tokens["namespace"] + ) return None @property @@ -82,7 +86,9 @@ class ClassName(TypeName): def gir_class_exists(self): if self.gir_type is not None and not isinstance(self.gir_type, Class): if isinstance(self.gir_type, Interface): - raise CompileError(f"{self.gir_type.full_name} is an interface, not a class") + raise CompileError( + f"{self.gir_type.full_name} is an interface, not a class" + ) else: raise CompileError(f"{self.gir_type.full_name} is not a class") @@ -93,6 +99,5 @@ class ConcreteClassName(ClassName): if isinstance(self.gir_type, Class) and self.gir_type.abstract: raise CompileError( f"{self.gir_type.full_name} can't be instantiated because it's abstract", - hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"] + hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"], ) - diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index 5a9c4fb..c277c56 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -27,16 +27,19 @@ from .common import * class UI(AstNode): - """ The AST node for the entire file """ + """The AST node for the entire file""" grammar = [ GtkDirective, ZeroOrMore(Import), - Until(AnyOf( - Template, - menu, - Object, - ), Eof()), + Until( + AnyOf( + Template, + menu, + Object, + ), + Eof(), + ), ] @property @@ -61,11 +64,13 @@ class UI(AstNode): 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 } - + return { + obj.tokens["id"]: obj + for obj in self.iterate_children_recursive() + if obj.tokens["id"] is not None + } @validate() def gir_errors(self): @@ -74,7 +79,6 @@ class UI(AstNode): if len(self._gir_errors): raise MultipleErrors(self._gir_errors) - @validate() def unique_ids(self): passed = {} @@ -84,5 +88,7 @@ class UI(AstNode): if obj.tokens["id"] in passed: token = obj.group.tokens["id"] - raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end) + raise CompileError( + f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end + ) passed[obj.tokens["id"]] = obj diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 2a889ec..db28490 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -84,13 +84,21 @@ class QuotedValue(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): + 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): + elif ( + isinstance(type, gir.Class) + or isinstance(type, gir.Interface) + or isinstance(type, gir.Boxed) + ): parseable_types = [ "Gdk.Paintable", "Gdk.Texture", @@ -106,8 +114,12 @@ class QuotedValue(Value): 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) + 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}") @@ -127,7 +139,9 @@ class NumberValue(Value): try: int(self.tokens["value"]) except: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer") + raise CompileError( + f"Cannot convert {self.group.tokens['value']} to integer" + ) elif isinstance(type, gir.UIntType): try: @@ -135,13 +149,17 @@ class NumberValue(Value): if int(self.tokens["value"]) < 0: raise Exception() except: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer") + 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") + raise CompileError( + f"Cannot convert {self.group.tokens['value']} to float" + ) elif type is not None: raise CompileError(f"Cannot convert number to {type.full_name}") @@ -164,7 +182,7 @@ class Flag(AstNode): if isinstance(type, gir.Bitfield) and 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()), + did_you_mean=(self.tokens["value"], type.members.keys()), ) @@ -189,14 +207,14 @@ class IdentValue(Value): 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()), + 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"]), + did_you_mean=(self.tokens["value"], ["true", "false"]), ) elif type is not None: @@ -204,14 +222,13 @@ class IdentValue(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()), + 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 @@ -223,9 +240,7 @@ class IdentValue(Value): 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/lsp.py b/blueprintcompiler/lsp.py index 6e43e3b..26a519e 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -35,6 +35,7 @@ def command(json_method): def decorator(func): func._json_method = json_method return func + return decorator @@ -50,8 +51,16 @@ class OpenFile: def apply_changes(self, changes): for change in changes: - start = utils.pos_to_idx(change["range"]["start"]["line"], change["range"]["start"]["character"], self.text) - end = utils.pos_to_idx(change["range"]["end"]["line"], change["range"]["end"]["character"], self.text) + start = utils.pos_to_idx( + change["range"]["start"]["line"], + change["range"]["start"]["character"], + self.text, + ) + end = utils.pos_to_idx( + change["range"]["end"]["line"], + change["range"]["end"]["character"], + self.text, + ) self.text = self.text[:start] + change["text"] + self.text[end:] self._update() @@ -69,16 +78,17 @@ class OpenFile: except CompileError as e: self.diagnostics.append(e) - def calc_semantic_tokens(self) -> T.List[int]: tokens = list(self.ast.get_semantic_tokens()) token_lists = [ [ - *utils.idx_to_pos(token.start, self.text), # line and column - token.end - token.start, # length + *utils.idx_to_pos(token.start, self.text), # line and column + token.end - token.start, # length token.type, - 0, # token modifiers - ] for token in tokens] + 0, # token modifiers + ] + for token in tokens + ] # convert line, column numbers to deltas for i, token_list in enumerate(token_lists[1:]): @@ -125,53 +135,60 @@ class LanguageServer: except Exception as e: printerr(traceback.format_exc()) - def _send(self, data): data["jsonrpc"] = "2.0" line = json.dumps(data, separators=(",", ":")) + "\r\n" printerr("output: " + line) - sys.stdout.write(f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}") + sys.stdout.write( + f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}" + ) sys.stdout.flush() def _send_response(self, id, result): - self._send({ - "id": id, - "result": result, - }) + self._send( + { + "id": id, + "result": result, + } + ) def _send_notification(self, method, params): - self._send({ - "method": method, - "params": params, - }) - + self._send( + { + "method": method, + "params": params, + } + ) @command("initialize") def initialize(self, id, params): from . import main self.client_capabilities = params.get("capabilities") - self._send_response(id, { - "capabilities": { - "textDocumentSync": { - "openClose": True, - "change": TextDocumentSyncKind.Incremental, - }, - "semanticTokensProvider": { - "legend": { - "tokenTypes": ["enumMember"], + self._send_response( + id, + { + "capabilities": { + "textDocumentSync": { + "openClose": True, + "change": TextDocumentSyncKind.Incremental, }, - "full": True, + "semanticTokensProvider": { + "legend": { + "tokenTypes": ["enumMember"], + }, + "full": True, + }, + "completionProvider": {}, + "codeActionProvider": {}, + "hoverProvider": True, + }, + "serverInfo": { + "name": "Blueprint", + "version": main.VERSION, }, - "completionProvider": {}, - "codeActionProvider": {}, - "hoverProvider": True, }, - "serverInfo": { - "name": "Blueprint", - "version": main.VERSION, - }, - }) + ) @command("textDocument/didOpen") def didOpen(self, id, params): @@ -198,14 +215,23 @@ class LanguageServer: @command("textDocument/hover") def hover(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] - docs = open_file.ast and open_file.ast.get_docs(utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text)) + docs = open_file.ast and open_file.ast.get_docs( + utils.pos_to_idx( + params["position"]["line"], + params["position"]["character"], + open_file.text, + ) + ) if docs: - self._send_response(id, { - "contents": { - "kind": "markdown", - "value": docs, - } - }) + self._send_response( + id, + { + "contents": { + "kind": "markdown", + "value": docs, + } + }, + ) else: self._send_response(id, None) @@ -217,40 +243,59 @@ class LanguageServer: self._send_response(id, []) return - idx = utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text) + idx = utils.pos_to_idx( + params["position"]["line"], params["position"]["character"], open_file.text + ) completions = complete(open_file.ast, open_file.tokens, idx) - self._send_response(id, [completion.to_json(True) for completion in completions]) - + self._send_response( + id, [completion.to_json(True) for completion in completions] + ) @command("textDocument/semanticTokens/full") def semantic_tokens(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] - self._send_response(id, { - "data": open_file.calc_semantic_tokens(), - }) - + self._send_response( + id, + { + "data": open_file.calc_semantic_tokens(), + }, + ) @command("textDocument/codeAction") def code_actions(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] - range_start = utils.pos_to_idx(params["range"]["start"]["line"], params["range"]["start"]["character"], open_file.text) - range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text) + range_start = utils.pos_to_idx( + params["range"]["start"]["line"], + params["range"]["start"]["character"], + open_file.text, + ) + range_end = utils.pos_to_idx( + params["range"]["end"]["line"], + params["range"]["end"]["character"], + open_file.text, + ) actions = [ { "title": action.title, "kind": "quickfix", - "diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, diagnostic)], + "diagnostics": [ + self._create_diagnostic(open_file.text, open_file.uri, diagnostic) + ], "edit": { "changes": { - open_file.uri: [{ - "range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text), - "newText": action.replace_with - }] + open_file.uri: [ + { + "range": utils.idxs_to_range( + diagnostic.start, diagnostic.end, open_file.text + ), + "newText": action.replace_with, + } + ] } - } + }, } for diagnostic in open_file.diagnostics if not (diagnostic.end < range_start or diagnostic.start > range_end) @@ -259,23 +304,30 @@ class LanguageServer: self._send_response(id, actions) - def _send_file_updates(self, open_file: OpenFile): - self._send_notification("textDocument/publishDiagnostics", { - "uri": open_file.uri, - "diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, err) for err in open_file.diagnostics], - }) + self._send_notification( + "textDocument/publishDiagnostics", + { + "uri": open_file.uri, + "diagnostics": [ + self._create_diagnostic(open_file.text, open_file.uri, err) + for err in open_file.diagnostics + ], + }, + ) def _create_diagnostic(self, text, uri, err): message = err.message for hint in err.hints: - message += '\nhint: ' + hint + message += "\nhint: " + hint result = { "range": utils.idxs_to_range(err.start, err.end, text), "message": message, - "severity": DiagnosticSeverity.Warning if isinstance(err, CompileWarning) else DiagnosticSeverity.Error, + "severity": DiagnosticSeverity.Warning + if isinstance(err, CompileWarning) + else DiagnosticSeverity.Error, } if len(err.references) > 0: @@ -285,7 +337,7 @@ class LanguageServer: "uri": uri, "range": utils.idxs_to_range(ref.start, ref.end, text), }, - "message": ref.message + "message": ref.message, } for ref in err.references ] @@ -297,4 +349,3 @@ for name in dir(LanguageServer): item = getattr(LanguageServer, name) if callable(item) and hasattr(item, "_json_method"): LanguageServer.commands[item._json_method] = item - diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 5664ab6..5e4ef89 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -31,13 +31,16 @@ class TextDocumentSyncKind(enum.IntEnum): Full = 1 Incremental = 2 + class CompletionItemTag(enum.IntEnum): Deprecated = 1 + class InsertTextFormat(enum.IntEnum): PlainText = 1 Snippet = 2 + class CompletionItemKind(enum.IntEnum): Text = 1 Method = 2 @@ -91,12 +94,14 @@ class Completion: "documentation": { "kind": "markdown", "value": self.docs, - } if self.docs else None, + } + if self.docs + else None, "deprecated": self.deprecated, "insertText": insert_text, "insertTextFormat": insert_text_format, } - return { k: v for k, v in result.items() if v is not None } + return {k: v for k, v in result.items() if v is not None} class SemanticTokenType(enum.IntEnum): @@ -110,7 +115,6 @@ class DiagnosticSeverity(enum.IntEnum): Hint = 4 - @dataclass class SemanticToken: start: int diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 124654d..4e4d378 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -30,24 +30,41 @@ from .outputs import XmlOutput VERSION = "uninstalled" LIBDIR = None + class BlueprintApp: def main(self): self.parser = argparse.ArgumentParser() self.subparsers = self.parser.add_subparsers(metavar="command") self.parser.set_defaults(func=self.cmd_help) - compile = self.add_subcommand("compile", "Compile blueprint files", self.cmd_compile) + compile = self.add_subcommand( + "compile", "Compile blueprint files", self.cmd_compile + ) compile.add_argument("--output", dest="output", default="-") - compile.add_argument("input", metavar="filename", default=sys.stdin, type=argparse.FileType('r')) + compile.add_argument( + "input", metavar="filename", default=sys.stdin, type=argparse.FileType("r") + ) - batch_compile = self.add_subcommand("batch-compile", "Compile many blueprint files at once", self.cmd_batch_compile) + batch_compile = self.add_subcommand( + "batch-compile", + "Compile many blueprint files at once", + self.cmd_batch_compile, + ) batch_compile.add_argument("output_dir", metavar="output-dir") batch_compile.add_argument("input_dir", metavar="input-dir") - batch_compile.add_argument("inputs", nargs="+", metavar="filenames", default=sys.stdin, type=argparse.FileType('r')) + batch_compile.add_argument( + "inputs", + nargs="+", + metavar="filenames", + default=sys.stdin, + type=argparse.FileType("r"), + ) port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port) - lsp = self.add_subcommand("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp) + lsp = self.add_subcommand( + "lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp + ) self.add_subcommand("help", "Show this message", self.cmd_help) @@ -65,17 +82,14 @@ class BlueprintApp: except: report_bug() - def add_subcommand(self, name, help, func): parser = self.subparsers.add_parser(name, help=help) parser.set_defaults(func=func) return parser - def cmd_help(self, opts): self.parser.print_help() - def cmd_compile(self, opts): data = opts.input.read() try: @@ -93,14 +107,15 @@ class BlueprintApp: e.pretty_print(opts.input.name, data) sys.exit(1) - def cmd_batch_compile(self, opts): for file in opts.inputs: data = file.read() try: if not os.path.commonpath([file.name, opts.input_dir]): - print(f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}") + print( + f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}" + ) sys.exit(1) xml, warnings = self._compile(data) @@ -111,9 +126,8 @@ class BlueprintApp: path = os.path.join( opts.output_dir, os.path.relpath( - os.path.splitext(file.name)[0] + ".ui", - opts.input_dir - ) + os.path.splitext(file.name)[0] + ".ui", opts.input_dir + ), ) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: @@ -122,16 +136,13 @@ class BlueprintApp: e.pretty_print(file.name, data) sys.exit(1) - def cmd_lsp(self, opts): langserv = LanguageServer() langserv.run() - def cmd_port(self, opts): interactive_port.run(opts) - def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]: tokens = tokenizer.tokenize(data) ast, errors, warnings = parser.parse(tokens) diff --git a/blueprintcompiler/outputs/__init__.py b/blueprintcompiler/outputs/__init__.py index e3054a3..6cdb07b 100644 --- a/blueprintcompiler/outputs/__init__.py +++ b/blueprintcompiler/outputs/__init__.py @@ -1,7 +1,9 @@ from ..language import UI + class OutputFormat: def emit(self, ui: UI) -> str: raise NotImplementedError() + from .xml import XmlOutput diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 0e2644b..6634994 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -24,7 +24,13 @@ import typing as T from collections import defaultdict from enum import Enum -from .errors import assert_true, CompilerBugError, CompileError, CompileWarning, UnexpectedTokenError +from .errors import ( + assert_true, + CompilerBugError, + CompileError, + CompileWarning, + UnexpectedTokenError, +) from .tokenizer import Token, TokenType @@ -32,15 +38,15 @@ SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE] class ParseResult(Enum): - """ Represents the result of parsing. The extra EMPTY result is necessary + """Represents the result of parsing. The extra EMPTY result is necessary to avoid freezing the parser: imagine a ZeroOrMore node containing a node that can match empty. It will repeatedly match empty and never advance the parser. So, ZeroOrMore stops when a failed *or empty* match is - made. """ + made.""" SUCCESS = 0 FAILURE = 1 - EMPTY = 2 + EMPTY = 2 def matched(self): return self == ParseResult.SUCCESS @@ -53,10 +59,10 @@ class ParseResult(Enum): class ParseGroup: - """ A matching group. Match groups have an AST type, children grouped by + """A matching group. Match groups have an AST type, children grouped by type, and key=value pairs. At the end of parsing, the match groups will be converted to AST nodes by passing the children and key=value pairs to - the AST node constructor. """ + the AST node constructor.""" def __init__(self, ast_type, start: int): self.ast_type = ast_type @@ -77,23 +83,27 @@ class ParseGroup: self.tokens[key] = token def to_ast(self): - """ Creates an AST node from the match group. """ + """Creates an AST node from the match group.""" children = [child.to_ast() for child in self.children] try: return self.ast_type(self, children, self.keys, incomplete=self.incomplete) except TypeError as e: - raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.") + raise CompilerBugError( + f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace." + ) def __str__(self): result = str(self.ast_type.__name__) result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n" - result += "\n".join([str(child) for children in self.children.values() for child in children]) + result += "\n".join( + [str(child) for children in self.children.values() for child in children] + ) return result.replace("\n", "\n ") class ParseContext: - """ Contains the state of the parser. """ + """Contains the state of the parser.""" def __init__(self, tokens, index=0): self.tokens = list(tokens) @@ -110,12 +120,11 @@ class ParseContext: self.errors = [] self.warnings = [] - def create_child(self): - """ Creates a new ParseContext at this context's position. The new + """Creates a new ParseContext at this context's position. The new context will be used to parse one node. If parsing is successful, the new context will be applied to "self". If parsing fails, the new - context will be discarded. """ + context will be discarded.""" ctx = ParseContext(self.tokens, self.index) ctx.errors = self.errors ctx.warnings = self.warnings @@ -123,7 +132,7 @@ class ParseContext: return ctx def apply_child(self, other): - """ Applies a child context to this context. """ + """Applies a child context to this context.""" if other.group is not None: # If the other context had a match group, collect all the matched @@ -150,43 +159,44 @@ class ParseContext: elif other.last_group: self.last_group = other.last_group - def start_group(self, ast_type): - """ Sets this context to have its own match group. """ + """Sets this context to have its own match group.""" assert_true(self.group is None) self.group = ParseGroup(ast_type, self.tokens[self.index].start) def set_group_val(self, key, value, token): - """ Sets a matched key=value pair on the current match group. """ + """Sets a matched key=value pair on the current match group.""" assert_true(key not in self.group_keys) self.group_keys[key] = (value, token) def set_group_incomplete(self): - """ Marks the current match group as incomplete (it could not be fully - parsed, but the parser recovered). """ + """Marks the current match group as incomplete (it could not be fully + parsed, but the parser recovered).""" self.group_incomplete = True - def skip(self): - """ Skips whitespace and comments. """ - while self.index < len(self.tokens) and self.tokens[self.index].type in SKIP_TOKENS: + """Skips whitespace and comments.""" + while ( + self.index < len(self.tokens) + and self.tokens[self.index].type in SKIP_TOKENS + ): self.index += 1 def next_token(self) -> Token: - """ Advances the token iterator and returns the next token. """ + """Advances the token iterator and returns the next token.""" self.skip() token = self.tokens[self.index] self.index += 1 return token def peek_token(self) -> Token: - """ Returns the next token without advancing the iterator. """ + """Returns the next token without advancing the iterator.""" self.skip() token = self.tokens[self.index] return token def skip_unexpected_token(self): - """ Skips a token and logs an "unexpected token" error. """ + """Skips a token and logs an "unexpected token" error.""" self.skip() start = self.tokens[self.index].start @@ -194,9 +204,11 @@ class ParseContext: self.skip() end = self.tokens[self.index - 1].end - if (len(self.errors) - and isinstance((err := self.errors[-1]), UnexpectedTokenError) - and err.end == start): + if ( + len(self.errors) + and isinstance((err := self.errors[-1]), UnexpectedTokenError) + and err.end == start + ): err.end = end else: self.errors.append(UnexpectedTokenError(start, end)) @@ -206,10 +218,10 @@ class ParseContext: class ParseNode: - """ Base class for the nodes in the parser tree. """ + """Base class for the nodes in the parser tree.""" def parse(self, ctx: ParseContext) -> ParseResult: - """ Attempts to match the ParseNode at the context's current location. """ + """Attempts to match the ParseNode at the context's current location.""" start_idx = ctx.index inner_ctx = ctx.create_child() @@ -226,22 +238,22 @@ class ParseNode: raise NotImplementedError() def err(self, message): - """ Causes this ParseNode to raise an exception if it fails to parse. + """Causes this ParseNode to raise an exception if it fails to parse. This prevents the parser from backtracking, so you should understand - what it does and how the parser works before using it. """ + what it does and how the parser works before using it.""" return Err(self, message) def expected(self, expect): - """ Convenience method for err(). """ + """Convenience method for err().""" return self.err("Expected " + expect) def warn(self, message): - """ Causes this ParseNode to emit a warning if it parses successfully. """ + """Causes this ParseNode to emit a warning if it parses successfully.""" return Warning(self, message) class Err(ParseNode): - """ ParseNode that emits a compile error if it fails to parse. """ + """ParseNode that emits a compile error if it fails to parse.""" def __init__(self, child, message): self.child = to_parse_node(child) @@ -260,7 +272,7 @@ class Err(ParseNode): class Warning(ParseNode): - """ ParseNode that emits a compile warning if it parses successfully. """ + """ParseNode that emits a compile warning if it parses successfully.""" def __init__(self, child, message): self.child = to_parse_node(child) @@ -272,12 +284,14 @@ class Warning(ParseNode): if self.child.parse(ctx).succeeded(): start_token = ctx.tokens[start_idx] end_token = ctx.tokens[ctx.index] - ctx.warnings.append(CompileWarning(self.message, start_token.start, end_token.end)) + ctx.warnings.append( + CompileWarning(self.message, start_token.start, end_token.end) + ) return True class Fail(ParseNode): - """ ParseNode that emits a compile error if it parses successfully. """ + """ParseNode that emits a compile error if it parses successfully.""" def __init__(self, child, message): self.child = to_parse_node(child) @@ -296,7 +310,8 @@ class Fail(ParseNode): class Group(ParseNode): - """ ParseNode that creates a match group. """ + """ParseNode that creates a match group.""" + def __init__(self, ast_type, child): self.ast_type = ast_type self.child = to_parse_node(child) @@ -308,7 +323,8 @@ class Group(ParseNode): class Sequence(ParseNode): - """ ParseNode that attempts to match all of its children in sequence. """ + """ParseNode that attempts to match all of its children in sequence.""" + def __init__(self, *children): self.children = [to_parse_node(child) for child in children] @@ -320,8 +336,9 @@ class Sequence(ParseNode): class Statement(ParseNode): - """ ParseNode that attempts to match all of its children in sequence. If any - child raises an error, the error will be logged but parsing will continue. """ + """ParseNode that attempts to match all of its children in sequence. If any + child raises an error, the error will be logged but parsing will continue.""" + def __init__(self, *children): self.children = [to_parse_node(child) for child in children] @@ -344,14 +361,16 @@ class Statement(ParseNode): class AnyOf(ParseNode): - """ ParseNode that attempts to match exactly one of its children. Child - nodes are attempted in order. """ + """ParseNode that attempts to match exactly one of its children. Child + nodes are attempted in order.""" + def __init__(self, *children): self.children = children @property def children(self): return self._children + @children.setter def children(self, children): self._children = [to_parse_node(child) for child in children] @@ -364,9 +383,10 @@ class AnyOf(ParseNode): class Until(ParseNode): - """ ParseNode that repeats its child until a delimiting token is found. If + """ParseNode that repeats its child until a delimiting token is found. If the child does not match, one token is skipped and the match is attempted - again. """ + again.""" + def __init__(self, child, delimiter): self.child = to_parse_node(child) self.delimiter = to_parse_node(delimiter) @@ -387,13 +407,13 @@ class Until(ParseNode): class ZeroOrMore(ParseNode): - """ ParseNode that matches its child any number of times (including zero + """ParseNode that matches its child any number of times (including zero times). It cannot fail to parse. If its child raises an exception, one token - will be skipped and parsing will continue. """ + will be skipped and parsing will continue.""" + def __init__(self, child): self.child = to_parse_node(child) - def _parse(self, ctx): while True: try: @@ -405,8 +425,9 @@ class ZeroOrMore(ParseNode): class Delimited(ParseNode): - """ ParseNode that matches its first child any number of times (including zero - times) with its second child in between and optionally at the end. """ + """ParseNode that matches its first child any number of times (including zero + times) with its second child in between and optionally at the end.""" + def __init__(self, child, delimiter): self.child = to_parse_node(child) self.delimiter = to_parse_node(delimiter) @@ -418,8 +439,9 @@ class Delimited(ParseNode): class Optional(ParseNode): - """ ParseNode that matches its child zero or one times. It cannot fail to - parse. """ + """ParseNode that matches its child zero or one times. It cannot fail to + parse.""" + def __init__(self, child): self.child = to_parse_node(child) @@ -429,14 +451,16 @@ class Optional(ParseNode): class Eof(ParseNode): - """ ParseNode that matches an EOF token. """ + """ParseNode that matches an EOF token.""" + def _parse(self, ctx: ParseContext) -> bool: token = ctx.next_token() return token.type == TokenType.EOF class Match(ParseNode): - """ ParseNode that matches the given literal token. """ + """ParseNode that matches the given literal token.""" + def __init__(self, op): self.op = op @@ -445,7 +469,7 @@ class Match(ParseNode): return str(token) == self.op def expected(self, expect: T.Optional[str] = None): - """ Convenience method for err(). """ + """Convenience method for err().""" if expect is None: return self.err(f"Expected '{self.op}'") else: @@ -453,8 +477,9 @@ class Match(ParseNode): class UseIdent(ParseNode): - """ ParseNode that matches any identifier and sets it in a key=value pair on - the containing match group. """ + """ParseNode that matches any identifier and sets it in a key=value pair on + the containing match group.""" + def __init__(self, key): self.key = key @@ -468,8 +493,9 @@ class UseIdent(ParseNode): class UseNumber(ParseNode): - """ ParseNode that matches a number and sets it in a key=value pair on - the containing match group. """ + """ParseNode that matches a number and sets it in a key=value pair on + the containing match group.""" + def __init__(self, key): self.key = key @@ -486,8 +512,9 @@ class UseNumber(ParseNode): class UseNumberText(ParseNode): - """ ParseNode that matches a number, but sets its *original text* it in a - key=value pair on the containing match group. """ + """ParseNode that matches a number, but sets its *original text* it in a + key=value pair on the containing match group.""" + def __init__(self, key): self.key = key @@ -501,8 +528,9 @@ class UseNumberText(ParseNode): class UseQuoted(ParseNode): - """ ParseNode that matches a quoted string and sets it in a key=value pair - on the containing match group. """ + """ParseNode that matches a quoted string and sets it in a key=value pair + on the containing match group.""" + def __init__(self, key): self.key = key @@ -511,19 +539,22 @@ class UseQuoted(ParseNode): if token.type != TokenType.QUOTED: return False - string = (str(token)[1:-1] + string = ( + str(token)[1:-1] .replace("\\n", "\n") - .replace("\\\"", "\"") + .replace('\\"', '"') .replace("\\\\", "\\") - .replace("\\'", "\'")) + .replace("\\'", "'") + ) ctx.set_group_val(self.key, string, token) return True class UseLiteral(ParseNode): - """ ParseNode that doesn't match anything, but rather sets a static key=value + """ParseNode that doesn't match anything, but rather sets a static key=value pair on the containing group. Useful for, e.g., property and signal flags: - `Sequence(Keyword("swapped"), UseLiteral("swapped", True))` """ + `Sequence(Keyword("swapped"), UseLiteral("swapped", True))`""" + def __init__(self, key, literal): self.key = key self.literal = literal @@ -534,8 +565,9 @@ class UseLiteral(ParseNode): class Keyword(ParseNode): - """ Matches the given identifier and sets it as a named token, with the name - being the identifier itself. """ + """Matches the given identifier and sets it as a named token, with the name + being the identifier itself.""" + def __init__(self, kw): self.kw = kw self.set_token = True @@ -565,12 +597,13 @@ class Infix(ParseNode): def __lt__(self, other): return self.binding_power < other.binding_power + def __eq__(self, other): return self.binding_power == other.binding_power class Pratt(ParseNode): - """ Basic Pratt parser implementation. """ + """Basic Pratt parser implementation.""" def __init__(self, *children): self.children = children @@ -578,11 +611,14 @@ class Pratt(ParseNode): @property def children(self): return self._children + @children.setter def children(self, children): self._children = children self.prefixes = [child for child in children if isinstance(child, Prefix)] - self.infixes = sorted([child for child in children if isinstance(child, Infix)], reverse=True) + self.infixes = sorted( + [child for child in children if isinstance(child, Infix)], reverse=True + ) def _parse(self, ctx: ParseContext) -> bool: for prefix in self.prefixes: diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 739a165..12c893a 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -25,7 +25,7 @@ from .language import OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]: - """ Parses a list of tokens into an abstract syntax tree. """ + """Parses a list of tokens into an abstract syntax tree.""" ctx = ParseContext(tokens) AnyOf(UI).parse(ctx) diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 2e7fa1b..4991967 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -26,28 +26,28 @@ from .errors import CompileError class TokenType(Enum): - EOF = 0 - IDENT = 1 - QUOTED = 2 - NUMBER = 3 - OP = 4 - WHITESPACE = 5 - COMMENT = 6 - PUNCTUATION = 7 + EOF = 0 + IDENT = 1 + QUOTED = 2 + NUMBER = 3 + OP = 4 + WHITESPACE = 5 + COMMENT = 6 + PUNCTUATION = 7 _tokens = [ - (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), - (TokenType.QUOTED, r'"(\\"|[^"\n])*"'), - (TokenType.QUOTED, r"'(\\'|[^'\n])*'"), - (TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), - (TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"), - (TokenType.NUMBER, r"[-+]?\.[\d_]+"), - (TokenType.WHITESPACE, r"\s+"), - (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), - (TokenType.COMMENT, r"\/\/[^\n]*"), - (TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), - (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), + (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), + (TokenType.QUOTED, r'"(\\"|[^"\n])*"'), + (TokenType.QUOTED, r"'(\\'|[^'\n])*'"), + (TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), + (TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"), + (TokenType.NUMBER, r"[-+]?\.[\d_]+"), + (TokenType.WHITESPACE, r"\s+"), + (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), + (TokenType.COMMENT, r"\/\/[^\n]*"), + (TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), + (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), ] _TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] @@ -60,7 +60,7 @@ class Token: self.string = string def __str__(self): - return self.string[self.start:self.end] + return self.string[self.start : self.end] def get_number(self): if self.type != TokenType.NUMBER: @@ -73,7 +73,9 @@ class Token: else: return float(string.replace("_", "")) except: - raise CompileError(f"{str(self)} is not a valid number literal", self.start, self.end) + raise CompileError( + f"{str(self)} is not a valid number literal", self.start, self.end + ) def _tokenize(ui_ml: str): @@ -90,7 +92,9 @@ def _tokenize(ui_ml: str): break if not matched: - raise CompileError("Could not determine what kind of syntax is meant here", i, i) + raise CompileError( + "Could not determine what kind of syntax is meant here", i, i + ) yield Token(TokenType.EOF, i, i, ui_ml) diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index 0946320..88e7b57 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -241,7 +241,9 @@ class Typelib: return self._typelib_file[loc:end].decode("utf-8") def _int(self, size, signed): - return int.from_bytes(self._typelib_file[self._offset:self._offset + size], sys.byteorder) + return int.from_bytes( + self._typelib_file[self._offset : self._offset + size], sys.byteorder + ) class TypelibHeader(Typelib): diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index 2d5451d..1c69fd9 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -21,15 +21,15 @@ import typing as T class Colors: - RED = '\033[91m' - GREEN = '\033[92m' - YELLOW = '\033[33m' - FAINT = '\033[2m' - BOLD = '\033[1m' - BLUE = '\033[34m' - UNDERLINE = '\033[4m' - NO_UNDERLINE = '\033[24m' - CLEAR = '\033[0m' + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[33m" + FAINT = "\033[2m" + BOLD = "\033[1m" + BLUE = "\033[34m" + UNDERLINE = "\033[4m" + NO_UNDERLINE = "\033[24m" + CLEAR = "\033[0m" def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: @@ -56,12 +56,16 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: cost = 1 else: cost = 2 - distances[i][j] = min(distances[i-1][j] + 2, distances[i][j-1] + 2, distances[i-1][j-1] + cost) + distances[i][j] = min( + distances[i - 1][j] + 2, + distances[i][j - 1] + 2, + distances[i - 1][j - 1] + cost, + ) - return distances[m-1][n-1] + return distances[m - 1][n - 1] distances = [(option, levenshtein(word, option)) for option in options] - closest = min(distances, key=lambda item:item[1]) + closest = min(distances, key=lambda item: item[1]) if closest[1] <= 5: return closest[0] return None @@ -75,10 +79,12 @@ def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]: col_num = len(sp[-1]) return (line_num - 1, col_num) + def pos_to_idx(line: int, col: int, text: str) -> int: lines = text.splitlines(keepends=True) return sum([len(line) for line in lines[:line]]) + col + def idxs_to_range(start: int, end: int, text: str): start_l, start_c = idx_to_pos(start, text) end_l, end_c = idx_to_pos(end, text) diff --git a/blueprintcompiler/xml_reader.py b/blueprintcompiler/xml_reader.py index 24ae5ff..c0552f5 100644 --- a/blueprintcompiler/xml_reader.py +++ b/blueprintcompiler/xml_reader.py @@ -25,11 +25,24 @@ from xml import sax # To speed up parsing, we ignore all tags except these -PARSE_GIR = set([ - "repository", "namespace", "class", "interface", "property", "glib:signal", - "include", "implements", "type", "parameter", "parameters", "enumeration", - "member", "bitfield", -]) +PARSE_GIR = set( + [ + "repository", + "namespace", + "class", + "interface", + "property", + "glib:signal", + "include", + "implements", + "type", + "parameter", + "parameters", + "enumeration", + "member", + "bitfield", + ] +) class Element: @@ -41,14 +54,10 @@ class Element: @cached_property def cdata(self): - return ''.join(self.cdata_chunks) + return "".join(self.cdata_chunks) def get_elements(self, name) -> T.List["Element"]: - return [ - child - for child in self.children - if child.tag == name - ] + return [child for child in self.children if child.tag == name] def __getitem__(self, key): return self.attrs.get(key) diff --git a/tests/fuzz.py b/tests/fuzz.py index 1ebd02d..0f6a1a7 100644 --- a/tests/fuzz.py +++ b/tests/fuzz.py @@ -7,10 +7,16 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from blueprintcompiler import tokenizer, parser, decompiler, gir from blueprintcompiler.completions import complete -from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError, CompilerBugError +from blueprintcompiler.errors import ( + PrintableError, + MultipleErrors, + CompileError, + CompilerBugError, +) from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler import utils + @PythonFuzz def fuzz(buf): try: @@ -29,6 +35,7 @@ def fuzz(buf): except UnicodeDecodeError: pass + if __name__ == "__main__": # Make sure Gtk 4.0 is accessible, otherwise every test will fail on that # and nothing interesting will be tested diff --git a/tests/test_samples.py b/tests/test_samples.py index 5f0d9e5..e63c36c 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import difflib # I love Python +import difflib # I love Python from pathlib import Path import traceback import unittest @@ -59,23 +59,26 @@ class TestSamples(unittest.TestCase): xml = XmlOutput() actual = xml.emit(ast) - if actual.strip() != expected.strip(): # pragma: no cover + if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() self.assert_docs_dont_crash(blueprint, ast) self.assert_completions_dont_crash(blueprint, ast, tokens) - except PrintableError as e: # pragma: no cover + except PrintableError as e: # pragma: no cover e.pretty_print(name + ".blp", blueprint) raise AssertionError() - def assert_sample_error(self, name): try: - with open((Path(__file__).parent / f"sample_errors/{name}.blp").resolve()) as f: + with open( + (Path(__file__).parent / f"sample_errors/{name}.blp").resolve() + ) as f: blueprint = f.read() - with open((Path(__file__).parent / f"sample_errors/{name}.err").resolve()) as f: + with open( + (Path(__file__).parent / f"sample_errors/{name}.err").resolve() + ) as f: expected = f.read() tokens = tokenizer.tokenize(blueprint) @@ -91,6 +94,7 @@ class TestSamples(unittest.TestCase): if len(warnings): raise MultipleErrors(warnings) except PrintableError as e: + def error_str(error): line, col = utils.idx_to_pos(error.start + 1, blueprint) len = error.end - error.start @@ -100,17 +104,16 @@ class TestSamples(unittest.TestCase): actual = error_str(e) elif isinstance(e, MultipleErrors): actual = "\n".join([error_str(error) for error in e.errors]) - else: # pragma: no cover + else: # pragma: no cover raise AssertionError() - if actual.strip() != expected.strip(): # pragma: no cover + if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() - else: # pragma: no cover + else: # pragma: no cover raise AssertionError("Expected a compiler error, but none was emitted") - def assert_decompile(self, name): try: with open((Path(__file__).parent / f"samples/{name}.blp").resolve()) as f: @@ -121,15 +124,14 @@ class TestSamples(unittest.TestCase): actual = decompiler.decompile(ui_path) - if actual.strip() != expected.strip(): # pragma: no cover + if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() - except PrintableError as e: # pragma: no cover + except PrintableError as e: # pragma: no cover e.pretty_print(name + ".blp", blueprint) raise AssertionError() - def test_samples(self): self.assert_sample("accessibility") self.assert_sample("action_widgets") @@ -161,7 +163,6 @@ class TestSamples(unittest.TestCase): self.assert_sample("unchecked_class") self.assert_sample("using") - def test_sample_errors(self): self.assert_sample_error("a11y_in_non_widget") self.assert_sample_error("a11y_prop_dne") @@ -209,7 +210,6 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("using_invalid_namespace") self.assert_sample_error("widgets_in_non_size_group") - def test_decompiler(self): self.assert_decompile("accessibility_dec") self.assert_decompile("binding") diff --git a/tests/test_tokenizer.py b/tests/test_tokenizer.py index 1c87e50..2bca595 100644 --- a/tests/test_tokenizer.py +++ b/tests/test_tokenizer.py @@ -32,47 +32,57 @@ class TestTokenizer(unittest.TestCase): for token, (type, token_str) in zip(tokens, expect): self.assertEqual(token.type, type) self.assertEqual(str(token), token_str) - except PrintableError as e: # pragma: no cover + except PrintableError as e: # pragma: no cover e.pretty_print("", string) raise e - def test_basic(self): - self.assert_tokenize("ident(){}; \n <<+>>*/=", [ - (TokenType.IDENT, "ident"), - (TokenType.PUNCTUATION, "("), - (TokenType.PUNCTUATION, ")"), - (TokenType.PUNCTUATION, "{"), - (TokenType.PUNCTUATION, "}"), - (TokenType.PUNCTUATION, ";"), - (TokenType.WHITESPACE, " \n "), - (TokenType.OP, "<<"), - (TokenType.OP, "+"), - (TokenType.OP, ">>"), - (TokenType.OP, "*"), - (TokenType.OP, "/"), - (TokenType.OP, "="), - (TokenType.EOF, ""), - ]) + self.assert_tokenize( + "ident(){}; \n <<+>>*/=", + [ + (TokenType.IDENT, "ident"), + (TokenType.PUNCTUATION, "("), + (TokenType.PUNCTUATION, ")"), + (TokenType.PUNCTUATION, "{"), + (TokenType.PUNCTUATION, "}"), + (TokenType.PUNCTUATION, ";"), + (TokenType.WHITESPACE, " \n "), + (TokenType.OP, "<<"), + (TokenType.OP, "+"), + (TokenType.OP, ">>"), + (TokenType.OP, "*"), + (TokenType.OP, "/"), + (TokenType.OP, "="), + (TokenType.EOF, ""), + ], + ) def test_quotes(self): - self.assert_tokenize(r'"this is a \n string""this is \\another \"string\""', [ - (TokenType.QUOTED, r'"this is a \n string"'), - (TokenType.QUOTED, r'"this is \\another \"string\""'), - (TokenType.EOF, ""), - ]) + self.assert_tokenize( + r'"this is a \n string""this is \\another \"string\""', + [ + (TokenType.QUOTED, r'"this is a \n string"'), + (TokenType.QUOTED, r'"this is \\another \"string\""'), + (TokenType.EOF, ""), + ], + ) def test_comments(self): - self.assert_tokenize('/* \n \\n COMMENT /* */', [ - (TokenType.COMMENT, '/* \n \\n COMMENT /* */'), - (TokenType.EOF, ""), - ]) - self.assert_tokenize('line // comment\nline', [ - (TokenType.IDENT, 'line'), - (TokenType.WHITESPACE, ' '), - (TokenType.COMMENT, '// comment'), - (TokenType.WHITESPACE, '\n'), - (TokenType.IDENT, 'line'), - (TokenType.EOF, ""), - ]) - + self.assert_tokenize( + "/* \n \\n COMMENT /* */", + [ + (TokenType.COMMENT, "/* \n \\n COMMENT /* */"), + (TokenType.EOF, ""), + ], + ) + self.assert_tokenize( + "line // comment\nline", + [ + (TokenType.IDENT, "line"), + (TokenType.WHITESPACE, " "), + (TokenType.COMMENT, "// comment"), + (TokenType.WHITESPACE, "\n"), + (TokenType.IDENT, "line"), + (TokenType.EOF, ""), + ], + ) From 83a7503e3a06d70a5babf5b65000b6fedae974e2 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 12:03:50 -0600 Subject: [PATCH 053/290] ci: Check formatting --- .gitlab-ci.yml | 1 + build-aux/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 598481b..90a9f8d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ build: image: registry.gitlab.gnome.org/jwestman/blueprint-compiler stage: build script: + - black --check --diff blueprintcompiler tests - mypy --python-version=3.9 blueprintcompiler - coverage run -m unittest - coverage report diff --git a/build-aux/Dockerfile b/build-aux/Dockerfile index 4b9e0af..27c5a44 100644 --- a/build-aux/Dockerfile +++ b/build-aux/Dockerfile @@ -2,7 +2,7 @@ FROM fedora:latest RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel \ libadwaita-devel python3-devel python3-gobject git -RUN pip3 install furo mypy sphinx coverage +RUN pip3 install furo mypy sphinx coverage black # The version on PyPI is very old and doesn't install. Use the upstream package registry instead. RUN pip install pythonfuzz --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple \ No newline at end of file From 219891584c057f0e575390bd188794af40687f86 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 12:22:37 -0600 Subject: [PATCH 054/290] ci: Fix Dockerfile --- build-aux/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/Dockerfile b/build-aux/Dockerfile index 27c5a44..e2c1081 100644 --- a/build-aux/Dockerfile +++ b/build-aux/Dockerfile @@ -1,7 +1,7 @@ FROM fedora:latest RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel \ - libadwaita-devel python3-devel python3-gobject git + libadwaita-devel python3-devel python3-gobject git diffutils RUN pip3 install furo mypy sphinx coverage black # The version on PyPI is very old and doesn't install. Use the upstream package registry instead. From 8758bac40a14703eb5748249c577eb9f3541269f Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 13:53:52 -0600 Subject: [PATCH 055/290] tests: Test XML outputs Load the outputs of the tests in Gtk.Builder and make sure they work. Some of them don't and need to be fixed. Others will require a bit more work to set up callbacks, templates, etc. --- tests/samples/action_widgets.blp | 2 +- tests/samples/action_widgets.ui | 4 +-- tests/samples/numbers.blp | 3 ++- tests/samples/numbers.ui | 2 +- tests/samples/object_prop.blp | 6 ++--- tests/samples/object_prop.ui | 10 ++++---- tests/test_samples.py | 43 ++++++++++++++++++++++++-------- 7 files changed, 46 insertions(+), 24 deletions(-) diff --git a/tests/samples/action_widgets.blp b/tests/samples/action_widgets.blp index 34293a0..2d4d6ae 100644 --- a/tests/samples/action_widgets.blp +++ b/tests/samples/action_widgets.blp @@ -1,6 +1,6 @@ using Gtk 4.0; -template MyDialog : Dialog { +Dialog { [action response=cancel] Button cancel_button { label: _("Cancel"); diff --git a/tests/samples/action_widgets.ui b/tests/samples/action_widgets.ui index 8c41bb2..91b6e64 100644 --- a/tests/samples/action_widgets.ui +++ b/tests/samples/action_widgets.ui @@ -1,7 +1,7 @@ - + diff --git a/tests/samples/numbers.blp b/tests/samples/numbers.blp index 6364dd2..9ac25dd 100644 --- a/tests/samples/numbers.blp +++ b/tests/samples/numbers.blp @@ -2,6 +2,7 @@ using Gtk 4.0; Gtk.Label { xalign: .5; - margin-end: 1_000_000; + height-request: 1_000_000; margin-top: 0x30; + } diff --git a/tests/samples/numbers.ui b/tests/samples/numbers.ui index bb70bd8..03dee06 100644 --- a/tests/samples/numbers.ui +++ b/tests/samples/numbers.ui @@ -3,7 +3,7 @@ 0.5 - 1000000 + 1000000 48 diff --git a/tests/samples/object_prop.blp b/tests/samples/object_prop.blp index eaccfd9..b7270ac 100644 --- a/tests/samples/object_prop.blp +++ b/tests/samples/object_prop.blp @@ -1,7 +1,7 @@ using Gtk 4.0; -template TestTemplate : Label { - test-property: Button { - label: "Hello, world!"; +Range { + adjustment: Adjustment { + lower: 10; }; } diff --git a/tests/samples/object_prop.ui b/tests/samples/object_prop.ui index 46a62d9..5224073 100644 --- a/tests/samples/object_prop.ui +++ b/tests/samples/object_prop.ui @@ -1,11 +1,11 @@ - + diff --git a/tests/test_samples.py b/tests/test_samples.py index e63c36c..c745226 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -20,9 +20,13 @@ import difflib # I love Python from pathlib import Path -import traceback import unittest +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk + from blueprintcompiler import tokenizer, parser, decompiler from blueprintcompiler.completions import complete from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError @@ -40,7 +44,8 @@ class TestSamples(unittest.TestCase): for i in range(len(text)): list(complete(ast, tokens, i)) - def assert_sample(self, name): + def assert_sample(self, name, skip_run=False): + print(f'assert_sample("{name}", skip_run={skip_run})') try: with open((Path(__file__).parent / f"samples/{name}.blp").resolve()) as f: blueprint = f.read() @@ -70,7 +75,12 @@ class TestSamples(unittest.TestCase): e.pretty_print(name + ".blp", blueprint) raise AssertionError() + # Make sure the sample runs + if not skip_run: + Gtk.Builder.new_from_string(actual, -1) + def assert_sample_error(self, name): + print(f'assert_sample_error("{name}")') try: with open( (Path(__file__).parent / f"sample_errors/{name}.blp").resolve() @@ -115,6 +125,7 @@ class TestSamples(unittest.TestCase): raise AssertionError("Expected a compiler error, but none was emitted") def assert_decompile(self, name): + print(f'assert_decompile("{name}")') try: with open((Path(__file__).parent / f"samples/{name}.blp").resolve()) as f: expected = f.read() @@ -140,27 +151,37 @@ class TestSamples(unittest.TestCase): self.assert_sample("combo_box_text") self.assert_sample("comments") self.assert_sample("enum") - self.assert_sample("expr_lookup") + self.assert_sample("expr_lookup", skip_run=True) # TODO: Fix self.assert_sample("file_filter") - self.assert_sample("flags") + self.assert_sample("flags", skip_run=True) # TODO: Fix self.assert_sample("id_prop") self.assert_sample("layout") - self.assert_sample("menu") + self.assert_sample("menu", skip_run=True) # TODO: Fix self.assert_sample("numbers") self.assert_sample("object_prop") - self.assert_sample("parseable") + self.assert_sample( + "parseable", skip_run=True + ) # The image resource doesn't exist self.assert_sample("property") - self.assert_sample("signal") + self.assert_sample("signal", skip_run=True) # The callback doesn't exist self.assert_sample("size_group") self.assert_sample("string_list") self.assert_sample("strings") self.assert_sample("style") - self.assert_sample("template") - self.assert_sample("template_no_parent") + self.assert_sample( + "template", skip_run=True + ) # The template class doesn't exist + self.assert_sample( + "template_no_parent", skip_run=True + ) # The template class doesn't exist self.assert_sample("translated") - self.assert_sample("typeof") + self.assert_sample( + "typeof", skip_run=True + ) # The custom object type doesn't exist self.assert_sample("uint") - self.assert_sample("unchecked_class") + self.assert_sample( + "unchecked_class", skip_run=True + ) # The custom object type doesn't exist self.assert_sample("using") def test_sample_errors(self): From 8a1dba662a6982038dc1ec1ca66fb8163f362532 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 13:58:40 -0600 Subject: [PATCH 056/290] ci: Run tests with G_DEBUG=fatal-warnings --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 90a9f8d..380f288 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,7 @@ build: script: - black --check --diff blueprintcompiler tests - mypy --python-version=3.9 blueprintcompiler - - coverage run -m unittest + - G_DEBUG=fatal-warnings coverage run -m unittest - coverage report - coverage html - coverage xml From 51d8969ced0bcee74b9d204d500d4b8b87faa7ee Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 14:36:32 -0600 Subject: [PATCH 057/290] Fix menus - Menus require an ID - The top level menu block can't have attributes --- blueprintcompiler/language/gtk_menu.py | 27 ++++++++++++++++++- tests/sample_errors/menu_no_id.blp | 3 +++ tests/sample_errors/menu_no_id.err | 1 + .../sample_errors/menu_toplevel_attribute.blp | 5 ++++ .../sample_errors/menu_toplevel_attribute.err | 2 ++ tests/samples/menu.blp | 5 +--- tests/samples/menu.ui | 4 +-- tests/samples/menu_dec.blp | 5 +--- tests/test_samples.py | 4 ++- 9 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 tests/sample_errors/menu_no_id.blp create mode 100644 tests/sample_errors/menu_no_id.err create mode 100644 tests/sample_errors/menu_toplevel_attribute.blp create mode 100644 tests/sample_errors/menu_toplevel_attribute.err diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 3d3e6ee..73aa039 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -39,6 +39,11 @@ class Menu(AstNode): def tag(self) -> str: return self.tokens["tag"] + @validate("menu") + def has_id(self): + if self.tokens["tag"] == "menu" and self.tokens["id"] is None: + raise CompileError("Menu requires an ID") + class MenuAttribute(BaseAttribute): tag_name = "attribute" @@ -137,7 +142,27 @@ menu_contents.children = [ menu: Group = Group( Menu, - ["menu", UseLiteral("tag", "menu"), Optional(UseIdent("id")), menu_contents], + [ + Keyword("menu"), + UseLiteral("tag", "menu"), + Optional(UseIdent("id")), + [ + Match("{"), + Until( + AnyOf( + menu_section, + menu_submenu, + menu_item_shorthand, + menu_item, + Fail( + menu_attribute, + "Attributes are not permitted at the top level of a menu", + ), + ), + "}", + ), + ], + ], ) from .ui import UI diff --git a/tests/sample_errors/menu_no_id.blp b/tests/sample_errors/menu_no_id.blp new file mode 100644 index 0000000..5f6396e --- /dev/null +++ b/tests/sample_errors/menu_no_id.blp @@ -0,0 +1,3 @@ +using Gtk 4.0; + +menu {} \ No newline at end of file diff --git a/tests/sample_errors/menu_no_id.err b/tests/sample_errors/menu_no_id.err new file mode 100644 index 0000000..e97f033 --- /dev/null +++ b/tests/sample_errors/menu_no_id.err @@ -0,0 +1 @@ +3,1,4,Menu requires an ID \ No newline at end of file diff --git a/tests/sample_errors/menu_toplevel_attribute.blp b/tests/sample_errors/menu_toplevel_attribute.blp new file mode 100644 index 0000000..21ceeff --- /dev/null +++ b/tests/sample_errors/menu_toplevel_attribute.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +menu { + not-allowed: true; +} \ No newline at end of file diff --git a/tests/sample_errors/menu_toplevel_attribute.err b/tests/sample_errors/menu_toplevel_attribute.err new file mode 100644 index 0000000..45713cf --- /dev/null +++ b/tests/sample_errors/menu_toplevel_attribute.err @@ -0,0 +1,2 @@ +4,5,19,Attributes are not permitted at the top level of a menu +4,16,8,Unexpected tokens \ No newline at end of file diff --git a/tests/samples/menu.blp b/tests/samples/menu.blp index f15dbdb..4c52bab 100644 --- a/tests/samples/menu.blp +++ b/tests/samples/menu.blp @@ -1,9 +1,6 @@ using Gtk 4.0; -menu { - label: _("menu label"); - test-custom-attribute: 3.1415; - +menu my-menu { submenu { section { label: "test section"; diff --git a/tests/samples/menu.ui b/tests/samples/menu.ui index a84fa57..3ba1fed 100644 --- a/tests/samples/menu.ui +++ b/tests/samples/menu.ui @@ -1,9 +1,7 @@ - - menu label - 3.1415 +
test section diff --git a/tests/samples/menu_dec.blp b/tests/samples/menu_dec.blp index bc4ddf1..64a6021 100644 --- a/tests/samples/menu_dec.blp +++ b/tests/samples/menu_dec.blp @@ -1,9 +1,6 @@ using Gtk 4.0; -menu { - label: _("menu label"); - test-custom-attribute: "3.1415"; - +menu my-menu { submenu { section { label: "test section"; diff --git a/tests/test_samples.py b/tests/test_samples.py index c745226..279ceb6 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -156,7 +156,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("flags", skip_run=True) # TODO: Fix self.assert_sample("id_prop") self.assert_sample("layout") - self.assert_sample("menu", skip_run=True) # TODO: Fix + self.assert_sample("menu") self.assert_sample("numbers") self.assert_sample("object_prop") self.assert_sample( @@ -213,6 +213,8 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("inline_menu") self.assert_sample_error("invalid_bool") self.assert_sample_error("layout_in_non_widget") + self.assert_sample_error("menu_no_id") + self.assert_sample_error("menu_toplevel_attribute") self.assert_sample_error("no_import_version") self.assert_sample_error("ns_not_imported") self.assert_sample_error("not_a_class") From 6c67e1fc5afcc3f4fb10e00234526121edcb12a7 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 15:07:31 -0600 Subject: [PATCH 058/290] xml: Fix flags and enums GtkBuilder XML uses enum nicknames, full names, or integer values, but we accept GIR names, so passing those through doesn't work if the name has an underscore (which traditionally turns into a dash in the nickname). Avoid the problem by always writing the integer value of the enum member. --- .gitlab-ci.yml | 2 +- blueprintcompiler/decompiler.py | 6 +++++- blueprintcompiler/gir.py | 6 +++--- blueprintcompiler/language/values.py | 14 ++++++++++++++ blueprintcompiler/outputs/xml/__init__.py | 6 ++++-- tests/samples/accessibility.ui | 2 +- tests/samples/enum.ui | 2 +- tests/samples/flags.ui | 4 ++-- tests/samples/property.ui | 2 +- tests/samples/size_group.ui | 2 +- tests/test_samples.py | 2 +- 11 files changed, 34 insertions(+), 14 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 380f288..f7d8a39 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,7 +19,7 @@ build: - ninja -C _build docs/en - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - cd blueprint-regression-tests - - git checkout e1a2b04ce13838794eec9678deff95802fa278d1 + - git checkout 58fda9381dac4a9c42c18a4b06149ed59ee702dc - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index 145e4be..565d420 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -127,7 +127,11 @@ class DecompileCtx: def print_attribute(self, name, value, type): def get_enum_name(value): for member in type.members.values(): - if member.nick == value or member.c_ident == value: + if ( + member.nick == value + or member.c_ident == value + or str(member.value) == value + ): return member.name return value.replace("-", "_") diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index a1bb419..12c9772 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -462,11 +462,11 @@ class EnumMember(GirNode): super().__init__(ns, tl) @property - def value(self): + def value(self) -> int: return self.tl.VALUE_VALUE @cached_property - def name(self): + def name(self) -> str: return self.tl.VALUE_NAME @cached_property @@ -487,7 +487,7 @@ class Enumeration(GirNode, GirType): super().__init__(ns, tl) @cached_property - def members(self): + def members(self) -> T.Dict[str, EnumMember]: members = {} n_values = self.tl.ENUM_N_VALUES values = self.tl.ENUM_VALUES diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index db28490..4a71247 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -168,6 +168,20 @@ class NumberValue(Value): class Flag(AstNode): grammar = UseIdent("value") + @property + def name(self) -> str: + return self.tokens["value"] + + @property + def value(self) -> T.Optional[int]: + type = self.parent.parent.value_type + if not isinstance(type, Enumeration): + return None + elif member := type.members.get(self.tokens["value"]): + return member.value + else: + return None + @docs() def docs(self): type = self.parent.parent.value_type diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index e7e33d9..79f24f3 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -159,14 +159,16 @@ class XmlOutput(OutputFormat): if isinstance(value, IdentValue): if isinstance(value.parent.value_type, gir.Enumeration): xml.put_text( - value.parent.value_type.members[value.tokens["value"]].nick + str(value.parent.value_type.members[value.tokens["value"]].value) ) else: xml.put_text(value.tokens["value"]) elif isinstance(value, QuotedValue) or isinstance(value, NumberValue): xml.put_text(value.value) elif isinstance(value, FlagsValue): - xml.put_text("|".join([flag.tokens["value"] for flag in value.children])) + xml.put_text( + "|".join([str(flag.value or flag.name) for flag in value.children]) + ) elif isinstance(value, TranslatedStringValue): raise CompilerBugError("translated values must be handled in the parent") elif isinstance(value, TypeValue): diff --git a/tests/samples/accessibility.ui b/tests/samples/accessibility.ui index 50e98c8..321f20f 100644 --- a/tests/samples/accessibility.ui +++ b/tests/samples/accessibility.ui @@ -5,7 +5,7 @@ Hello, world! my_label - true + 1
diff --git a/tests/samples/enum.ui b/tests/samples/enum.ui index acad161..d2cda1e 100644 --- a/tests/samples/enum.ui +++ b/tests/samples/enum.ui @@ -2,6 +2,6 @@ - top-left + 0 diff --git a/tests/samples/flags.ui b/tests/samples/flags.ui index d2bac55..56fbf31 100644 --- a/tests/samples/flags.ui +++ b/tests/samples/flags.ui @@ -2,9 +2,9 @@ - is_service|handles_open + 1|4 - vertical + 1 diff --git a/tests/samples/property.ui b/tests/samples/property.ui index a2d5a1b..ba8089d 100644 --- a/tests/samples/property.ui +++ b/tests/samples/property.ui @@ -2,6 +2,6 @@ - vertical + 1 diff --git a/tests/samples/size_group.ui b/tests/samples/size_group.ui index 6d92edd..218b023 100644 --- a/tests/samples/size_group.ui +++ b/tests/samples/size_group.ui @@ -2,7 +2,7 @@ - horizontal + 1 diff --git a/tests/test_samples.py b/tests/test_samples.py index 279ceb6..f038b60 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -153,7 +153,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("enum") self.assert_sample("expr_lookup", skip_run=True) # TODO: Fix self.assert_sample("file_filter") - self.assert_sample("flags", skip_run=True) # TODO: Fix + self.assert_sample("flags") self.assert_sample("id_prop") self.assert_sample("layout") self.assert_sample("menu") From 039d88ab45001cf799c421e58d4669a0596c4d29 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 15:21:12 -0600 Subject: [PATCH 059/290] Fix CI --- .gitlab-ci.yml | 3 +-- build-aux/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f7d8a39..b924a7d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,13 +8,12 @@ build: script: - black --check --diff blueprintcompiler tests - mypy --python-version=3.9 blueprintcompiler - - G_DEBUG=fatal-warnings coverage run -m unittest + - G_DEBUG=fatal-warnings xvfb-run coverage run -m unittest - coverage report - coverage html - coverage xml - meson _build -Ddocs=true --prefix=/usr - ninja -C _build - - ninja -C _build test - ninja -C _build install - ninja -C _build docs/en - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git diff --git a/build-aux/Dockerfile b/build-aux/Dockerfile index e2c1081..c7841d9 100644 --- a/build-aux/Dockerfile +++ b/build-aux/Dockerfile @@ -1,7 +1,7 @@ FROM fedora:latest RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel \ - libadwaita-devel python3-devel python3-gobject git diffutils + libadwaita-devel python3-devel python3-gobject git diffutils xorg-x11-server-Xvfb RUN pip3 install furo mypy sphinx coverage black # The version on PyPI is very old and doesn't install. Use the upstream package registry instead. From f7aa7d0be200cc6778ce3fa92eb6eadfa3629de6 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 4 Dec 2022 15:10:52 +0100 Subject: [PATCH 060/290] lsp: Support change events with no range range is optional https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent --- blueprintcompiler/lsp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 26a519e..f579ab4 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -51,6 +51,9 @@ class OpenFile: def apply_changes(self, changes): for change in changes: + if "range" not in change: + self.text = change["text"] + continue start = utils.pos_to_idx( change["range"]["start"]["line"], change["range"]["start"]["character"], From 2033bd9e1653b2aa89ea7eda03241dd27c823d3b Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 23 Dec 2022 20:13:14 -0600 Subject: [PATCH 061/290] types: Add UncheckedType This allows us to remember information about an external type, such as its name, while still marking it as unchecked. --- blueprintcompiler/completions.py | 4 +-- blueprintcompiler/gir.py | 31 ++++++++++++++++++- blueprintcompiler/language/common.py | 10 +++++- .../language/gobject_property.py | 4 +-- blueprintcompiler/language/gobject_signal.py | 4 +-- .../language/gtkbuilder_template.py | 2 ++ blueprintcompiler/language/types.py | 11 +++++-- 7 files changed, 55 insertions(+), 11 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 085baee..9940055 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -120,7 +120,7 @@ def gtk_object_completer(ast_node, match_variables): matches=new_statement_patterns, ) def property_completer(ast_node, match_variables): - if ast_node.gir_class: + if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.UncheckedType): for prop in ast_node.gir_class.properties: yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;") @@ -144,7 +144,7 @@ def prop_value_completer(ast_node, match_variables): matches=new_statement_patterns, ) def signal_completer(ast_node, match_variables): - if ast_node.gir_class: + if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.UncheckedType): for signal in ast_node.gir_class.signals: if not isinstance(ast_node.parent, language.Object): name = "on" diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 12c9772..ab8af75 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -93,13 +93,36 @@ class GirType: def doc(self): return None - def assignable_to(self, other) -> bool: + def assignable_to(self, other: "GirType") -> bool: raise NotImplementedError() @property def full_name(self) -> str: + """The GIR name of the type to use in diagnostics""" raise NotImplementedError() + @property + def glib_type_name(self) -> str: + """The name of the type in the GObject type system, suitable to pass to `g_type_from_name()`.""" + raise NotImplementedError() + + +class UncheckedType(GirType): + def __init__(self, name) -> None: + super().__init__() + self._name = name + + def assignable_to(self, other: GirType) -> bool: + return True + + @property + def full_name(self) -> str: + return self._name + + @property + def glib_type_name(self) -> str: + return self._name + class BasicType(GirType): name: str = "unknown type" @@ -111,6 +134,7 @@ class BasicType(GirType): class BoolType(BasicType): name = "bool" + glib_type_name: str = "gboolean" def assignable_to(self, other) -> bool: return isinstance(other, BoolType) @@ -118,6 +142,7 @@ class BoolType(BasicType): class IntType(BasicType): name = "int" + glib_type_name: str = "gint" def assignable_to(self, other) -> bool: return ( @@ -129,6 +154,7 @@ class IntType(BasicType): class UIntType(BasicType): name = "uint" + glib_type_name: str = "guint" def assignable_to(self, other) -> bool: return ( @@ -140,6 +166,7 @@ class UIntType(BasicType): class FloatType(BasicType): name = "float" + glib_type_name: str = "gfloat" def assignable_to(self, other) -> bool: return isinstance(other, FloatType) @@ -147,6 +174,7 @@ class FloatType(BasicType): class StringType(BasicType): name = "string" + glib_type_name: str = "gchararray" def assignable_to(self, other) -> bool: return isinstance(other, StringType) @@ -154,6 +182,7 @@ class StringType(BasicType): class TypeType(BasicType): name = "GType" + glib_type_name: str = "GType" def assignable_to(self, other) -> bool: return isinstance(other, TypeType) diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index f6a8f8e..ec15c7a 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -24,7 +24,15 @@ from ..errors import CompileError, MultipleErrors from ..completions_utils import * from .. import decompiler as decompile from ..decompiler import DecompileCtx, decompiler -from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration +from ..gir import ( + StringType, + BoolType, + IntType, + FloatType, + GirType, + Enumeration, + UncheckedType, +) from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 2c1e2ae..c6db999 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -69,7 +69,7 @@ class Property(AstNode): @property def gir_property(self): - if self.gir_class is not None: + if self.gir_class is not None and not isinstance(self.gir_class, UncheckedType): return self.gir_class.properties.get(self.tokens["name"]) @property @@ -79,7 +79,7 @@ class Property(AstNode): @validate("name") def property_exists(self): - if self.gir_class is None: + if self.gir_class is None or isinstance(self.gir_class, UncheckedType): # Objects that we have no gir data on should not be validated # This happens for classes defined by the app itself return diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index d50792e..11a69e0 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -71,7 +71,7 @@ class Signal(AstNode): @property def gir_signal(self): - if self.gir_class is not None: + if self.gir_class is not None and not isinstance(self.gir_class, UncheckedType): return self.gir_class.signals.get(self.tokens["name"]) @property @@ -80,7 +80,7 @@ class Signal(AstNode): @validate("name") def signal_exists(self): - if self.gir_class is None: + if self.gir_class is None or isinstance(self.gir_class, UncheckedType): # Objects that we have no gir data on should not be validated # This happens for classes defined by the app itself return diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index a215cdf..40ac7f7 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -53,6 +53,8 @@ class Template(Object): # Templates might not have a parent class defined if class_name := self.class_name: return class_name.gir_type + else: + return gir.UncheckedType(self.id) @validate("id") def unique_in_parent(self): diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 0987262..3b454bc 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -56,12 +56,13 @@ class TypeName(AstNode): return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") @property - def gir_type(self) -> T.Optional[gir.Class]: + def gir_type(self) -> gir.GirType: if self.tokens["class_name"] and not self.tokens["ignore_gir"]: return self.root.gir.get_type( self.tokens["class_name"], self.tokens["namespace"] ) - return None + + return gir.UncheckedType(self.tokens["class_name"]) @property def glib_type_name(self) -> str: @@ -84,7 +85,11 @@ class TypeName(AstNode): class ClassName(TypeName): @validate("namespace", "class_name") def gir_class_exists(self): - if self.gir_type is not None and not isinstance(self.gir_type, Class): + if ( + self.gir_type + and not isinstance(self.gir_type, UncheckedType) + and not isinstance(self.gir_type, Class) + ): if isinstance(self.gir_type, Interface): raise CompileError( f"{self.gir_type.full_name} is an interface, not a class" From 5cf9b63547deb7f58d72395cf357df01b1e81801 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 23 Dec 2022 23:24:29 -0600 Subject: [PATCH 062/290] language: Add cast expressions --- blueprintcompiler/language/__init__.py | 2 +- blueprintcompiler/language/expression.py | 84 +++++++++++++++++-- blueprintcompiler/language/gobject_object.py | 4 +- .../language/gobject_property.py | 6 +- blueprintcompiler/outputs/xml/__init__.py | 13 ++- tests/sample_errors/expr_cast_conversion.blp | 5 ++ tests/sample_errors/expr_cast_conversion.err | 1 + tests/sample_errors/expr_lookup_dne.blp | 5 ++ tests/sample_errors/expr_lookup_dne.err | 1 + .../expr_lookup_no_properties.blp | 5 ++ .../expr_lookup_no_properties.err | 1 + tests/sample_errors/property_dne.err | 2 +- tests/samples/expr_lookup.blp | 2 +- tests/samples/expr_lookup.ui | 6 +- tests/test_samples.py | 5 +- 15 files changed, 122 insertions(+), 20 deletions(-) create mode 100644 tests/sample_errors/expr_cast_conversion.blp create mode 100644 tests/sample_errors/expr_cast_conversion.err create mode 100644 tests/sample_errors/expr_lookup_dne.blp create mode 100644 tests/sample_errors/expr_lookup_dne.err create mode 100644 tests/sample_errors/expr_lookup_no_properties.blp create mode 100644 tests/sample_errors/expr_lookup_no_properties.err 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") From 59aa054c4cd0e5a6ca609f98448c86a2fe09bb95 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 24 Dec 2022 21:46:03 -0600 Subject: [PATCH 063/290] language: Add closure expressions --- blueprintcompiler/gir.py | 6 ++ blueprintcompiler/language/__init__.py | 2 +- blueprintcompiler/language/expression.py | 57 +++++++++++++++++-- blueprintcompiler/outputs/xml/__init__.py | 10 +++- blueprintcompiler/tokenizer.py | 2 +- tests/sample_errors/expr_closure_not_cast.blp | 5 ++ tests/sample_errors/expr_closure_not_cast.err | 1 + tests/samples/expr_closure.blp | 5 ++ tests/samples/expr_closure.ui | 13 +++++ tests/test_samples.py | 2 + 10 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 tests/sample_errors/expr_closure_not_cast.blp create mode 100644 tests/sample_errors/expr_closure_not_cast.err create mode 100644 tests/samples/expr_closure.blp create mode 100644 tests/samples/expr_closure.ui diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index ab8af75..4ee2b1e 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -189,7 +189,10 @@ class TypeType(BasicType): _BASIC_TYPES = { + "bool": BoolType, "gboolean": BoolType, + "string": StringType, + "gchararray": StringType, "int": IntType, "gint": IntType, "gint64": IntType, @@ -730,6 +733,9 @@ class GirContext: return None def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: + if ns is None and name in _BASIC_TYPES: + return _BASIC_TYPES[name]() + ns = ns or "Gtk" if ns not in self.namespaces: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 0f1132b..4063943 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,5 +1,5 @@ from .attributes import BaseAttribute, BaseTypedAttribute -from .expression import CastExpr, IdentExpr, LookupOp, ExprChain +from .expression import CastExpr, ClosureExpr, Expr, ExprChain, IdentExpr, LookupOp 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 2d50984..f2b2ea1 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -25,13 +25,24 @@ from .types import TypeName expr = Pratt() -class Expr: +class Expr(AstNode): @property def type(self) -> T.Optional[GirType]: raise NotImplementedError() + @property + def rhs(self) -> T.Optional["Expr"]: + if isinstance(self.parent, ExprChain): + children = list(self.parent.children) + if children.index(self) + 1 < len(children): + return children[children.index(self) + 1] + else: + return self.parent.rhs + else: + return None -class ExprChain(Expr, AstNode): + +class ExprChain(Expr): grammar = expr @property @@ -43,14 +54,14 @@ class ExprChain(Expr, AstNode): return self.last.type -class InfixExpr(Expr, AstNode): +class InfixExpr(Expr): @property def lhs(self): children = list(self.parent_by_type(ExprChain).children) return children[children.index(self) - 1] -class IdentExpr(Expr, AstNode): +class IdentExpr(Expr): grammar = UseIdent("ident") @property @@ -124,7 +135,45 @@ class CastExpr(InfixExpr): ) +class ClosureExpr(Expr): + grammar = [ + Optional(["$", UseLiteral("extern", True)]), + UseIdent("name"), + "(", + Delimited(ExprChain, ","), + ")", + ] + + @property + def type(self) -> T.Optional[GirType]: + if isinstance(self.rhs, CastExpr): + return self.rhs.type + else: + return None + + @property + def closure_name(self) -> str: + return self.tokens["name"] + + @property + def args(self) -> T.List[ExprChain]: + return self.children[ExprChain] + + @validate() + def cast_to_return_type(self): + if not isinstance(self.rhs, CastExpr): + raise CompileError( + "Closure expression must be cast to the closure's return type" + ) + + @validate() + def builtin_exists(self): + if not self.tokens["extern"]: + raise CompileError(f"{self.closure_name} is not a builtin function") + + expr.children = [ + Prefix(ClosureExpr), Prefix(IdentExpr), Prefix(["(", ExprChain, ")"]), Infix(10, LookupOp), diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 69ce12f..ac2aea8 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -179,7 +179,7 @@ class XmlOutput(OutputFormat): def _emit_expression(self, expression: ExprChain, xml: XmlEmitter): self._emit_expression_part(expression.children[-1], xml) - def _emit_expression_part(self, expression, xml: XmlEmitter): + def _emit_expression_part(self, expression: Expr, xml: XmlEmitter): if isinstance(expression, IdentExpr): self._emit_ident_expr(expression, xml) elif isinstance(expression, LookupOp): @@ -188,6 +188,8 @@ class XmlOutput(OutputFormat): self._emit_expression(expression, xml) elif isinstance(expression, CastExpr): self._emit_cast_expr(expression, xml) + elif isinstance(expression, ClosureExpr): + self._emit_closure_expr(expression, xml) else: raise CompilerBugError() @@ -204,6 +206,12 @@ class XmlOutput(OutputFormat): def _emit_cast_expr(self, expr: CastExpr, xml: XmlEmitter): self._emit_expression_part(expr.lhs, xml) + def _emit_closure_expr(self, expr: ClosureExpr, xml: XmlEmitter): + xml.start_tag("closure", function=expr.closure_name, type=expr.type) + for arg in expr.args: + self._emit_expression_part(arg, xml) + xml.end_tag() + def _emit_attribute( self, tag: str, attr: str, name: str, value: Value, xml: XmlEmitter ): diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 4991967..516bc0b 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -46,7 +46,7 @@ _tokens = [ (TokenType.WHITESPACE, r"\s+"), (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), (TokenType.COMMENT, r"\/\/[^\n]*"), - (TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), + (TokenType.OP, r"\$|<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), ] _TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] diff --git a/tests/sample_errors/expr_closure_not_cast.blp b/tests/sample_errors/expr_closure_not_cast.blp new file mode 100644 index 0000000..7903bb6 --- /dev/null +++ b/tests/sample_errors/expr_closure_not_cast.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: bind $closure(); +} \ No newline at end of file diff --git a/tests/sample_errors/expr_closure_not_cast.err b/tests/sample_errors/expr_closure_not_cast.err new file mode 100644 index 0000000..fcc2dfb --- /dev/null +++ b/tests/sample_errors/expr_closure_not_cast.err @@ -0,0 +1 @@ +4,15,10,Closure expression must be cast to the closure's return type \ No newline at end of file diff --git a/tests/samples/expr_closure.blp b/tests/samples/expr_closure.blp new file mode 100644 index 0000000..99874e8 --- /dev/null +++ b/tests/samples/expr_closure.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label my-label { + label: bind ($my-closure(my-label.margin-bottom)) as (string); +} \ No newline at end of file diff --git a/tests/samples/expr_closure.ui b/tests/samples/expr_closure.ui new file mode 100644 index 0000000..1581d65 --- /dev/null +++ b/tests/samples/expr_closure.ui @@ -0,0 +1,13 @@ + + + + + + + + my-label + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 1195cdf..6d809c5 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -151,6 +151,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("combo_box_text") self.assert_sample("comments") self.assert_sample("enum") + self.assert_sample("expr_closure", skip_run=True) # The closure doesn't exist self.assert_sample("expr_lookup") self.assert_sample("file_filter") self.assert_sample("flags") @@ -208,6 +209,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("empty") self.assert_sample_error("enum_member_dne") self.assert_sample_error("expr_cast_conversion") + self.assert_sample_error("expr_closure_not_cast") self.assert_sample_error("expr_lookup_dne") self.assert_sample_error("expr_lookup_no_properties") self.assert_sample_error("filters_in_non_file_filter") From 40f493b378cf73d1cc3f128895e842e8665a56c3 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Thu, 5 Jan 2023 00:51:02 +0100 Subject: [PATCH 064/290] cli: Print compile errors to stderr --- blueprintcompiler/errors.py | 6 +++--- blueprintcompiler/main.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 7ddaef0..816bd4a 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -28,7 +28,7 @@ class PrintableError(Exception): """Parent class for errors that can be pretty-printed for the user, e.g. compilation warnings and errors.""" - def pretty_print(self, filename, code): + def pretty_print(self, filename, code, stream=sys.stdout): raise NotImplementedError() @@ -144,9 +144,9 @@ class MultipleErrors(PrintableError): super().__init__() self.errors = errors - def pretty_print(self, filename, code) -> None: + def pretty_print(self, filename, code, stream=sys.stdout) -> None: for error in self.errors: - error.pretty_print(filename, code) + error.pretty_print(filename, code, stream) if len(self.errors) != 1: print(f"{len(self.errors)} errors") diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 4e4d378..a3a70a2 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -104,7 +104,7 @@ class BlueprintApp: with open(opts.output, "w") as file: file.write(xml) except PrintableError as e: - e.pretty_print(opts.input.name, data) + e.pretty_print(opts.input.name, data, stream=sys.stderr) sys.exit(1) def cmd_batch_compile(self, opts): From 7ef314ff949f85c88cd941d0dca5ab0c88ff8dfe Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 25 Dec 2022 16:02:55 -0600 Subject: [PATCH 065/290] Fix diagnostic location reporting Text positions at the beginning of a line were being shown on the previous line. --- blueprintcompiler/utils.py | 5 ++--- tests/sample_errors/no_import_version.err | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index 1c69fd9..5e939c7 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -74,9 +74,8 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]: if idx == 0 or len(text) == 0: return (0, 0) - sp = text[:idx].splitlines(keepends=True) - line_num = len(sp) - col_num = len(sp[-1]) + line_num = text.count("\n", 0, idx) + 1 + col_num = idx - text.rfind("\n", 0, idx) - 1 return (line_num - 1, col_num) diff --git a/tests/sample_errors/no_import_version.err b/tests/sample_errors/no_import_version.err index 4ee792f..db830e0 100644 --- a/tests/sample_errors/no_import_version.err +++ b/tests/sample_errors/no_import_version.err @@ -1 +1 @@ -1,10,0,Expected a version number for GTK +1,11,0,Expected a version number for GTK From be284de879d9ec6ee8a265220d8890351eaddffe Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 25 Dec 2022 16:08:37 -0600 Subject: [PATCH 066/290] parse_tree: Fix Warning node --- blueprintcompiler/parse_tree.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 6634994..ef8586b 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -287,7 +287,9 @@ class Warning(ParseNode): ctx.warnings.append( CompileWarning(self.message, start_token.start, end_token.end) ) - return True + return True + else: + return False class Fail(ParseNode): From 0b402db4d5567a9c3319dab8880cdccfe18840cf Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 25 Dec 2022 16:11:17 -0600 Subject: [PATCH 067/290] language: Change extern type syntax Use a '$' instead of a '.' to indicate a type provided in application code. The reason for the change is to have a consistent "extern" symbol that isn't widely used elsewhere and isn't ambiguous in expressions. --- blueprintcompiler/errors.py | 5 +++++ blueprintcompiler/language/common.py | 8 +++++++- blueprintcompiler/language/gobject_object.py | 2 +- blueprintcompiler/language/types.py | 20 ++++++++++++++------ blueprintcompiler/utils.py | 1 + tests/sample_errors/warn_old_extern.blp | 5 +++++ tests/sample_errors/warn_old_extern.err | 2 ++ tests/samples/typeof.blp | 2 +- tests/samples/unchecked_class.blp | 4 ++-- tests/samples/unchecked_class_dec.blp | 4 ++-- tests/test_samples.py | 1 + 11 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 tests/sample_errors/warn_old_extern.blp create mode 100644 tests/sample_errors/warn_old_extern.err diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 816bd4a..01f9066 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -124,6 +124,11 @@ class CompileWarning(CompileError): color = Colors.YELLOW +class UpgradeWarning(CompileWarning): + category = "upgrade" + color = Colors.PURPLE + + class UnexpectedTokenError(CompileError): def __init__(self, start, end): super().__init__("Unexpected tokens", start, end) diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index ec15c7a..8ae8d96 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -20,7 +20,13 @@ from .. import gir from ..ast_utils import AstNode, validate, docs -from ..errors import CompileError, MultipleErrors +from ..errors import ( + CompileError, + MultipleErrors, + UpgradeWarning, + CompileWarning, + CodeAction, +) from ..completions_utils import * from .. import decompiler as decompile from ..decompiler import DecompileCtx, decompiler diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index edf8b2a..a66ffe6 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -87,7 +87,7 @@ def validate_parent_type(node, ns: str, name: str, err_msg: str): def decompile_object(ctx, gir, klass, id=None): gir_class = ctx.type_by_cname(klass) klass_name = ( - decompile.full_name(gir_class) if gir_class is not None else "." + klass + decompile.full_name(gir_class) if gir_class is not None else "$" + klass ) if id is None: ctx.print(f"{klass_name} {{") diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 3b454bc..e1e715d 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -31,33 +31,41 @@ class TypeName(AstNode): UseIdent("class_name"), ], [ - ".", + AnyOf("$", [".", UseLiteral("old_extern", True)]), UseIdent("class_name"), - UseLiteral("ignore_gir", True), + UseLiteral("extern", True), ], UseIdent("class_name"), ) + @validate() + def old_extern(self): + if self.tokens["old_extern"]: + raise UpgradeWarning( + "Use the '$' extern syntax introduced in blueprint 0.8.0", + actions=[CodeAction("Use '$' syntax", "$" + self.tokens["class_name"])], + ) + @validate("class_name") def type_exists(self): - if not self.tokens["ignore_gir"] and self.gir_ns is not None: + if not self.tokens["extern"] and self.gir_ns is not None: self.root.gir.validate_type( self.tokens["class_name"], self.tokens["namespace"] ) @validate("namespace") def gir_ns_exists(self): - if not self.tokens["ignore_gir"]: + if not self.tokens["extern"]: self.root.gir.validate_ns(self.tokens["namespace"]) @property def gir_ns(self): - if not self.tokens["ignore_gir"]: + if not self.tokens["extern"]: return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") @property def gir_type(self) -> gir.GirType: - if self.tokens["class_name"] and not self.tokens["ignore_gir"]: + if self.tokens["class_name"] and not self.tokens["extern"]: return self.root.gir.get_type( self.tokens["class_name"], self.tokens["namespace"] ) diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index 5e939c7..4c4b44a 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -24,6 +24,7 @@ class Colors: RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[33m" + PURPLE = "\033[35m" FAINT = "\033[2m" BOLD = "\033[1m" BLUE = "\033[34m" diff --git a/tests/sample_errors/warn_old_extern.blp b/tests/sample_errors/warn_old_extern.blp new file mode 100644 index 0000000..e50fb0f --- /dev/null +++ b/tests/sample_errors/warn_old_extern.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +.MyClass { + prop: typeof(.MyOtherClass); +} \ No newline at end of file diff --git a/tests/sample_errors/warn_old_extern.err b/tests/sample_errors/warn_old_extern.err new file mode 100644 index 0000000..8209d23 --- /dev/null +++ b/tests/sample_errors/warn_old_extern.err @@ -0,0 +1,2 @@ +3,1,8,Use the '$' extern syntax introduced in blueprint 0.8.0 +4,16,13,Use the '$' extern syntax introduced in blueprint 0.8.0 \ No newline at end of file diff --git a/tests/samples/typeof.blp b/tests/samples/typeof.blp index 5c5e2e5..cdc1b46 100644 --- a/tests/samples/typeof.blp +++ b/tests/samples/typeof.blp @@ -7,5 +7,5 @@ Gio.ListStore { } Gio.ListStore { - item-type: typeof(.MyObject); + item-type: typeof($MyObject); } \ No newline at end of file diff --git a/tests/samples/unchecked_class.blp b/tests/samples/unchecked_class.blp index 3be0b04..3003842 100644 --- a/tests/samples/unchecked_class.blp +++ b/tests/samples/unchecked_class.blp @@ -1,7 +1,7 @@ using Gtk 4.0; -.MyComponent component { - .MyComponent2 { +$MyComponent component { + $MyComponent2 { flags-value: a | b; } } diff --git a/tests/samples/unchecked_class_dec.blp b/tests/samples/unchecked_class_dec.blp index 81d342e..dbbfe14 100644 --- a/tests/samples/unchecked_class_dec.blp +++ b/tests/samples/unchecked_class_dec.blp @@ -1,7 +1,7 @@ using Gtk 4.0; -.MyComponent component { - .MyComponent2 { +$MyComponent component { + $MyComponent2 { flags-value: "a|b"; } } diff --git a/tests/test_samples.py b/tests/test_samples.py index 6d809c5..6f8a4eb 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -236,6 +236,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("two_templates") self.assert_sample_error("uint") self.assert_sample_error("using_invalid_namespace") + self.assert_sample_error("warn_old_extern") self.assert_sample_error("widgets_in_non_size_group") def test_decompiler(self): From 122b049ce99e69b234246b098e87a5c2119d295d Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 25 Dec 2022 16:22:33 -0600 Subject: [PATCH 068/290] language: Use new extern syntax in signal handlers --- blueprintcompiler/language/gobject_signal.py | 13 +++++++++++-- tests/sample_errors/signal_dne.blp | 2 +- tests/sample_errors/signal_object_dne.blp | 2 +- tests/sample_errors/signal_object_dne.err | 2 +- tests/sample_errors/warn_old_extern.blp | 1 + tests/sample_errors/warn_old_extern.err | 3 ++- tests/samples/signal.blp | 6 +++--- tests/samples/template.blp | 2 +- 8 files changed, 21 insertions(+), 10 deletions(-) diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 11a69e0..74d7472 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -33,6 +33,7 @@ class Signal(AstNode): ] ), "=>", + Optional(["$", UseLiteral("extern", True)]), UseIdent("handler").expected("the name of a function to handle the signal"), Match("(").expected("argument list"), Optional(UseIdent("object")).expected("object identifier"), @@ -78,6 +79,14 @@ class Signal(AstNode): def gir_class(self): return self.parent.parent.gir_class + @validate("handler") + def old_extern(self): + if not self.tokens["extern"]: + raise UpgradeWarning( + "Use the '$' extern syntax introduced in blueprint 0.8.0", + actions=[CodeAction("Use '$' syntax", "$" + self.tokens["handler"])], + ) + @validate("name") def signal_exists(self): if self.gir_class is None or isinstance(self.gir_class, UncheckedType): @@ -116,7 +125,7 @@ def decompile_signal(ctx, gir, name, handler, swapped="false", object=None): object_name = object or "" name = name.replace("_", "-") if decompile.truthy(swapped): - ctx.print(f"{name} => {handler}({object_name}) swapped;") + ctx.print(f"{name} => ${handler}({object_name}) swapped;") else: - ctx.print(f"{name} => {handler}({object_name});") + ctx.print(f"{name} => ${handler}({object_name});") return gir diff --git a/tests/sample_errors/signal_dne.blp b/tests/sample_errors/signal_dne.blp index 9c5d046..0f90432 100644 --- a/tests/sample_errors/signal_dne.blp +++ b/tests/sample_errors/signal_dne.blp @@ -1,5 +1,5 @@ using Gtk 4.0; Button { - eaten-by-velociraptors => on_eaten_by_velociraptors(); + eaten-by-velociraptors => $on_eaten_by_velociraptors(); } diff --git a/tests/sample_errors/signal_object_dne.blp b/tests/sample_errors/signal_object_dne.blp index 8c9610c..5492117 100644 --- a/tests/sample_errors/signal_object_dne.blp +++ b/tests/sample_errors/signal_object_dne.blp @@ -1,5 +1,5 @@ using Gtk 4.0; Button { - clicked => function(dinosaur); + clicked => $function(dinosaur); } \ No newline at end of file diff --git a/tests/sample_errors/signal_object_dne.err b/tests/sample_errors/signal_object_dne.err index dfffc0f..b76b98a 100644 --- a/tests/sample_errors/signal_object_dne.err +++ b/tests/sample_errors/signal_object_dne.err @@ -1 +1 @@ -4,25,8,Could not find object with ID 'dinosaur' \ No newline at end of file +4,26,8,Could not find object with ID 'dinosaur' \ No newline at end of file diff --git a/tests/sample_errors/warn_old_extern.blp b/tests/sample_errors/warn_old_extern.blp index e50fb0f..c7ad01d 100644 --- a/tests/sample_errors/warn_old_extern.blp +++ b/tests/sample_errors/warn_old_extern.blp @@ -2,4 +2,5 @@ using Gtk 4.0; .MyClass { prop: typeof(.MyOtherClass); + clicked => handler(); } \ No newline at end of file diff --git a/tests/sample_errors/warn_old_extern.err b/tests/sample_errors/warn_old_extern.err index 8209d23..c3b3fe2 100644 --- a/tests/sample_errors/warn_old_extern.err +++ b/tests/sample_errors/warn_old_extern.err @@ -1,2 +1,3 @@ 3,1,8,Use the '$' extern syntax introduced in blueprint 0.8.0 -4,16,13,Use the '$' extern syntax introduced in blueprint 0.8.0 \ No newline at end of file +4,16,13,Use the '$' extern syntax introduced in blueprint 0.8.0 +5,14,7,Use the '$' extern syntax introduced in blueprint 0.8.0 \ No newline at end of file diff --git a/tests/samples/signal.blp b/tests/samples/signal.blp index 1965e74..5f3aa7f 100644 --- a/tests/samples/signal.blp +++ b/tests/samples/signal.blp @@ -1,10 +1,10 @@ using Gtk 4.0; Entry { - activate => click(button); + activate => $click(button); } Button button { - clicked => on_button_clicked() swapped; - notify::visible => on_button_notify_visible(); + clicked => $on_button_clicked() swapped; + notify::visible => $on_button_notify_visible(); } diff --git a/tests/samples/template.blp b/tests/samples/template.blp index 7773e25..a0ed5cc 100644 --- a/tests/samples/template.blp +++ b/tests/samples/template.blp @@ -2,7 +2,7 @@ using Gtk 4.0; template TestTemplate : ApplicationWindow { test-property: "Hello, world"; - test-signal => on_test_signal(); + test-signal => $on_test_signal(); } Dialog { From b6ee649458a39323dce40360b2df691a29a751cd Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 25 Dec 2022 17:10:21 -0600 Subject: [PATCH 069/290] Simplify error & warning handling --- blueprintcompiler/ast_utils.py | 14 ++++++++++++- blueprintcompiler/interactive_port.py | 6 +++--- blueprintcompiler/lsp.py | 1 - blueprintcompiler/main.py | 6 +++--- blueprintcompiler/parser.py | 21 ++++++++++++------- tests/fuzz.py | 2 +- .../sample_errors/menu_toplevel_attribute.blp | 2 +- tests/test_samples.py | 7 +++---- 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index e4f2efa..16298ae 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -79,7 +79,19 @@ class AstNode: @cached_property def errors(self): - return list(self._get_errors()) + return list( + error + for error in self._get_errors() + if not isinstance(error, CompileWarning) + ) + + @cached_property + def warnings(self): + return list( + warning + for warning in self._get_errors() + if isinstance(warning, CompileWarning) + ) def _get_errors(self): for validator in self.validators: diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index dd00317..ddb5e28 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -24,7 +24,7 @@ import os from . import decompiler, tokenizer, parser from .outputs.xml import XmlOutput -from .errors import MultipleErrors, PrintableError +from .errors import MultipleErrors, PrintableError, CompilerBugError from .utils import Colors @@ -57,8 +57,8 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: if errors: raise errors - if len(ast.errors): - raise MultipleErrors(ast.errors) + if not ast: + raise CompilerBugError() output = XmlOutput() output.emit(ast) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index f579ab4..890eff0 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -75,7 +75,6 @@ class OpenFile: self.diagnostics += warnings if errors is not None: self.diagnostics += errors.errors - self.diagnostics += self.ast.errors except MultipleErrors as e: self.diagnostics += e.errors except CompileError as e: diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index a3a70a2..345f430 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -21,7 +21,7 @@ import typing as T import argparse, json, os, sys -from .errors import PrintableError, report_bug, MultipleErrors +from .errors import PrintableError, report_bug, MultipleErrors, CompilerBugError from .lsp import LanguageServer from . import parser, tokenizer, decompiler, interactive_port from .utils import Colors @@ -149,8 +149,8 @@ class BlueprintApp: if errors: raise errors - if len(ast.errors): - raise MultipleErrors(ast.errors) + if ast is None: + raise CompilerBugError() formatter = XmlOutput() diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 12c893a..a44f709 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -24,14 +24,21 @@ from .tokenizer import TokenType from .language import OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI -def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]: +def parse( + tokens: T.List[Token], +) -> T.Tuple[T.Optional[UI], T.Optional[MultipleErrors], T.List[PrintableError]]: """Parses a list of tokens into an abstract syntax tree.""" - ctx = ParseContext(tokens) - AnyOf(UI).parse(ctx) + try: + ctx = ParseContext(tokens) + AnyOf(UI).parse(ctx) + ast_node = ctx.last_group.to_ast() if ctx.last_group else None - ast_node = ctx.last_group.to_ast() if ctx.last_group else None - errors = MultipleErrors(ctx.errors) if len(ctx.errors) else None - warnings = ctx.warnings + errors = [*ctx.errors, *ast_node.errors] + warnings = [*ctx.warnings, *ast_node.warnings] - return (ast_node, errors, warnings) + return (ast_node, MultipleErrors(errors) if len(errors) else None, warnings) + except MultipleErrors as e: + return (None, e, []) + except CompileError as e: + return (None, MultipleErrors([e]), []) diff --git a/tests/fuzz.py b/tests/fuzz.py index 0f6a1a7..ad1c764 100644 --- a/tests/fuzz.py +++ b/tests/fuzz.py @@ -26,7 +26,7 @@ def fuzz(buf): ast, errors, warnings = parser.parse(tokens) xml = XmlOutput() - if errors is None and len(ast.errors) == 0: + if errors is None and ast is not None: xml.emit(ast) except CompilerBugError as e: raise e diff --git a/tests/sample_errors/menu_toplevel_attribute.blp b/tests/sample_errors/menu_toplevel_attribute.blp index 21ceeff..e9923c2 100644 --- a/tests/sample_errors/menu_toplevel_attribute.blp +++ b/tests/sample_errors/menu_toplevel_attribute.blp @@ -1,5 +1,5 @@ using Gtk 4.0; -menu { +menu menu { not-allowed: true; } \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 6f8a4eb..e9e7697 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -57,8 +57,6 @@ class TestSamples(unittest.TestCase): if errors: raise errors - if len(ast.errors): - raise MultipleErrors(ast.errors) if len(warnings): raise MultipleErrors(warnings) @@ -94,8 +92,9 @@ class TestSamples(unittest.TestCase): tokens = tokenizer.tokenize(blueprint) ast, errors, warnings = parser.parse(tokens) - self.assert_docs_dont_crash(blueprint, ast) - self.assert_completions_dont_crash(blueprint, ast, tokens) + if ast is not None: + self.assert_docs_dont_crash(blueprint, ast) + self.assert_completions_dont_crash(blueprint, ast, tokens) if errors: raise errors From 0b7dbaf90d17073dd13249393b65dd130c9e5c58 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 25 Dec 2022 18:32:23 -0600 Subject: [PATCH 070/290] Add some type hints --- blueprintcompiler/decompiler.py | 42 ++++--- blueprintcompiler/errors.py | 30 ++--- blueprintcompiler/gir.py | 166 +++++++++++++------------- blueprintcompiler/interactive_port.py | 2 +- blueprintcompiler/lsp.py | 11 +- blueprintcompiler/main.py | 2 +- blueprintcompiler/parse_tree.py | 71 +++++------ blueprintcompiler/tokenizer.py | 10 +- blueprintcompiler/typelib.py | 26 ++-- blueprintcompiler/xml_reader.py | 6 +- 10 files changed, 193 insertions(+), 173 deletions(-) diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index 565d420..c068c93 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -51,17 +51,17 @@ class LineType(Enum): class DecompileCtx: - def __init__(self): - self._result = "" + def __init__(self) -> None: + self._result: str = "" self.gir = GirContext() - self._indent = 0 - self._blocks_need_end = [] - self._last_line_type = LineType.NONE + self._indent: int = 0 + self._blocks_need_end: T.List[str] = [] + self._last_line_type: LineType = LineType.NONE self.gir.add_namespace(get_namespace("Gtk", "4.0")) @property - def result(self): + def result(self) -> str: imports = "\n".join( [ f"using {ns} {namespace.version};" @@ -70,7 +70,7 @@ class DecompileCtx: ) return imports + "\n" + self._result - def type_by_cname(self, cname): + def type_by_cname(self, cname: str) -> T.Optional[GirType]: if type := self.gir.get_type_by_cname(cname): return type @@ -83,17 +83,19 @@ class DecompileCtx: except: pass - def start_block(self): - self._blocks_need_end.append(None) + return None - def end_block(self): + def start_block(self) -> None: + self._blocks_need_end.append("") + + def end_block(self) -> None: if close := self._blocks_need_end.pop(): self.print(close) - def end_block_with(self, text): + def end_block_with(self, text: str) -> None: self._blocks_need_end[-1] = text - def print(self, line, newline=True): + def print(self, line: str, newline: bool = True) -> None: if line == "}" or line == "]": self._indent -= 1 @@ -124,7 +126,7 @@ class DecompileCtx: self._blocks_need_end[-1] = _CLOSING[line[-1]] self._indent += 1 - def print_attribute(self, name, value, type): + def print_attribute(self, name: str, value: str, type: GirType) -> None: def get_enum_name(value): for member in type.members.values(): if ( @@ -169,13 +171,17 @@ class DecompileCtx: self.print(f'{name}: "{escape_quote(value)}";') -def _decompile_element(ctx: DecompileCtx, gir, xml): +def _decompile_element( + ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element +) -> None: try: decompiler = _DECOMPILERS.get(xml.tag) if decompiler is None: raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>") - args = {canon(name): value for name, value in xml.attrs.items()} + args: T.Dict[str, T.Optional[str]] = { + canon(name): value for name, value in xml.attrs.items() + } if decompiler._cdata: if len(xml.children): args["cdata"] = None @@ -196,7 +202,7 @@ def _decompile_element(ctx: DecompileCtx, gir, xml): raise UnsupportedError(tag=xml.tag) -def decompile(data): +def decompile(data: str) -> str: ctx = DecompileCtx() xml = parse(data) @@ -216,11 +222,11 @@ def truthy(string: str) -> bool: return string.lower() in ["yes", "true", "t", "y", "1"] -def full_name(gir): +def full_name(gir) -> str: return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name -def lookup_by_cname(gir, cname: str): +def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]: if isinstance(gir, GirContext): return gir.get_type_by_cname(cname) else: diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 01f9066..4a18589 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -47,15 +47,15 @@ class CompileError(PrintableError): def __init__( self, - message, - start=None, - end=None, - did_you_mean=None, - hints=None, - actions=None, - fatal=False, - references=None, - ): + message: str, + start: T.Optional[int] = None, + end: T.Optional[int] = None, + did_you_mean: T.Optional[T.Tuple[str, T.List[str]]] = None, + hints: T.Optional[T.List[str]] = None, + actions: T.Optional[T.List["CodeAction"]] = None, + fatal: bool = False, + references: T.Optional[T.List[ErrorReference]] = None, + ) -> None: super().__init__(message) self.message = message @@ -69,11 +69,11 @@ class CompileError(PrintableError): if did_you_mean is not None: self._did_you_mean(*did_you_mean) - def hint(self, hint: str): + def hint(self, hint: str) -> "CompileError": self.hints.append(hint) return self - def _did_you_mean(self, word: str, options: T.List[str]): + def _did_you_mean(self, word: str, options: T.List[str]) -> None: if word.replace("_", "-") in options: self.hint(f"use '-', not '_': `{word.replace('_', '-')}`") return @@ -89,7 +89,9 @@ class CompileError(PrintableError): self.hint("Did you check your spelling?") self.hint("Are your dependencies up to date?") - def pretty_print(self, filename, code, stream=sys.stdout): + def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None: + assert self.start is not None + line_num, col_num = utils.idx_to_pos(self.start + 1, code) line = code.splitlines(True)[line_num] @@ -130,7 +132,7 @@ class UpgradeWarning(CompileWarning): class UnexpectedTokenError(CompileError): - def __init__(self, start, end): + def __init__(self, start, end) -> None: super().__init__("Unexpected tokens", start, end) @@ -145,7 +147,7 @@ class MultipleErrors(PrintableError): a list and re-thrown using the MultipleErrors exception. It will pretty-print all of the errors and a count of how many errors there are.""" - def __init__(self, errors: T.List[CompileError]): + def __init__(self, errors: T.List[CompileError]) -> None: super().__init__() self.errors = errors diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 4ee2b1e..70d8e89 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -33,7 +33,7 @@ _namespace_cache: T.Dict[str, "Namespace"] = {} _xml_cache = {} -def get_namespace(namespace, version) -> "Namespace": +def get_namespace(namespace: str, version: str) -> "Namespace": search_paths = GIRepository.Repository.get_search_path() filename = f"{namespace}-{version}.typelib" @@ -58,10 +58,7 @@ def get_namespace(namespace, version) -> "Namespace": return _namespace_cache[filename] -def get_xml(namespace, version): - from .main import VERSION - from xml.etree import ElementTree - +def get_xml(namespace: str, version: str): search_paths = [] if data_paths := os.environ.get("XDG_DATA_DIRS"): @@ -90,12 +87,17 @@ def get_xml(namespace, version): class GirType: @property - def doc(self): + def doc(self) -> T.Optional[str]: return None def assignable_to(self, other: "GirType") -> bool: raise NotImplementedError() + @property + def name(self) -> str: + """The GIR name of the type, not including the namespace""" + raise NotImplementedError() + @property def full_name(self) -> str: """The GIR name of the type to use in diagnostics""" @@ -108,7 +110,7 @@ class GirType: class UncheckedType(GirType): - def __init__(self, name) -> None: + def __init__(self, name: str) -> None: super().__init__() self._name = name @@ -136,7 +138,7 @@ class BoolType(BasicType): name = "bool" glib_type_name: str = "gboolean" - def assignable_to(self, other) -> bool: + def assignable_to(self, other: GirType) -> bool: return isinstance(other, BoolType) @@ -144,7 +146,7 @@ class IntType(BasicType): name = "int" glib_type_name: str = "gint" - def assignable_to(self, other) -> bool: + def assignable_to(self, other: GirType) -> bool: return ( isinstance(other, IntType) or isinstance(other, UIntType) @@ -156,7 +158,7 @@ class UIntType(BasicType): name = "uint" glib_type_name: str = "guint" - def assignable_to(self, other) -> bool: + def assignable_to(self, other: GirType) -> bool: return ( isinstance(other, IntType) or isinstance(other, UIntType) @@ -168,7 +170,7 @@ class FloatType(BasicType): name = "float" glib_type_name: str = "gfloat" - def assignable_to(self, other) -> bool: + def assignable_to(self, other: GirType) -> bool: return isinstance(other, FloatType) @@ -176,7 +178,7 @@ class StringType(BasicType): name = "string" glib_type_name: str = "gchararray" - def assignable_to(self, other) -> bool: + def assignable_to(self, other: GirType) -> bool: return isinstance(other, StringType) @@ -184,7 +186,7 @@ class TypeType(BasicType): name = "GType" glib_type_name: str = "GType" - def assignable_to(self, other) -> bool: + def assignable_to(self, other: GirType) -> bool: return isinstance(other, TypeType) @@ -208,14 +210,17 @@ _BASIC_TYPES = { } +TNode = T.TypeVar("TNode", bound="GirNode") + + class GirNode: - def __init__(self, container, tl): + def __init__(self, container: T.Optional["GirNode"], tl: typelib.Typelib) -> None: self.container = container self.tl = tl - def get_containing(self, container_type): + def get_containing(self, container_type: T.Type[TNode]) -> TNode: if self.container is None: - return None + raise CompilerBugError() elif isinstance(self.container, container_type): return self.container else: @@ -228,11 +233,11 @@ class GirNode: return el @cached_property - def glib_type_name(self): + def glib_type_name(self) -> str: return self.tl.OBJ_GTYPE_NAME @cached_property - def full_name(self): + def full_name(self) -> str: if self.container is None: return self.name else: @@ -273,20 +278,16 @@ class GirNode: return None @property - def type_name(self): - return self.type.name - - @property - def type(self): + def type(self) -> GirType: raise NotImplementedError() class Property(GirNode): - def __init__(self, klass, tl: typelib.Typelib): + def __init__(self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib): super().__init__(klass, tl) @cached_property - def name(self): + def name(self) -> str: return self.tl.PROP_NAME @cached_property @@ -295,24 +296,26 @@ class Property(GirNode): @cached_property def signature(self): - return f"{self.type_name} {self.container.name}.{self.name}" + return f"{self.full_name} {self.container.name}.{self.name}" @property - def writable(self): + def writable(self) -> bool: return self.tl.PROP_WRITABLE == 1 @property - def construct_only(self): + def construct_only(self) -> bool: return self.tl.PROP_CONSTRUCT_ONLY == 1 class Parameter(GirNode): - def __init__(self, container: GirNode, tl: typelib.Typelib): + def __init__(self, container: GirNode, tl: typelib.Typelib) -> None: super().__init__(container, tl) class Signal(GirNode): - def __init__(self, klass, tl: typelib.Typelib): + def __init__( + self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib + ) -> None: super().__init__(klass, tl) # if parameters := xml.get_elements('parameters'): # self.params = [Parameter(self, child) for child in parameters[0].get_elements('parameter')] @@ -328,11 +331,11 @@ class Signal(GirNode): class Interface(GirNode, GirType): - def __init__(self, ns, tl: typelib.Typelib): + def __init__(self, ns: "Namespace", tl: typelib.Typelib): super().__init__(ns, tl) @cached_property - def properties(self): + def properties(self) -> T.Mapping[str, Property]: n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE offset += (n_prerequisites + n_prerequisites % 2) * 2 @@ -345,7 +348,7 @@ class Interface(GirNode, GirType): return result @cached_property - def signals(self): + def signals(self) -> T.Mapping[str, Signal]: n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE offset += (n_prerequisites + n_prerequisites % 2) * 2 @@ -362,7 +365,7 @@ class Interface(GirNode, GirType): return result @cached_property - def prerequisites(self): + def prerequisites(self) -> T.List["Interface"]: n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES result = [] for i in range(n_prerequisites): @@ -370,7 +373,7 @@ class Interface(GirNode, GirType): result.append(self.get_containing(Repository)._resolve_dir_entry(entry)) return result - def assignable_to(self, other) -> bool: + def assignable_to(self, other: GirType) -> bool: if self == other: return True for pre in self.prerequisites: @@ -380,15 +383,15 @@ class Interface(GirNode, GirType): class Class(GirNode, GirType): - def __init__(self, ns, tl: typelib.Typelib): + def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: super().__init__(ns, tl) @property - def abstract(self): + def abstract(self) -> bool: return self.tl.OBJ_ABSTRACT == 1 @cached_property - def implements(self): + def implements(self) -> T.List[Interface]: n_interfaces = self.tl.OBJ_N_INTERFACES result = [] for i in range(n_interfaces): @@ -397,7 +400,7 @@ class Class(GirNode, GirType): return result @cached_property - def own_properties(self): + def own_properties(self) -> T.Mapping[str, Property]: n_interfaces = self.tl.OBJ_N_INTERFACES offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE offset += (n_interfaces + n_interfaces % 2) * 2 @@ -414,7 +417,7 @@ class Class(GirNode, GirType): return result @cached_property - def own_signals(self): + def own_signals(self) -> T.Mapping[str, Signal]: n_interfaces = self.tl.OBJ_N_INTERFACES offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE offset += (n_interfaces + n_interfaces % 2) * 2 @@ -433,16 +436,18 @@ class Class(GirNode, GirType): return result @cached_property - def parent(self): + def parent(self) -> T.Optional["Class"]: if entry := self.tl.OBJ_PARENT: return self.get_containing(Repository)._resolve_dir_entry(entry) else: return None @cached_property - def signature(self): + def signature(self) -> str: + assert self.container is not None result = f"class {self.container.name}.{self.name}" if self.parent is not None: + assert self.parent.container is not None result += f" : {self.parent.container.name}.{self.parent.name}" if len(self.implements): result += " implements " + ", ".join( @@ -451,14 +456,14 @@ class Class(GirNode, GirType): return result @cached_property - def properties(self): + def properties(self) -> T.Mapping[str, Property]: return {p.name: p for p in self._enum_properties()} @cached_property - def signals(self): + def signals(self) -> T.Mapping[str, Signal]: return {s.name: s for s in self._enum_signals()} - def assignable_to(self, other) -> bool: + def assignable_to(self, other: GirType) -> bool: if self == other: return True elif self.parent and self.parent.assignable_to(other): @@ -470,7 +475,7 @@ class Class(GirNode, GirType): return False - def _enum_properties(self): + def _enum_properties(self) -> T.Iterable[Property]: yield from self.own_properties.values() if self.parent is not None: @@ -479,7 +484,7 @@ class Class(GirNode, GirType): for impl in self.implements: yield from impl.properties.values() - def _enum_signals(self): + def _enum_signals(self) -> T.Iterable[Signal]: yield from self.own_signals.values() if self.parent is not None: @@ -490,8 +495,8 @@ class Class(GirNode, GirType): class EnumMember(GirNode): - def __init__(self, ns, tl: typelib.Typelib): - super().__init__(ns, tl) + def __init__(self, enum: "Enumeration", tl: typelib.Typelib) -> None: + super().__init__(enum, tl) @property def value(self) -> int: @@ -502,20 +507,20 @@ class EnumMember(GirNode): return self.tl.VALUE_NAME @cached_property - def nick(self): + def nick(self) -> str: return self.name.replace("_", "-") @property - def c_ident(self): + def c_ident(self) -> str: return self.tl.attr("c:identifier") @property - def signature(self): + def signature(self) -> str: return f"enum member {self.full_name} = {self.value}" class Enumeration(GirNode, GirType): - def __init__(self, ns, tl: typelib.Typelib): + def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: super().__init__(ns, tl) @cached_property @@ -530,43 +535,43 @@ class Enumeration(GirNode, GirType): return members @property - def signature(self): + def signature(self) -> str: return f"enum {self.full_name}" - def assignable_to(self, type): + def assignable_to(self, type: GirType) -> bool: return type == self class Boxed(GirNode, GirType): - def __init__(self, ns, tl: typelib.Typelib): + def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: super().__init__(ns, tl) @property - def signature(self): + def signature(self) -> str: return f"boxed {self.full_name}" - def assignable_to(self, type): + def assignable_to(self, type) -> bool: return type == self class Bitfield(Enumeration): - def __init__(self, ns, tl: typelib.Typelib): + def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: super().__init__(ns, tl) class Namespace(GirNode): - def __init__(self, repo, tl: typelib.Typelib): + def __init__(self, repo: "Repository", tl: typelib.Typelib) -> None: super().__init__(repo, tl) - self.entries: T.Dict[str, GirNode] = {} + self.entries: T.Dict[str, GirType] = {} - n_local_entries = tl.HEADER_N_ENTRIES - directory = tl.HEADER_DIRECTORY + n_local_entries: int = tl.HEADER_N_ENTRIES + directory: typelib.Typelib = tl.HEADER_DIRECTORY for i in range(n_local_entries): entry = directory[i * tl.HEADER_ENTRY_BLOB_SIZE] - entry_name = entry.DIR_ENTRY_NAME - entry_type = entry.DIR_ENTRY_BLOB_TYPE - entry_blob = entry.DIR_ENTRY_OFFSET + entry_name: str = entry.DIR_ENTRY_NAME + entry_type: int = entry.DIR_ENTRY_BLOB_TYPE + entry_blob: typelib.Typelib = entry.DIR_ENTRY_OFFSET if entry_type == typelib.BLOB_TYPE_ENUM: self.entries[entry_name] = Enumeration(self, entry_blob) @@ -595,11 +600,11 @@ class Namespace(GirNode): return self.tl.HEADER_NSVERSION @property - def signature(self): + def signature(self) -> str: return f"namespace {self.name} {self.version}" @cached_property - def classes(self): + def classes(self) -> T.Mapping[str, Class]: return { name: entry for name, entry in self.entries.items() @@ -607,24 +612,25 @@ class Namespace(GirNode): } @cached_property - def interfaces(self): + def interfaces(self) -> T.Mapping[str, Interface]: return { name: entry for name, entry in self.entries.items() if isinstance(entry, Interface) } - def get_type(self, name): + def get_type(self, name) -> T.Optional[GirType]: """Gets a type (class, interface, enum, etc.) from this namespace.""" return self.entries.get(name) - def get_type_by_cname(self, cname: str): + def get_type_by_cname(self, cname: str) -> T.Optional[GirType]: """Gets a type from this namespace by its C name.""" for item in self.entries.values(): if hasattr(item, "cname") and item.cname == cname: return item + return None - def lookup_type(self, type_name: str): + def lookup_type(self, type_name: str) -> T.Optional[GirType]: """Looks up a type in the scope of this namespace (including in the namespace's dependencies).""" @@ -638,7 +644,7 @@ class Namespace(GirNode): class Repository(GirNode): - def __init__(self, tl: typelib.Typelib): + def __init__(self, tl: typelib.Typelib) -> None: super().__init__(None, tl) self.namespace = Namespace(self, tl) @@ -654,10 +660,10 @@ class Repository(GirNode): else: self.includes = {} - def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: + def get_type(self, name: str, ns: str) -> T.Optional[GirType]: return self.lookup_namespace(ns).get_type(name) - def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: + def get_type_by_cname(self, name: str) -> T.Optional[GirType]: for ns in [self.namespace, *self.includes.values()]: if type := ns.get_type_by_cname(name): return type @@ -679,7 +685,7 @@ class Repository(GirNode): ns = dir_entry.DIR_ENTRY_NAMESPACE return self.lookup_namespace(ns).get_type(dir_entry.DIR_ENTRY_NAME) - def _resolve_type_id(self, type_id: int): + def _resolve_type_id(self, type_id: int) -> GirType: if type_id & 0xFFFFFF == 0: type_id = (type_id >> 27) & 0x1F # simple type @@ -726,13 +732,13 @@ class GirContext: self.namespaces[namespace.name] = namespace - def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: + def get_type_by_cname(self, name: str) -> T.Optional[GirType]: for ns in self.namespaces.values(): if type := ns.get_type_by_cname(name): return type return None - def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: + def get_type(self, name: str, ns: str) -> T.Optional[GirType]: if ns is None and name in _BASIC_TYPES: return _BASIC_TYPES[name]() @@ -750,7 +756,7 @@ class GirContext: else: return None - def validate_ns(self, ns: str): + def validate_ns(self, ns: str) -> None: """Raises an exception if there is a problem looking up the given namespace.""" @@ -762,7 +768,7 @@ class GirContext: did_you_mean=(ns, self.namespaces.keys()), ) - def validate_type(self, name: str, ns: str): + def validate_type(self, name: str, ns: str) -> None: """Raises an exception if there is a problem looking up the given type.""" self.validate_ns(ns) diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index ddb5e28..ffc4292 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -32,7 +32,7 @@ from .utils import Colors class CouldNotPort: - def __init__(self, message): + def __init__(self, message: str): self.message = message diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 890eff0..dd12905 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -31,7 +31,7 @@ def printerr(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) -def command(json_method): +def command(json_method: str): def decorator(func): func._json_method = json_method return func @@ -40,7 +40,7 @@ def command(json_method): class OpenFile: - def __init__(self, uri, text, version): + def __init__(self, uri: str, text: str, version: int): self.uri = uri self.text = text self.version = version @@ -81,6 +81,9 @@ class OpenFile: self.diagnostics.append(e) def calc_semantic_tokens(self) -> T.List[int]: + if self.ast is None: + return [] + tokens = list(self.ast.get_semantic_tokens()) token_lists = [ [ @@ -318,9 +321,11 @@ class LanguageServer: }, ) - def _create_diagnostic(self, text, uri, err): + def _create_diagnostic(self, text: str, uri: str, err: CompileError): message = err.message + assert err.start is not None and err.end is not None + for hint in err.hints: message += "\nhint: " + hint diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 345f430..6127630 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -82,7 +82,7 @@ class BlueprintApp: except: report_bug() - def add_subcommand(self, name, help, func): + def add_subcommand(self, name: str, help: str, func): parser = self.subparsers.add_parser(name, help=help) parser.set_defaults(func=func) return parser diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index ef8586b..670c72e 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -23,6 +23,7 @@ import typing as T from collections import defaultdict from enum import Enum +from .ast_utils import AstNode from .errors import ( assert_true, @@ -64,19 +65,19 @@ class ParseGroup: be converted to AST nodes by passing the children and key=value pairs to the AST node constructor.""" - def __init__(self, ast_type, start: int): + def __init__(self, ast_type: T.Type[AstNode], start: int): self.ast_type = ast_type self.children: T.List[ParseGroup] = [] self.keys: T.Dict[str, T.Any] = {} - self.tokens: T.Dict[str, Token] = {} + self.tokens: T.Dict[str, T.Optional[Token]] = {} self.start = start - self.end = None + self.end: T.Optional[int] = None self.incomplete = False - def add_child(self, child): + def add_child(self, child: "ParseGroup"): self.children.append(child) - def set_val(self, key, val, token): + def set_val(self, key: str, val: T.Any, token: T.Optional[Token]): assert_true(key not in self.keys) self.keys[key] = val @@ -105,22 +106,22 @@ class ParseGroup: class ParseContext: """Contains the state of the parser.""" - def __init__(self, tokens, index=0): + def __init__(self, tokens: T.List[Token], index=0): self.tokens = list(tokens) self.binding_power = 0 self.index = index self.start = index - self.group = None - self.group_keys = {} - self.group_children = [] - self.last_group = None + self.group: T.Optional[ParseGroup] = None + self.group_keys: T.Dict[str, T.Tuple[T.Any, T.Optional[Token]]] = {} + self.group_children: T.List[ParseGroup] = [] + self.last_group: T.Optional[ParseGroup] = None self.group_incomplete = False - self.errors = [] - self.warnings = [] + self.errors: T.List[CompileError] = [] + self.warnings: T.List[CompileWarning] = [] - def create_child(self): + def create_child(self) -> "ParseContext": """Creates a new ParseContext at this context's position. The new context will be used to parse one node. If parsing is successful, the new context will be applied to "self". If parsing fails, the new @@ -131,7 +132,7 @@ class ParseContext: ctx.binding_power = self.binding_power return ctx - def apply_child(self, other): + def apply_child(self, other: "ParseContext"): """Applies a child context to this context.""" if other.group is not None: @@ -159,12 +160,12 @@ class ParseContext: elif other.last_group: self.last_group = other.last_group - def start_group(self, ast_type): + def start_group(self, ast_type: T.Type[AstNode]): """Sets this context to have its own match group.""" assert_true(self.group is None) self.group = ParseGroup(ast_type, self.tokens[self.index].start) - def set_group_val(self, key, value, token): + def set_group_val(self, key: str, value: T.Any, token: T.Optional[Token]): """Sets a matched key=value pair on the current match group.""" assert_true(key not in self.group_keys) self.group_keys[key] = (value, token) @@ -213,7 +214,7 @@ class ParseContext: else: self.errors.append(UnexpectedTokenError(start, end)) - def is_eof(self) -> Token: + def is_eof(self) -> bool: return self.index >= len(self.tokens) or self.peek_token().type == TokenType.EOF @@ -237,17 +238,17 @@ class ParseNode: def _parse(self, ctx: ParseContext) -> bool: raise NotImplementedError() - def err(self, message): + def err(self, message: str) -> "Err": """Causes this ParseNode to raise an exception if it fails to parse. This prevents the parser from backtracking, so you should understand what it does and how the parser works before using it.""" return Err(self, message) - def expected(self, expect): + def expected(self, expect) -> "Err": """Convenience method for err().""" return self.err("Expected " + expect) - def warn(self, message): + def warn(self, message) -> "Warning": """Causes this ParseNode to emit a warning if it parses successfully.""" return Warning(self, message) @@ -255,11 +256,11 @@ class ParseNode: class Err(ParseNode): """ParseNode that emits a compile error if it fails to parse.""" - def __init__(self, child, message): + def __init__(self, child, message: str): self.child = to_parse_node(child) self.message = message - def _parse(self, ctx): + def _parse(self, ctx: ParseContext): if self.child.parse(ctx).failed(): start_idx = ctx.start while ctx.tokens[start_idx].type in SKIP_TOKENS: @@ -274,11 +275,11 @@ class Err(ParseNode): class Warning(ParseNode): """ParseNode that emits a compile warning if it parses successfully.""" - def __init__(self, child, message): + def __init__(self, child, message: str): self.child = to_parse_node(child) self.message = message - def _parse(self, ctx): + def _parse(self, ctx: ParseContext): ctx.skip() start_idx = ctx.index if self.child.parse(ctx).succeeded(): @@ -295,11 +296,11 @@ class Warning(ParseNode): class Fail(ParseNode): """ParseNode that emits a compile error if it parses successfully.""" - def __init__(self, child, message): + def __init__(self, child, message: str): self.child = to_parse_node(child) self.message = message - def _parse(self, ctx): + def _parse(self, ctx: ParseContext): if self.child.parse(ctx).succeeded(): start_idx = ctx.start while ctx.tokens[start_idx].type in SKIP_TOKENS: @@ -314,7 +315,7 @@ class Fail(ParseNode): class Group(ParseNode): """ParseNode that creates a match group.""" - def __init__(self, ast_type, child): + def __init__(self, ast_type: T.Type[AstNode], child): self.ast_type = ast_type self.child = to_parse_node(child) @@ -393,7 +394,7 @@ class Until(ParseNode): self.child = to_parse_node(child) self.delimiter = to_parse_node(delimiter) - def _parse(self, ctx): + def _parse(self, ctx: ParseContext): while not self.delimiter.parse(ctx).succeeded(): if ctx.is_eof(): return False @@ -463,7 +464,7 @@ class Eof(ParseNode): class Match(ParseNode): """ParseNode that matches the given literal token.""" - def __init__(self, op): + def __init__(self, op: str): self.op = op def _parse(self, ctx: ParseContext) -> bool: @@ -482,7 +483,7 @@ class UseIdent(ParseNode): """ParseNode that matches any identifier and sets it in a key=value pair on the containing match group.""" - def __init__(self, key): + def __init__(self, key: str): self.key = key def _parse(self, ctx: ParseContext): @@ -498,7 +499,7 @@ class UseNumber(ParseNode): """ParseNode that matches a number and sets it in a key=value pair on the containing match group.""" - def __init__(self, key): + def __init__(self, key: str): self.key = key def _parse(self, ctx: ParseContext): @@ -517,7 +518,7 @@ class UseNumberText(ParseNode): """ParseNode that matches a number, but sets its *original text* it in a key=value pair on the containing match group.""" - def __init__(self, key): + def __init__(self, key: str): self.key = key def _parse(self, ctx: ParseContext): @@ -533,7 +534,7 @@ class UseQuoted(ParseNode): """ParseNode that matches a quoted string and sets it in a key=value pair on the containing match group.""" - def __init__(self, key): + def __init__(self, key: str): self.key = key def _parse(self, ctx: ParseContext): @@ -557,7 +558,7 @@ class UseLiteral(ParseNode): pair on the containing group. Useful for, e.g., property and signal flags: `Sequence(Keyword("swapped"), UseLiteral("swapped", True))`""" - def __init__(self, key, literal): + def __init__(self, key: str, literal: T.Any): self.key = key self.literal = literal @@ -570,7 +571,7 @@ class Keyword(ParseNode): """Matches the given identifier and sets it as a named token, with the name being the identifier itself.""" - def __init__(self, kw): + def __init__(self, kw: str): self.kw = kw self.set_token = True diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 516bc0b..170316c 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -22,7 +22,7 @@ import typing as T import re from enum import Enum -from .errors import CompileError +from .errors import CompileError, CompilerBugError class TokenType(Enum): @@ -53,18 +53,18 @@ _TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] class Token: - def __init__(self, type, start, end, string): + def __init__(self, type: TokenType, start: int, end: int, string: str): self.type = type self.start = start self.end = end self.string = string - def __str__(self): + def __str__(self) -> str: return self.string[self.start : self.end] - def get_number(self): + def get_number(self) -> T.Union[int, float]: if self.type != TokenType.NUMBER: - return None + raise CompilerBugError() string = str(self).replace("_", "") try: diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index 88e7b57..48ec416 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -58,14 +58,14 @@ TYPE_UNICHAR = 21 class Field: - def __init__(self, offset, type, shift=0, mask=None): + def __init__(self, offset: int, type: str, shift=0, mask=None): self._offset = offset self._type = type self._shift = shift self._mask = (1 << mask) - 1 if mask else None self._name = f"{offset}__{type}__{shift}__{mask}" - def __get__(self, typelib, _objtype=None): + def __get__(self, typelib: "Typelib", _objtype=None): if typelib is None: return self @@ -181,47 +181,47 @@ class Typelib: VALUE_NAME = Field(0x4, "string") VALUE_VALUE = Field(0x8, "i32") - def __init__(self, typelib_file, offset): + def __init__(self, typelib_file, offset: int): self._typelib_file = typelib_file self._offset = offset - def __getitem__(self, index): + def __getitem__(self, index: int): return Typelib(self._typelib_file, self._offset + index) def attr(self, name): return self.header.attr(self._offset, name) @property - def header(self): + def header(self) -> "TypelibHeader": return TypelibHeader(self._typelib_file) @property - def u8(self): + def u8(self) -> int: """Gets the 8-bit unsigned int at this location.""" return self._int(1, False) @property - def u16(self): + def u16(self) -> int: """Gets the 16-bit unsigned int at this location.""" return self._int(2, False) @property - def u32(self): + def u32(self) -> int: """Gets the 32-bit unsigned int at this location.""" return self._int(4, False) @property - def i8(self): + def i8(self) -> int: """Gets the 8-bit unsigned int at this location.""" return self._int(1, True) @property - def i16(self): + def i16(self) -> int: """Gets the 16-bit unsigned int at this location.""" return self._int(2, True) @property - def i32(self): + def i32(self) -> int: """Gets the 32-bit unsigned int at this location.""" return self._int(4, True) @@ -240,7 +240,7 @@ class Typelib: end += 1 return self._typelib_file[loc:end].decode("utf-8") - def _int(self, size, signed): + def _int(self, size, signed) -> int: return int.from_bytes( self._typelib_file[self._offset : self._offset + size], sys.byteorder ) @@ -250,7 +250,7 @@ class TypelibHeader(Typelib): def __init__(self, typelib_file): super().__init__(typelib_file, 0) - def dir_entry(self, index): + def dir_entry(self, index) -> T.Optional[Typelib]: if index == 0: return None else: diff --git a/blueprintcompiler/xml_reader.py b/blueprintcompiler/xml_reader.py index c0552f5..5e31773 100644 --- a/blueprintcompiler/xml_reader.py +++ b/blueprintcompiler/xml_reader.py @@ -46,7 +46,7 @@ PARSE_GIR = set( class Element: - def __init__(self, tag, attrs: T.Dict[str, str]): + def __init__(self, tag: str, attrs: T.Dict[str, str]): self.tag = tag self.attrs = attrs self.children: T.List["Element"] = [] @@ -56,10 +56,10 @@ class Element: def cdata(self): return "".join(self.cdata_chunks) - def get_elements(self, name) -> T.List["Element"]: + def get_elements(self, name: str) -> T.List["Element"]: return [child for child in self.children if child.tag == name] - def __getitem__(self, key): + def __getitem__(self, key: str): return self.attrs.get(key) From 693826795203863eba3973fedf5d23d5505438f8 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 9 Jan 2023 21:55:14 -0600 Subject: [PATCH 071/290] Add properties to AST types I want to have a cleaner API that relies less on the specifics of the grammar and parser. --- blueprintcompiler/language/gtk_menu.py | 4 ++++ blueprintcompiler/language/ui.py | 20 +++++++++++++++++++- blueprintcompiler/language/values.py | 4 ++++ blueprintcompiler/outputs/xml/__init__.py | 16 +++++++--------- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 73aa039..dedf6e3 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -39,6 +39,10 @@ class Menu(AstNode): def tag(self) -> str: return self.tokens["tag"] + @property + def items(self) -> T.List[T.Union["Menu", "MenuAttribute"]]: + return self.children + @validate("menu") def has_id(self): if self.tokens["tag"] == "menu" and self.tokens["id"] is None: diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index c277c56..d45fe4c 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -22,7 +22,7 @@ from .. import gir from .imports import GtkDirective, Import from .gtkbuilder_template import Template from .gobject_object import Object -from .gtk_menu import menu +from .gtk_menu import menu, Menu from .common import * @@ -64,6 +64,24 @@ class UI(AstNode): return gir_ctx + @property + def using(self) -> T.List[Import]: + return self.children[Import] + + @property + def gtk_decl(self) -> GtkDirective: + return self.children[GtkDirective][0] + + @property + def contents(self) -> T.List[T.Union[Object, Template, Menu]]: + return [ + child + for child in self.children + if isinstance(child, Object) + or isinstance(child, Template) + or isinstance(child, Menu) + ] + @property def objects_by_id(self): return { diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 4a71247..8447267 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -203,6 +203,10 @@ class Flag(AstNode): class FlagsValue(Value): grammar = [Flag, "|", Delimited(Flag, "|")] + @property + def flags(self) -> T.List[Flag]: + return self.children + @validate() def parent_is_bitfield(self): type = self.parent.value_type diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index ac2aea8..48a18ac 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -14,12 +14,10 @@ class XmlOutput(OutputFormat): def _emit_ui(self, ui: UI, xml: XmlEmitter): xml.start_tag("interface") - for x in ui.children: - if isinstance(x, GtkDirective): - self._emit_gtk_directive(x, xml) - elif isinstance(x, Import): - pass - elif isinstance(x, Template): + self._emit_gtk_directive(ui.gtk_decl, xml) + + for x in ui.contents: + if isinstance(x, Template): self._emit_template(x, xml) elif isinstance(x, Object): self._emit_object(x, xml) @@ -74,7 +72,7 @@ class XmlOutput(OutputFormat): def _emit_menu(self, menu: Menu, xml: XmlEmitter): xml.start_tag(menu.tag, id=menu.id) - for child in menu.children: + for child in menu.items: if isinstance(child, Menu): self._emit_menu(child, xml) elif isinstance(child, MenuAttribute): @@ -167,7 +165,7 @@ class XmlOutput(OutputFormat): xml.put_text(value.value) elif isinstance(value, FlagsValue): xml.put_text( - "|".join([str(flag.value or flag.name) for flag in value.children]) + "|".join([str(flag.value or flag.name) for flag in value.flags]) ) elif isinstance(value, TranslatedStringValue): raise CompilerBugError("translated values must be handled in the parent") @@ -177,7 +175,7 @@ class XmlOutput(OutputFormat): raise CompilerBugError() def _emit_expression(self, expression: ExprChain, xml: XmlEmitter): - self._emit_expression_part(expression.children[-1], xml) + self._emit_expression_part(expression.last, xml) def _emit_expression_part(self, expression: Expr, xml: XmlEmitter): if isinstance(expression, IdentExpr): From 1df46b5a06f4310d2c4ad0b3734d34b5d4bde251 Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 12 Jan 2023 13:19:15 -0600 Subject: [PATCH 072/290] Change the way values work Change the parsing for values to make them more reusable, in particular for when I implement extensions. --- .gitlab-ci.yml | 2 +- blueprintcompiler/ast_utils.py | 59 +++- blueprintcompiler/decompiler.py | 2 +- blueprintcompiler/language/__init__.py | 28 +- blueprintcompiler/language/attributes.py | 2 +- blueprintcompiler/language/binding.py | 55 ++++ blueprintcompiler/language/common.py | 3 +- blueprintcompiler/language/contexts.py | 28 ++ blueprintcompiler/language/expression.py | 33 ++- .../language/gobject_property.py | 105 +++----- blueprintcompiler/language/gobject_signal.py | 9 +- blueprintcompiler/language/gtk_a11y.py | 15 +- .../language/gtk_combo_box_text.py | 19 +- blueprintcompiler/language/gtk_layout.py | 22 +- blueprintcompiler/language/gtk_menu.py | 21 +- blueprintcompiler/language/gtk_string_list.py | 11 +- blueprintcompiler/language/imports.py | 3 + .../language/property_binding.py | 139 ++++++++++ blueprintcompiler/language/types.py | 2 +- blueprintcompiler/language/values.py | 251 +++++++++++------- blueprintcompiler/outputs/xml/__init__.py | 143 +++++----- blueprintcompiler/parse_tree.py | 13 + blueprintcompiler/parser.py | 2 +- tests/sample_errors/obj_prop_type.err | 2 +- .../warn_old_bind.blp} | 0 tests/sample_errors/warn_old_bind.err | 2 + tests/samples/property_binding.blp | 11 + .../{binding.ui => property_binding.ui} | 0 tests/samples/property_binding_dec.blp | 11 + tests/test_samples.py | 5 +- 30 files changed, 707 insertions(+), 291 deletions(-) create mode 100644 blueprintcompiler/language/binding.py create mode 100644 blueprintcompiler/language/contexts.py create mode 100644 blueprintcompiler/language/property_binding.py rename tests/{samples/binding.blp => sample_errors/warn_old_bind.blp} (100%) create mode 100644 tests/sample_errors/warn_old_bind.err create mode 100644 tests/samples/property_binding.blp rename tests/samples/{binding.ui => property_binding.ui} (100%) create mode 100644 tests/samples/property_binding_dec.blp diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b924a7d..0295b09 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 58fda9381dac4a9c42c18a4b06149ed59ee702dc + - git checkout 59eecfbd73020889410da6cc9f5ce90e5b6f9e24 - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 16298ae..7bd5418 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -24,6 +24,8 @@ import typing as T from .errors import * from .lsp_utils import SemanticToken +TType = T.TypeVar("TType") + class Children: """Allows accessing children by type using array syntax.""" @@ -34,6 +36,14 @@ class Children: def __iter__(self): return iter(self._children) + @T.overload + def __getitem__(self, key: T.Type[TType]) -> T.List[TType]: + ... + + @T.overload + def __getitem__(self, key: int) -> "AstNode": + ... + def __getitem__(self, key): if isinstance(key, int): return self._children[key] @@ -41,6 +51,27 @@ class Children: return [child for child in self._children if isinstance(child, key)] +TCtx = T.TypeVar("TCtx") +TAttr = T.TypeVar("TAttr") + + +class Ctx: + """Allows accessing values from higher in the syntax tree.""" + + def __init__(self, node: "AstNode") -> None: + self.node = node + + def __getitem__(self, key: T.Type[TCtx]) -> T.Optional[TCtx]: + attrs = self.node._attrs_by_type(Context) + for name, attr in attrs: + if attr.type == key: + return getattr(self.node, name) + if self.node.parent is not None: + return self.node.parent.context[key] + else: + return None + + class AstNode: """Base class for nodes in the abstract syntax tree.""" @@ -62,6 +93,10 @@ class AstNode: getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator") ] + @cached_property + def context(self): + return Ctx(self) + @property def root(self): if self.parent is None: @@ -105,7 +140,9 @@ class AstNode: for child in self.children: yield from child._get_errors() - def _attrs_by_type(self, attr_type): + def _attrs_by_type( + self, attr_type: T.Type[TAttr] + ) -> T.Iterator[T.Tuple[str, TAttr]]: for name in dir(type(self)): item = getattr(type(self), name) if isinstance(item, attr_type): @@ -217,3 +254,23 @@ def docs(*args, **kwargs): return Docs(func, *args, **kwargs) return decorator + + +class Context: + def __init__(self, type: T.Type[TCtx], func: T.Callable[[AstNode], TCtx]) -> None: + self.type = type + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self + return self.func(instance) + + +def context(type: T.Type[TCtx]): + """Decorator for functions that return a context object, which is passed down to .""" + + def decorator(func: T.Callable[[AstNode], TCtx]) -> Context: + return Context(type, func) + + return decorator diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index c068c93..036c86f 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -295,7 +295,7 @@ def decompile_property( flags += " inverted" if "bidirectional" in bind_flags: flags += " bidirectional" - ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") + ctx.print(f"{name}: bind-property {bind_source}.{bind_property}{flags};") elif truthy(translatable): if context is not None: ctx.print( diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 4063943..822a91a 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,4 +1,6 @@ from .attributes import BaseAttribute, BaseTypedAttribute +from .binding import Binding +from .contexts import ValueTypeCtx from .expression import CastExpr, ClosureExpr, Expr, ExprChain, IdentExpr, LookupOp from .gobject_object import Object, ObjectContent from .gobject_property import Property @@ -14,16 +16,21 @@ from .gtk_styles import Styles from .gtkbuilder_child import Child from .gtkbuilder_template import Template from .imports import GtkDirective, Import +from .property_binding import PropertyBinding from .ui import UI from .types import ClassName from .values import ( - TypeValue, - IdentValue, - TranslatedStringValue, - FlagsValue, Flag, - QuotedValue, - NumberValue, + Flags, + IdentLiteral, + Literal, + NumberLiteral, + ObjectValue, + QuotedLiteral, + Translated, + TranslatedWithContext, + TranslatedWithoutContext, + TypeLiteral, Value, ) @@ -43,12 +50,3 @@ OBJECT_CONTENT_HOOKS.children = [ Strings, Child, ] - -VALUE_HOOKS.children = [ - TypeValue, - TranslatedStringValue, - FlagsValue, - IdentValue, - QuotedValue, - NumberValue, -] diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py index 77f01f2..c713917 100644 --- a/blueprintcompiler/language/attributes.py +++ b/blueprintcompiler/language/attributes.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .values import Value, TranslatedStringValue +from .values import Value, Translated from .common import * diff --git a/blueprintcompiler/language/binding.py b/blueprintcompiler/language/binding.py new file mode 100644 index 0000000..91d5a6b --- /dev/null +++ b/blueprintcompiler/language/binding.py @@ -0,0 +1,55 @@ +# 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 dataclasses import dataclass + +from .common import * +from .expression import ExprChain, LookupOp, IdentExpr +from .contexts import ValueTypeCtx + + +class Binding(AstNode): + grammar = [ + Keyword("bind"), + ExprChain, + ] + + @property + def expression(self) -> ExprChain: + return self.children[ExprChain][0] + + @property + def simple_binding(self) -> T.Optional["SimpleBinding"]: + if isinstance(self.expression.last, LookupOp): + if isinstance(self.expression.last.lhs, IdentExpr): + return SimpleBinding( + self.expression.last.lhs.ident, self.expression.last.property_name + ) + return None + + @validate("bind") + def not_bindable(self) -> None: + if binding_error := self.context[ValueTypeCtx].binding_error: + raise binding_error + + +@dataclass +class SimpleBinding: + source: str + property_name: str diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 8ae8d96..636f15d 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -19,7 +19,7 @@ from .. import gir -from ..ast_utils import AstNode, validate, docs +from ..ast_utils import AstNode, validate, docs, context from ..errors import ( CompileError, MultipleErrors, @@ -44,4 +44,3 @@ from ..parse_tree import * OBJECT_CONTENT_HOOKS = AnyOf() -VALUE_HOOKS = AnyOf() diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py new file mode 100644 index 0000000..f5b92c2 --- /dev/null +++ b/blueprintcompiler/language/contexts.py @@ -0,0 +1,28 @@ +# contexts.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 + +import typing as T +from dataclasses import dataclass +from .common import * + + +@dataclass +class ValueTypeCtx: + value_type: T.Optional[GirType] + binding_error: T.Optional[CompileError] = None diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index f2b2ea1..e75b961 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -20,6 +20,7 @@ from .common import * from .types import TypeName +from .gtkbuilder_template import Template expr = Pratt() @@ -30,6 +31,10 @@ class Expr(AstNode): def type(self) -> T.Optional[GirType]: raise NotImplementedError() + @property + def type_complete(self) -> bool: + return True + @property def rhs(self) -> T.Optional["Expr"]: if isinstance(self.parent, ExprChain): @@ -53,6 +58,10 @@ class ExprChain(Expr): def type(self) -> T.Optional[GirType]: return self.last.type + @property + def type_complete(self) -> bool: + return self.last.type_complete + class InfixExpr(Expr): @property @@ -83,6 +92,13 @@ class IdentExpr(Expr): else: return None + @property + def type_complete(self) -> bool: + if object := self.root.objects_by_id.get(self.ident): + return not isinstance(object, Template) + else: + return True + class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] @@ -103,17 +119,24 @@ class LookupOp(InfixExpr): @validate("property") def property_exists(self): - if self.lhs.type is None or isinstance(self.lhs.type, UncheckedType): + if ( + self.lhs.type is None + or not self.lhs.type_complete + 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}" + f"{self.lhs.type.full_name} does not have a property called {self.property_name}", + did_you_mean=(self.property_name, self.lhs.type.properties.keys()), ) @@ -124,9 +147,13 @@ class CastExpr(InfixExpr): def type(self) -> T.Optional[GirType]: return self.children[TypeName][0].gir_type + @property + def type_complete(self) -> bool: + return True + @validate() def cast_makes_sense(self): - if self.lhs.type is None: + if self.type is None or self.lhs.type is None: return if not self.type.assignable_to(self.lhs.type): diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 10770a8..13374f8 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -17,51 +17,28 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +from dataclasses import dataclass from .expression import ExprChain from .gobject_object import Object from .gtkbuilder_template import Template -from .values import Value, TranslatedStringValue +from .values import Value, Translated from .common import * +from .contexts import ValueTypeCtx +from .property_binding import PropertyBinding +from .binding import Binding class Property(AstNode): - grammar = AnyOf( - [ - UseIdent("name"), - ":", - Keyword("bind"), - UseIdent("bind_source"), - ".", - UseIdent("bind_property"), - ZeroOrMore( - AnyOf( - ["no-sync-create", UseLiteral("no_sync_create", True)], - ["inverted", UseLiteral("inverted", True)], - ["bidirectional", UseLiteral("bidirectional", True)], - Match("sync-create").warn( - "sync-create is deprecated in favor of no-sync-create" - ), - ) - ), - ";", - ], - Statement( - UseIdent("name"), - UseLiteral("binding", True), - ":", - "bind", - ExprChain, - ), - Statement( - UseIdent("name"), - ":", - AnyOf( - Object, - VALUE_HOOKS, - ).expected("a value"), - ), - ) + grammar = [UseIdent("name"), ":", Value, ";"] + + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def value(self) -> Value: + return self.children[0] @property def gir_class(self): @@ -72,10 +49,29 @@ class Property(AstNode): if self.gir_class is not None and not isinstance(self.gir_class, UncheckedType): return self.gir_class.properties.get(self.tokens["name"]) - @property - def value_type(self): + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + if ( + ( + isinstance(self.value.child, PropertyBinding) + or isinstance(self.value.child, Binding) + ) + and self.gir_property is not None + and self.gir_property.construct_only + ): + binding_error = CompileError( + f"{self.gir_property.full_name} can't be bound because it is construct-only", + hints=["construct-only properties may only be set to a static value"], + ) + else: + binding_error = None + if self.gir_property is not None: - return self.gir_property.type + type = self.gir_property.type + else: + type = None + + return ValueTypeCtx(type, binding_error) @validate("name") def property_exists(self): @@ -95,40 +91,11 @@ class Property(AstNode): did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()), ) - @validate("bind") - def property_bindable(self): - if ( - self.tokens["bind"] - and self.gir_property is not None - and self.gir_property.construct_only - ): - raise CompileError( - f"{self.gir_property.full_name} can't be bound because it is construct-only", - hints=["construct-only properties may only be set to a static value"], - ) - @validate("name") def property_writable(self): if self.gir_property is not None and not self.gir_property.writable: raise CompileError(f"{self.gir_property.full_name} is not writable") - @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}" - ) - @validate("name") def unique_in_parent(self): self.validate_unique_in_parent( diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 74d7472..0c649b7 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -82,10 +82,11 @@ class Signal(AstNode): @validate("handler") def old_extern(self): if not self.tokens["extern"]: - raise UpgradeWarning( - "Use the '$' extern syntax introduced in blueprint 0.8.0", - actions=[CodeAction("Use '$' syntax", "$" + self.tokens["handler"])], - ) + if self.handler is not None: + raise UpgradeWarning( + "Use the '$' extern syntax introduced in blueprint 0.8.0", + actions=[CodeAction("Use '$' syntax", "$" + self.handler)], + ) @validate("name") def signal_exists(self): diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index f2066ff..a3e1888 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -21,6 +21,7 @@ from .gobject_object import ObjectContent, validate_parent_type from .attributes import BaseTypedAttribute from .values import Value from .common import * +from .contexts import ValueTypeCtx def get_property_types(gir): @@ -108,7 +109,7 @@ class A11yProperty(BaseTypedAttribute): grammar = Statement( UseIdent("name"), ":", - VALUE_HOOKS.expected("a value"), + Value, ) @property @@ -129,8 +130,12 @@ class A11yProperty(BaseTypedAttribute): return self.tokens["name"].replace("_", "-") @property - def value_type(self) -> GirType: - return get_types(self.root.gir).get(self.tokens["name"]) + def value(self) -> Value: + return self.children[0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(get_types(self.root.gir).get(self.tokens["name"])) @validate("name") def is_valid_property(self): @@ -161,6 +166,10 @@ class A11y(AstNode): Until(A11yProperty, "}"), ] + @property + def properties(self) -> T.List[A11yProperty]: + return self.children[A11yProperty] + @validate("accessibility") def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "accessibility properties") diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index f0f6f37..e1a8a12 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -21,15 +21,22 @@ from .attributes import BaseTypedAttribute from .gobject_object import ObjectContent, validate_parent_type from .common import * +from .contexts import ValueTypeCtx +from .values import Value -class Item(BaseTypedAttribute): - tag_name = "item" - attr_name = "id" +class Item(AstNode): + @property + def name(self) -> str: + return self.tokens["name"] @property - def value_type(self): - return StringType() + def value(self) -> Value: + return self.children[Value][0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) item = Group( @@ -41,7 +48,7 @@ item = Group( ":", ] ), - VALUE_HOOKS, + Value, ], ) diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 9af82fd..4862148 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -21,15 +21,25 @@ from .attributes import BaseAttribute from .gobject_object import ObjectContent, validate_parent_type from .common import * +from .contexts import ValueTypeCtx +from .values import Value -class LayoutProperty(BaseAttribute): +class LayoutProperty(AstNode): tag_name = "property" @property - def value_type(self): + def name(self) -> str: + return self.tokens["name"] + + @property + def value(self) -> Value: + return self.children[Value][0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: # there isn't really a way to validate these - return None + return ValueTypeCtx(None) @validate("name") def unique_in_parent(self): @@ -41,11 +51,7 @@ class LayoutProperty(BaseAttribute): layout_prop = Group( LayoutProperty, - Statement( - UseIdent("name"), - ":", - VALUE_HOOKS.expected("a value"), - ), + Statement(UseIdent("name"), ":", Err(Value, "Expected a value")), ) diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index dedf6e3..df4b031 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -24,6 +24,7 @@ from blueprintcompiler.language.values import Value from .attributes import BaseAttribute from .gobject_object import Object, ObjectContent from .common import * +from .contexts import ValueTypeCtx class Menu(AstNode): @@ -49,17 +50,23 @@ class Menu(AstNode): raise CompileError("Menu requires an ID") -class MenuAttribute(BaseAttribute): +class MenuAttribute(AstNode): tag_name = "attribute" @property - def value_type(self): - return None + def name(self) -> str: + return self.tokens["name"] @property def value(self) -> Value: return self.children[Value][0] + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx( + None, binding_error=CompileError("Bindings are not permitted in menus") + ) + menu_contents = Sequence() @@ -78,7 +85,7 @@ menu_attribute = Group( [ UseIdent("name"), ":", - VALUE_HOOKS.expected("a value"), + Err(Value, "Expected a value"), Match(";").expected(), ], ) @@ -102,7 +109,7 @@ menu_item_shorthand = Group( "(", Group( MenuAttribute, - [UseLiteral("name", "label"), VALUE_HOOKS], + [UseLiteral("name", "label"), Value], ), Optional( [ @@ -111,14 +118,14 @@ menu_item_shorthand = Group( [ Group( MenuAttribute, - [UseLiteral("name", "action"), VALUE_HOOKS], + [UseLiteral("name", "action"), Value], ), Optional( [ ",", Group( MenuAttribute, - [UseLiteral("name", "icon"), VALUE_HOOKS], + [UseLiteral("name", "icon"), Value], ), ] ), diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 347b9e8..b07fa69 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -20,16 +20,17 @@ from .attributes import BaseTypedAttribute from .gobject_object import ObjectContent, validate_parent_type -from .values import Value, TranslatedStringValue +from .values import Value, Translated from .common import * +from .contexts import ValueTypeCtx class Item(AstNode): - grammar = VALUE_HOOKS + grammar = Value - @property - def value_type(self): - return StringType() + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) class Strings(AstNode): diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index be6a003..224e0a3 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -64,6 +64,9 @@ class GtkDirective(AstNode): self.gtk_version() if self.tokens["version"] is not None: return gir.get_namespace("Gtk", self.tokens["version"]) + else: + # For better error handling, just assume it's 4.0 + return gir.get_namespace("Gtk", "4.0") class Import(AstNode): diff --git a/blueprintcompiler/language/property_binding.py b/blueprintcompiler/language/property_binding.py new file mode 100644 index 0000000..37a5c91 --- /dev/null +++ b/blueprintcompiler/language/property_binding.py @@ -0,0 +1,139 @@ +# 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 ValueTypeCtx +from .gobject_object import Object +from .gtkbuilder_template import Template + + +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'", "")], + ) + + +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]: + return self.root.objects_by_id.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.root.objects_by_id.keys()), + ) + + @validate("property") + def property_exists(self) -> None: + if self.source_obj is None: + return + + gir_class = self.source_obj.gir_class + + if ( + isinstance(self.source_obj, Template) + or gir_class is None + or isinstance(gir_class, UncheckedType) + ): + # 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-property") + def not_bindable(self) -> None: + if binding_error := self.context[ValueTypeCtx].binding_error: + raise binding_error + + @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")], + ) diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index e1e715d..702ed32 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -94,7 +94,7 @@ class ClassName(TypeName): @validate("namespace", "class_name") def gir_class_exists(self): if ( - self.gir_type + self.gir_type is not None and not isinstance(self.gir_type, UncheckedType) and not isinstance(self.gir_type, Class) ): diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 8447267..4300c39 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -21,41 +21,57 @@ import typing as T from .common import * from .types import TypeName +from .property_binding import PropertyBinding +from .binding import Binding +from .gobject_object import Object +from .contexts import ValueTypeCtx -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(), - ], - ) +class TranslatedWithoutContext(AstNode): + grammar = ["_", "(", UseQuoted("string"), Optional(","), ")"] @property def string(self) -> str: - return self.tokens["value"] + return self.tokens["string"] + + +class TranslatedWithContext(AstNode): + grammar = [ + "C_", + "(", + UseQuoted("context"), + ",", + UseQuoted("string"), + Optional(","), + ")", + ] @property - def context(self) -> T.Optional[str]: + def string(self) -> str: + return self.tokens["string"] + + @property + def context(self) -> str: return self.tokens["context"] -class TypeValue(Value): +class Translated(AstNode): + grammar = AnyOf(TranslatedWithoutContext, TranslatedWithContext) + + @property + def child(self) -> T.Union[TranslatedWithContext, TranslatedWithoutContext]: + return self.children[0] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not expected_type.assignable_to(StringType()): + raise CompileError( + f"Cannot convert translated string to {expected_type.full_name}" + ) + + +class TypeLiteral(AstNode): grammar = [ "typeof", "(", @@ -64,17 +80,17 @@ class TypeValue(Value): ] @property - def type_name(self): + def type_name(self) -> TypeName: return self.children[TypeName][0] @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}") + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not isinstance(expected_type, gir.TypeType): + raise CompileError(f"Cannot convert GType to {expected_type.full_name}") -class QuotedValue(Value): +class QuotedLiteral(AstNode): grammar = UseQuoted("value") @property @@ -82,22 +98,22 @@ class QuotedValue(Value): return self.tokens["value"] @validate() - def validate_for_type(self): - type = self.parent.value_type + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type if ( - isinstance(type, gir.IntType) - or isinstance(type, gir.UIntType) - or isinstance(type, gir.FloatType) + isinstance(expected_type, gir.IntType) + or isinstance(expected_type, gir.UIntType) + or isinstance(expected_type, gir.FloatType) ): raise CompileError(f"Cannot convert string to number") - elif isinstance(type, gir.StringType): + elif isinstance(expected_type, gir.StringType): pass elif ( - isinstance(type, gir.Class) - or isinstance(type, gir.Interface) - or isinstance(type, gir.Boxed) + isinstance(expected_type, gir.Class) + or isinstance(expected_type, gir.Interface) + or isinstance(expected_type, gir.Boxed) ): parseable_types = [ "Gdk.Paintable", @@ -111,31 +127,32 @@ class QuotedValue(Value): "Gsk.Transform", "GLib.Variant", ] - if type.full_name not in parseable_types: + if expected_type.full_name not in parseable_types: hints = [] - if isinstance(type, gir.TypeType): - hints.append( - f"use the typeof operator: 'typeof({self.tokens('value')})'" - ) + if isinstance(expected_type, gir.TypeType): + hints.append(f"use the typeof operator: 'typeof({self.value})'") raise CompileError( - f"Cannot convert string to {type.full_name}", hints=hints + f"Cannot convert string to {expected_type.full_name}", hints=hints ) - elif type is not None: - raise CompileError(f"Cannot convert string to {type.full_name}") + elif expected_type is not None: + raise CompileError(f"Cannot convert string to {expected_type.full_name}") -class NumberValue(Value): - grammar = UseNumber("value") +class NumberLiteral(AstNode): + grammar = [ + Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), + UseNumber("value"), + ] @property def value(self) -> T.Union[int, float]: return self.tokens["value"] @validate() - def validate_for_type(self): - type = self.parent.value_type - if isinstance(type, gir.IntType): + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if isinstance(expected_type, gir.IntType): try: int(self.tokens["value"]) except: @@ -143,7 +160,7 @@ class NumberValue(Value): f"Cannot convert {self.group.tokens['value']} to integer" ) - elif isinstance(type, gir.UIntType): + elif isinstance(expected_type, gir.UIntType): try: int(self.tokens["value"]) if int(self.tokens["value"]) < 0: @@ -153,7 +170,7 @@ class NumberValue(Value): f"Cannot convert {self.group.tokens['value']} to unsigned integer" ) - elif isinstance(type, gir.FloatType): + elif isinstance(expected_type, gir.FloatType): try: float(self.tokens["value"]) except: @@ -161,8 +178,8 @@ class NumberValue(Value): f"Cannot convert {self.group.tokens['value']} to float" ) - elif type is not None: - raise CompileError(f"Cannot convert number to {type.full_name}") + elif expected_type is not None: + raise CompileError(f"Cannot convert number to {expected_type.full_name}") class Flag(AstNode): @@ -174,17 +191,17 @@ class Flag(AstNode): @property def value(self) -> T.Optional[int]: - type = self.parent.parent.value_type + type = self.context[ValueTypeCtx].value_type if not isinstance(type, Enumeration): return None - elif member := type.members.get(self.tokens["value"]): + elif member := type.members.get(self.name): return member.value else: return None @docs() def docs(self): - type = self.parent.parent.value_type + type = self.context[ValueTypeCtx].value_type if not isinstance(type, Enumeration): return if member := type.members.get(self.tokens["value"]): @@ -192,15 +209,18 @@ class Flag(AstNode): @validate() def validate_for_type(self): - type = self.parent.parent.value_type - if isinstance(type, gir.Bitfield) and self.tokens["value"] not in type.members: + expected_type = self.context[ValueTypeCtx].value_type + if ( + isinstance(expected_type, gir.Bitfield) + and self.tokens["value"] not in expected_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()), + f"{self.tokens['value']} is not a member of {expected_type.full_name}", + did_you_mean=(self.tokens["value"], expected_type.members.keys()), ) -class FlagsValue(Value): +class Flags(AstNode): grammar = [Flag, "|", Delimited(Flag, "|")] @property @@ -208,57 +228,104 @@ class FlagsValue(Value): return self.children @validate() - def parent_is_bitfield(self): - type = self.parent.value_type - if type is not None and not isinstance(type, gir.Bitfield): - raise CompileError(f"{type.full_name} is not a bitfield type") + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not isinstance(expected_type, gir.Bitfield): + raise CompileError(f"{expected_type.full_name} is not a bitfield type") -class IdentValue(Value): +class IdentLiteral(AstNode): grammar = UseIdent("value") + @property + def ident(self) -> str: + return self.tokens["value"] + @validate() - def validate_for_type(self): - type = self.parent.value_type + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if isinstance(expected_type, gir.BoolType): + if self.ident not in ["true", "false"]: + raise CompileError(f"Expected 'true' or 'false' for boolean value") - if isinstance(type, gir.Enumeration): - if self.tokens["value"] not in type.members: + elif isinstance(expected_type, gir.Enumeration): + if self.ident not in expected_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()), + f"{self.ident} is not a member of {expected_type.full_name}", + did_you_mean=(self.ident, list(expected_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"]) + elif expected_type is not None: + object = self.root.objects_by_id.get(self.ident) 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()), + f"Could not find object with ID {self.ident}", + did_you_mean=(self.ident, self.root.objects_by_id.keys()), ) - elif object.gir_class and not object.gir_class.assignable_to(type): + elif object.gir_class and not object.gir_class.assignable_to(expected_type): raise CompileError( - f"Cannot assign {object.gir_class.full_name} to {type.full_name}" + f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}" ) @docs() - def docs(self): - type = self.parent.value_type + def docs(self) -> T.Optional[str]: + type = self.context[ValueTypeCtx].value_type if isinstance(type, gir.Enumeration): - if member := type.members.get(self.tokens["value"]): + if member := type.members.get(self.ident): return member.doc else: return type.doc elif isinstance(type, gir.GirNode): return type.doc + else: + return None 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 Literal(AstNode): + grammar = AnyOf( + TypeLiteral, + QuotedLiteral, + NumberLiteral, + IdentLiteral, + ) + + @property + def value( + self, + ) -> T.Union[TypeLiteral, QuotedLiteral, NumberLiteral, IdentLiteral]: + return self.children[0] + + +class ObjectValue(AstNode): + grammar = Object + + @property + def object(self) -> Object: + return self.children[Object][0] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if ( + expected_type is not None + and self.object.gir_class is not None + and not self.object.gir_class.assignable_to(expected_type) + ): + raise CompileError( + f"Cannot assign {self.object.gir_class.full_name} to {expected_type.full_name}" + ) + + +class Value(AstNode): + grammar = AnyOf(PropertyBinding, Binding, Translated, ObjectValue, Flags, Literal) + + @property + def child( + self, + ) -> T.Union[PropertyBinding, Binding, Translated, ObjectValue, Flags, Literal,]: + return self.children[0] diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 48a18ac..c38c070 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -82,51 +82,57 @@ class XmlOutput(OutputFormat): xml.end_tag() def _emit_property(self, property: Property, xml: XmlEmitter): - values = property.children[Value] - value = values[0] if len(values) == 1 else None + value = property.value + child = value.child - bind_flags = [] - if property.tokens["bind_source"] and not property.tokens["no_sync_create"]: - bind_flags.append("sync-create") - if property.tokens["inverted"]: - bind_flags.append("invert-boolean") - if property.tokens["bidirectional"]: - bind_flags.append("bidirectional") - bind_flags_str = "|".join(bind_flags) or None - - props = { - "name": property.tokens["name"], - "bind-source": property.tokens["bind_source"], - "bind-property": property.tokens["bind_property"], - "bind-flags": bind_flags_str, + props: T.Dict[str, T.Optional[str]] = { + "name": property.name, } - if isinstance(value, TranslatedStringValue): - xml.start_tag("property", **props, **self._translated_string_attrs(value)) - xml.put_text(value.string) + if isinstance(child, Translated): + xml.start_tag("property", **props, **self._translated_string_attrs(child)) + xml.put_text(child.child.string) xml.end_tag() - elif len(property.children[Object]) == 1: + elif isinstance(child, Object): xml.start_tag("property", **props) - self._emit_object(property.children[Object][0], xml) + self._emit_object(child, xml) xml.end_tag() - elif value is None: - if property.tokens["binding"]: - xml.start_tag("binding", **props) - self._emit_expression(property.children[ExprChain][0], xml) - xml.end_tag() - else: + elif isinstance(child, Binding): + if simple := child.simple_binding: + props["bind-source"] = simple.source + props["bind-property"] = simple.property_name + props["bind-flags"] = "sync-create" xml.put_self_closing("property", **props) + else: + xml.start_tag("binding", **props) + self._emit_expression(child.expression, xml) + xml.end_tag() + elif isinstance(child, PropertyBinding): + bind_flags = [] + if not child.no_sync_create: + bind_flags.append("sync-create") + if child.inverted: + bind_flags.append("invert-boolean") + if child.bidirectional: + bind_flags.append("bidirectional") + + props["bind-source"] = child.source + props["bind-property"] = child.property_name + props["bind-flags"] = "|".join(bind_flags) or None + xml.put_self_closing("property", **props) else: xml.start_tag("property", **props) self._emit_value(value, xml) xml.end_tag() def _translated_string_attrs( - self, translated: TranslatedStringValue + self, translated: Translated ) -> T.Dict[str, T.Optional[str]]: return { "translatable": "true", - "context": translated.context, + "context": translated.child.context + if isinstance(translated.child, TranslatedWithContext) + else None, } def _emit_signal(self, signal: Signal, xml: XmlEmitter): @@ -154,23 +160,30 @@ class XmlOutput(OutputFormat): xml.end_tag() def _emit_value(self, value: Value, xml: XmlEmitter): - if isinstance(value, IdentValue): - if isinstance(value.parent.value_type, gir.Enumeration): - xml.put_text( - str(value.parent.value_type.members[value.tokens["value"]].value) - ) + if isinstance(value.child, Literal): + literal = value.child.value + if isinstance(literal, IdentLiteral): + value_type = value.context[ValueTypeCtx].value_type + if isinstance(value_type, gir.BoolType): + xml.put_text(literal.ident) + elif isinstance(value_type, gir.Enumeration): + xml.put_text(str(value_type.members[literal.ident].value)) + else: + xml.put_text(literal.ident) + elif isinstance(literal, TypeLiteral): + xml.put_text(literal.type_name.glib_type_name) else: - xml.put_text(value.tokens["value"]) - elif isinstance(value, QuotedValue) or isinstance(value, NumberValue): - xml.put_text(value.value) - elif isinstance(value, FlagsValue): + xml.put_text(literal.value) + elif isinstance(value.child, Flags): xml.put_text( - "|".join([str(flag.value or flag.name) for flag in value.flags]) + "|".join([str(flag.value or flag.name) for flag in value.child.flags]) ) - elif isinstance(value, TranslatedStringValue): + elif isinstance(value.child, Translated): raise CompilerBugError("translated values must be handled in the parent") - elif isinstance(value, TypeValue): - xml.put_text(value.type_name.glib_type_name) + elif isinstance(value.child, TypeLiteral): + xml.put_text(value.child.type_name.glib_type_name) + elif isinstance(value.child, ObjectValue): + self._emit_object(value.child.object, xml) else: raise CompilerBugError() @@ -215,9 +228,9 @@ class XmlOutput(OutputFormat): ): attrs = {attr: name} - if isinstance(value, TranslatedStringValue): - xml.start_tag(tag, **attrs, **self._translated_string_attrs(value)) - xml.put_text(value.string) + if isinstance(value.child, Translated): + xml.start_tag(tag, **attrs, **self._translated_string_attrs(value.child)) + xml.put_text(value.child.child.string) xml.end_tag() else: xml.start_tag(tag, **attrs) @@ -227,43 +240,37 @@ class XmlOutput(OutputFormat): def _emit_extensions(self, extension, xml: XmlEmitter): if isinstance(extension, A11y): xml.start_tag("accessibility") - for child in extension.children: - self._emit_attribute( - child.tag_name, "name", child.name, child.children[Value][0], xml - ) + for prop in extension.properties: + self._emit_attribute(prop.tag_name, "name", prop.name, prop.value, xml) xml.end_tag() elif isinstance(extension, Filters): xml.start_tag(extension.tokens["tag_name"]) - for child in extension.children: - xml.start_tag(child.tokens["tag_name"]) - xml.put_text(child.tokens["name"]) + for prop in extension.children: + xml.start_tag(prop.tokens["tag_name"]) + xml.put_text(prop.tokens["name"]) xml.end_tag() xml.end_tag() elif isinstance(extension, Items): xml.start_tag("items") - for child in extension.children: - self._emit_attribute( - "item", "id", child.name, child.children[Value][0], xml - ) + for prop in extension.children: + self._emit_attribute("item", "id", prop.name, prop.value, xml) xml.end_tag() elif isinstance(extension, Layout): xml.start_tag("layout") - for child in extension.children: - self._emit_attribute( - "property", "name", child.name, child.children[Value][0], xml - ) + for prop in extension.children: + self._emit_attribute("property", "name", prop.name, prop.value, xml) xml.end_tag() elif isinstance(extension, Strings): xml.start_tag("items") - for child in extension.children: - value = child.children[Value][0] - if isinstance(value, TranslatedStringValue): + for prop in extension.children: + value = prop.children[Value][0] + if isinstance(value.child, Translated): xml.start_tag("item", **self._translated_string_attrs(value)) - xml.put_text(value.string) + xml.put_text(value.child.child.string) xml.end_tag() else: xml.start_tag("item") @@ -273,14 +280,14 @@ class XmlOutput(OutputFormat): elif isinstance(extension, Styles): xml.start_tag("style") - for child in extension.children: - xml.put_self_closing("class", name=child.tokens["name"]) + for prop in extension.children: + xml.put_self_closing("class", name=prop.tokens["name"]) xml.end_tag() elif isinstance(extension, Widgets): xml.start_tag("widgets") - for child in extension.children: - xml.put_self_closing("widget", name=child.tokens["name"]) + for prop in extension.children: + xml.put_self_closing("widget", name=prop.tokens["name"]) xml.end_tag() else: diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 670c72e..c85015a 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -567,6 +567,19 @@ class UseLiteral(ParseNode): return True +class UseExact(ParseNode): + """Matches the given identifier and sets it as a named token.""" + + def __init__(self, key: str, string: str): + self.key = key + self.string = string + + def _parse(self, ctx: ParseContext): + token = ctx.next_token() + ctx.set_group_val(self.key, self.string, token) + return str(token) == self.string + + class Keyword(ParseNode): """Matches the given identifier and sets it as a named token, with the name being the identifier itself.""" diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index a44f709..edef840 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -21,7 +21,7 @@ from .errors import MultipleErrors, PrintableError from .parse_tree import * from .tokenizer import TokenType -from .language import OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI +from .language import OBJECT_CONTENT_HOOKS, Template, UI def parse( diff --git a/tests/sample_errors/obj_prop_type.err b/tests/sample_errors/obj_prop_type.err index 01f1202..6687c7f 100644 --- a/tests/sample_errors/obj_prop_type.err +++ b/tests/sample_errors/obj_prop_type.err @@ -1 +1 @@ -4,3,21,Cannot assign Gtk.Label to Gtk.Adjustment +4,15,8,Cannot assign Gtk.Label to Gtk.Adjustment diff --git a/tests/samples/binding.blp b/tests/sample_errors/warn_old_bind.blp similarity index 100% rename from tests/samples/binding.blp rename to tests/sample_errors/warn_old_bind.blp diff --git a/tests/sample_errors/warn_old_bind.err b/tests/sample_errors/warn_old_bind.err new file mode 100644 index 0000000..f1acc86 --- /dev/null +++ b/tests/sample_errors/warn_old_bind.err @@ -0,0 +1,2 @@ +4,12,4,Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags +6,12,4,Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags \ No newline at end of file diff --git a/tests/samples/property_binding.blp b/tests/samples/property_binding.blp new file mode 100644 index 0000000..1d7d6ea --- /dev/null +++ b/tests/samples/property_binding.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; + +Box { + visible: bind-property box2.visible inverted; + orientation: bind box2.orientation; + spacing: bind-property box2.spacing no-sync-create; +} + +Box box2 { + spacing: 6; +} diff --git a/tests/samples/binding.ui b/tests/samples/property_binding.ui similarity index 100% rename from tests/samples/binding.ui rename to tests/samples/property_binding.ui diff --git a/tests/samples/property_binding_dec.blp b/tests/samples/property_binding_dec.blp new file mode 100644 index 0000000..7e74ab8 --- /dev/null +++ b/tests/samples/property_binding_dec.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; + +Box { + visible: bind-property box2.visible inverted; + orientation: bind-property box2.orientation; + spacing: bind-property box2.spacing no-sync-create; +} + +Box box2 { + spacing: 6; +} diff --git a/tests/test_samples.py b/tests/test_samples.py index e9e7697..c5caca5 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -145,7 +145,6 @@ class TestSamples(unittest.TestCase): def test_samples(self): self.assert_sample("accessibility") self.assert_sample("action_widgets") - self.assert_sample("binding") self.assert_sample("child_type") self.assert_sample("combo_box_text") self.assert_sample("comments") @@ -163,6 +162,7 @@ class TestSamples(unittest.TestCase): "parseable", skip_run=True ) # The image resource doesn't exist self.assert_sample("property") + self.assert_sample("property_binding") self.assert_sample("signal", skip_run=True) # The callback doesn't exist self.assert_sample("size_group") self.assert_sample("string_list") @@ -235,12 +235,12 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("two_templates") self.assert_sample_error("uint") self.assert_sample_error("using_invalid_namespace") + self.assert_sample_error("warn_old_bind") self.assert_sample_error("warn_old_extern") self.assert_sample_error("widgets_in_non_size_group") def test_decompiler(self): self.assert_decompile("accessibility_dec") - self.assert_decompile("binding") self.assert_decompile("child_type") self.assert_decompile("file_filter") self.assert_decompile("flags") @@ -248,6 +248,7 @@ class TestSamples(unittest.TestCase): self.assert_decompile("layout_dec") self.assert_decompile("menu_dec") self.assert_decompile("property") + self.assert_decompile("property_binding_dec") self.assert_decompile("placeholder_dec") self.assert_decompile("signal") self.assert_decompile("strings") From 9fcb63a0135c9bd8b2cf712c136b9ce3648be984 Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 16 Feb 2023 20:39:43 -0600 Subject: [PATCH 073/290] typelib: Fix crash when handling array types --- blueprintcompiler/gir.py | 28 +++++++++++++++++++++++++--- blueprintcompiler/typelib.py | 4 +++- tests/sample_errors/strv.blp | 6 ++++++ tests/sample_errors/strv.err | 1 + tests/test_samples.py | 1 + 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 tests/sample_errors/strv.blp create mode 100644 tests/sample_errors/strv.err diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 70d8e89..a9f5693 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -126,6 +126,22 @@ class UncheckedType(GirType): return self._name +class ArrayType(GirType): + def __init__(self, inner: GirType) -> None: + self._inner = inner + + def assignable_to(self, other: GirType) -> bool: + return isinstance(other, ArrayType) and self._inner.assignable_to(other._inner) + + @property + def name(self) -> str: + return self._inner.name + "[]" + + @property + def full_name(self) -> str: + return self._inner.full_name + "[]" + + class BasicType(GirType): name: str = "unknown type" @@ -714,9 +730,15 @@ class Repository(GirNode): else: raise CompilerBugError("Unknown type ID", type_id) else: - return self._resolve_dir_entry( - self.tl.header[type_id].INTERFACE_TYPE_INTERFACE - ) + blob = self.tl.header[type_id] + if blob.TYPE_BLOB_TAG == typelib.TYPE_INTERFACE: + return self._resolve_dir_entry( + self.tl.header[type_id].TYPE_BLOB_INTERFACE + ) + elif blob.TYPE_BLOB_TAG == typelib.TYPE_ARRAY: + return ArrayType(self._resolve_type_id(blob.TYPE_BLOB_ARRAY_INNER)) + else: + raise CompilerBugError(f"{blob.TYPE_BLOB_TAG}") class GirContext: diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index 48ec416..6babc10 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -136,7 +136,9 @@ class Typelib: ATTR_NAME = Field(0x0, "string") ATTR_VALUE = Field(0x0, "string") - INTERFACE_TYPE_INTERFACE = Field(0x2, "dir_entry") + TYPE_BLOB_TAG = Field(0x0, "u8", 3, 5) + TYPE_BLOB_INTERFACE = Field(0x2, "dir_entry") + TYPE_BLOB_ARRAY_INNER = Field(0x4, "u32") BLOB_NAME = Field(0x4, "string") diff --git a/tests/sample_errors/strv.blp b/tests/sample_errors/strv.blp new file mode 100644 index 0000000..2f4983a --- /dev/null +++ b/tests/sample_errors/strv.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; +using Adw 1; + +Adw.AboutWindow { + developers: Gtk.StringList {}; +} \ No newline at end of file diff --git a/tests/sample_errors/strv.err b/tests/sample_errors/strv.err new file mode 100644 index 0000000..f0d6961 --- /dev/null +++ b/tests/sample_errors/strv.err @@ -0,0 +1 @@ +5,15,17,Cannot assign Gtk.StringList to string[] \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index c5caca5..7bc7d28 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -231,6 +231,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("signal_object_dne") self.assert_sample_error("size_group_non_widget") self.assert_sample_error("size_group_obj_dne") + self.assert_sample_error("strv") self.assert_sample_error("styles_in_non_widget") self.assert_sample_error("two_templates") self.assert_sample_error("uint") From 8874cf60b394890a5d56587df903e7d25271b9b4 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 11 Mar 2023 21:05:27 -0600 Subject: [PATCH 074/290] parse_tree: Remove Pratt parser It isn't actually needed; the way we parse expressions as a prefix followed by zero or more suffixes is enough. --- .vscode/settings.json | 3 ++ blueprintcompiler/language/expression.py | 9 ++-- blueprintcompiler/parse_tree.py | 62 ------------------------ 3 files changed, 6 insertions(+), 68 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de288e1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index e75b961..82787ef 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -23,7 +23,7 @@ from .types import TypeName from .gtkbuilder_template import Template -expr = Pratt() +expr = Sequence() class Expr(AstNode): @@ -200,9 +200,6 @@ class ClosureExpr(Expr): expr.children = [ - Prefix(ClosureExpr), - Prefix(IdentExpr), - Prefix(["(", ExprChain, ")"]), - Infix(10, LookupOp), - Infix(10, CastExpr), + AnyOf(ClosureExpr, IdentExpr, ["(", ExprChain, ")"]), + ZeroOrMore(AnyOf(LookupOp, CastExpr)), ] diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index c85015a..7a44c80 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -594,68 +594,6 @@ class Keyword(ParseNode): return str(token) == self.kw -class Prefix(ParseNode): - def __init__(self, child): - self.child = to_parse_node(child) - - def _parse(self, ctx: ParseContext): - return self.child.parse(ctx).succeeded() - - -class Infix(ParseNode): - def __init__(self, binding_power: int, child): - self.binding_power = binding_power - self.child = to_parse_node(child) - - def _parse(self, ctx: ParseContext): - ctx.binding_power = self.binding_power - return self.child.parse(ctx).succeeded() - - def __lt__(self, other): - return self.binding_power < other.binding_power - - def __eq__(self, other): - return self.binding_power == other.binding_power - - -class Pratt(ParseNode): - """Basic Pratt parser implementation.""" - - def __init__(self, *children): - self.children = children - - @property - def children(self): - return self._children - - @children.setter - def children(self, children): - self._children = children - self.prefixes = [child for child in children if isinstance(child, Prefix)] - self.infixes = sorted( - [child for child in children if isinstance(child, Infix)], reverse=True - ) - - def _parse(self, ctx: ParseContext) -> bool: - for prefix in self.prefixes: - if prefix.parse(ctx).succeeded(): - break - else: - # none of the prefixes could be parsed - return False - - while True: - succeeded = False - for infix in self.infixes: - if infix.binding_power <= ctx.binding_power: - break - if infix.parse(ctx).succeeded(): - succeeded = True - break - if not succeeded: - return True - - def to_parse_node(value) -> ParseNode: if isinstance(value, str): return Match(value) From 0f5f08ade955ba2e80675b8e6447c4f399e8c51c Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 11 Mar 2023 21:24:52 -0600 Subject: [PATCH 075/290] Fix flag syntax Unlike commas, no trailing "|" is allowed. --- blueprintcompiler/language/values.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 4300c39..d03fc84 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -221,7 +221,7 @@ class Flag(AstNode): class Flags(AstNode): - grammar = [Flag, "|", Delimited(Flag, "|")] + grammar = [Flag, "|", Flag, ZeroOrMore(["|", Flag])] @property def flags(self) -> T.List[Flag]: @@ -327,5 +327,5 @@ class Value(AstNode): @property def child( self, - ) -> T.Union[PropertyBinding, Binding, Translated, ObjectValue, Flags, Literal,]: + ) -> T.Union[PropertyBinding, Binding, Translated, ObjectValue, Flags, Literal]: return self.children[0] From fad3b3553105b6007927297cce24e6441dc3cc90 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 11 Mar 2023 21:36:27 -0600 Subject: [PATCH 076/290] types: Remove g* type names They aren't used in GIR parsing anymore since we use typelibs, and blueprint files should use the non-prefixed names. --- blueprintcompiler/gir.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index a9f5693..f03a37f 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -208,20 +208,11 @@ class TypeType(BasicType): _BASIC_TYPES = { "bool": BoolType, - "gboolean": BoolType, "string": StringType, - "gchararray": StringType, "int": IntType, - "gint": IntType, - "gint64": IntType, - "guint": UIntType, - "guint64": UIntType, - "gfloat": FloatType, - "gdouble": FloatType, + "uint": UIntType, "float": FloatType, "double": FloatType, - "utf8": StringType, - "gtype": TypeType, "type": TypeType, } From b636d9ed7134f3ccff7edd1b4f86390c0562f055 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 12 Mar 2023 14:29:20 -0500 Subject: [PATCH 077/290] Fix bugs in number literals --- blueprintcompiler/language/response_id.py | 18 +++++++---- blueprintcompiler/language/values.py | 30 ++++++++----------- blueprintcompiler/tokenizer.py | 6 ++-- .../action_widget_negative_response.err | 2 +- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index 073173a..e3b44a9 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -31,7 +31,13 @@ class ResponseId(AstNode): grammar = [ Keyword("response"), "=", - AnyOf(UseIdent("response_id"), UseNumber("response_id")), + AnyOf( + UseIdent("response_id"), + [ + Optional(UseExact("sign", "-")), + UseNumber("response_id"), + ], + ), Optional([Keyword("default"), UseLiteral("is_default", True)]), ] @@ -81,14 +87,14 @@ class ResponseId(AstNode): gir = self.root.gir response = self.tokens["response_id"] - if isinstance(response, int): - if response < 0: - raise CompileError("Numeric response type can't be negative") - elif isinstance(response, float): + if self.tokens["sign"] == "-": + raise CompileError("Numeric response type can't be negative") + + if isinstance(response, float): raise CompileError( "Response type must be GtkResponseType member or integer," " not float" ) - else: + elif not isinstance(response, int): responses = gir.get_type("ResponseType", "Gtk").members.keys() if response not in responses: raise CompileError(f'Response type "{response}" doesn\'t exist') diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index d03fc84..ca10f50 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -145,39 +145,35 @@ class NumberLiteral(AstNode): UseNumber("value"), ] + @property + def type(self) -> gir.GirType: + if isinstance(self.value, int): + return gir.IntType() + else: + return gir.FloatType() + @property def value(self) -> T.Union[int, float]: - return self.tokens["value"] + if self.tokens["sign"] == "-": + return -self.tokens["value"] + else: + return self.tokens["value"] @validate() def validate_for_type(self) -> None: expected_type = self.context[ValueTypeCtx].value_type if isinstance(expected_type, gir.IntType): - try: - int(self.tokens["value"]) - except: + if not isinstance(self.value, int): raise CompileError( f"Cannot convert {self.group.tokens['value']} to integer" ) elif isinstance(expected_type, gir.UIntType): - try: - int(self.tokens["value"]) - if int(self.tokens["value"]) < 0: - raise Exception() - except: + if self.value < 0: raise CompileError( f"Cannot convert {self.group.tokens['value']} to unsigned integer" ) - elif isinstance(expected_type, gir.FloatType): - try: - float(self.tokens["value"]) - except: - raise CompileError( - f"Cannot convert {self.group.tokens['value']} to float" - ) - elif expected_type is not None: raise CompileError(f"Cannot convert number to {expected_type.full_name}") diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 170316c..5a40803 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -41,8 +41,8 @@ _tokens = [ (TokenType.QUOTED, r'"(\\"|[^"\n])*"'), (TokenType.QUOTED, r"'(\\'|[^'\n])*'"), (TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), - (TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"), - (TokenType.NUMBER, r"[-+]?\.[\d_]+"), + (TokenType.NUMBER, r"[\d_]+(\.[\d_]+)?"), + (TokenType.NUMBER, r"\.[\d_]+"), (TokenType.WHITESPACE, r"\s+"), (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), (TokenType.COMMENT, r"\/\/[^\n]*"), @@ -71,7 +71,7 @@ class Token: if string.startswith("0x"): return int(string, 16) else: - return float(string.replace("_", "")) + return float(string) except: raise CompileError( f"{str(self)} is not a valid number literal", self.start, self.end diff --git a/tests/sample_errors/action_widget_negative_response.err b/tests/sample_errors/action_widget_negative_response.err index 6e6836d..6f0a627 100644 --- a/tests/sample_errors/action_widget_negative_response.err +++ b/tests/sample_errors/action_widget_negative_response.err @@ -1 +1 @@ -4,24,4,Numeric response type can't be negative +4,25,3,Numeric response type can't be negative From 98ba7d467a2e92f09fd824b52753ce1b2b59b8da Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 12 Mar 2023 15:35:05 -0500 Subject: [PATCH 078/290] Improve expression type checking --- blueprintcompiler/language/__init__.py | 12 ++- blueprintcompiler/language/binding.py | 14 ++-- blueprintcompiler/language/common.py | 1 + blueprintcompiler/language/expression.py | 90 ++++++++++++++++------- blueprintcompiler/language/values.py | 24 +++++- blueprintcompiler/outputs/xml/__init__.py | 44 ++++++----- tests/sample_errors/expr_cast_needed.blp | 7 ++ tests/sample_errors/expr_cast_needed.err | 1 + tests/samples/expr_closure_args.blp | 5 ++ tests/samples/expr_closure_args.ui | 13 ++++ tests/test_samples.py | 4 + 11 files changed, 160 insertions(+), 55 deletions(-) create mode 100644 tests/sample_errors/expr_cast_needed.blp create mode 100644 tests/sample_errors/expr_cast_needed.err create mode 100644 tests/samples/expr_closure_args.blp create mode 100644 tests/samples/expr_closure_args.ui diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 822a91a..8f68647 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,7 +1,15 @@ from .attributes import BaseAttribute, BaseTypedAttribute from .binding import Binding from .contexts import ValueTypeCtx -from .expression import CastExpr, ClosureExpr, Expr, ExprChain, IdentExpr, LookupOp +from .expression import ( + CastExpr, + ClosureArg, + ClosureExpr, + Expr, + ExprChain, + LiteralExpr, + LookupOp, +) from .gobject_object import Object, ObjectContent from .gobject_property import Property from .gobject_signal import Signal @@ -50,3 +58,5 @@ OBJECT_CONTENT_HOOKS.children = [ Strings, Child, ] + +LITERAL.children = [Literal] diff --git a/blueprintcompiler/language/binding.py b/blueprintcompiler/language/binding.py index 91d5a6b..ab544ca 100644 --- a/blueprintcompiler/language/binding.py +++ b/blueprintcompiler/language/binding.py @@ -20,7 +20,7 @@ from dataclasses import dataclass from .common import * -from .expression import ExprChain, LookupOp, IdentExpr +from .expression import ExprChain, LookupOp, LiteralExpr from .contexts import ValueTypeCtx @@ -37,10 +37,14 @@ class Binding(AstNode): @property def simple_binding(self) -> T.Optional["SimpleBinding"]: if isinstance(self.expression.last, LookupOp): - if isinstance(self.expression.last.lhs, IdentExpr): - return SimpleBinding( - self.expression.last.lhs.ident, self.expression.last.property_name - ) + if isinstance(self.expression.last.lhs, LiteralExpr): + from .values import IdentLiteral + + if isinstance(self.expression.last.lhs.literal.value, IdentLiteral): + return SimpleBinding( + self.expression.last.lhs.literal.value.ident, + self.expression.last.property_name, + ) return None @validate("bind") diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 636f15d..734e59b 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -44,3 +44,4 @@ from ..parse_tree import * OBJECT_CONTENT_HOOKS = AnyOf() +LITERAL = AnyOf() diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 82787ef..5347a88 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -19,6 +19,7 @@ from .common import * +from .contexts import ValueTypeCtx from .types import TypeName from .gtkbuilder_template import Template @@ -27,6 +28,13 @@ expr = Sequence() class Expr(AstNode): + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + if rhs := self.rhs: + return rhs.context[ValueTypeCtx] + else: + return self.parent.context[ValueTypeCtx] + @property def type(self) -> T.Optional[GirType]: raise NotImplementedError() @@ -70,39 +78,45 @@ class InfixExpr(Expr): return children[children.index(self) - 1] -class IdentExpr(Expr): - grammar = UseIdent("ident") +class LiteralExpr(Expr): + grammar = LITERAL @property - def ident(self) -> str: - return self.tokens["ident"] + def is_object(self) -> bool: + from .values import IdentLiteral - @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()), - ) + return ( + isinstance(self.literal.value, IdentLiteral) + and self.literal.value.ident in self.root.objects_by_id + ) + + @property + def literal(self): + from .values import Literal + + return self.children[Literal][0] @property def type(self) -> T.Optional[GirType]: - if object := self.root.objects_by_id.get(self.ident): - return object.gir_class - else: - return None + return self.literal.value.type @property def type_complete(self) -> bool: - if object := self.root.objects_by_id.get(self.ident): - return not isinstance(object, Template) - else: - return True + from .values import IdentLiteral + + if isinstance(self.literal, IdentLiteral): + if object := self.root.objects_by_id.get(self.ident): + return not isinstance(object, Template) + return True class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(None) + @property def property_name(self) -> str: return self.tokens["property"] @@ -119,11 +133,15 @@ class LookupOp(InfixExpr): @validate("property") def property_exists(self): - if ( - self.lhs.type is None - or not self.lhs.type_complete - or isinstance(self.lhs.type, UncheckedType) - ): + if self.lhs.type is None: + raise CompileError( + f"Could not determine the type of the preceding expression", + hints=[ + f"add a type cast so blueprint knows which type the property {self.property_name} belongs to" + ], + ) + + if isinstance(self.lhs.type, UncheckedType): return elif not isinstance(self.lhs.type, gir.Class) and not isinstance( @@ -143,6 +161,10 @@ class LookupOp(InfixExpr): class CastExpr(InfixExpr): grammar = ["as", "(", TypeName, ")"] + @context(ValueTypeCtx) + def value_type(self): + return ValueTypeCtx(self.type) + @property def type(self) -> T.Optional[GirType]: return self.children[TypeName][0].gir_type @@ -162,12 +184,24 @@ class CastExpr(InfixExpr): ) +class ClosureArg(AstNode): + grammar = ExprChain + + @property + def expr(self) -> ExprChain: + return self.children[ExprChain][0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(None) + + class ClosureExpr(Expr): grammar = [ Optional(["$", UseLiteral("extern", True)]), UseIdent("name"), "(", - Delimited(ExprChain, ","), + Delimited(ClosureArg, ","), ")", ] @@ -183,8 +217,8 @@ class ClosureExpr(Expr): return self.tokens["name"] @property - def args(self) -> T.List[ExprChain]: - return self.children[ExprChain] + def args(self) -> T.List[ClosureArg]: + return self.children[ClosureArg] @validate() def cast_to_return_type(self): @@ -200,6 +234,6 @@ class ClosureExpr(Expr): expr.children = [ - AnyOf(ClosureExpr, IdentExpr, ["(", ExprChain, ")"]), + AnyOf(ClosureExpr, LiteralExpr, ["(", ExprChain, ")"]), ZeroOrMore(AnyOf(LookupOp, CastExpr)), ] diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index ca10f50..83de28e 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -79,6 +79,10 @@ class TypeLiteral(AstNode): Match(")").expected(), ] + @property + def type(self): + return gir.TypeType() + @property def type_name(self) -> TypeName: return self.children[TypeName][0] @@ -97,6 +101,10 @@ class QuotedLiteral(AstNode): def value(self) -> str: return self.tokens["value"] + @property + def type(self): + return gir.StringType() + @validate() def validate_for_type(self) -> None: expected_type = self.context[ValueTypeCtx].value_type @@ -171,10 +179,10 @@ class NumberLiteral(AstNode): elif isinstance(expected_type, gir.UIntType): if self.value < 0: raise CompileError( - f"Cannot convert {self.group.tokens['value']} to unsigned integer" + f"Cannot convert -{self.group.tokens['value']} to unsigned integer" ) - elif expected_type is not None: + elif not isinstance(expected_type, gir.FloatType) and expected_type is not None: raise CompileError(f"Cannot convert number to {expected_type.full_name}") @@ -237,6 +245,18 @@ class IdentLiteral(AstNode): def ident(self) -> str: return self.tokens["value"] + @property + def type(self) -> T.Optional[gir.GirType]: + # If the expected type is known, then use that. Otherwise, guess. + if expected_type := self.context[ValueTypeCtx].value_type: + return expected_type + elif self.ident in ["true", "false"]: + return gir.BoolType() + elif object := self.root.objects_by_id.get(self.ident): + return object.gir_class + else: + return None + @validate() def validate_for_type(self) -> None: expected_type = self.context[ValueTypeCtx].value_type diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index c38c070..f379710 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -159,21 +159,24 @@ class XmlOutput(OutputFormat): self._emit_object(child.object, xml) xml.end_tag() + def _emit_literal(self, literal: Literal, xml: XmlEmitter): + literal = literal.value + if isinstance(literal, IdentLiteral): + value_type = literal.context[ValueTypeCtx].value_type + if isinstance(value_type, gir.BoolType): + xml.put_text(literal.ident) + elif isinstance(value_type, gir.Enumeration): + xml.put_text(str(value_type.members[literal.ident].value)) + else: + xml.put_text(literal.ident) + elif isinstance(literal, TypeLiteral): + xml.put_text(literal.type_name.glib_type_name) + else: + xml.put_text(literal.value) + def _emit_value(self, value: Value, xml: XmlEmitter): if isinstance(value.child, Literal): - literal = value.child.value - if isinstance(literal, IdentLiteral): - value_type = value.context[ValueTypeCtx].value_type - if isinstance(value_type, gir.BoolType): - xml.put_text(literal.ident) - elif isinstance(value_type, gir.Enumeration): - xml.put_text(str(value_type.members[literal.ident].value)) - else: - xml.put_text(literal.ident) - elif isinstance(literal, TypeLiteral): - xml.put_text(literal.type_name.glib_type_name) - else: - xml.put_text(literal.value) + self._emit_literal(value.child, xml) elif isinstance(value.child, Flags): xml.put_text( "|".join([str(flag.value or flag.name) for flag in value.child.flags]) @@ -191,8 +194,8 @@ class XmlOutput(OutputFormat): self._emit_expression_part(expression.last, xml) def _emit_expression_part(self, expression: Expr, xml: XmlEmitter): - if isinstance(expression, IdentExpr): - self._emit_ident_expr(expression, xml) + if isinstance(expression, LiteralExpr): + self._emit_literal_expr(expression, xml) elif isinstance(expression, LookupOp): self._emit_lookup_op(expression, xml) elif isinstance(expression, ExprChain): @@ -204,9 +207,12 @@ class XmlOutput(OutputFormat): else: raise CompilerBugError() - def _emit_ident_expr(self, expr: IdentExpr, xml: XmlEmitter): - xml.start_tag("constant") - xml.put_text(expr.ident) + def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter): + if expr.is_object: + xml.start_tag("constant") + else: + xml.start_tag("constant", type=expr.type) + self._emit_literal(expr.literal, xml) xml.end_tag() def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter): @@ -220,7 +226,7 @@ class XmlOutput(OutputFormat): def _emit_closure_expr(self, expr: ClosureExpr, xml: XmlEmitter): xml.start_tag("closure", function=expr.closure_name, type=expr.type) for arg in expr.args: - self._emit_expression_part(arg, xml) + self._emit_expression_part(arg.expr, xml) xml.end_tag() def _emit_attribute( diff --git a/tests/sample_errors/expr_cast_needed.blp b/tests/sample_errors/expr_cast_needed.blp new file mode 100644 index 0000000..f647cb5 --- /dev/null +++ b/tests/sample_errors/expr_cast_needed.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +template GtkListItem { + Label { + label: bind GtkListItem.item.label; + } +} \ No newline at end of file diff --git a/tests/sample_errors/expr_cast_needed.err b/tests/sample_errors/expr_cast_needed.err new file mode 100644 index 0000000..51269d2 --- /dev/null +++ b/tests/sample_errors/expr_cast_needed.err @@ -0,0 +1 @@ +5,34,5,Could not determine the type of the preceding expression \ No newline at end of file diff --git a/tests/samples/expr_closure_args.blp b/tests/samples/expr_closure_args.blp new file mode 100644 index 0000000..d09c881 --- /dev/null +++ b/tests/samples/expr_closure_args.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: bind $my-closure (true, 10, "Hello") as (string); +} \ No newline at end of file diff --git a/tests/samples/expr_closure_args.ui b/tests/samples/expr_closure_args.ui new file mode 100644 index 0000000..1b539ac --- /dev/null +++ b/tests/samples/expr_closure_args.ui @@ -0,0 +1,13 @@ + + + + + + + true + 10 + Hello + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 7bc7d28..29d2f14 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -150,6 +150,9 @@ class TestSamples(unittest.TestCase): self.assert_sample("comments") self.assert_sample("enum") self.assert_sample("expr_closure", skip_run=True) # The closure doesn't exist + self.assert_sample( + "expr_closure_args", skip_run=True + ) # The closure doesn't exist self.assert_sample("expr_lookup") self.assert_sample("file_filter") self.assert_sample("flags") @@ -208,6 +211,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("empty") self.assert_sample_error("enum_member_dne") self.assert_sample_error("expr_cast_conversion") + self.assert_sample_error("expr_cast_needed") self.assert_sample_error("expr_closure_not_cast") self.assert_sample_error("expr_lookup_dne") self.assert_sample_error("expr_lookup_no_properties") From 90001bd885b1ffa00e82fe48ad556a8a15ddb56c Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 12 Mar 2023 20:56:31 -0500 Subject: [PATCH 079/290] Fix mypy errors & other bugs --- blueprintcompiler/language/expression.py | 6 +++--- blueprintcompiler/outputs/xml/__init__.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 5347a88..fca7f0e 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -104,8 +104,8 @@ class LiteralExpr(Expr): def type_complete(self) -> bool: from .values import IdentLiteral - if isinstance(self.literal, IdentLiteral): - if object := self.root.objects_by_id.get(self.ident): + if isinstance(self.literal.value, IdentLiteral): + if object := self.root.objects_by_id.get(self.literal.value.ident): return not isinstance(object, Template) return True @@ -141,7 +141,7 @@ class LookupOp(InfixExpr): ], ) - if isinstance(self.lhs.type, UncheckedType): + if isinstance(self.lhs.type, UncheckedType) or not self.lhs.type_complete: return elif not isinstance(self.lhs.type, gir.Class) and not isinstance( diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index f379710..6f71595 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -160,19 +160,19 @@ class XmlOutput(OutputFormat): xml.end_tag() def _emit_literal(self, literal: Literal, xml: XmlEmitter): - literal = literal.value - if isinstance(literal, IdentLiteral): - value_type = literal.context[ValueTypeCtx].value_type + value = literal.value + if isinstance(value, IdentLiteral): + value_type = value.context[ValueTypeCtx].value_type if isinstance(value_type, gir.BoolType): - xml.put_text(literal.ident) + xml.put_text(value.ident) elif isinstance(value_type, gir.Enumeration): - xml.put_text(str(value_type.members[literal.ident].value)) + xml.put_text(str(value_type.members[value.ident].value)) else: - xml.put_text(literal.ident) - elif isinstance(literal, TypeLiteral): - xml.put_text(literal.type_name.glib_type_name) + xml.put_text(value.ident) + elif isinstance(value, TypeLiteral): + xml.put_text(value.type_name.glib_type_name) else: - xml.put_text(literal.value) + xml.put_text(value.value) def _emit_value(self, value: Value, xml: XmlEmitter): if isinstance(value.child, Literal): From 8c3c43a34ad894e9a14cddbb52ecc040db2653cb Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 16 Mar 2023 15:10:04 -0500 Subject: [PATCH 080/290] Add --typelib-path command line argument Allows adding directories to search for typelib files. --- blueprintcompiler/gir.py | 8 +++++++- blueprintcompiler/main.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index f03a37f..d795789 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -32,9 +32,15 @@ from . import typelib, xml_reader _namespace_cache: T.Dict[str, "Namespace"] = {} _xml_cache = {} +_user_search_paths = [] + + +def add_typelib_search_path(path: str): + _user_search_paths.append(path) + def get_namespace(namespace: str, version: str) -> "Namespace": - search_paths = GIRepository.Repository.get_search_path() + search_paths = [*GIRepository.Repository.get_search_path(), *_user_search_paths] filename = f"{namespace}-{version}.typelib" diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 6127630..6ac7a11 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -22,6 +22,7 @@ import typing as T import argparse, json, os, sys from .errors import PrintableError, report_bug, MultipleErrors, CompilerBugError +from .gir import add_typelib_search_path from .lsp import LanguageServer from . import parser, tokenizer, decompiler, interactive_port from .utils import Colors @@ -41,6 +42,7 @@ class BlueprintApp: "compile", "Compile blueprint files", self.cmd_compile ) compile.add_argument("--output", dest="output", default="-") + compile.add_argument("--typelib-path", nargs="?", action="append") compile.add_argument( "input", metavar="filename", default=sys.stdin, type=argparse.FileType("r") ) @@ -52,6 +54,7 @@ class BlueprintApp: ) batch_compile.add_argument("output_dir", metavar="output-dir") batch_compile.add_argument("input_dir", metavar="input-dir") + batch_compile.add_argument("--typelib-path", nargs="?", action="append") batch_compile.add_argument( "inputs", nargs="+", @@ -91,6 +94,10 @@ class BlueprintApp: self.parser.print_help() def cmd_compile(self, opts): + if opts.typelib_path != None: + for typelib_path in opts.typelib_path: + add_typelib_search_path(typelib_path) + data = opts.input.read() try: xml, warnings = self._compile(data) @@ -108,6 +115,10 @@ class BlueprintApp: sys.exit(1) def cmd_batch_compile(self, opts): + if opts.typelib_path != None: + for typelib_path in opts.typelib_path: + add_typelib_search_path(typelib_path) + for file in opts.inputs: data = file.read() From 6f4806bfb3514c3fa01e0880c4bd9d399e058a44 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Sun, 19 Mar 2023 22:14:42 +0000 Subject: [PATCH 081/290] lsp: Add compile an decompile commands --- CONTRIBUTING.md | 4 +++ blueprintcompiler/decompiler.py | 11 ++++++- blueprintcompiler/lsp.py | 52 +++++++++++++++++++++++++++++++-- blueprintcompiler/lsp_utils.py | 4 +++ blueprintcompiler/xml_reader.py | 6 ++++ 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d817250..20a865d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,10 @@ If you learn something useful, please add it to this file. python -m unittest ``` +# Formatting + +Blueprint uses [Black](https://github.com/psf/black) for code formatting. + # Build the docs ```sh diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index 036c86f..1d9f4dd 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -22,7 +22,7 @@ from enum import Enum import typing as T from dataclasses import dataclass -from .xml_reader import Element, parse +from .xml_reader import Element, parse, parse_string from .gir import * from .utils import Colors @@ -211,6 +211,15 @@ def decompile(data: str) -> str: return ctx.result +def decompile_string(data): + ctx = DecompileCtx() + + xml = parse_string(data) + _decompile_element(ctx, None, xml) + + return ctx.result + + def canon(string: str) -> str: if string == "class": return "klass" diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index dd12905..b44d631 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -24,7 +24,8 @@ import json, sys, traceback from .completions import complete from .errors import PrintableError, CompileError, MultipleErrors from .lsp_utils import * -from . import tokenizer, parser, utils, xml_reader +from .outputs.xml import XmlOutput +from . import tokenizer, parser, utils, xml_reader, decompiler def printerr(*args, **kwargs): @@ -149,6 +150,18 @@ class LanguageServer: ) sys.stdout.flush() + def _send_error(self, id, code, message, data=None): + self._send( + { + "id": id, + "error": { + "code": code, + "message": message, + "data": data, + }, + } + ) + def _send_response(self, id, result): self._send( { @@ -169,7 +182,7 @@ class LanguageServer: def initialize(self, id, params): from . import main - self.client_capabilities = params.get("capabilities") + self.client_capabilities = params.get("capabilities", {}) self._send_response( id, { @@ -256,6 +269,41 @@ class LanguageServer: id, [completion.to_json(True) for completion in completions] ) + @command("textDocument/x-blueprint-compile") + def compile(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + + if open_file.ast is None: + self._send_error(id, ErrorCode.RequestFailed, "Document is not open") + return + + xml = None + try: + output = XmlOutput() + xml = output.emit(open_file.ast) + except: + printerr(traceback.format_exc()) + self._send_error(id, ErrorCode.RequestFailed, "Could not compile document") + return + self._send_response(id, {"xml": xml}) + + @command("x-blueprint/decompile") + def decompile(self, id, params): + text = params.get("text") + blp = None + + try: + blp = decompiler.decompile_string(text) + except decompiler.UnsupportedError as e: + self._send_error(id, ErrorCode.RequestFailed, e.message) + return + except: + printerr(traceback.format_exc()) + self._send_error(id, ErrorCode.RequestFailed, "Invalid input") + return + + self._send_response(id, {"blp": blp}) + @command("textDocument/semanticTokens/full") def semantic_tokens(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 5e4ef89..219cade 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -69,6 +69,10 @@ class CompletionItemKind(enum.IntEnum): TypeParameter = 25 +class ErrorCode(enum.IntEnum): + RequestFailed = -32803 + + @dataclass class Completion: label: str diff --git a/blueprintcompiler/xml_reader.py b/blueprintcompiler/xml_reader.py index 5e31773..b2d579b 100644 --- a/blueprintcompiler/xml_reader.py +++ b/blueprintcompiler/xml_reader.py @@ -92,3 +92,9 @@ def parse(filename): parser.setContentHandler(handler) parser.parse(filename) return handler.root + + +def parse_string(xml): + handler = Handler() + parser = sax.parseString(xml, handler) + return handler.root From 3f27e92eb0f247b208453dcb0d55530bd21033b6 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 20 Mar 2023 13:27:21 -0500 Subject: [PATCH 082/290] Remove unnecessary list() call --- blueprintcompiler/parse_tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 7a44c80..8b2963a 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -107,7 +107,7 @@ class ParseContext: """Contains the state of the parser.""" def __init__(self, tokens: T.List[Token], index=0): - self.tokens = list(tokens) + self.tokens = tokens self.binding_power = 0 self.index = index From 402677f687ecdb78026864188bae50b9d85949de Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 20 Mar 2023 13:34:17 -0500 Subject: [PATCH 083/290] performance: Cache some properties --- blueprintcompiler/ast_utils.py | 19 +++++++++++-------- blueprintcompiler/language/ui.py | 3 ++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 7bd5418..73ffe62 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -76,6 +76,7 @@ class AstNode: """Base class for nodes in the abstract syntax tree.""" completers: T.List = [] + attrs_by_type: T.Dict[T.Type, T.List] = {} def __init__(self, group, children, tokens, incomplete=False): self.group = group @@ -92,12 +93,13 @@ class AstNode: cls.validators = [ getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator") ] + cls.attrs_by_type = {} @cached_property def context(self): return Ctx(self) - @property + @cached_property def root(self): if self.parent is None: return self @@ -140,13 +142,14 @@ class AstNode: for child in self.children: yield from child._get_errors() - def _attrs_by_type( - self, attr_type: T.Type[TAttr] - ) -> T.Iterator[T.Tuple[str, TAttr]]: - for name in dir(type(self)): - item = getattr(type(self), name) - if isinstance(item, attr_type): - yield name, item + def _attrs_by_type(self, attr_type: T.Type[TAttr]) -> T.List[T.Tuple[str, TAttr]]: + if attr_type not in self.attrs_by_type: + self.attrs_by_type[attr_type] = [] + for name in dir(type(self)): + item = getattr(type(self), name) + if isinstance(item, attr_type): + self.attrs_by_type[attr_type].append((name, item)) + return self.attrs_by_type[attr_type] def get_docs(self, idx: int) -> T.Optional[str]: for name, attr in self._attrs_by_type(Docs): diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index d45fe4c..aeff186 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -17,6 +17,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +from functools import cached_property from .. import gir from .imports import GtkDirective, Import @@ -82,7 +83,7 @@ class UI(AstNode): or isinstance(child, Menu) ] - @property + @cached_property def objects_by_id(self): return { obj.tokens["id"]: obj From bc605c5df86a33ff415f627ab1aa8cca53780c81 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 21 Mar 2023 11:31:02 -0500 Subject: [PATCH 084/290] Reduce errors when a namespace is not found When the typelib for a namespace is not found, don't emit "namespace not imported" errors. Just emit the one error on the import statement. --- blueprintcompiler/gir.py | 3 ++- blueprintcompiler/language/imports.py | 8 ++++++++ blueprintcompiler/language/ui.py | 2 ++ tests/sample_errors/ns_not_found.blp | 6 ++++++ tests/sample_errors/ns_not_found.err | 1 + tests/test_samples.py | 1 + 6 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/sample_errors/ns_not_found.blp create mode 100644 tests/sample_errors/ns_not_found.err diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index d795789..a8831af 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -741,6 +741,7 @@ class Repository(GirNode): class GirContext: def __init__(self): self.namespaces = {} + self.not_found_namespaces: T.Set[str] = set() def add_namespace(self, namespace: Namespace): other = self.namespaces.get(namespace.name) @@ -781,7 +782,7 @@ class GirContext: ns = ns or "Gtk" - if ns not in self.namespaces: + if ns not in self.namespaces and ns not in self.not_found_namespaces: raise CompileError( f"Namespace {ns} was not imported", did_you_mean=(ns, self.namespaces.keys()), diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index 224e0a3..e34901c 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -76,6 +76,14 @@ class Import(AstNode): UseNumberText("version").expected("a version number"), ) + @property + def namespace(self): + return self.tokens["namespace"] + + @property + def version(self): + return self.tokens["version"] + @validate("namespace", "version") def namespace_exists(self): gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index aeff186..8d27c0e 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -58,6 +58,8 @@ class UI(AstNode): try: if i.gir_namespace is not None: gir_ctx.add_namespace(i.gir_namespace) + else: + gir_ctx.not_found_namespaces.add(i.namespace) except CompileError as e: e.start = i.group.tokens["namespace"].start e.end = i.group.tokens["version"].end diff --git a/tests/sample_errors/ns_not_found.blp b/tests/sample_errors/ns_not_found.blp new file mode 100644 index 0000000..071d554 --- /dev/null +++ b/tests/sample_errors/ns_not_found.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; +using NotARealNamespace 1; + +NotARealNamespace.Widget widget { + property: true; +} \ No newline at end of file diff --git a/tests/sample_errors/ns_not_found.err b/tests/sample_errors/ns_not_found.err new file mode 100644 index 0000000..4a6a111 --- /dev/null +++ b/tests/sample_errors/ns_not_found.err @@ -0,0 +1 @@ +2,7,19,Namespace NotARealNamespace-1 could not be found \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 29d2f14..45fb692 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -224,6 +224,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("menu_no_id") self.assert_sample_error("menu_toplevel_attribute") self.assert_sample_error("no_import_version") + self.assert_sample_error("ns_not_found") self.assert_sample_error("ns_not_imported") self.assert_sample_error("not_a_class") self.assert_sample_error("object_dne") From 7e20983b44c9f917dc8ecfe0c83d520b1d9fb39f Mon Sep 17 00:00:00 2001 From: Cameron Dehning Date: Fri, 24 Mar 2023 16:27:22 +0000 Subject: [PATCH 085/290] Lsp hotfix --- blueprintcompiler/language/values.py | 3 ++- blueprintcompiler/tokenizer.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 83de28e..e446b29 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -297,7 +297,8 @@ class IdentLiteral(AstNode): return None def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: - if isinstance(self.parent.value_type, gir.Enumeration): + type = self.context[ValueTypeCtx].value_type + if isinstance(type, gir.Enumeration): token = self.group.tokens["value"] yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 5a40803..f68f5a7 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -82,7 +82,7 @@ def _tokenize(ui_ml: str): i = 0 while i < len(ui_ml): matched = False - for (type, regex) in _TOKENS: + for type, regex in _TOKENS: match = regex.match(ui_ml, i) if match is not None: From 749ee03e86f4e92e44bbcf99546abea13543c9f3 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 28 Mar 2023 10:09:20 -0500 Subject: [PATCH 086/290] Fix misleading error message for missing semicolon Fixes #105. --- blueprintcompiler/language/gobject_property.py | 2 +- tests/sample_errors/expected_semicolon.blp | 6 ++++++ tests/sample_errors/expected_semicolon.err | 1 + tests/test_samples.py | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 tests/sample_errors/expected_semicolon.blp create mode 100644 tests/sample_errors/expected_semicolon.err diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 13374f8..b57f3b0 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -30,7 +30,7 @@ from .binding import Binding class Property(AstNode): - grammar = [UseIdent("name"), ":", Value, ";"] + grammar = Statement(UseIdent("name"), ":", Value) @property def name(self) -> str: diff --git a/tests/sample_errors/expected_semicolon.blp b/tests/sample_errors/expected_semicolon.blp new file mode 100644 index 0000000..973726d --- /dev/null +++ b/tests/sample_errors/expected_semicolon.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; + +Button { + child: Label { + } +} \ No newline at end of file diff --git a/tests/sample_errors/expected_semicolon.err b/tests/sample_errors/expected_semicolon.err new file mode 100644 index 0000000..bfabc9a --- /dev/null +++ b/tests/sample_errors/expected_semicolon.err @@ -0,0 +1 @@ +6,1,1,Expected `;` \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 45fb692..c292214 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -210,6 +210,7 @@ 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("expected_semicolon") self.assert_sample_error("expr_cast_conversion") self.assert_sample_error("expr_cast_needed") self.assert_sample_error("expr_closure_not_cast") From 0cf9a8e4fc61bc9783bb228093ca9ee987ef74e7 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 19 Mar 2023 18:19:31 -0500 Subject: [PATCH 087/290] Add Adw.MessageDialog responses extension --- blueprintcompiler/decompiler.py | 33 ++++- blueprintcompiler/language/__init__.py | 2 + .../language/adw_message_dialog.py | 131 ++++++++++++++++++ blueprintcompiler/outputs/xml/__init__.py | 35 ++++- tests/samples/responses.blp | 10 ++ tests/samples/responses.ui | 11 ++ tests/test_samples.py | 13 ++ 7 files changed, 221 insertions(+), 14 deletions(-) create mode 100644 blueprintcompiler/language/adw_message_dialog.py create mode 100644 tests/samples/responses.blp create mode 100644 tests/samples/responses.ui diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index 1d9f4dd..f7d858c 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -275,9 +275,28 @@ def decompile_placeholder(ctx, gir): pass +def decompile_translatable( + string: str, + translatable: T.Optional[str], + context: T.Optional[str], + comments: T.Optional[str], +) -> T.Tuple[T.Optional[str], str]: + if translatable is not None and truthy(translatable): + if comments is not None: + comments = comments.replace("/*", " ").replace("*/", " ") + comments = f"/* Translators: {comments} */" + + if context is not None: + return comments, f'C_("{escape_quote(context)}", "{escape_quote(string)}")' + else: + return comments, f'_("{escape_quote(string)}")' + else: + return comments, f'"{escape_quote(string)}"' + + @decompiler("property", cdata=True) def decompile_property( - ctx, + ctx: DecompileCtx, gir, name, cdata, @@ -306,12 +325,12 @@ def decompile_property( flags += " bidirectional" ctx.print(f"{name}: bind-property {bind_source}.{bind_property}{flags};") elif truthy(translatable): - if context is not None: - ctx.print( - f'{name}: C_("{escape_quote(context)}", "{escape_quote(cdata)}");' - ) - else: - ctx.print(f'{name}: _("{escape_quote(cdata)}");') + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + if comments is not None: + ctx.print(comments) + ctx.print(f"{name}: {translatable};") elif gir is None or gir.properties.get(name) is None: ctx.print(f'{name}: "{escape_quote(cdata)}";') else: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 8f68647..a7334b1 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,3 +1,4 @@ +from .adw_message_dialog import Responses from .attributes import BaseAttribute, BaseTypedAttribute from .binding import Binding from .contexts import ValueTypeCtx @@ -56,6 +57,7 @@ OBJECT_CONTENT_HOOKS.children = [ Widgets, Items, Strings, + Responses, Child, ] diff --git a/blueprintcompiler/language/adw_message_dialog.py b/blueprintcompiler/language/adw_message_dialog.py new file mode 100644 index 0000000..2911735 --- /dev/null +++ b/blueprintcompiler/language/adw_message_dialog.py @@ -0,0 +1,131 @@ +# adw_message_dialog.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 ..decompiler import truthy, decompile_translatable +from .common import * +from .contexts import ValueTypeCtx +from .gobject_object import ObjectContent, validate_parent_type +from .values import QuotedLiteral, Translated + + +class Response(AstNode): + grammar = [ + UseIdent("id"), + Match(":").expected(), + AnyOf(QuotedLiteral, Translated).expected("a value"), + ZeroOrMore( + AnyOf(Keyword("destructive"), Keyword("suggested"), Keyword("disabled")) + ), + ] + + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def appearance(self) -> T.Optional[str]: + if "destructive" in self.tokens: + return "destructive" + if "suggested" in self.tokens: + return "suggested" + return None + + @property + def enabled(self) -> bool: + return "disabled" not in self.tokens + + @property + def value(self) -> T.Union[QuotedLiteral, Translated]: + return self.children[0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) + + @validate("id") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate response ID '{self.id}'", + check=lambda child: child.id == self.id, + ) + + +class Responses(AstNode): + grammar = [ + Keyword("responses"), + Match("[").expected(), + Delimited(Response, ","), + "]", + ] + + @property + def responses(self) -> T.List[Response]: + return self.children + + @validate("responses") + def container_is_message_dialog(self): + validate_parent_type(self, "Adw", "MessageDialog", "responses") + + @validate("responses") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate responses block") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Adw", "MessageDialog"), + matches=new_statement_patterns, +) +def style_completer(ast_node, match_variables): + yield Completion( + "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" + ) + + +@decompiler("responses") +def decompile_responses(ctx, gir): + ctx.print(f"responses [") + + +@decompiler("response", cdata=True) +def decompile_response( + ctx, + gir, + cdata, + id, + appearance=None, + enabled=None, + translatable=None, + context=None, + comments=None, +): + comments, translated = decompile_translatable( + cdata, translatable, context, comments + ) + if comments is not None: + ctx.print(comments) + + flags = "" + if appearance is not None: + flags += f" {appearance}" + if enabled is not None and not truthy(enabled): + flags += " disabled" + + ctx.print(f"{id}: {translated}{flags},") diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 6f71595..ad6e658 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -126,14 +126,17 @@ class XmlOutput(OutputFormat): xml.end_tag() def _translated_string_attrs( - self, translated: Translated + self, translated: T.Union[QuotedLiteral, Translated] ) -> T.Dict[str, T.Optional[str]]: - return { - "translatable": "true", - "context": translated.child.context - if isinstance(translated.child, TranslatedWithContext) - else None, - } + if isinstance(translated, QuotedLiteral): + return {} + else: + return { + "translatable": "true", + "context": translated.child.context + if isinstance(translated.child, TranslatedWithContext) + else None, + } def _emit_signal(self, signal: Signal, xml: XmlEmitter): name = signal.name @@ -270,6 +273,24 @@ class XmlOutput(OutputFormat): self._emit_attribute("property", "name", prop.name, prop.value, xml) xml.end_tag() + elif isinstance(extension, Responses): + xml.start_tag("responses") + for response in extension.responses: + # todo: translated + xml.start_tag( + "response", + id=response.id, + **self._translated_string_attrs(response.value), + enabled=None if response.enabled else "false", + appearance=response.appearance, + ) + if isinstance(response.value, Translated): + xml.put_text(response.value.child.string) + else: + xml.put_text(response.value.value) + xml.end_tag() + xml.end_tag() + elif isinstance(extension, Strings): xml.start_tag("items") for prop in extension.children: diff --git a/tests/samples/responses.blp b/tests/samples/responses.blp new file mode 100644 index 0000000..d7032a7 --- /dev/null +++ b/tests/samples/responses.blp @@ -0,0 +1,10 @@ +using Gtk 4.0; +using Adw 1; + +Adw.MessageDialog { + responses [ + cancel: _("Cancel"), + discard: _("Discard") destructive, + save: "Save" suggested disabled, + ] +} \ No newline at end of file diff --git a/tests/samples/responses.ui b/tests/samples/responses.ui new file mode 100644 index 0000000..ba26de5 --- /dev/null +++ b/tests/samples/responses.ui @@ -0,0 +1,11 @@ + + + + + + Cancel + Discard + Save + + + \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index c292214..9988bb6 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -143,6 +143,17 @@ class TestSamples(unittest.TestCase): raise AssertionError() def test_samples(self): + try: + import gi + + gi.require_version("Adw", "1") + from gi.repository import Adw + + have_adw = True + Adw.init() + except: + have_adw = False + self.assert_sample("accessibility") self.assert_sample("action_widgets") self.assert_sample("child_type") @@ -166,6 +177,7 @@ class TestSamples(unittest.TestCase): ) # The image resource doesn't exist self.assert_sample("property") self.assert_sample("property_binding") + self.assert_sample("responses", skip_run=not have_adw) self.assert_sample("signal", skip_run=True) # The callback doesn't exist self.assert_sample("size_group") self.assert_sample("string_list") @@ -257,6 +269,7 @@ class TestSamples(unittest.TestCase): self.assert_decompile("property") self.assert_decompile("property_binding_dec") self.assert_decompile("placeholder_dec") + self.assert_decompile("responses") self.assert_decompile("signal") self.assert_decompile("strings") self.assert_decompile("style_dec") From a2fb86bc3119fc31f0bb40b569c6439b6c91dcb0 Mon Sep 17 00:00:00 2001 From: Cameron Dehning Date: Sat, 8 Apr 2023 01:34:47 +0000 Subject: [PATCH 088/290] Builder list factory --- .gitignore | 4 ++- blueprintcompiler/language/__init__.py | 2 ++ .../language/gtk_list_item_factory.py | 32 +++++++++++++++++++ blueprintcompiler/outputs/xml/__init__.py | 10 ++++++ blueprintcompiler/outputs/xml/xml_emitter.py | 4 +++ docs/examples.rst | 15 +++++++++ tests/samples/list_factory.blp | 11 +++++++ tests/samples/list_factory.ui | 20 ++++++++++++ tests/test_samples.py | 1 + 9 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 blueprintcompiler/language/gtk_list_item_factory.py create mode 100644 tests/samples/list_factory.blp create mode 100644 tests/samples/list_factory.ui diff --git a/.gitignore b/.gitignore index 43e51bd..9aa4dd4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ coverage.xml /blueprint-regression-tests /corpus -/crashes \ No newline at end of file +/crashes + +.vscode \ No newline at end of file diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index a7334b1..d251fae 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,3 +1,4 @@ +from .gtk_list_item_factory import ListItemFactory from .adw_message_dialog import Responses from .attributes import BaseAttribute, BaseTypedAttribute from .binding import Binding @@ -57,6 +58,7 @@ OBJECT_CONTENT_HOOKS.children = [ Widgets, Items, Strings, + ListItemFactory, Responses, Child, ] diff --git a/blueprintcompiler/language/gtk_list_item_factory.py b/blueprintcompiler/language/gtk_list_item_factory.py new file mode 100644 index 0000000..d25b96d --- /dev/null +++ b/blueprintcompiler/language/gtk_list_item_factory.py @@ -0,0 +1,32 @@ +from .gobject_object import ObjectContent, validate_parent_type +from ..parse_tree import Keyword +from ..ast_utils import AstNode, validate + + +class ListItemFactory(AstNode): + grammar = [Keyword("template"), ObjectContent] + + @property + def gir_class(self): + return self.root.gir.get_type("ListItem", "Gtk") + + @validate("template") + def container_is_builder_list(self): + validate_parent_type( + self, + "Gtk", + "BuilderListItemFactory", + "sub-templates", + ) + + @property + def content(self) -> ObjectContent: + return self.children[ObjectContent][0] + + @property + def action_widgets(self): + """ + The sub-template shouldn't have it`s own actions this is + just hear to satisfy XmlOutput._emit_object_or_template + """ + return None diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index ad6e658..d6879fd 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -304,6 +304,16 @@ class XmlOutput(OutputFormat): self._emit_value(value, xml) xml.end_tag() xml.end_tag() + elif isinstance(extension, ListItemFactory): + child_xml = XmlEmitter() + child_xml.start_tag("interface") + child_xml.start_tag("template", **{"class": "GtkListItem"}) + self._emit_object_or_template(extension, child_xml) + child_xml.end_tag() + child_xml.end_tag() + xml.start_tag("property", name="bytes") + xml.put_cdata(child_xml.result) + xml.end_tag() elif isinstance(extension, Styles): xml.start_tag("style") diff --git a/blueprintcompiler/outputs/xml/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py index 374b406..3dd09a0 100644 --- a/blueprintcompiler/outputs/xml/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -62,6 +62,10 @@ class XmlEmitter: self.result += saxutils.escape(str(text)) self._needs_newline = False + def put_cdata(self, text: str): + self.result += f"" + self._needs_newline = False + def _indent(self): if self.indent is not None: self.result += "\n" + " " * (self.indent * len(self._tag_stack)) diff --git a/docs/examples.rst b/docs/examples.rst index 13b97a8..d1a9762 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -414,6 +414,21 @@ Gtk.SizeGroup Gtk.Label label1 {} Gtk.Label label2 {} +Gtk.BuilderListItemFactory +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: + + Gtk.ListView { + factory: Gtk.BuilderListItemFactory { + template { + child: Label { + label: "Hello"; + }; + } + }; + } + Gtk.StringList ~~~~~~~~~~~~~~ diff --git a/tests/samples/list_factory.blp b/tests/samples/list_factory.blp new file mode 100644 index 0000000..ead74c3 --- /dev/null +++ b/tests/samples/list_factory.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; + +Gtk.ListView { + factory: Gtk.BuilderListItemFactory list_item_factory { + template { + child: Label { + label: "Hello"; + }; + } + }; +} \ No newline at end of file diff --git a/tests/samples/list_factory.ui b/tests/samples/list_factory.ui new file mode 100644 index 0000000..664de85 --- /dev/null +++ b/tests/samples/list_factory.ui @@ -0,0 +1,20 @@ + + + + + + + + + +]]> + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 9988bb6..823488b 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -169,6 +169,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("flags") self.assert_sample("id_prop") self.assert_sample("layout") + self.assert_sample("list_factory") self.assert_sample("menu") self.assert_sample("numbers") self.assert_sample("object_prop") From 64879491a1143496453f069e493c4519cb00528b Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 7 Apr 2023 20:35:14 -0500 Subject: [PATCH 089/290] Fix mypy error --- blueprintcompiler/outputs/xml/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index d6879fd..8a2bd6c 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -45,7 +45,9 @@ class XmlOutput(OutputFormat): self._emit_object_or_template(obj, xml) xml.end_tag() - def _emit_object_or_template(self, obj: T.Union[Object, Template], xml: XmlEmitter): + def _emit_object_or_template( + self, obj: T.Union[Object, Template, ListItemFactory], xml: XmlEmitter + ): for child in obj.content.children: if isinstance(child, Property): self._emit_property(child, xml) From 88f5b4f1c7282b29836861ebecfb945e32e71792 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 28 Mar 2023 10:41:42 -0500 Subject: [PATCH 090/290] Fix template types --- blueprintcompiler/completions.py | 4 +- blueprintcompiler/gir.py | 64 ++++++++++++++++++- blueprintcompiler/language/common.py | 2 +- blueprintcompiler/language/expression.py | 2 +- .../language/gobject_property.py | 4 +- blueprintcompiler/language/gobject_signal.py | 4 +- .../language/gtkbuilder_template.py | 7 +- .../language/property_binding.py | 6 +- blueprintcompiler/language/types.py | 6 +- tests/samples/template_binding.blp | 5 ++ tests/samples/template_binding.ui | 13 ++++ tests/samples/template_binding_extern.blp | 5 ++ tests/samples/template_binding_extern.ui | 13 ++++ tests/test_samples.py | 6 ++ 14 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 tests/samples/template_binding.blp create mode 100644 tests/samples/template_binding.ui create mode 100644 tests/samples/template_binding_extern.blp create mode 100644 tests/samples/template_binding_extern.ui diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 9940055..6a944dd 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -120,7 +120,7 @@ def gtk_object_completer(ast_node, match_variables): matches=new_statement_patterns, ) def property_completer(ast_node, match_variables): - if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.UncheckedType): + if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): for prop in ast_node.gir_class.properties: yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;") @@ -144,7 +144,7 @@ def prop_value_completer(ast_node, match_variables): matches=new_statement_patterns, ) def signal_completer(ast_node, match_variables): - if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.UncheckedType): + if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): for signal in ast_node.gir_class.signals: if not isinstance(ast_node.parent, language.Object): name = "on" diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index a8831af..fc415fd 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -114,8 +114,12 @@ class GirType: """The name of the type in the GObject type system, suitable to pass to `g_type_from_name()`.""" raise NotImplementedError() + @property + def incomplete(self) -> bool: + return False -class UncheckedType(GirType): + +class ExternType(GirType): def __init__(self, name: str) -> None: super().__init__() self._name = name @@ -131,6 +135,10 @@ class UncheckedType(GirType): def glib_type_name(self) -> str: return self._name + @property + def incomplete(self) -> bool: + return True + class ArrayType(GirType): def __init__(self, inner: GirType) -> None: @@ -507,6 +515,60 @@ class Class(GirNode, GirType): yield from impl.signals.values() +class TemplateType(GirType): + def __init__(self, name: str, parent: T.Optional[Class]): + self._name = name + self.parent = parent + + @property + def name(self) -> str: + return self._name + + @property + def full_name(self) -> str: + return self._name + + @property + def glib_type_name(self) -> str: + return self._name + + @cached_property + def properties(self) -> T.Mapping[str, Property]: + if self.parent is None or isinstance(self.parent, ExternType): + return {} + else: + return self.parent.properties + + @cached_property + def signals(self) -> T.Mapping[str, Signal]: + if self.parent is None or isinstance(self.parent, ExternType): + return {} + else: + return self.parent.signals + + def assignable_to(self, other: "GirType") -> bool: + if self == other: + return True + elif isinstance(other, Interface): + # we don't know the template type's interfaces, assume yes + return True + elif self.parent is None or isinstance(self.parent, ExternType): + return isinstance(other, Class) + else: + return self.parent.assignable_to(other) + + @cached_property + def signature(self) -> str: + if self.parent is None: + return f"template {self.name}" + else: + return f"template {self.name} : {self.parent.full_name}" + + @property + def incomplete(self) -> bool: + return True + + class EnumMember(GirNode): def __init__(self, enum: "Enumeration", tl: typelib.Typelib) -> None: super().__init__(enum, tl) diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 734e59b..082aaa4 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -37,7 +37,7 @@ from ..gir import ( FloatType, GirType, Enumeration, - UncheckedType, + ExternType, ) from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index fca7f0e..69323f4 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -141,7 +141,7 @@ class LookupOp(InfixExpr): ], ) - if isinstance(self.lhs.type, UncheckedType) or not self.lhs.type_complete: + if self.lhs.type.incomplete: return elif not isinstance(self.lhs.type, gir.Class) and not isinstance( diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index b57f3b0..18b91ae 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -46,7 +46,7 @@ class Property(AstNode): @property def gir_property(self): - if self.gir_class is not None and not isinstance(self.gir_class, UncheckedType): + if self.gir_class is not None and not isinstance(self.gir_class, ExternType): return self.gir_class.properties.get(self.tokens["name"]) @context(ValueTypeCtx) @@ -75,7 +75,7 @@ class Property(AstNode): @validate("name") def property_exists(self): - if self.gir_class is None or isinstance(self.gir_class, UncheckedType): + if self.gir_class is None or self.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 diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 0c649b7..25d789b 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -72,7 +72,7 @@ class Signal(AstNode): @property def gir_signal(self): - if self.gir_class is not None and not isinstance(self.gir_class, UncheckedType): + if self.gir_class is not None and not isinstance(self.gir_class, ExternType): return self.gir_class.signals.get(self.tokens["name"]) @property @@ -90,7 +90,7 @@ class Signal(AstNode): @validate("name") def signal_exists(self): - if self.gir_class is None or isinstance(self.gir_class, UncheckedType): + if self.gir_class is None or self.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 diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 40ac7f7..46ff6e6 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -50,11 +50,10 @@ class Template(Object): @property def gir_class(self): - # Templates might not have a parent class defined - if class_name := self.class_name: - return class_name.gir_type + if self.class_name is None: + return gir.TemplateType(self.id, None) else: - return gir.UncheckedType(self.id) + return gir.TemplateType(self.id, self.class_name.gir_type) @validate("id") def unique_in_parent(self): diff --git a/blueprintcompiler/language/property_binding.py b/blueprintcompiler/language/property_binding.py index 37a5c91..5314934 100644 --- a/blueprintcompiler/language/property_binding.py +++ b/blueprintcompiler/language/property_binding.py @@ -108,11 +108,7 @@ class PropertyBinding(AstNode): gir_class = self.source_obj.gir_class - if ( - isinstance(self.source_obj, Template) - or gir_class is None - or isinstance(gir_class, UncheckedType) - ): + 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 diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 702ed32..510dbc7 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -20,7 +20,7 @@ import typing as T from .common import * -from ..gir import Class, Interface +from ..gir import Class, ExternType, Interface class TypeName(AstNode): @@ -70,7 +70,7 @@ class TypeName(AstNode): self.tokens["class_name"], self.tokens["namespace"] ) - return gir.UncheckedType(self.tokens["class_name"]) + return gir.ExternType(self.tokens["class_name"]) @property def glib_type_name(self) -> str: @@ -95,7 +95,7 @@ class ClassName(TypeName): def gir_class_exists(self): if ( self.gir_type is not None - and not isinstance(self.gir_type, UncheckedType) + and not isinstance(self.gir_type, ExternType) and not isinstance(self.gir_type, Class) ): if isinstance(self.gir_type, Interface): diff --git a/tests/samples/template_binding.blp b/tests/samples/template_binding.blp new file mode 100644 index 0000000..fa4d53e --- /dev/null +++ b/tests/samples/template_binding.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +template MyTemplate : Box { + prop1: bind MyTemplate.prop2 as ($MyObject).prop3; +} \ No newline at end of file diff --git a/tests/samples/template_binding.ui b/tests/samples/template_binding.ui new file mode 100644 index 0000000..7c8b49d --- /dev/null +++ b/tests/samples/template_binding.ui @@ -0,0 +1,13 @@ + + + + + diff --git a/tests/samples/template_binding_extern.blp b/tests/samples/template_binding_extern.blp new file mode 100644 index 0000000..a8a42c3 --- /dev/null +++ b/tests/samples/template_binding_extern.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +template MyTemplate : $MyParentClass { + prop1: bind MyTemplate.prop2 as ($MyObject).prop3; +} \ No newline at end of file diff --git a/tests/samples/template_binding_extern.ui b/tests/samples/template_binding_extern.ui new file mode 100644 index 0000000..2bbc88f --- /dev/null +++ b/tests/samples/template_binding_extern.ui @@ -0,0 +1,13 @@ + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 823488b..8ee9015 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -187,6 +187,12 @@ class TestSamples(unittest.TestCase): self.assert_sample( "template", skip_run=True ) # The template class doesn't exist + self.assert_sample( + "template_binding", skip_run=True + ) # The template class doesn't exist + self.assert_sample( + "template_binding_extern", skip_run=True + ) # The template class doesn't exist self.assert_sample( "template_no_parent", skip_run=True ) # The template class doesn't exist From d6bd282e58341b5a131e7bfa21852ce239fb94a2 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 9 Apr 2023 16:50:36 -0500 Subject: [PATCH 091/290] errors: Report version in compiler bug message --- blueprintcompiler/errors.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 4a18589..3fc2666 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -170,8 +170,11 @@ def assert_true(truth: bool, message: T.Optional[str] = None): def report_bug(): # pragma: no cover """Report an error and ask people to report it.""" + from . import main + print(traceback.format_exc()) - print(f"Arguments: {sys.argv}\n") + print(f"Arguments: {sys.argv}") + print(f"Version: {main.VERSION}\n") print( f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** The blueprint-compiler program has crashed. Please report the above stacktrace, From 02796fd830a09156de6a4b4e3b42cbbf94185d94 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 10 Apr 2023 09:38:56 -0500 Subject: [PATCH 092/290] Use <> instead of () for casts & typeof This makes it clearer that they aren't functions, and it eliminates syntactic ambiguity with closure expressions. --- blueprintcompiler/language/expression.py | 25 +++++++++++++++++++- blueprintcompiler/language/types.py | 9 ++++++++ blueprintcompiler/language/values.py | 28 ++++++++++++++++++++--- tests/sample_errors/warn_old_extern.err | 1 + tests/samples/expr_closure.blp | 2 +- tests/samples/expr_closure_args.blp | 2 +- tests/samples/expr_lookup.blp | 2 +- tests/samples/template_binding.blp | 2 +- tests/samples/template_binding_extern.blp | 2 +- tests/samples/typeof.blp | 4 ++-- 10 files changed, 66 insertions(+), 11 deletions(-) diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 69323f4..16ee33c 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -159,7 +159,17 @@ class LookupOp(InfixExpr): class CastExpr(InfixExpr): - grammar = ["as", "(", TypeName, ")"] + grammar = [ + "as", + AnyOf( + ["<", TypeName, Match(">").expected()], + [ + UseExact("lparen", "("), + TypeName, + UseExact("rparen", ")").expected("')'"), + ], + ), + ] @context(ValueTypeCtx) def value_type(self): @@ -183,6 +193,19 @@ class CastExpr(InfixExpr): f"Invalid cast. No instance of {self.lhs.type.full_name} can be an instance of {self.type.full_name}." ) + @validate("lparen", "rparen") + def upgrade_to_angle_brackets(self): + if self.tokens["lparen"]: + raise UpgradeWarning( + "Use angle bracket syntax introduced in blueprint 0.8.0", + actions=[ + CodeAction( + "Use <> instead of ()", + f"<{self.children[TypeName][0].as_string}>", + ) + ], + ) + class ClosureArg(AstNode): grammar = ExprChain diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 510dbc7..34e9558 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -89,6 +89,15 @@ class TypeName(AstNode): if self.gir_type: return self.gir_type.doc + @property + def as_string(self) -> str: + if self.tokens["extern"]: + return "$" + self.tokens["class_name"] + elif self.tokens["namespace"]: + return f"{self.tokens['namespace']}.{self.tokens['class_name']}" + else: + return self.tokens["class_name"] + class ClassName(TypeName): @validate("namespace", "class_name") diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index e446b29..0141a2f 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -74,9 +74,18 @@ class Translated(AstNode): class TypeLiteral(AstNode): grammar = [ "typeof", - "(", - to_parse_node(TypeName).expected("type name"), - Match(")").expected(), + AnyOf( + [ + "<", + to_parse_node(TypeName).expected("type name"), + Match(">").expected(), + ], + [ + UseExact("lparen", "("), + to_parse_node(TypeName).expected("type name"), + UseExact("rparen", ")").expected("')'"), + ], + ), ] @property @@ -93,6 +102,19 @@ class TypeLiteral(AstNode): if expected_type is not None and not isinstance(expected_type, gir.TypeType): raise CompileError(f"Cannot convert GType to {expected_type.full_name}") + @validate("lparen", "rparen") + def upgrade_to_angle_brackets(self): + if self.tokens["lparen"]: + raise UpgradeWarning( + "Use angle bracket syntax introduced in blueprint 0.8.0", + actions=[ + CodeAction( + "Use <> instead of ()", + f"<{self.children[TypeName][0].as_string}>", + ) + ], + ) + class QuotedLiteral(AstNode): grammar = UseQuoted("value") diff --git a/tests/sample_errors/warn_old_extern.err b/tests/sample_errors/warn_old_extern.err index c3b3fe2..5adf398 100644 --- a/tests/sample_errors/warn_old_extern.err +++ b/tests/sample_errors/warn_old_extern.err @@ -1,3 +1,4 @@ 3,1,8,Use the '$' extern syntax introduced in blueprint 0.8.0 +4,15,15,Use angle bracket syntax introduced in blueprint 0.8.0 4,16,13,Use the '$' extern syntax introduced in blueprint 0.8.0 5,14,7,Use the '$' extern syntax introduced in blueprint 0.8.0 \ No newline at end of file diff --git a/tests/samples/expr_closure.blp b/tests/samples/expr_closure.blp index 99874e8..81c3f2c 100644 --- a/tests/samples/expr_closure.blp +++ b/tests/samples/expr_closure.blp @@ -1,5 +1,5 @@ using Gtk 4.0; Label my-label { - label: bind ($my-closure(my-label.margin-bottom)) as (string); + label: bind ($my-closure(my-label.margin-bottom)) as ; } \ No newline at end of file diff --git a/tests/samples/expr_closure_args.blp b/tests/samples/expr_closure_args.blp index d09c881..5699094 100644 --- a/tests/samples/expr_closure_args.blp +++ b/tests/samples/expr_closure_args.blp @@ -1,5 +1,5 @@ using Gtk 4.0; Label { - label: bind $my-closure (true, 10, "Hello") as (string); + label: bind $my-closure (true, 10, "Hello") as ; } \ No newline at end of file diff --git a/tests/samples/expr_lookup.blp b/tests/samples/expr_lookup.blp index 2556f9a..12e2de1 100644 --- a/tests/samples/expr_lookup.blp +++ b/tests/samples/expr_lookup.blp @@ -5,5 +5,5 @@ Overlay { } Label { - label: bind (label.parent) as (Overlay).child as (Label).label; + label: bind (label.parent) as .child as