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): def _get_errors(self):
for validator in self.validators: for validator in self.validators:
try: try:
validator(self) validator.validate(self)
except CompileError as e: except CompileError as e:
yield e yield e
if e.fatal: if e.fatal:
@ -149,43 +149,61 @@ class AstNode:
def validate(token_name=None, end_token_name=None, skip_incomplete=False): def validate(token_name=None, end_token_name=None, skip_incomplete=False):
""" Decorator for functions that validate an AST node. Exceptions raised """ 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 decorator(func):
def inner(self): class Validator:
if skip_incomplete and self.incomplete: _validator = True
return
try: def __get__(self, instance, owner):
func(self) if instance is None:
except CompileError as e: return self
# If the node is only partially complete, then an error must
# have already been reported at the parsing stage try:
if self.incomplete: 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 return
# This mess of code sets the error's start and end positions try:
# from the tokens passed to the decorator, if they have not return func(node)
# already been set except CompileError as e:
if e.start is None: # If the node is only partially complete, then an error must
if token := self.group.tokens.get(token_name): # have already been reported at the parsing stage
e.start = token.start if node.incomplete:
else: return
e.start = self.group.start
if e.end is None: # This mess of code sets the error's start and end positions
if token := self.group.tokens.get(end_token_name): # from the tokens passed to the decorator, if they have not
e.end = token.end # already been set
elif token := self.group.tokens.get(token_name): if e.start is None:
e.end = token.end if token := node.group.tokens.get(token_name):
else: e.start = token.start
e.end = self.group.end else:
e.start = node.group.start
# Re-raise the exception if e.end is None:
raise e 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 # Re-raise the exception
return inner raise e
return Validator()
return decorator return decorator

View file

@ -110,7 +110,7 @@ def gtk_object_completer(ast_node, match_variables):
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def property_completer(ast_node, match_variables): 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: for prop in ast_node.gir_class.properties:
yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;") 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, matches=new_statement_patterns,
) )
def signal_completer(ast_node, match_variables): 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: for signal in ast_node.gir_class.signals:
if not isinstance(ast_node.parent, language.Object): if not isinstance(ast_node.parent, language.Object):
name = "on" name = "on"

View file

@ -96,6 +96,9 @@ class GirType:
def full_name(self) -> str: def full_name(self) -> str:
raise NotImplementedError() raise NotImplementedError()
def lookup_property(self, property: str):
raise CompileError(f"Type {self.full_name} does not have properties")
class BasicType(GirType): class BasicType(GirType):
name: str = "unknown type" name: str = "unknown type"
@ -314,6 +317,17 @@ class Interface(GirNode, GirType):
result.append(self.get_containing(Repository)._resolve_dir_entry(entry)) result.append(self.get_containing(Repository)._resolve_dir_entry(entry))
return result 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: def assignable_to(self, other) -> bool:
if self == other: if self == other:
return True return True
@ -323,7 +337,82 @@ class Interface(GirNode, GirType):
return False 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): def __init__(self, ns, tl: typelib.Typelib):
super().__init__(ns, tl) super().__init__(ns, tl)
@ -388,43 +477,31 @@ class Class(GirNode, GirType):
result += " implements " + ", ".join([impl.full_name for impl in self.implements]) result += " implements " + ", ".join([impl.full_name for impl in self.implements])
return result return result
@cached_property
def properties(self):
return { p.name: p for p in self._enum_properties() }
@cached_property class PartialClass(GirClass):
def signals(self): def __init__(self, name: str, parent: GirType = None):
return { s.name: s for s in self._enum_signals() } self._name = name
self._parent = parent
def assignable_to(self, other) -> bool: @property
if self == other: def full_name(self):
return True return self._name
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 @property
def glib_type_name(self):
return self._name
def _enum_properties(self): @property
yield from self.own_properties.values() def abstract(self):
return False
if self.parent is not None: @property
yield from self.parent.properties.values() def parent(self):
return self._parent
for impl in self.implements: @property
yield from impl.properties.values() def is_partial(self):
return True
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()
class EnumMember(GirNode): class EnumMember(GirNode):

View file

@ -126,9 +126,8 @@ class LookupOp(InfixExpr):
@property @property
def gir_type(self): def gir_type(self):
if parent_type := self.lhs.gir_type: if parent_type := self.lhs.gir_type:
if isinstance(parent_type, gir.Class) or isinstance(parent_type, gir.Interface): if prop := parent_type.lookup_property(self.tokens["property"]):
if prop := parent_type.properties.get(self.tokens["property"]): return prop.type
return prop.type
@property @property
def glib_type_name(self): def glib_type_name(self):
@ -137,15 +136,11 @@ class LookupOp(InfixExpr):
@validate("property") @validate("property")
def property_exists(self): def property_exists(self):
if parent_type := self.lhs.gir_type: try:
if not (isinstance(parent_type, gir.Class) or isinstance(parent_type, gir.Interface)): self.gir_type
raise CompileError(f"Type {parent_type.full_name} does not have properties") except CompileError as e:
elif self.tokens["property"] not in parent_type.properties: e.hints.append("Do you need to cast the previous expression?")
raise CompileError( raise e
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()),
)
def emit_xml(self, xml: XmlEmitter): def emit_xml(self, xml: XmlEmitter):
if isinstance(self.lhs, IdentExpr) and self.lhs.is_this: if isinstance(self.lhs, IdentExpr) and self.lhs.is_this:

View file

@ -48,14 +48,7 @@ class Object(AstNode):
@property @property
def gir_class(self): def gir_class(self):
class_names = self.children[ClassName] return self.children[ClassName][0].gir_type
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
@docs("namespace") @docs("namespace")
def namespace_docs(self): def namespace_docs(self):
@ -83,7 +76,7 @@ class Object(AstNode):
def emit_start_tag(self, xml: XmlEmitter): def emit_start_tag(self, xml: XmlEmitter):
xml.start_tag("object", **{ xml.start_tag("object", **{
"class": self.glib_type_name, "class": self.gir_class,
"id": self.tokens["id"], "id": self.tokens["id"],
}) })

View file

