From 00a31d87bbac71133a7080a027e02371b511bdef Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 26 Nov 2022 17:20:04 -0600 Subject: [PATCH 001/241] 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 002/241] 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 003/241] 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 004/241] 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 005/241] 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 006/241] 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 007/241] 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 008/241] 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 009/241] 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 010/241] 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 011/241] 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 012/241] 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 013/241] 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 014/241] 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 015/241] 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 016/241] 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 017/241] 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 018/241] 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 019/241] 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 020/241] 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 021/241] 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 022/241] 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 023/241] 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 024/241] 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 025/241] 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 026/241] 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 027/241] 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 028/241] 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 029/241] 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 030/241] 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 031/241] 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 032/241] 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 033/241] 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 034/241] 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 035/241] 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 036/241] 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 037/241] 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 038/241] 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 039/241] 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 040/241] 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 041/241] 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 042/241] 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 043/241] 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