Change template syntax

Templates now use a TypeName instead of an identifier, which makes it
clearer that it's an extern symbol (or that it's a Gtk.ListItem).
This commit is contained in:
James Westman 2023-05-09 19:42:33 -05:00
parent aebf8be278
commit 04509e4b2e
31 changed files with 175 additions and 55 deletions

View file

@ -23,6 +23,7 @@ from functools import cached_property
from .common import *
from .gobject_object import Object
from .gtkbuilder_template import Template
@dataclass
@ -51,9 +52,12 @@ class ScopeCtx:
if obj.tokens["id"] in passed:
token = obj.group.tokens["id"]
raise CompileError(
f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end
)
if not isinstance(obj, Template):
raise CompileError(
f"Duplicate object ID '{obj.tokens['id']}'",
token.start,
token.end,
)
passed[obj.tokens["id"]] = obj
def _iter_recursive(self, node: AstNode):

View file

@ -85,9 +85,9 @@ class LiteralExpr(ExprBase):
def is_object(self) -> bool:
from .values import IdentLiteral
return (
isinstance(self.literal.value, IdentLiteral)
and self.literal.value.ident in self.context[ScopeCtx].objects
return isinstance(self.literal.value, IdentLiteral) and (
self.literal.value.ident in self.context[ScopeCtx].objects
or self.root.is_legacy_template(self.literal.value.ident)
)
@property

View file

@ -49,7 +49,7 @@ class Object(AstNode):
return self.tokens["id"]
@property
def class_name(self) -> T.Optional[ClassName]:
def class_name(self) -> ClassName:
return self.children[ClassName][0]
@property
@ -78,7 +78,9 @@ class Object(AstNode):
@validate("id")
def object_id_not_reserved(self):
if self.id in RESERVED_IDS:
from .gtkbuilder_template import Template
if not isinstance(self, Template) and self.id in RESERVED_IDS:
raise CompileWarning(f"{self.id} may be a confusing object ID")

View file

@ -19,15 +19,18 @@
import typing as T
from blueprintcompiler.language.common import GirType
from .gobject_object import Object, ObjectContent
from .common import *
from .types import ClassName
from ..gir import TemplateType
from .types import ClassName, TemplateClassName
class Template(Object):
grammar = [
"template",
UseIdent("id").expected("template class name"),
UseExact("id", "template"),
to_parse_node(TemplateClassName).expected("template type"),
Optional(
[
Match(":"),
@ -39,21 +42,29 @@ class Template(Object):
@property
def id(self) -> str:
return self.tokens["id"]
return "template"
@property
def class_name(self) -> T.Optional[ClassName]:
if len(self.children[ClassName]):
return self.children[ClassName][0]
def gir_class(self) -> GirType:
if isinstance(self.class_name.gir_type, ExternType):
if gir := self.parent_type:
return TemplateType(self.class_name.gir_type.full_name, gir.gir_type)
return self.class_name.gir_type
@property
def parent_type(self) -> T.Optional[ClassName]:
if len(self.children[ClassName]) == 2:
return self.children[ClassName][1]
else:
return None
@property
def gir_class(self):
if self.class_name is None:
return gir.TemplateType(self.id, None)
else:
return gir.TemplateType(self.id, self.class_name.gir_type)
@validate()
def parent_only_if_extern(self):
if not isinstance(self.class_name.gir_type, ExternType):
if self.parent_type is not None:
raise CompileError(
"Parent type may only be specified if the template type is extern"
)
@validate("id")
def unique_in_parent(self):
@ -63,10 +74,15 @@ class Template(Object):
@decompiler("template")
def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"):
gir_class = ctx.type_by_cname(parent)
if gir_class is None:
ctx.print(f"template {klass} : .{parent} {{")
else:
ctx.print(f"template {klass} : {decompile.full_name(gir_class)} {{")
return gir_class
def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):
def class_name(cname: str) -> str:
if gir := ctx.type_by_cname(cname):
return decompile.full_name(gir)
else:
return "$" + cname
ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{")
ctx.template_class = klass
return ctx.type_by_cname(klass) or ctx.type_by_cname(parent)

View file

@ -70,6 +70,8 @@ class PropertyBinding(AstNode):
@property
def source_obj(self) -> T.Optional[Object]:
if self.root.is_legacy_template(self.source):
return self.root.template
return self.context[ScopeCtx].objects.get(self.source)
@property
@ -127,3 +129,11 @@ class PropertyBinding(AstNode):
"Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags",
actions=[CodeAction("Use 'bind-property'", "bind-property")],
)
@validate("source")
def legacy_template(self):
if self.root.is_legacy_template(self.source):
raise UpgradeWarning(
"Use 'template' instead of the class name (introduced in 0.8.0)",
actions=[CodeAction("Use 'template'", "template")],
)

View file

@ -122,3 +122,42 @@ class ConcreteClassName(ClassName):
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}?"],
)
class TemplateClassName(ClassName):
"""Handles the special case of a template type. The old syntax uses an identifier,
which is ambiguous with the new syntax. So this class displays an appropriate
upgrade warning instead of a class not found error."""
@property
def is_legacy(self):
return (
self.tokens["extern"] is None
and self.tokens["namespace"] is None
and self.root.gir.get_type(self.tokens["class_name"], "Gtk") is None
)
@property
def gir_type(self) -> gir.GirType:
if self.is_legacy:
return gir.ExternType(self.tokens["class_name"])
else:
return super().gir_type
@validate("class_name")
def type_exists(self):
if self.is_legacy:
if type := self.root.gir.get_type_by_cname(self.tokens["class_name"]):
replacement = type.full_name
else:
replacement = "$" + self.tokens["class_name"]
raise UpgradeWarning(
"Use type syntax here (introduced in blueprint 0.8.0)",
actions=[CodeAction("Use type syntax", replace_with=replacement)],
)
if not self.tokens["extern"] and self.gir_ns is not None:
self.root.gir.validate_type(
self.tokens["class_name"], self.tokens["namespace"]
)

View file

@ -43,7 +43,7 @@ class UI(AstNode):
]
@property
def gir(self):
def gir(self) -> gir.GirContext:
gir_ctx = gir.GirContext()
self._gir_errors = []
@ -84,6 +84,20 @@ class UI(AstNode):
or isinstance(child, Menu)
]
@property
def template(self) -> T.Optional[Template]:
if len(self.children[Template]):
return self.children[Template][0]
else:
return None
def is_legacy_template(self, id: str) -> bool:
return (
id not in self.context[ScopeCtx].objects
and self.template is not None
and self.template.class_name.glib_type_name == id
)
@context(ScopeCtx)
def scope_ctx(self) -> ScopeCtx:
return ScopeCtx(node=self)

View file

@ -260,6 +260,8 @@ class IdentLiteral(AstNode):
return gir.BoolType()
elif object := self.context[ScopeCtx].objects.get(self.ident):
return object.gir_class
elif self.root.is_legacy_template(self.ident):
return self.root.template.class_name.gir_type
else:
return None
@ -277,6 +279,12 @@ class IdentLiteral(AstNode):
did_you_mean=(self.ident, list(expected_type.members.keys())),
)
elif self.root.is_legacy_template(self.ident):
raise UpgradeWarning(
"Use 'template' instead of the class name (introduced in 0.8.0)",
actions=[CodeAction("Use 'template'", "template")],
)
elif expected_type is not None:
object = self.context[ScopeCtx].objects.get(self.ident)
if object is None: