WIP: Add gir.PartialClass

This commit is contained in:
James Westman 2022-07-19 20:25:46 -05:00
parent 632e9d7df6
commit 6fdb12fd5d
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
17 changed files with 231 additions and 147 deletions

View file

@ -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:
return
class Validator:
_validator = True
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:
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
# 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):
e.start = token.start
else:
e.start = self.group.start
try:
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 node.incomplete:
return
if e.end is None:
if token := self.group.tokens.get(end_token_name):
e.end = token.end
elif token := self.group.tokens.get(token_name):
e.end = token.end
else:
e.end = self.group.end
# 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 := node.group.tokens.get(token_name):
e.start = token.start
else:
e.start = node.group.start
# Re-raise the exception
raise e
if e.end is None:
if token := node.group.tokens.get(end_token_name):
e.end = token.end
elif token := node.group.tokens.get(token_name):
e.end = token.end
else:
e.end = node.group.end
inner._validator = True
return inner
# Re-raise the exception
raise e
return Validator()
return decorator

View file

@ -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"

View file

@ -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
return False
@property
def glib_type_name(self):
return self._name
def _enum_properties(self):
yield from self.own_properties.values()
@property
def abstract(self):
return False
if self.parent is not None:
yield from self.parent.properties.values()
@property
def parent(self):
return self._parent
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):

View file

@ -126,9 +126,8 @@ 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"]):
return prop.type
if prop := parent_type.lookup_property(self.tokens["property"]):
return prop.type
@property
def glib_type_name(self):
@ -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:

View file

@ -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"],
})

View file

@ -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:

View file

@ -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"])
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)
@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"],
)
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(

View file

@ -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?")

View file

@ -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):

View file

@ -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",

View file

@ -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:

View file

@ -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()
}

View file

@ -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'

View file

@ -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

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
template MyWidget {
label: bind MyWidget.other-label;
}

View 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>

View file

@ -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")