Merge branch blueprint-compiler:main into main

This commit is contained in:
kotontrion 2025-03-02 15:26:42 +00:00
commit c1fbcef6d0
94 changed files with 850 additions and 232 deletions

View file

@ -4,7 +4,6 @@ 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
@ -20,7 +19,7 @@ from .expression import (
from .gobject_object import Object, ObjectContent
from .gobject_property import Property
from .gobject_signal import Signal
from .gtk_a11y import ExtAccessibility
from .gtk_a11y import A11yProperty, ExtAccessibility
from .gtk_combo_box_text import ExtComboBoxItems
from .gtk_file_filter import (
Filters,
@ -42,6 +41,7 @@ from .types import ClassName
from .ui import UI
from .values import (
ArrayValue,
ExprValue,
Flag,
Flags,
IdentLiteral,

View file

@ -20,7 +20,6 @@
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
@ -94,10 +93,6 @@ 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(

View file

@ -1,32 +0,0 @@
# attributes.py
#
# Copyright 2022 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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"]

View file

@ -79,3 +79,9 @@ 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."""

View file

@ -38,10 +38,6 @@ 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):
@ -65,10 +61,6 @@ 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
@ -89,6 +81,16 @@ 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
@ -99,14 +101,14 @@ class LiteralExpr(ExprBase):
def type(self) -> T.Optional[GirType]:
return self.literal.value.type
@property
def type_complete(self) -> bool:
from .values import IdentLiteral
@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')
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
if not isinstance(self.rhs.rhs, LookupOp):
raise CompileError('"item" can only be used for looking up properties')
class LookupOp(InfixExpr):
@ -211,10 +213,6 @@ 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:
@ -306,6 +304,9 @@ 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:
@ -325,6 +326,8 @@ 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
@ -339,6 +342,9 @@ 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")
@ -351,6 +357,9 @@ 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:

View file

@ -28,7 +28,18 @@ from .common import *
from .response_id import ExtResponse
from .types import ClassName, ConcreteClassName
RESERVED_IDS = {"this", "self", "template", "true", "false", "null", "none"}
RESERVED_IDS = {
"this",
"self",
"template",
"true",
"false",
"null",
"none",
"item",
"expr",
"typeof",
}
class ObjectContent(AstNode):

View file

@ -21,13 +21,12 @@
from .binding import Binding
from .common import *
from .contexts import ValueTypeCtx
from .gtkbuilder_template import Template
from .values import ArrayValue, ObjectValue, Value
from .values import ArrayValue, ExprValue, ObjectValue, Value
class Property(AstNode):
grammar = Statement(
UseIdent("name"), ":", AnyOf(Binding, ObjectValue, Value, ArrayValue)
UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue)
)
@property
@ -35,7 +34,7 @@ class Property(AstNode):
return self.tokens["name"]
@property
def value(self) -> T.Union[Binding, ObjectValue, Value, ArrayValue]:
def value(self) -> T.Union[Binding, ExprValue, ObjectValue, Value, ArrayValue]:
return self.children[0]
@property
@ -51,7 +50,7 @@ class Property(AstNode):
@property
def document_symbol(self) -> DocumentSymbol:
if isinstance(self.value, ObjectValue):
if isinstance(self.value, ObjectValue) or self.value is None:
detail = None
else:
detail = self.value.range.text

View file

@ -27,6 +27,7 @@ from .gtkbuilder_template import Template
class SignalFlag(AstNode):
grammar = AnyOf(
UseExact("flag", "swapped"),
UseExact("flag", "not-swapped"),
UseExact("flag", "after"),
)
@ -40,6 +41,27 @@ 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")
@ -92,9 +114,17 @@ 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) -> bool:
return any(x.flag == "swapped" for x in self.flags)
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
@property
def is_after(self) -> bool:
@ -113,16 +143,17 @@ 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,
self.ranges["detail_start", "detail_end"].text,
detail.text if detail is not None else None,
)
def get_reference(self, idx: int) -> T.Optional[LocationLink]:
if idx in self.group.tokens["object"].range:
if self.object_id is not None and idx in self.group.tokens["object"].range:
obj = self.context[ScopeCtx].objects.get(self.object_id)
if obj is not None:
return LocationLink(
@ -194,15 +225,16 @@ class Signal(AstNode):
@decompiler("signal")
def decompile_signal(
ctx, gir, name, handler, swapped="false", after="false", object=None
):
def decompile_signal(ctx, gir, name, handler, swapped=None, 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"

View file

@ -19,8 +19,6 @@
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
@ -119,7 +117,7 @@ def _get_docs(gir, name):
return gir_type.doc
class A11yProperty(BaseAttribute):
class A11yProperty(AstNode):
grammar = Statement(
UseIdent("name"),
":",

View file

@ -19,7 +19,6 @@
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue

View file

@ -50,7 +50,7 @@ class ExtListItemFactory(AstNode):
else:
return self.root.gir.get_type("ListItem", "Gtk")
@validate("template")
@validate("id")
def container_is_builder_list(self):
validate_parent_type(
self,
@ -59,7 +59,7 @@ class ExtListItemFactory(AstNode):
"sub-templates",
)
@validate("template")
@validate("id")
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("template")
@validate("id")
def type_name_upgrade(self):
if self.type_name is None:
raise UpgradeWarning(
@ -103,10 +103,7 @@ class ExtListItemFactory(AstNode):
@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
"""
# The sub-template shouldn't have its own actions, this is just here to satisfy XmlOutput._emit_object_or_template
return None
@docs("id")

View file

@ -59,14 +59,8 @@ class GtkDirective(AstNode):
@property
def gir_namespace(self):
# 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")
# For better error handling, just assume it's 4.0
return gir.get_namespace("Gtk", "4.0")
@docs()
def ref_docs(self):
@ -90,7 +84,7 @@ class Import(AstNode):
@validate("namespace", "version")
def namespace_exists(self):
gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
gir.get_namespace(self.namespace, self.version)
@validate()
def unused(self):
@ -106,7 +100,7 @@ class Import(AstNode):
@property
def gir_namespace(self):
try:
return gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
return gir.get_namespace(self.namespace, self.version)
except CompileError:
return None

View file

@ -23,7 +23,8 @@ from blueprintcompiler.gir import ArrayType
from blueprintcompiler.lsp_utils import SemanticToken
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx
from .expression import Expression
from .gobject_object import Object
from .types import TypeName
@ -57,6 +58,19 @@ 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")
@ -319,7 +333,12 @@ class IdentLiteral(AstNode):
if self.ident == "null":
if not self.context[ValueTypeCtx].allow_null:
raise CompileError("null is not permitted here")
else:
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"]:
raise CompileError(
f"Could not find object with ID {self.ident}",
did_you_mean=(
@ -407,6 +426,35 @@ 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)
@ -452,6 +500,14 @@ 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)