@ -64,37 +64,16 @@ class Property(AstNode):
def gir_class(self): def gir_class(self):
return self.parent.parent.gir_class return self.parent.parent.gir_class
@validate("name")
@property
def gir_property(self): def gir_property(self):
if self.gir_class is not None: 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 @property
def value_type(self): def value_type(self):
if self.gir_property is not None: if self.gir_property is not None:
return self.gir_property.type 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") @validate("bind")
def property_bindable(self): def property_bindable(self):
if self.tokens["bind"] and self.gir_property is not None and self.gir_property.construct_only: 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): def container_is_file_filter(self):
validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") validate_parent_type(self, "Gtk", "FileFilter", "file filter properties")
@validate() @validate("tag")
def unique_in_parent(self): def wrapped_validator(self):
# The token argument to validate() needs to be calculated based on self.validate_unique_in_parent(
# the instance, hence wrapping it like this. f"Duplicate {self.tokens['tag_name']} block",
@validate(self.tokens["tag_name"]) check=lambda child: child.tokens["tag_name"] == 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)
def emit_xml(self, xml: XmlEmitter): def emit_xml(self, xml: XmlEmitter):
xml.start_tag(self.tokens["tag_name"]) xml.start_tag(self.tokens["tag_name"])
@ -57,7 +52,7 @@ def create_node(tag_name: str, singular: str):
return Group( return Group(
Filters, Filters,
[ [
Keyword(tag_name), Keyword(tag_name, "tag"),
UseLiteral("tag_name", tag_name), UseLiteral("tag_name", tag_name),
"[", "[",
Delimited( Delimited(

View file

@ -49,6 +49,10 @@ class Child(AstNode):
if gir_class.assignable_to(parent_type): if gir_class.assignable_to(parent_type):
break break
else: 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"] hints=["only Gio.ListStore or Gtk.Buildable implementors can have children"]
if "child" in gir_class.properties: if "child" in gir_class.properties:
hints.append("did you mean to assign this object to the 'child' property?") 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 # SPDX-License-Identifier: LGPL-3.0-or-later
from functools import cached_property
from .gobject_object import Object, ObjectContent from .gobject_object import Object, ObjectContent
from .common import * from .common import *
@ -34,11 +35,14 @@ class Template(Object):
ObjectContent, ObjectContent,
] ]
@property @cached_property
def gir_class(self): def gir_class(self):
# Templates might not have a parent class defined # Templates might not have a parent class defined
parent = None
if len(self.children[ClassName]): 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") @validate("id")
def unique_in_parent(self): def unique_in_parent(self):

View file

@ -25,7 +25,7 @@ from .common import *
class GtkDirective(AstNode): class GtkDirective(AstNode):
grammar = Statement( grammar = Statement(
Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), 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"), UseNumberText("version").expected("a version number for GTK"),
) )
@ -33,15 +33,23 @@ class GtkDirective(AstNode):
def gtk_version(self): def gtk_version(self):
version = self.tokens["version"] version = self.tokens["version"]
if version not in ["4.0"]: 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"): if version and version.startswith("4"):
err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.") err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.")
else: else:
err.hint("Expected 'using Gtk 4.0;'") err.hint("Expected 'using Gtk 4.0;'")
raise err raise err
return version
@validate()
def gir_namespace(self):
if self.gtk_version is None:
# use that error message instead
return
try: try:
gir.get_namespace("Gtk", version) return gir.get_namespace("Gtk", self.gtk_version)
except CompileError as e: except CompileError as e:
raise CompileError( raise CompileError(
"Could not find GTK 4 introspection files. Is gobject-introspection installed?", "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): def emit_xml(self, xml: XmlEmitter):
xml.put_self_closing("requires", lib="gtk", version=self.tokens["version"]) xml.put_self_closing("requires", lib="gtk", version=self.tokens["version"])
class Import(AstNode): class Import(AstNode):
grammar = Statement( grammar = Statement(
"using", "using",

View file

@ -19,8 +19,9 @@
import typing as T import typing as T
from functools import cached_property
from .common import * from .common import *
from ..gir import Class, Interface from ..gir import GirClass, Class, Interface
class TypeName(AstNode): class TypeName(AstNode):
@ -53,18 +54,16 @@ class TypeName(AstNode):
if not self.tokens["ignore_gir"]: if not self.tokens["ignore_gir"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
@property @cached_property
def gir_type(self) -> T.Optional[gir.Class]: 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 self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"])
return None
@property @property
def glib_type_name(self) -> str: def glib_type_name(self) -> str:
if gir_type := self.gir_type: return self.gir_type.glib_type_name
return gir_type.glib_type_name
else:
return self.tokens["class_name"]
@docs("namespace") @docs("namespace")
def namespace_docs(self): def namespace_docs(self):
@ -83,7 +82,7 @@ class TypeName(AstNode):
class ClassName(TypeName): class ClassName(TypeName):
@validate("namespace", "class_name") @validate("namespace", "class_name")
def gir_class_exists(self): 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): if isinstance(self.gir_type, Interface):
raise CompileError(f"{self.gir_type.full_name} is an interface, not a class") raise CompileError(f"{self.gir_type.full_name} is an interface, not a class")
else: else:

View file

@ -42,7 +42,8 @@ class UI(AstNode, Scope):
self._gir_errors = [] self._gir_errors = []
try: 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: except CompileError as e:
self._gir_errors.append(e) self._gir_errors.append(e)
@ -70,7 +71,7 @@ class UI(AstNode, Scope):
xml.end_tag() xml.end_tag()
return { 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() 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 6,43,5,Type int does not have properties
7,17,7,Could not find object with ID 'nothing' 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) raise MultipleErrors(warnings)
except PrintableError as e: except PrintableError as e:
def error_str(error): 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) line, col = utils.idx_to_pos(error.start + 1, blueprint)
len = error.end - error.start len = error.end - error.start
return ",".join([str(line + 1), str(col), str(len), error.message]) 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("strings")
self.assert_sample("style") self.assert_sample("style")
self.assert_sample("template") self.assert_sample("template")
self.assert_sample("template_binding")
self.assert_sample("template_no_parent") self.assert_sample("template_no_parent")
self.assert_sample("translated") self.assert_sample("translated")
self.assert_sample("uint") self.assert_sample("uint")