diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 3ab4fa2..220c117 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -8,7 +8,7 @@ in the NEWS file. 3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag. 4. Create a "Post-release version bump" commit. 5. Go to the Releases page in GitLab and create a new release from the tag. -6. Announce the release through relevant channels (Mastodon, TWIG, etc.) +6. Announce the release through relevant channels (Twitter, TWIG, etc.) ## Related projects diff --git a/NEWS.md b/NEWS.md index a12dab0..389f82c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,35 +1,3 @@ -# v0.16.0 - -## Added -- Added more "go to reference" implementations in the language server -- Added semantic token support for flag members in the language server -- Added property documentation to the hover tooltip for notify signals -- The language server now shows relevant sections of the reference documentation when hovering over keywords and symbols -- Added `not-swapped` flag to signal handlers, which may be needed for signal handlers that specify an object -- Added expression literals, which allow you to specify a Gtk.Expression property (as opposed to the existing expression support, which is for property bindings) - -## Changed -- The formatter adds trailing commas to lists (Alexey Yerin) -- The formatter removes trailing whitespace from comments (Alexey Yerin) -- Autocompleting a commonly translated property automatically adds the `_("")` syntax -- Marking a single-quoted string as translatable now generates a warning, since gettext does not recognize it when using the configuration recommended in the blueprint documentation - -## Fixed -- Added support for libgirepository-2.0 so that blueprint doesn't crash due to import conflicts on newer versions of PyGObject (Jordan Petridis) -- Fixed a bug when decompiling/porting files with enum values -- Fixed several issues where tests would fail with versions of GTK that added new deprecations -- Addressed a problem with the language server protocol in some editors (Luoyayu) -- Fixed an issue where the compiler would crash instead of reporting compiler errors -- Fixed a crash in the language server that occurred when a detailed signal (e.g. `notify::*`) was not complete -- The language server now properly implements the shutdown command, fixing support for some editors and improving robustness when restarting (Alexey Yerin) -- Marking a string in an array as translatable now generates an error, since it doesn't work -- - -## Documentation -- Added mention of `null` in the Literal Values section -- Add apps to Built with Blueprint section (Benedek Dévényi, Vladimir Vaskov) -- Corrected and updated many parts of the documentation - # v0.14.0 ## Added diff --git a/blueprintcompiler/annotations.py b/blueprintcompiler/annotations.py deleted file mode 100644 index c40de13..0000000 --- a/blueprintcompiler/annotations.py +++ /dev/null @@ -1,191 +0,0 @@ -# annotations.py -# -# Copyright 2024 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 - -# Extra information about types in common libraries that's used for things like completions. - -import typing as T -from dataclasses import dataclass - -from . import gir - - -@dataclass -class Annotation: - translatable_properties: T.List[str] - - -def is_property_translated(property: gir.Property): - ns = property.get_containing(gir.Namespace) - ns_name = ns.name + "-" + ns.version - if annotation := _ANNOTATIONS.get(ns_name): - assert property.container is not None - return ( - property.container.name + ":" + property.name - in annotation.translatable_properties - ) - else: - return False - - -_ANNOTATIONS = { - "Gtk-4.0": Annotation( - translatable_properties=[ - "AboutDialog:comments", - "AboutDialog:translator-credits", - "AboutDialog:website-label", - "AlertDialog:detail", - "AlertDialog:message", - "AppChooserButton:heading", - "AppChooserDialog:heading", - "AppChooserWidget:default-text", - "AssistantPage:title", - "Button:label", - "CellRendererText:markup", - "CellRendererText:placeholder-text", - "CellRendererText:text", - "CheckButton:label", - "ColorButton:title", - "ColorDialog:title", - "ColumnViewColumn:title", - "ColumnViewRow:accessible-description", - "ColumnViewRow:accessible-label", - "Entry:placeholder-text", - "Entry:primary-icon-tooltip-markup", - "Entry:primary-icon-tooltip-text", - "Entry:secondary-icon-tooltip-markup", - "Entry:secondary-icon-tooltip-text", - "EntryBuffer:text", - "Expander:label", - "FileChooserNative:accept-label", - "FileChooserNative:cancel-label", - "FileChooserWidget:subtitle", - "FileDialog:accept-label", - "FileDialog:title", - "FileDialog:initial-name", - "FileFilter:name", - "FontButton:title", - "FontDialog:title", - "Frame:label", - "Inscription:markup", - "Inscription:text", - "Label:label", - "ListItem:accessible-description", - "ListItem:accessible-label", - "LockButton:text-lock", - "LockButton:text-unlock", - "LockButton:tooltip-lock", - "LockButton:tooltip-not-authorized", - "LockButton:tooltip-unlock", - "MenuButton:label", - "MessageDialog:secondary-text", - "MessageDialog:text", - "NativeDialog:title", - "NotebookPage:menu-label", - "NotebookPage:tab-label", - "PasswordEntry:placeholder-text", - "Picture:alternative-text", - "PrintDialog:accept-label", - "PrintDialog:title", - "Printer:name", - "PrintJob:title", - "PrintOperation:custom-tab-label", - "PrintOperation:export-filename", - "PrintOperation:job-name", - "ProgressBar:text", - "SearchEntry:placeholder-text", - "ShortcutLabel:disabled-text", - "ShortcutsGroup:title", - "ShortcutsSection:title", - "ShortcutsShortcut:title", - "ShortcutsShortcut:subtitle", - "StackPage:title", - "Text:placeholder-text", - "TextBuffer:text", - "TreeViewColumn:title", - "Widget:tooltip-markup", - "Widget:tooltip-text", - "Window:title", - "Editable:text", - "FontChooser:preview-text", - ] - ), - "Adw-1": Annotation( - translatable_properties=[ - "AboutDialog:comments", - "AboutDialog:translator-credits", - "AboutWindow:comments", - "AboutWindow:translator-credits", - "ActionRow:subtitle", - "ActionRow:title", - "AlertDialog:body", - "AlertDialog:heading", - "Avatar:text", - "Banner:button-label", - "Banner:title", - "ButtonContent:label", - "Dialog:title", - "ExpanderRow:subtitle", - "MessageDialog:body", - "MessageDialog:heading", - "NavigationPage:title", - "PreferencesGroup:description", - "PreferencesGroup:title", - "PreferencesPage:description", - "PreferencesPage:title", - "PreferencesRow:title", - "SplitButton:dropdown-tooltip", - "SplitButton:label", - "StatusPage:description", - "StatusPage:title", - "TabPage:indicator-tooltip", - "TabPage:keyword", - "TabPage:title", - "Toast:button-label", - "Toast:title", - "ViewStackPage:title", - "ViewSwitcherTitle:subtitle", - "ViewSwitcherTitle:title", - "WindowTitle:subtitle", - "WindowTitle:title", - ] - ), - "Shumate-1.0": Annotation( - translatable_properties=[ - "License:extra-text", - "MapSource:license", - "MapSource:name", - ] - ), - "GtkSource-5": Annotation( - translatable_properties=[ - "CompletionCell:markup", - "CompletionCell:text", - "CompletionSnippets:title", - "CompletionWords:title", - "GutterRendererText:markup", - "GutterRendererText:text", - "SearchSettings:search-text", - "Snippet:description", - "Snippet:name", - "SnippetChunk:tooltip-text", - "StyleScheme:description", - "StyleScheme:name", - ] - ), -} diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 8f742e0..bd5befa 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -160,11 +160,6 @@ class AstNode: yield e if e.fatal: return - except MultipleErrors as e: - for error in e.errors: - yield error - if error.fatal: - return for child in self.children: yield from child._get_errors() @@ -254,7 +249,14 @@ def validate( if skip_incomplete and self.incomplete: return - def fill_error(e: CompileError): + try: + func(self) + except CompileError as e: + # If the node is only partially complete, then an error must + # have already been reported at the parsing stage + if self.incomplete: + return + if e.range is None: e.range = ( Range.join( @@ -264,26 +266,8 @@ def validate( or self.range ) - try: - func(self) - except CompileError as e: - # If the node is only partially complete, then an error must - # have already been reported at the parsing stage - if self.incomplete: - return - - fill_error(e) - # Re-raise the exception raise e - except MultipleErrors as e: - if self.incomplete: - return - - for error in e.errors: - fill_error(error) - - raise e inner._validator = True return inner diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index b10ec3e..e05d6ee 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -17,9 +17,10 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import sys import typing as T -from . import annotations, gir, language +from . import gir, language from .ast_utils import AstNode from .completions_utils import * from .language.types import ClassName @@ -30,6 +31,10 @@ from .tokenizer import Token, TokenType Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] +def debug(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + def _complete( lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int ) -> T.Iterator[Completion]: @@ -134,7 +139,7 @@ def gtk_object_completer(lsp, ast_node, match_variables): matches=new_statement_patterns, ) def property_completer(lsp, ast_node, match_variables): - if ast_node.gir_class and hasattr(ast_node.gir_class, "properties"): + if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): for prop_name, prop in ast_node.gir_class.properties.items(): if ( isinstance(prop.type, gir.BoolType) @@ -149,17 +154,11 @@ def property_completer(lsp, ast_node, match_variables): detail=prop.detail, ) elif isinstance(prop.type, gir.StringType): - snippet = ( - f'{prop_name}: _("$0");' - if annotations.is_property_translated(prop) - else f'{prop_name}: "$0";' - ) - yield Completion( prop_name, CompletionItemKind.Property, sort_text=f"0 {prop_name}", - snippet=snippet, + snippet=f'{prop_name}: "$0";', docs=prop.doc, detail=prop.detail, ) @@ -177,15 +176,6 @@ def property_completer(lsp, ast_node, match_variables): docs=prop.doc, detail=prop.detail, ) - elif prop.type.full_name == "Gtk.Expression": - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: expr $0;", - docs=prop.doc, - detail=prop.detail, - ) else: yield Completion( prop_name, @@ -198,7 +188,7 @@ def property_completer(lsp, ast_node, match_variables): @completer( - applies_in=[language.Property, language.A11yProperty], + applies_in=[language.Property, language.BaseAttribute], matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) def prop_value_completer(lsp, ast_node, match_variables): @@ -222,7 +212,7 @@ def prop_value_completer(lsp, ast_node, match_variables): matches=new_statement_patterns, ) def signal_completer(lsp, ast_node, match_variables): - if ast_node.gir_class and hasattr(ast_node.gir_class, "signals"): + if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): for signal_name, signal in ast_node.gir_class.signals.items(): if not isinstance(ast_node.parent, language.Object): name = "on" diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index eccf125..03bec0f 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -31,6 +31,17 @@ new_statement_patterns = [ ] +def applies_to(*ast_types): + """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 decorator(func): def inner(prev_tokens: T.List[Token], ast_node, lsp): diff --git a/blueprintcompiler/formatter.py b/blueprintcompiler/formatter.py index f438675..c003d45 100644 --- a/blueprintcompiler/formatter.py +++ b/blueprintcompiler/formatter.py @@ -20,8 +20,7 @@ import re from enum import Enum -from . import tokenizer -from .errors import CompilerBugError +from . import tokenizer, utils from .tokenizer import TokenType OPENING_TOKENS = ("{", "[") @@ -146,10 +145,8 @@ def format(data, tab_size=2, insert_space=True): is_child_type = False elif str_item in CLOSING_TOKENS: - if str_item == "]" and str(last_not_whitespace) != "[": + if str_item == "]" and last_not_whitespace != ",": current_line = current_line[:-1] - if str(last_not_whitespace) != ",": - current_line += "," commit_current_line() current_line = "]" elif str(last_not_whitespace) in OPENING_TOKENS: @@ -193,13 +190,10 @@ def format(data, tab_size=2, insert_space=True): elif prev_line_type in require_extra_newline: newlines = 2 - current_line = "\n".join( - [line.rstrip() for line in current_line.split("\n")] - ) commit_current_line(LineType.COMMENT, newlines_before=newlines) - else: # pragma: no cover - raise CompilerBugError() + else: + commit_current_line() elif str_item == "(" and ( re.match(r"^([A-Za-z_\-])+\s*\(", current_line) or watch_parentheses diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 333f4ac..30a5eaa 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -24,20 +24,8 @@ from functools import cached_property import gi # type: ignore -try: - gi.require_version("GIRepository", "3.0") - from gi.repository import GIRepository # type: ignore - - _repo = GIRepository.Repository() -except ValueError: - # We can remove this once we can bump the minimum dependencies - # to glib 2.80 and pygobject 3.52 - # dependency('glib-2.0', version: '>= 2.80.0') - # dependency('girepository-2.0', version: '>= 2.80.0') - gi.require_version("GIRepository", "2.0") - from gi.repository import GIRepository # type: ignore - - _repo = GIRepository.Repository +gi.require_version("GIRepository", "2.0") +from gi.repository import GIRepository # type: ignore from . import typelib, xml_reader from .errors import CompileError, CompilerBugError @@ -54,7 +42,7 @@ def add_typelib_search_path(path: str): def get_namespace(namespace: str, version: str) -> "Namespace": - search_paths = [*_repo.get_search_path(), *_user_search_paths] + search_paths = [*GIRepository.Repository.get_search_path(), *_user_search_paths] filename = f"{namespace}-{version}.typelib" @@ -86,7 +74,7 @@ def get_available_namespaces() -> T.List[T.Tuple[str, str]]: return _available_namespaces search_paths: list[str] = [ - *_repo.get_search_path(), + *GIRepository.Repository.get_search_path(), *_user_search_paths, ] @@ -467,13 +455,10 @@ class Signature(GirNode): return result @cached_property - def return_type(self) -> T.Optional[GirType]: - if self.tl.SIGNATURE_RETURN_TYPE == 0: - return None - else: - return self.get_containing(Repository)._resolve_type_id( - self.tl.SIGNATURE_RETURN_TYPE - ) + def return_type(self) -> GirType: + return self.get_containing(Repository)._resolve_type_id( + self.tl.SIGNATURE_RETURN_TYPE + ) class Signal(GirNode): @@ -493,10 +478,7 @@ class Signal(GirNode): args = ", ".join( [f"{a.type.full_name} {a.name}" for a in self.gir_signature.args] ) - result = f"signal {self.container.full_name}::{self.name} ({args})" - if self.gir_signature.return_type is not None: - result += f" -> {self.gir_signature.return_type.full_name}" - return result + return f"signal {self.container.full_name}::{self.name} ({args})" @property def online_docs(self) -> T.Optional[str]: @@ -908,6 +890,14 @@ class Namespace(GirNode): if isinstance(entry, Class) } + @cached_property + 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) -> T.Optional[GirType]: """Gets a type (class, interface, enum, etc.) from this namespace.""" return self.entries.get(name) @@ -931,8 +921,13 @@ class Namespace(GirNode): """Looks up a type in the scope of this namespace (including in the namespace's dependencies).""" - ns, name = type_name.split(".", 1) - return self.get_containing(Repository).get_type(name, ns) + if type_name in _BASIC_TYPES: + return _BASIC_TYPES[type_name]() + elif "." in type_name: + ns, name = type_name.split(".", 1) + return self.get_containing(Repository).get_type(name, ns) + else: + return self.get_type(type_name) @property def online_docs(self) -> T.Optional[str]: @@ -951,7 +946,7 @@ class Repository(GirNode): self.includes = { name: get_namespace(name, version) for name, version in deps } - except: # pragma: no cover + except: raise CompilerBugError(f"Failed to load dependencies.") else: self.includes = {} @@ -959,6 +954,12 @@ class Repository(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[GirType]: + 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.""" if ns == self.namespace.name: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 5eb2b60..b302686 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -4,6 +4,7 @@ from .adw_breakpoint import ( AdwBreakpointSetters, ) from .adw_response_dialog import ExtAdwResponseDialog +from .attributes import BaseAttribute from .binding import Binding from .common import * from .contexts import ScopeCtx, ValueTypeCtx @@ -19,7 +20,7 @@ from .expression import ( from .gobject_object import Object, ObjectContent from .gobject_property import Property from .gobject_signal import Signal -from .gtk_a11y import A11yProperty, ExtAccessibility +from .gtk_a11y import ExtAccessibility from .gtk_combo_box_text import ExtComboBoxItems from .gtk_file_filter import ( Filters, @@ -41,7 +42,6 @@ from .types import ClassName from .ui import UI from .values import ( ArrayValue, - ExprValue, Flag, Flags, IdentLiteral, diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index d2680fd..5493d4d 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -20,6 +20,7 @@ from ..decompiler import decompile_translatable, truthy from .common import * +from .contexts import ValueTypeCtx from .gobject_object import ObjectContent, validate_parent_type from .values import StringValue @@ -93,6 +94,10 @@ class ExtAdwResponseDialogResponse(AstNode): self.value.range.text, ) + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) + @validate("id") def unique_in_parent(self): self.validate_unique_in_parent( diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py new file mode 100644 index 0000000..8ff1f0b --- /dev/null +++ b/blueprintcompiler/language/attributes.py @@ -0,0 +1,32 @@ +# attributes.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * + + +class BaseAttribute(AstNode): + """A helper class for attribute syntax of the form `name: literal_value;`""" + + tag_name: str = "" + attr_name: str = "name" + + @property + def name(self): + return self.tokens["name"] diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index 6e26048..c5e97b3 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -79,9 +79,3 @@ class ScopeCtx: for child in node.children: if child.context[ScopeCtx] is self: yield from self._iter_recursive(child) - - -@dataclass -class ExprValueCtx: - """Indicates that the context is an expression literal, where the - "item" keyword may be used.""" diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index e0b4246..ae9c399 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -38,6 +38,10 @@ class ExprBase(AstNode): def type(self) -> T.Optional[GirType]: raise NotImplementedError() + @property + def type_complete(self) -> bool: + return True + @property def rhs(self) -> T.Optional["ExprBase"]: if isinstance(self.parent, Expression): @@ -61,6 +65,10 @@ class Expression(ExprBase): def type(self) -> T.Optional[GirType]: return self.last.type + @property + def type_complete(self) -> bool: + return self.last.type_complete + class InfixExpr(ExprBase): @property @@ -81,16 +89,6 @@ class LiteralExpr(ExprBase): or self.root.is_legacy_template(self.literal.value.ident) ) - @property - def is_this(self) -> bool: - from .values import IdentLiteral - - return ( - not self.is_object - and isinstance(self.literal.value, IdentLiteral) - and self.literal.value.ident == "item" - ) - @property def literal(self): from .values import Literal @@ -101,14 +99,14 @@ class LiteralExpr(ExprBase): def type(self) -> T.Optional[GirType]: return self.literal.value.type - @validate() - def item_validations(self): - if self.is_this: - if not isinstance(self.rhs, CastExpr): - raise CompileError('"item" must be cast to its object type') + @property + def type_complete(self) -> bool: + from .values import IdentLiteral - if not isinstance(self.rhs.rhs, LookupOp): - raise CompileError('"item" can only be used for looking up properties') + if isinstance(self.literal.value, IdentLiteral): + if object := self.context[ScopeCtx].objects.get(self.literal.value.ident): + return not object.gir_class.incomplete + return True class LookupOp(InfixExpr): @@ -213,6 +211,10 @@ 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.type is None or self.lhs.type is None: @@ -304,9 +306,6 @@ expr.children = [ def decompile_lookup( ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str ): - if ctx.parent_node is not None and ctx.parent_node.tag == "property": - ctx.print("expr ") - if t := ctx.type_by_cname(type): type = decompile.full_name(t) else: @@ -326,8 +325,6 @@ def decompile_lookup( if constant is not None: if constant == ctx.template_class: ctx.print("template." + name) - elif constant == "": - ctx.print("item as <" + type + ">." + name) else: ctx.print(constant + "." + name) return @@ -342,9 +339,6 @@ def decompile_lookup( def decompile_constant( ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None ): - if ctx.parent_node is not None and ctx.parent_node.tag == "property": - ctx.print("expr ") - if type is None: if cdata == ctx.template_class: ctx.print("template") @@ -357,9 +351,6 @@ def decompile_constant( @decompiler("closure", skip_children=True) def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str): - if ctx.parent_node is not None and ctx.parent_node.tag == "property": - ctx.print("expr ") - if t := ctx.type_by_cname(type): type = decompile.full_name(t) else: diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 1def15b..54cb297 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -28,18 +28,7 @@ from .common import * from .response_id import ExtResponse from .types import ClassName, ConcreteClassName -RESERVED_IDS = { - "this", - "self", - "template", - "true", - "false", - "null", - "none", - "item", - "expr", - "typeof", -} +RESERVED_IDS = {"this", "self", "template", "true", "false", "null", "none"} class ObjectContent(AstNode): diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 50a7512..5d0c867 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -21,12 +21,13 @@ from .binding import Binding from .common import * from .contexts import ValueTypeCtx -from .values import ArrayValue, ExprValue, ObjectValue, Value +from .gtkbuilder_template import Template +from .values import ArrayValue, ObjectValue, Value class Property(AstNode): grammar = Statement( - UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue) + UseIdent("name"), ":", AnyOf(Binding, ObjectValue, Value, ArrayValue) ) @property @@ -34,7 +35,7 @@ class Property(AstNode): return self.tokens["name"] @property - def value(self) -> T.Union[Binding, ExprValue, ObjectValue, Value, ArrayValue]: + def value(self) -> T.Union[Binding, ObjectValue, Value, ArrayValue]: return self.children[0] @property @@ -50,7 +51,7 @@ class Property(AstNode): @property def document_symbol(self) -> DocumentSymbol: - if isinstance(self.value, ObjectValue) or self.value is None: + if isinstance(self.value, ObjectValue): detail = None else: detail = self.value.range.text diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 9c27b97..79f9ae7 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -27,7 +27,6 @@ from .gtkbuilder_template import Template class SignalFlag(AstNode): grammar = AnyOf( UseExact("flag", "swapped"), - UseExact("flag", "not-swapped"), UseExact("flag", "after"), ) @@ -41,27 +40,6 @@ class SignalFlag(AstNode): f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag ) - @validate() - def swapped_exclusive(self): - if self.flag in ["swapped", "not-swapped"]: - self.validate_unique_in_parent( - "'swapped' and 'not-swapped' flags cannot be used together", - lambda x: x.flag in ["swapped", "not-swapped"], - ) - - @validate() - def swapped_unnecessary(self): - if self.flag == "not-swapped" and self.parent.object_id is None: - raise CompileWarning( - "'not-swapped' is the default for handlers that do not specify an object", - actions=[CodeAction("Remove 'not-swapped' flag", "")], - ) - elif self.flag == "swapped" and self.parent.object_id is not None: - raise CompileWarning( - "'swapped' is the default for handlers that specify an object", - actions=[CodeAction("Remove 'swapped' flag", "")], - ) - @docs() def ref_docs(self): return get_docs_section("Syntax Signal") @@ -114,17 +92,9 @@ class Signal(AstNode): def flags(self) -> T.List[SignalFlag]: return self.children[SignalFlag] - # Returns True if the "swapped" flag is present, False if "not-swapped" is present, and None if neither are present. - # GtkBuilder's default if swapped is not specified is to not swap the arguments if no object is specified, and to - # swap them if an object is specified. @property - def is_swapped(self) -> T.Optional[bool]: - for flag in self.flags: - if flag.flag == "swapped": - return True - elif flag.flag == "not-swapped": - return False - return None + def is_swapped(self) -> bool: + return any(x.flag == "swapped" for x in self.flags) @property def is_after(self) -> bool: @@ -143,17 +113,16 @@ class Signal(AstNode): @property def document_symbol(self) -> DocumentSymbol: - detail = self.ranges["detail_start", "detail_end"] return DocumentSymbol( self.full_name, SymbolKind.Event, self.range, self.group.tokens["name"].range, - detail.text if detail is not None else None, + self.ranges["detail_start", "detail_end"].text, ) def get_reference(self, idx: int) -> T.Optional[LocationLink]: - if self.object_id is not None and idx in self.group.tokens["object"].range: + if idx in self.group.tokens["object"].range: obj = self.context[ScopeCtx].objects.get(self.object_id) if obj is not None: return LocationLink( @@ -225,16 +194,15 @@ class Signal(AstNode): @decompiler("signal") -def decompile_signal(ctx, gir, name, handler, swapped=None, after="false", object=None): +def decompile_signal( + ctx, gir, name, handler, swapped="false", after="false", object=None +): object_name = object or "" name = name.replace("_", "-") line = f"{name} => ${handler}({object_name})" if decompile.truthy(swapped): line += " swapped" - elif swapped is not None: - line += " not-swapped" - if decompile.truthy(after): line += " after" diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 0cc3cb3..3657565 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -19,6 +19,8 @@ import typing as T +from ..decompiler import escape_quote +from .attributes import BaseAttribute from .common import * from .contexts import ValueTypeCtx from .gobject_object import ObjectContent, validate_parent_type @@ -117,7 +119,7 @@ def _get_docs(gir, name): return gir_type.doc -class A11yProperty(AstNode): +class A11yProperty(BaseAttribute): grammar = Statement( UseIdent("name"), ":", diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 32b3486..312750a 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -19,6 +19,7 @@ from .common import * +from .contexts import ValueTypeCtx from .gobject_object import ObjectContent, validate_parent_type from .values import StringValue diff --git a/blueprintcompiler/language/gtk_list_item_factory.py b/blueprintcompiler/language/gtk_list_item_factory.py index c9e1399..3309c08 100644 --- a/blueprintcompiler/language/gtk_list_item_factory.py +++ b/blueprintcompiler/language/gtk_list_item_factory.py @@ -50,7 +50,7 @@ class ExtListItemFactory(AstNode): else: return self.root.gir.get_type("ListItem", "Gtk") - @validate("id") + @validate("template") def container_is_builder_list(self): validate_parent_type( self, @@ -59,7 +59,7 @@ class ExtListItemFactory(AstNode): "sub-templates", ) - @validate("id") + @validate("template") def unique_in_parent(self): self.validate_unique_in_parent("Duplicate template block") @@ -76,7 +76,7 @@ class ExtListItemFactory(AstNode): f"Only Gtk.ListItem, Gtk.ListHeader, Gtk.ColumnViewRow, or Gtk.ColumnViewCell is allowed as a type here" ) - @validate("id") + @validate("template") def type_name_upgrade(self): if self.type_name is None: raise UpgradeWarning( @@ -103,7 +103,10 @@ class ExtListItemFactory(AstNode): @property def action_widgets(self): - # The sub-template shouldn't have its own actions, this is just here to satisfy XmlOutput._emit_object_or_template + """ + The sub-template shouldn't have it`s own actions this is + just hear to satisfy XmlOutput._emit_object_or_template + """ return None @docs("id") diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index 2d4bcf6..3060bea 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -59,8 +59,14 @@ class GtkDirective(AstNode): @property def gir_namespace(self): - # For better error handling, just assume it's 4.0 - return gir.get_namespace("Gtk", "4.0") + # validate the GTK version first to make sure the more specific error + # message is emitted + 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") @docs() def ref_docs(self): @@ -84,7 +90,7 @@ class Import(AstNode): @validate("namespace", "version") def namespace_exists(self): - gir.get_namespace(self.namespace, self.version) + gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) @validate() def unused(self): @@ -100,7 +106,7 @@ class Import(AstNode): @property def gir_namespace(self): try: - return gir.get_namespace(self.namespace, self.version) + return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) except CompileError: return None diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 833a4a3..96787ee 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -23,8 +23,7 @@ from blueprintcompiler.gir import ArrayType from blueprintcompiler.lsp_utils import SemanticToken from .common import * -from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx -from .expression import Expression +from .contexts import ScopeCtx, ValueTypeCtx from .gobject_object import Object from .types import TypeName @@ -58,19 +57,6 @@ class Translated(AstNode): f"Cannot convert translated string to {expected_type.full_name}" ) - @validate("context") - def context_double_quoted(self): - if self.translate_context is None: - return - - if not str(self.group.tokens["context"]).startswith('"'): - raise CompileWarning("gettext may not recognize single-quoted strings") - - @validate("string") - def string_double_quoted(self): - if not str(self.group.tokens["string"]).startswith('"'): - raise CompileWarning("gettext may not recognize single-quoted strings") - @docs() def ref_docs(self): return get_docs_section("Syntax Translated") @@ -333,12 +319,7 @@ class IdentLiteral(AstNode): if self.ident == "null": if not self.context[ValueTypeCtx].allow_null: raise CompileError("null is not permitted here") - elif self.ident == "item": - if not self.context[ExprValueCtx]: - raise CompileError( - '"item" can only be used in an expression literal' - ) - elif self.ident not in ["true", "false"]: + else: raise CompileError( f"Could not find object with ID {self.ident}", did_you_mean=( @@ -426,35 +407,6 @@ class ObjectValue(AstNode): ) -class ExprValue(AstNode): - grammar = [Keyword("expr"), Expression] - - @property - def expression(self) -> Expression: - return self.children[Expression][0] - - @validate("expr") - def validate_for_type(self) -> None: - expected_type = self.parent.context[ValueTypeCtx].value_type - expr_type = self.root.gir.get_type("Expression", "Gtk") - if expected_type is not None and not expected_type.assignable_to(expr_type): - raise CompileError( - f"Cannot convert Gtk.Expression to {expected_type.full_name}" - ) - - @docs("expr") - def ref_docs(self): - return get_docs_section("Syntax ExprValue") - - @context(ExprValueCtx) - def expr_literal(self): - return ExprValueCtx() - - @context(ValueTypeCtx) - def value_type(self): - return ValueTypeCtx(None, must_infer_type=True) - - class Value(AstNode): grammar = AnyOf(Translated, Flags, Literal) @@ -500,14 +452,6 @@ class ArrayValue(AstNode): range=quoted_literal.range, ) ) - elif isinstance(value.child, Translated): - errors.append( - CompileError( - "Arrays can't contain translated strings", - range=value.child.range, - ) - ) - if len(errors) > 0: raise MultipleErrors(errors) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index c4076b4..0659154 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -118,7 +118,6 @@ class LanguageServer: self.client_capabilities = {} self.client_supports_completion_choice = False self._open_files: T.Dict[str, OpenFile] = {} - self._exited = False def run(self): # Read tags from gir files. During normal compilation these are @@ -126,7 +125,7 @@ class LanguageServer: xml_reader.PARSE_GIR.add("doc") try: - while not self._exited: + while True: line = "" content_len = -1 while content_len == -1 or (line != "\n" and line != "\r\n"): @@ -222,14 +221,6 @@ class LanguageServer: }, ) - @command("shutdown") - def shutdown(self, id, params): - self._send_response(id, None) - - @command("exit") - def exit(self, id, params): - self._exited = True - @command("textDocument/didOpen") def didOpen(self, id, params): doc = params.get("textDocument") diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 5c03761..5e43834 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -134,11 +134,6 @@ class XmlOutput(OutputFormat): self._emit_expression(value.expression, xml) xml.end_tag() - elif isinstance(value, ExprValue): - xml.start_tag("property", **props) - self._emit_expression(value.expression, xml) - xml.end_tag() - elif isinstance(value, ObjectValue): xml.start_tag("property", **props) self._emit_object(value.object, xml) @@ -174,7 +169,7 @@ class XmlOutput(OutputFormat): "signal", name=name, handler=signal.handler, - swapped=signal.is_swapped, + swapped=signal.is_swapped or None, after=signal.is_after or None, object=( self._object_id(signal, signal.object_id) if signal.object_id else None @@ -223,6 +218,12 @@ class XmlOutput(OutputFormat): xml.put_text( "|".join([str(flag.value or flag.name) for flag in value.child.flags]) ) + elif isinstance(value.child, Translated): + raise CompilerBugError("translated values must be handled in the parent") + 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() @@ -244,9 +245,6 @@ class XmlOutput(OutputFormat): raise CompilerBugError() def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter): - if expr.is_this: - return - if expr.is_object: xml.start_tag("constant") else: @@ -368,13 +366,12 @@ class XmlOutput(OutputFormat): elif isinstance(extension, ExtScaleMarks): xml.start_tag("marks") - for mark in extension.marks: - label = mark.label.child if mark.label is not None else None + for mark in extension.children: xml.start_tag( "mark", value=mark.value, position=mark.position, - **self._translated_string_attrs(label), + **self._translated_string_attrs(mark.label and mark.label.child), ) if mark.label is not None: xml.put_text(mark.label.string) diff --git a/blueprintcompiler/outputs/xml/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py index ea91e03..ca87a49 100644 --- a/blueprintcompiler/outputs/xml/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -40,9 +40,7 @@ class XmlEmitter: self._tag_stack = [] self._needs_newline = False - def start_tag( - self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None, float] - ): + def start_tag(self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None]): self._indent() self.result += f"<{tag}" for key, val in attrs.items(): diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index ae062fb..fff6e4a 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -95,11 +95,19 @@ class ParseGroup: try: return self.ast_type(self, children, self.keys, incomplete=self.incomplete) - except TypeError: # pragma: no cover + except TypeError as e: 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] + ) + return result.replace("\n", "\n ") + class ParseContext: """Contains the state of the parser.""" @@ -257,6 +265,10 @@ class ParseNode: """Convenience method for err().""" return self.err("Expected " + expect) + def warn(self, message) -> "Warning": + """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.""" @@ -278,6 +290,27 @@ class Err(ParseNode): return True +class Warning(ParseNode): + """ParseNode that emits a compile warning if it parses successfully.""" + + def __init__(self, child, message: str): + self.child = to_parse_node(child) + self.message = message + + def _parse(self, ctx: ParseContext): + ctx.skip() + start_idx = ctx.index + 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) + ) + return True + else: + return False + + class Fail(ParseNode): """ParseNode that emits a compile error if it parses successfully.""" diff --git a/docs/flatpak.rst b/docs/flatpak.rst index 86112cf..0071d2f 100644 --- a/docs/flatpak.rst +++ b/docs/flatpak.rst @@ -17,7 +17,7 @@ a module in your flatpak manifest: { "type": "git", "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", - "tag": "v0.16.0" + "tag": "v0.14.0" } ] } diff --git a/docs/reference/expressions.rst b/docs/reference/expressions.rst index 3d523d1..8688ff0 100644 --- a/docs/reference/expressions.rst +++ b/docs/reference/expressions.rst @@ -42,8 +42,8 @@ Expressions are composed of property lookups and/or closures. Property lookups a .. _Syntax LookupExpression: -Lookups -------- +Lookup Expressions +------------------ .. rst-class:: grammar-block @@ -56,8 +56,8 @@ The type of a property expression is the type of the property it refers to. .. _Syntax ClosureExpression: -Closures --------- +Closure Expressions +------------------- .. rst-class:: grammar-block @@ -72,8 +72,8 @@ Blueprint doesn't know the closure's return type, so closure expressions must be .. _Syntax CastExpression: -Casts ------ +Cast Expressions +---------------- .. rst-class:: grammar-block @@ -81,32 +81,7 @@ Casts Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types. -Example -~~~~~~~ - .. code-block:: blueprint // Cast the result of the closure so blueprint knows it's a string - label: bind $format_bytes(template.file-size) as - -.. _Syntax ExprValue: - -Expression Values ------------------ - -.. rst-class:: grammar-block - - ExprValue = 'expr' :ref:`Expression` - -Some APIs take *an expression itself*--not its result--as a property value. For example, `Gtk.BoolFilter `_ has an ``expression`` property of type `Gtk.Expression `_. This expression is evaluated for every item in a list model to determine whether the item should be filtered. - -To define an expression for such a property, use ``expr`` instead of ``bind``. Inside the expression, you can use the ``item`` keyword to refer to the item being evaluated. You must cast the item to the correct type using the ``as`` keyword, and you can only use ``item`` in a property lookup--you may not pass it to a closure. - -Example -~~~~~~~ - -.. code-block:: blueprint - - BoolFilter { - expression: expr item as <$UserAccount>.active; - } + label: bind $my_closure() as \ No newline at end of file diff --git a/docs/reference/objects.rst b/docs/reference/objects.rst index 6f76da6..699db49 100644 --- a/docs/reference/objects.rst +++ b/docs/reference/objects.rst @@ -58,7 +58,7 @@ Properties .. rst-class:: grammar-block - Property = `> ':' ( :ref:`Binding` | :ref:`ExprValue` | :ref:`ObjectValue` | :ref:`Value` ) ';' + Property = `> ':' ( :ref:`Binding` | :ref:`ObjectValue` | :ref:`Value` ) ';' Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container. @@ -91,7 +91,7 @@ Signal Handlers .. rst-class:: grammar-block Signal = `> ('::' `>)? '=>' '$' `> '(' `>? ')' (SignalFlag)* ';' - SignalFlag = 'after' | 'swapped' | 'not-swapped' + SignalFlag = 'after' | 'swapped' Signals are one way to respond to user input (another is `actions `_, which use the `action-name property `_). @@ -99,8 +99,6 @@ Signals provide a handle for your code to listen to events in the UI. The handle Optionally, you can provide an object ID to use when connecting the signal. -The ``swapped`` flag is used to swap the order of the object and userdata arguments in C applications. If an object argument is specified, then this is the default behavior, so the ``not-swapped`` flag can be used to prevent the swap. - Example ~~~~~~~ @@ -110,6 +108,7 @@ Example clicked => $on_button_clicked(); } + .. _Syntax Child: Children diff --git a/docs/translations.rst b/docs/translations.rst index 7af2099..7ebf929 100644 --- a/docs/translations.rst +++ b/docs/translations.rst @@ -24,8 +24,6 @@ If you're using Meson's `i18n module $on_icon_name_changed(label) swapped; styles [ - "destructive", + "destructive" ] } diff --git a/tests/sample_errors/expr_item_not_cast.blp b/tests/sample_errors/expr_item_not_cast.blp deleted file mode 100644 index 76a1d89..0000000 --- a/tests/sample_errors/expr_item_not_cast.blp +++ /dev/null @@ -1,5 +0,0 @@ -using Gtk 4.0; - -BoolFilter { - expression: expr item.visible; -} diff --git a/tests/sample_errors/expr_item_not_cast.err b/tests/sample_errors/expr_item_not_cast.err deleted file mode 100644 index f6cf7d4..0000000 --- a/tests/sample_errors/expr_item_not_cast.err +++ /dev/null @@ -1 +0,0 @@ -4,20,4,"item" must be cast to its object type \ No newline at end of file diff --git a/tests/sample_errors/expr_value_assignment.blp b/tests/sample_errors/expr_value_assignment.blp deleted file mode 100644 index 51d778f..0000000 --- a/tests/sample_errors/expr_value_assignment.blp +++ /dev/null @@ -1,5 +0,0 @@ -using Gtk 4.0; - -Label { - label: expr 1; -} diff --git a/tests/sample_errors/expr_value_assignment.err b/tests/sample_errors/expr_value_assignment.err deleted file mode 100644 index 1c7092a..0000000 --- a/tests/sample_errors/expr_value_assignment.err +++ /dev/null @@ -1 +0,0 @@ -4,10,4,Cannot convert Gtk.Expression to string \ No newline at end of file diff --git a/tests/sample_errors/expr_value_closure_arg.blp b/tests/sample_errors/expr_value_closure_arg.blp deleted file mode 100644 index 7f828c4..0000000 --- a/tests/sample_errors/expr_value_closure_arg.blp +++ /dev/null @@ -1,5 +0,0 @@ -using Gtk 4.0; - -BoolFilter { - expression: expr $closure(item as ) as ; -} diff --git a/tests/sample_errors/expr_value_closure_arg.err b/tests/sample_errors/expr_value_closure_arg.err deleted file mode 100644 index b9e19f8..0000000 --- a/tests/sample_errors/expr_value_closure_arg.err +++ /dev/null @@ -1 +0,0 @@ -4,29,4,"item" can only be used for looking up properties \ No newline at end of file diff --git a/tests/sample_errors/expr_value_item.blp b/tests/sample_errors/expr_value_item.blp deleted file mode 100644 index 141c806..0000000 --- a/tests/sample_errors/expr_value_item.blp +++ /dev/null @@ -1,5 +0,0 @@ -using Gtk 4.0; - -BoolFilter { - expression: expr item as