mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
WIP: Add gir.PartialClass
This commit is contained in:
parent
632e9d7df6
commit
6fdb12fd5d
17 changed files with 231 additions and 147 deletions
|
@ -81,7 +81,7 @@ class AstNode:
|
|||
def _get_errors(self):
|
||||
for validator in self.validators:
|
||||
try:
|
||||
validator(self)
|
||||
validator.validate(self)
|
||||
except CompileError as e:
|
||||
yield e
|
||||
if e.fatal:
|
||||
|
@ -149,43 +149,61 @@ class AstNode:
|
|||
|
||||
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. """
|
||||
during validation are marked with range information from the tokens.
|
||||
The return value of the function can also be used as a property, and if
|
||||
the function raises a CompileError, the property will be None. """
|
||||
|
||||
def decorator(func):
|
||||
def inner(self):
|
||||
if skip_incomplete and self.incomplete:
|
||||
class Validator:
|
||||
_validator = True
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
try:
|
||||
return self.validate(instance)
|
||||
except Exception as e:
|
||||
# Ignore errors and return None. This way, a diagnostic
|
||||
# is only emitted once. Don't ignore CompilerBugErrors
|
||||
# though, they should never happen.
|
||||
if isinstance(e, CompilerBugError):
|
||||
raise e
|
||||
return None
|
||||
|
||||
def validate(self, node):
|
||||
if skip_incomplete and node.incomplete:
|
||||
return
|
||||
|
||||
try:
|
||||
func(self)
|
||||
return func(node)
|
||||
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:
|
||||
if node.incomplete:
|
||||
return
|
||||
|
||||
# This mess of code sets the error's start and end positions
|
||||
# from the tokens passed to the decorator, if they have not
|
||||
# already been set
|
||||
if e.start is None:
|
||||
if token := self.group.tokens.get(token_name):
|
||||
if token := node.group.tokens.get(token_name):
|
||||
e.start = token.start
|
||||
else:
|
||||
e.start = self.group.start
|
||||
e.start = node.group.start
|
||||
|
||||
if e.end is None:
|
||||
if token := self.group.tokens.get(end_token_name):
|
||||
if token := node.group.tokens.get(end_token_name):
|
||||
e.end = token.end
|
||||
elif token := self.group.tokens.get(token_name):
|
||||
elif token := node.group.tokens.get(token_name):
|
||||
e.end = token.end
|
||||
else:
|
||||
e.end = self.group.end
|
||||
e.end = node.group.end
|
||||
|
||||
# Re-raise the exception
|
||||
raise e
|
||||
|
||||
inner._validator = True
|
||||
return inner
|
||||
return Validator()
|
||||
|
||||
return decorator
|
||||
|
||||
|
|
|
@ -110,7 +110,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 hasattr(ast_node.gir_class, "properties"):
|
||||
for prop in ast_node.gir_class.properties:
|
||||
yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;")
|
||||
|
||||
|
@ -136,7 +136,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 hasattr(ast_node.gir_class, "signals"):
|
||||
for signal in ast_node.gir_class.signals:
|
||||
if not isinstance(ast_node.parent, language.Object):
|
||||
name = "on"
|
||||
|
|
|
@ -96,6 +96,9 @@ class GirType:
|
|||
def full_name(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def lookup_property(self, property: str):
|
||||
raise CompileError(f"Type {self.full_name} does not have properties")
|
||||
|
||||
|
||||
class BasicType(GirType):
|
||||
name: str = "unknown type"
|
||||
|
@ -314,6 +317,17 @@ class Interface(GirNode, GirType):
|
|||
result.append(self.get_containing(Repository)._resolve_dir_entry(entry))
|
||||
return result
|
||||
|
||||
def lookup_property(self, property: str):
|
||||
if prop := self.properties.get(property):
|
||||
return prop
|
||||
elif self.is_partial:
|
||||
return None
|
||||
else:
|
||||
raise CompileError(
|
||||
f"Interface {self.full_name} does not have a property called {property}",
|
||||
did_you_mean=(property, self.properties.keys())
|
||||
)
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
if self == other:
|
||||
return True
|
||||
|
@ -323,7 +337,82 @@ class Interface(GirNode, GirType):
|
|||
return False
|
||||
|
||||
|
||||
class Class(GirNode, GirType):
|
||||
class GirClass(GirType):
|
||||
@property
|
||||
def abstract(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def is_partial(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def implements(self):
|
||||
return []
|
||||
|
||||
@property
|
||||
def own_properties(self):
|
||||
return {}
|
||||
|
||||
@property
|
||||
def own_signals(self):
|
||||
return {}
|
||||
|
||||
@cached_property
|
||||
def properties(self):
|
||||
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() }
|
||||
|
||||
def lookup_property(self, property: str):
|
||||
if prop := self.properties.get(property):
|
||||
return prop
|
||||
elif self.is_partial:
|
||||
return None
|
||||
else:
|
||||
raise CompileError(
|
||||
f"Class {self.full_name} does not have a property called {property}",
|
||||
did_you_mean=(property, self.properties.keys())
|
||||
)
|
||||
|
||||
def _enum_properties(self):
|
||||
yield from self.own_properties.values()
|
||||
|
||||
if self.parent is not None:
|
||||
yield from self.parent.properties.values()
|
||||
|
||||
for impl in self.implements:
|
||||
yield from impl.properties.values()
|
||||
|
||||
def _enum_signals(self):
|
||||
yield from self.own_signals.values()
|
||||
|
||||
if self.parent is not None:
|
||||
yield from self.parent.signals.values()
|
||||
|
||||
for impl in self.implements:
|
||||
yield from impl.signals.values()
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
if self == other:
|
||||
return True
|
||||
elif self.parent and self.parent.assignable_to(other):
|
||||
return True
|
||||
else:
|
||||
for iface in self.implements:
|
||||
if iface.assignable_to(other):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class Class(GirNode, GirClass):
|
||||
def __init__(self, ns, tl: typelib.Typelib):
|
||||
super().__init__(ns, tl)
|
||||
|
||||
|
@ -388,43 +477,31 @@ class Class(GirNode, GirType):
|
|||
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() }
|
||||
|
||||
@cached_property
|
||||
def signals(self):
|
||||
return { s.name: s for s in self._enum_signals() }
|
||||
class PartialClass(GirClass):
|
||||
def __init__(self, name: str, parent: GirType = None):
|
||||
self._name = name
|
||||
self._parent = parent
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
if self == other:
|
||||
return True
|
||||
elif self.parent and self.parent.assignable_to(other):
|
||||
return True
|
||||
else:
|
||||
for iface in self.implements:
|
||||
if iface.assignable_to(other):
|
||||
return True
|
||||
@property
|
||||
def full_name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def glib_type_name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def abstract(self):
|
||||
return False
|
||||
|
||||
def _enum_properties(self):
|
||||
yield from self.own_properties.values()
|
||||
@property
|
||||
def parent(self):
|
||||
return self._parent
|
||||
|
||||
if self.parent is not None:
|
||||
yield from self.parent.properties.values()
|
||||
|
||||
for impl in self.implements:
|
||||
yield from impl.properties.values()
|
||||
|
||||
def _enum_signals(self):
|
||||
yield from self.own_signals.values()
|
||||
|
||||
if self.parent is not None:
|
||||
yield from self.parent.signals.values()
|
||||
|
||||
for impl in self.implements:
|
||||
yield from impl.signals.values()
|
||||
@property
|
||||
def is_partial(self):
|
||||
return True
|
||||
|
||||
|
||||
class EnumMember(GirNode):
|
||||
|
|
|
@ -126,8 +126,7 @@ class LookupOp(InfixExpr):
|
|||
@property
|
||||
def gir_type(self):
|
||||
if parent_type := self.lhs.gir_type:
|
||||
if isinstance(parent_type, gir.Class) or isinstance(parent_type, gir.Interface):
|
||||
if prop := parent_type.properties.get(self.tokens["property"]):
|
||||
if prop := parent_type.lookup_property(self.tokens["property"]):
|
||||
return prop.type
|
||||
|
||||
@property
|
||||
|
@ -137,15 +136,11 @@ class LookupOp(InfixExpr):
|
|||
|
||||
@validate("property")
|
||||
def property_exists(self):
|
||||
if parent_type := self.lhs.gir_type:
|
||||
if not (isinstance(parent_type, gir.Class) or isinstance(parent_type, gir.Interface)):
|
||||
raise CompileError(f"Type {parent_type.full_name} does not have properties")
|
||||
elif self.tokens["property"] not in parent_type.properties:
|
||||
raise CompileError(
|
||||
f"{parent_type.full_name} does not have a property called {self.tokens['property']}",
|
||||
hints=["Do you need to cast the previous expression?"],
|
||||
did_you_mean=(self.tokens['property'], parent_type.properties.keys()),
|
||||
)
|
||||
try:
|
||||
self.gir_type
|
||||
except CompileError as e:
|
||||
e.hints.append("Do you need to cast the previous expression?")
|
||||
raise e
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
if isinstance(self.lhs, IdentExpr) and self.lhs.is_this:
|
||||
|
|
|
@ -48,14 +48,7 @@ class Object(AstNode):
|
|||
|
||||
@property
|
||||
def gir_class(self):
|
||||
class_names = self.children[ClassName]
|
||||
if len(class_names) > 0:
|
||||
if isinstance(class_names[0].gir_type, Class):
|
||||
return class_names[0].gir_type
|
||||
|
||||
@property
|
||||
def glib_type_name(self) -> str:
|
||||
return self.children[ClassName][0].glib_type_name
|
||||
return self.children[ClassName][0].gir_type
|
||||
|
||||
@docs("namespace")
|
||||
def namespace_docs(self):
|
||||
|
@ -83,7 +76,7 @@ class Object(AstNode):
|
|||
|
||||
def emit_start_tag(self, xml: XmlEmitter):
|
||||
xml.start_tag("object", **{
|
||||
"class": self.glib_type_name,
|
||||
"class": self.gir_class,
|
||||
"id": self.tokens["id"],
|
||||
})
|
||||
|
||||
|
|
|
@ -64,37 +64,16 @@ class Property(AstNode):
|
|||
def gir_class(self):
|
||||
return self.parent.parent.gir_class
|
||||
|
||||
|
||||
@property
|
||||
@validate("name")
|
||||
def gir_property(self):
|
||||
if self.gir_class is not None:
|
||||
return self.gir_class.properties.get(self.tokens["name"])
|
||||
|
||||
return self.gir_class.lookup_property(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:
|
||||
# Objects that we have no gir data on should not be validated
|
||||
# This happens for classes defined by the app itself
|
||||
return
|
||||
|
||||
if isinstance(self.parent.parent, Template):
|
||||
# If the property is part of a template, it might be defined by
|
||||
# the application and thus not in gir
|
||||
return
|
||||
|
||||
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())
|
||||
)
|
||||
|
||||
@validate("bind")
|
||||
def property_bindable(self):
|
||||
if self.tokens["bind"] and self.gir_property is not None and self.gir_property.construct_only:
|
||||
|
|
|
@ -27,17 +27,12 @@ class Filters(AstNode):
|
|||
def container_is_file_filter(self):
|
||||
validate_parent_type(self, "Gtk", "FileFilter", "file filter properties")
|
||||
|
||||
@validate()
|
||||
def unique_in_parent(self):
|
||||
# The token argument to validate() needs to be calculated based on
|
||||
# the instance, hence wrapping it like this.
|
||||
@validate(self.tokens["tag_name"])
|
||||
@validate("tag")
|
||||
def wrapped_validator(self):
|
||||
self.validate_unique_in_parent(
|
||||
f"Duplicate {self.tokens['tag_name']} block",
|
||||
check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"],
|
||||
)
|
||||
wrapped_validator(self)
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
xml.start_tag(self.tokens["tag_name"])
|
||||
|
@ -57,7 +52,7 @@ def create_node(tag_name: str, singular: str):
|
|||
return Group(
|
||||
Filters,
|
||||
[
|
||||
Keyword(tag_name),
|
||||
Keyword(tag_name, "tag"),
|
||||
UseLiteral("tag_name", tag_name),
|
||||
"[",
|
||||
Delimited(
|
||||
|
|
|
@ -49,6 +49,10 @@ class Child(AstNode):
|
|||
if gir_class.assignable_to(parent_type):
|
||||
break
|
||||
else:
|
||||
if gir_class.is_partial:
|
||||
# we don't know if the class implements Gtk.Buildable or not
|
||||
return
|
||||
|
||||
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?")
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from .gobject_object import Object, ObjectContent
|
||||
from .common import *
|
||||
|
@ -34,11 +35,14 @@ class Template(Object):
|
|||
ObjectContent,
|
||||
]
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def gir_class(self):
|
||||
# Templates might not have a parent class defined
|
||||
parent = None
|
||||
if len(self.children[ClassName]):
|
||||
return self.children[ClassName][0].gir_type
|
||||
parent = self.children[ClassName][0].gir_type
|
||||
|
||||
return gir.PartialClass(self.tokens["id"], parent)
|
||||
|
||||
@validate("id")
|
||||
def unique_in_parent(self):
|
||||
|
|
|
@ -25,7 +25,7 @@ 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;`)"),
|
||||
Keyword("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
|
||||
UseNumberText("version").expected("a version number for GTK"),
|
||||
)
|
||||
|
||||
|
@ -33,15 +33,23 @@ class GtkDirective(AstNode):
|
|||
def gtk_version(self):
|
||||
version = self.tokens["version"]
|
||||
if version not in ["4.0"]:
|
||||
err = CompileError("Only GTK 4 is supported")
|
||||
err = CompileError("Only GTK 4 is supported", fatal=True)
|
||||
if version and version.startswith("4"):
|
||||
err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.")
|
||||
else:
|
||||
err.hint("Expected 'using Gtk 4.0;'")
|
||||
raise err
|
||||
|
||||
return version
|
||||
|
||||
@validate()
|
||||
def gir_namespace(self):
|
||||
if self.gtk_version is None:
|
||||
# use that error message instead
|
||||
return
|
||||
|
||||
try:
|
||||
gir.get_namespace("Gtk", version)
|
||||
return gir.get_namespace("Gtk", self.gtk_version)
|
||||
except CompileError as e:
|
||||
raise CompileError(
|
||||
"Could not find GTK 4 introspection files. Is gobject-introspection installed?",
|
||||
|
@ -52,18 +60,9 @@ 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()
|
||||
return gir.get_namespace("Gtk", self.tokens["version"])
|
||||
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
xml.put_self_closing("requires", lib="gtk", version=self.tokens["version"])
|
||||
|
||||
|
||||
class Import(AstNode):
|
||||
grammar = Statement(
|
||||
"using",
|
||||
|
|
|
@ -19,8 +19,9 @@
|
|||
|
||||
|
||||
import typing as T
|
||||
from functools import cached_property
|
||||
from .common import *
|
||||
from ..gir import Class, Interface
|
||||
from ..gir import GirClass, Class, Interface
|
||||
|
||||
|
||||
class TypeName(AstNode):
|
||||
|
@ -53,18 +54,16 @@ class TypeName(AstNode):
|
|||
if not self.tokens["ignore_gir"]:
|
||||
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def gir_type(self) -> T.Optional[gir.Class]:
|
||||
if self.tokens["class_name"] and not self.tokens["ignore_gir"]:
|
||||
if self.tokens["ignore_gir"]:
|
||||
return gir.PartialClass(self.tokens["class_name"])
|
||||
else:
|
||||
return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"])
|
||||
return None
|
||||
|
||||
@property
|
||||
def glib_type_name(self) -> str:
|
||||
if gir_type := self.gir_type:
|
||||
return gir_type.glib_type_name
|
||||
else:
|
||||
return self.tokens["class_name"]
|
||||
return self.gir_type.glib_type_name
|
||||
|
||||
@docs("namespace")
|
||||
def namespace_docs(self):
|
||||
|
@ -83,7 +82,7 @@ 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 is not None and not isinstance(self.gir_type, GirClass):
|
||||
if isinstance(self.gir_type, Interface):
|
||||
raise CompileError(f"{self.gir_type.full_name} is an interface, not a class")
|
||||
else:
|
||||
|
|
|
@ -42,7 +42,8 @@ class UI(AstNode, Scope):
|
|||
self._gir_errors = []
|
||||
|
||||
try:
|
||||
gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace)
|
||||
if ns := self.children[GtkDirective][0].gir_namespace:
|
||||
gir_ctx.add_namespace(ns)
|
||||
except CompileError as e:
|
||||
self._gir_errors.append(e)
|
||||
|
||||
|
@ -70,7 +71,7 @@ class UI(AstNode, Scope):
|
|||
xml.end_tag()
|
||||
|
||||
return {
|
||||
id: ScopeVariable(id, obj.gir_class, lambda xml, id=id: emit_xml(xml, id), obj.glib_type_name)
|
||||
id: ScopeVariable(id, obj.gir_class, lambda xml, id=id: emit_xml(xml, id))
|
||||
for id, obj in self.objects_by_id.items()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
5,32,9,Gtk.Widget does not have a property called something
|
||||
5,32,9,Class Gtk.Widget does not have a property called something
|
||||
6,43,5,Type int does not have properties
|
||||
7,17,7,Could not find object with ID 'nothing'
|
|
@ -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
|
||||
|
|
5
tests/samples/template_binding.blp
Normal file
5
tests/samples/template_binding.blp
Normal file
|
@ -0,0 +1,5 @@
|
|||
using Gtk 4.0;
|
||||
|
||||
template MyWidget {
|
||||
label: bind MyWidget.other-label;
|
||||
}
|
11
tests/samples/template_binding.ui
Normal file
11
tests/samples/template_binding.ui
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<template class="MyWidget">
|
||||
<binding name="label">
|
||||
<lookup name="other-label" type="MyWidget">
|
||||
<constant>MyWidget</constant>
|
||||
</lookup>
|
||||
</binding>
|
||||
</template>
|
||||
</interface>
|
|
@ -90,6 +90,9 @@ class TestSamples(unittest.TestCase):
|
|||
raise MultipleErrors(warnings)
|
||||
except PrintableError as e:
|
||||
def error_str(error):
|
||||
if error.start is None:
|
||||
raise Exception("Error start/end range was never set")
|
||||
|
||||
line, col = utils.idx_to_pos(error.start + 1, blueprint)
|
||||
len = error.end - error.start
|
||||
return ",".join([str(line + 1), str(col), str(len), error.message])
|
||||
|
@ -157,6 +160,7 @@ class TestSamples(unittest.TestCase):
|
|||
self.assert_sample("strings")
|
||||
self.assert_sample("style")
|
||||
self.assert_sample("template")
|
||||
self.assert_sample("template_binding")
|
||||
self.assert_sample("template_no_parent")
|
||||
self.assert_sample("translated")
|
||||
self.assert_sample("uint")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue