reorganization: Finish moving parsing and AST

This commit is contained in:
James Westman 2022-01-26 22:37:51 -06:00
parent 8f9de81e24
commit ee5f32622f
27 changed files with 737 additions and 719 deletions

View file

@ -1,477 +0,0 @@
# ast.py
#
# Copyright 2021 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
import typing as T
from .ast_utils import *
from .errors import CompileError, CompilerBugError, MultipleErrors
from . import gir
from .lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType
from .parse_tree import *
from .tokenizer import Token
from .utils import lazy_prop
from .xml_emitter import XmlEmitter
class UI(AstNode):
""" The AST node for the entire file """
@property
def gir(self):
gir_ctx = gir.GirContext()
self._gir_errors = []
try:
gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace)
except CompileError as e:
e.start = self.children[GtkDirective][0].group.start
e.end = self.children[GtkDirective][0].group.end
self._gir_errors.append(e)
for i in self.children[Import]:
try:
if i.gir_namespace is not None:
gir_ctx.add_namespace(i.gir_namespace)
except CompileError as e:
e.start = i.group.tokens["namespace"].start
e.end = i.group.tokens["version"].end
self._gir_errors.append(e)
return gir_ctx
@lazy_prop
def objects_by_id(self):
return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None }
@validate()
def gir_errors(self):
# make sure gir is loaded
self.gir
if len(self._gir_errors):
raise MultipleErrors(self._gir_errors)
@validate()
def at_most_one_template(self):
if len(self.children[Template]) > 1:
for template in self.children[Template][1:]:
raise CompileError(
f"Only one template may be defined per file, but this file contains {len(self.children[Template])}",
template.group.tokens["name"].start, template.group.tokens["name"].end,
)
@validate()
def unique_ids(self):
passed = {}
for obj in self.iterate_children_recursive():
if obj.tokens["id"] is None:
continue
if obj.tokens["id"] in passed:
token = obj.group.tokens["id"]
raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end)
passed[obj.tokens["id"]] = obj
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("interface")
for x in self.children:
x.emit_xml(xml)
xml.end_tag()
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;`)"),
UseNumberText("version").expected("a version number for GTK"),
)
@validate("version")
def gtk_version(self):
if self.tokens["version"] not in ["4.0"]:
err = CompileError("Only GTK 4 is supported")
if self.tokens["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
@property
def gir_namespace(self):
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",
UseIdent("namespace").expected("a GIR namespace"),
UseNumberText("version").expected("a version number"),
)
@validate("namespace", "version")
def namespace_exists(self):
gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
@property
def gir_namespace(self):
try:
return gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
except CompileError:
return None
def emit_xml(self, xml):
pass
class Object(AstNode):
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["ignore_gir"]:
self.root.gir.validate_ns(self.tokens["namespace"])
@validate("class_name")
def gir_class_exists(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None:
self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"])
@property
def gir_ns(self):
if not self.tokens["ignore_gir"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
@property
def gir_class(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"]:
return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"])
@docs("namespace")
def namespace_docs(self):
if ns := self.root.gir.namespaces.get(self.tokens["namespace"]):
return ns.doc
@docs("class_name")
def class_docs(self):
if self.gir_class:
return self.gir_class.doc
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("object", **{
"class": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"],
"id": self.tokens["id"],
})
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
class Template(Object):
def emit_xml(self, xml: XmlEmitter):
if self.gir_class:
parent = self.gir_class.glib_type_name
elif self.tokens["class_name"]:
parent = self.tokens["class_name"]
else:
parent = None
xml.start_tag("template", **{"class": self.tokens["name"]}, parent=parent)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
class Child(AstNode):
def emit_xml(self, xml: XmlEmitter):
child_type = internal_child = None
if self.tokens["internal_child"]:
internal_child = self.tokens["child_type"]
else:
child_type = self.tokens["child_type"]
xml.start_tag("child", type=child_type, internal_child=internal_child)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
class ObjectContent(AstNode):
@property
def gir_class(self):
return self.parent.gir_class
# @validate()
# def only_one_style_class(self):
# if len(self.children[Style]) > 1:
# raise CompileError(
# f"Only one style directive allowed per object, but this object contains {len(self.children[Style])}",
# start=self.children[Style][1].group.start,
# )
def emit_xml(self, xml: XmlEmitter):
for x in self.children:
x.emit_xml(xml)
class Property(AstNode):
@property
def gir_class(self):
return self.parent.parent.gir_class
@property
def gir_property(self):
if self.gir_class is not None:
return self.gir_class.properties.get(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()
def obj_property_type(self):
if len(self.children[Object]) == 0:
return
object = self.children[Object][0]
type = self.value_type
if object and type and object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
@docs("name")
def property_docs(self):
if self.gir_property is not None:
return self.gir_property.doc
def emit_xml(self, xml: XmlEmitter):
values = self.children[Value]
value = values[0] if len(values) == 1 else None
bind_flags = []
if self.tokens["sync_create"]:
bind_flags.append("sync-create")
if self.tokens["inverted"]:
bind_flags.append("invert-boolean")
if self.tokens["bidirectional"]:
bind_flags.append("bidirectional")
bind_flags_str = "|".join(bind_flags) or None
props = {
"name": self.tokens["name"],
"bind-source": self.tokens["bind_source"],
"bind-property": self.tokens["bind_property"],
"bind-flags": bind_flags_str,
}
if isinstance(value, TranslatedStringValue):
props = { **props, **value.attrs }
if len(self.children[Object]) == 1:
xml.start_tag("property", **props)
self.children[Object][0].emit_xml(xml)
xml.end_tag()
elif value is None:
xml.put_self_closing("property", **props)
else:
xml.start_tag("property", **props)
value.emit_xml(xml)
xml.end_tag()
class Value(ast.AstNode):
pass
class TranslatedStringValue(Value):
@property
def attrs(self):
attrs = { "translatable": "true" }
if "context" in self.tokens:
attrs["context"] = self.tokens["context"]
return attrs
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
class LiteralValue(Value):
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
@validate()
def validate_for_type(self):
type = self.parent.value_type
if isinstance(type, gir.IntType):
try:
int(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer")
elif isinstance(type, gir.UIntType):
try:
int(self.tokens["value"])
if int(self.tokens["value"]) < 0:
raise Exception()
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer")
elif isinstance(type, gir.FloatType):
try:
float(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to float")
elif isinstance(type, gir.StringType):
pass
elif isinstance(type, gir.Class) or isinstance(type, gir.Interface):
parseable_types = [
"Gdk.Paintable",
"Gdk.Texture",
"Gdk.Pixbuf",
"GLib.File",
"Gtk.ShortcutTrigger",
"Gtk.ShortcutAction",
]
if type.full_name not in parseable_types:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
elif type is not None:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
class Flag(AstNode):
pass
class FlagsValue(Value):
def emit_xml(self, xml: XmlEmitter):
xml.put_text("|".join([flag.tokens["value"] for flag in self.children[Flag]]))
class IdentValue(Value):
def emit_xml(self, xml: XmlEmitter):
if isinstance(self.parent.value_type, gir.Enumeration):
xml.put_text(self.parent.value_type.members[self.tokens["value"]].nick)
else:
xml.put_text(self.tokens["value"])
@validate()
def validate_for_type(self):
type = self.parent.value_type
if isinstance(type, gir.Enumeration):
if self.tokens["value"] not in type.members:
raise CompileError(
f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=(self.tokens['value'], type.members.keys()),
)
elif isinstance(type, gir.BoolType):
if self.tokens["value"] not in ["true", "false"]:
raise CompileError(
f"Expected 'true' or 'false' for boolean value",
did_you_mean=(self.tokens['value'], ["true", "false"]),
)
elif type is not None:
object = self.root.objects_by_id.get(self.tokens["value"])
if object is None:
raise CompileError(
f"Could not find object with ID {self.tokens['value']}",
did_you_mean=(self.tokens['value'], self.root.objects_by_id.keys()),
)
elif object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
@docs()
def docs(self):
type = self.parent.value_type
if isinstance(type, gir.Enumeration):
if member := type.members.get(self.tokens["value"]):
return member.doc
else:
return type.doc
elif isinstance(type, gir.GirNode):
return type.doc
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
if isinstance(self.parent.value_type, gir.Enumeration):
token = self.group.tokens["value"]
yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember)
class BaseAttribute(AstNode):
""" A helper class for attribute syntax of the form `name: literal_value;`"""
tag_name: str = ""
attr_name: str = "name"
def emit_xml(self, xml: XmlEmitter):
value = self.children[Value][0]
attrs = { self.attr_name: self.tokens["name"] }
if isinstance(value, TranslatedStringValue):
attrs = { **attrs, **value.attrs }
xml.start_tag(self.tag_name, **attrs)
value.emit_xml(xml)
xml.end_tag()
class BaseTypedAttribute(BaseAttribute):
""" A BaseAttribute whose parent has a value_type property that can assist
in validation. """

View file

@ -20,7 +20,6 @@
import typing as T
from collections import ChainMap, defaultdict
from . import ast
from .errors import *
from .lsp_utils import SemanticToken
from .utils import lazy_prop
@ -72,12 +71,6 @@ class AstNode:
else:
return self.parent.parent_by_type(type)
def validate_parent_type(self, ns: str, name: str, err_msg: str):
parent = self.root.gir.get_type(name, ns)
container_type = self.parent_by_type(ast.Object).gir_class
if container_type and not container_type.assignable_to(parent):
raise CompileError(f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}")
@lazy_prop
def errors(self):
return list(self._get_errors())

View file

@ -19,7 +19,6 @@
import typing as T
from . import ast
from . import gir
from .completions_utils import *
from .lsp_utils import Completion, CompletionItemKind

View file

@ -20,7 +20,6 @@
import typing as T
from . import ast
from .tokenizer import Token, TokenType
from .lsp_utils import Completion

View file

@ -1,6 +1,8 @@
""" Contains all the syntax beyond basic objects, properties, signal, and
templates. """
from .gobject_object import Object
from .gobject_property import Property
from .gobject_signal import Signal
from .gtk_a11y import A11y
from .gtk_combo_box_text import Items
@ -10,12 +12,36 @@ from .gtk_menu import menu
from .gtk_size_group import Widgets
from .gtk_string_list import Strings
from .gtk_styles import Styles
from .gtkbuilder_child import Child
from .gtkbuilder_template import Template
from .ui import UI
from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue
from .common import *
OBJECT_HOOKS.children = [menu]
OBJECT_HOOKS.children = [
menu,
Object,
]
OBJECT_CONTENT_HOOKS.children = [
Signal, A11y, Styles, Layout, mime_types, patterns, suffixes, Widgets, Items,
Signal,
Property,
A11y,
Styles,
Layout,
mime_types,
patterns,
suffixes,
Widgets,
Items,
Strings,
Child,
]
VALUE_HOOKS.children = [
TranslatedStringValue,
FlagsValue,
IdentValue,
LiteralValue,
]

View file

@ -0,0 +1,45 @@
# 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 .values import Value, TranslatedStringValue
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"
def emit_xml(self, xml: XmlEmitter):
value = self.children[Value][0]
attrs = { self.attr_name: self.tokens["name"] }
if isinstance(value, TranslatedStringValue):
attrs = { **attrs, **value.attrs }
xml.start_tag(self.tag_name, **attrs)
value.emit_xml(xml)
xml.end_tag()
class BaseTypedAttribute(BaseAttribute):
""" A BaseAttribute whose parent has a value_type property that can assist
in validation. """

View file

@ -18,11 +18,11 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..ast import BaseTypedAttribute, Value, Template
from .. import gir
from ..ast_utils import AstNode, validate, docs
from ..completions_utils import *
from ..gir import StringType, BoolType, IntType, FloatType, GirType
from ..lsp_utils import Completion, CompletionItemKind
from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
@ -30,3 +30,4 @@ from ..xml_emitter import XmlEmitter
OBJECT_HOOKS = AnyOf()
OBJECT_CONTENT_HOOKS = AnyOf()
VALUE_HOOKS = AnyOf()

View file

@ -0,0 +1,97 @@
# gobject_object.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 ObjectContent(AstNode):
grammar = ["{", Until(OBJECT_CONTENT_HOOKS, "}")]
@property
def gir_class(self):
return self.parent.gir_class
# @validate()
# def only_one_style_class(self):
# if len(self.children[Style]) > 1:
# raise CompileError(
# f"Only one style directive allowed per object, but this object contains {len(self.children[Style])}",
# start=self.children[Style][1].group.start,
# )
def emit_xml(self, xml: XmlEmitter):
for x in self.children:
x.emit_xml(xml)
class Object(AstNode):
grammar = Sequence(
class_name,
Optional(UseIdent("id")),
ObjectContent,
)
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["ignore_gir"]:
self.root.gir.validate_ns(self.tokens["namespace"])
@validate("class_name")
def gir_class_exists(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None:
self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"])
@property
def gir_ns(self):
if not self.tokens["ignore_gir"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
@property
def gir_class(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"]:
return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"])
@docs("namespace")
def namespace_docs(self):
if ns := self.root.gir.namespaces.get(self.tokens["namespace"]):
return ns.doc
@docs("class_name")
def class_docs(self):
if self.gir_class:
return self.gir_class.doc
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("object", **{
"class": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"],
"id": self.tokens["id"],
})
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
def validate_parent_type(node, ns: str, name: str, err_msg: str):
parent = node.root.gir.get_type(name, ns)
container_type = node.parent_by_type(Object).gir_class
if container_type and not container_type.assignable_to(parent):
raise CompileError(f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}")

View file

@ -0,0 +1,139 @@
# gobject_property.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 .gobject_object import Object
from .gtkbuilder_template import Template
from .values import Value, TranslatedStringValue
from .common import *
class Property(AstNode):
grammar = AnyOf(
Statement(
UseIdent("name"),
":",
"bind",
UseIdent("bind_source").expected("the ID of a source object to bind from"),
".",
UseIdent("bind_property").expected("a property name to bind from"),
ZeroOrMore(AnyOf(
["sync-create", UseLiteral("sync_create", True)],
["inverted", UseLiteral("inverted", True)],
["bidirectional", UseLiteral("bidirectional", True)],
)),
),
Statement(
UseIdent("name"),
":",
AnyOf(
OBJECT_HOOKS,
VALUE_HOOKS,
).expected("a value"),
),
)
@property
def gir_class(self):
return self.parent.parent.gir_class
@property
def gir_property(self):
if self.gir_class is not None:
return self.gir_class.properties.get(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()
def obj_property_type(self):
if len(self.children[Object]) == 0:
return
object = self.children[Object][0]
type = self.value_type
if object and type and object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
@docs("name")
def property_docs(self):
if self.gir_property is not None:
return self.gir_property.doc
def emit_xml(self, xml: XmlEmitter):
values = self.children[Value]
value = values[0] if len(values) == 1 else None
bind_flags = []
if self.tokens["sync_create"]:
bind_flags.append("sync-create")
if self.tokens["inverted"]:
bind_flags.append("invert-boolean")
if self.tokens["bidirectional"]:
bind_flags.append("bidirectional")
bind_flags_str = "|".join(bind_flags) or None
props = {
"name": self.tokens["name"],
"bind-source": self.tokens["bind_source"],
"bind-property": self.tokens["bind_property"],
"bind-flags": bind_flags_str,
}
if isinstance(value, TranslatedStringValue):
props = { **props, **value.attrs }
if len(self.children[Object]) == 1:
xml.start_tag("property", **props)
self.children[Object][0].emit_xml(xml)
xml.end_tag()
elif value is None:
xml.put_self_closing("property", **props)
else:
xml.start_tag("property", **props)
value.emit_xml(xml)
xml.end_tag()

View file

@ -18,6 +18,7 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gtkbuilder_template import Template
from .common import *

View file

@ -17,14 +17,10 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..ast import BaseTypedAttribute, Value
from ..ast_utils import AstNode, validate, docs
from ..completions_utils import *
from ..gir import StringType, BoolType, IntType, FloatType, GirType
from ..lsp_utils import Completion, CompletionItemKind
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
from .gobject_object import ObjectContent, validate_parent_type
from .attributes import BaseTypedAttribute
from .values import Value
from .common import *
def get_property_types(gir):
@ -109,7 +105,7 @@ class A11yProperty(BaseTypedAttribute):
grammar = Statement(
UseIdent("name"),
":",
value.expected("a value"),
VALUE_HOOKS.expected("a value"),
)
@property
@ -153,7 +149,7 @@ class A11y(AstNode):
@validate("accessibility")
def container_is_widget(self):
self.validate_parent_type("Gtk", "Widget", "accessibility properties")
validate_parent_type(self, "Gtk", "Widget", "accessibility properties")
def emit_xml(self, xml: XmlEmitter):
@ -164,7 +160,7 @@ class A11y(AstNode):
@completer(
applies_in=[ast.ObjectContent],
applies_in=[ObjectContent],
matches=new_statement_patterns,
)
def a11y_completer(ast_node, match_variables):

View file

@ -18,14 +18,9 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..ast import BaseTypedAttribute
from ..ast_utils import AstNode, validate
from ..completions_utils import *
from ..gir import StringType
from ..lsp_utils import Completion, CompletionItemKind
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class Item(BaseTypedAttribute):
@ -44,7 +39,7 @@ item = Group(
UseIdent("name"),
":",
]),
value,
VALUE_HOOKS,
]
)
@ -59,7 +54,7 @@ class Items(AstNode):
@validate("items")
def container_is_combo_box_text(self):
self.validate_parent_type("Gtk", "ComboBoxText", "combo box items")
validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items")
def emit_xml(self, xml: XmlEmitter):
@ -70,7 +65,7 @@ class Items(AstNode):
@completer(
applies_in=[ast.ObjectContent],
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "ComboBoxText"),
matches=new_statement_patterns,
)

View file

@ -18,19 +18,14 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .. import ast
from ..ast_utils import AstNode, validate
from ..completions_utils import *
from ..lsp_utils import Completion, CompletionItemKind
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class Filters(AstNode):
@validate()
def container_is_file_filter(self):
self.validate_parent_type("Gtk", "FileFilter", "file filter properties")
validate_parent_type(self, "Gtk", "FileFilter", "file filter properties")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag(self.tokens["tag_name"])
@ -74,7 +69,7 @@ suffixes = create_node("suffixes", "suffix")
@completer(
applies_in=[ast.ObjectContent],
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "FileFilter"),
matches=new_statement_patterns,
)

View file

@ -18,13 +18,9 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..ast import BaseAttribute
from ..ast_utils import AstNode, validate
from ..completions_utils import *
from ..lsp_utils import Completion, CompletionItemKind
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
from .attributes import BaseAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class LayoutProperty(BaseAttribute):
@ -41,7 +37,7 @@ layout_prop = Group(
Statement(
UseIdent("name"),
":",
value.expected("a value"),
VALUE_HOOKS.expected("a value"),
)
)
@ -55,7 +51,7 @@ class Layout(AstNode):
@validate("layout")
def container_is_widget(self):
self.validate_parent_type("Gtk", "Widget", "layout properties")
validate_parent_type(self, "Gtk", "Widget", "layout properties")
def emit_xml(self, xml: XmlEmitter):
@ -66,7 +62,7 @@ class Layout(AstNode):
@completer(
applies_in=[ast.ObjectContent],
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns,
)

View file

@ -18,13 +18,10 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..ast import BaseAttribute
from ..ast_utils import AstNode
from ..completions_utils import *
from ..lsp_utils import Completion, CompletionItemKind
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
from .attributes import BaseAttribute
from .gobject_object import ObjectContent
from .ui import UI
from .common import *
class Menu(AstNode):
@ -74,7 +71,7 @@ menu_attribute = Group(
[
UseIdent("name"),
":",
value.expected("a value"),
VALUE_HOOKS.expected("a value"),
Match(";").expected(),
]
)
@ -98,20 +95,20 @@ menu_item_shorthand = Group(
"(",
Group(
MenuAttribute,
[UseLiteral("name", "label"), value],
[UseLiteral("name", "label"), VALUE_HOOKS],
),
Optional([
",",
Optional([
Group(
MenuAttribute,
[UseLiteral("name", "action"), value],
[UseLiteral("name", "action"), VALUE_HOOKS],
),
Optional([
",",
Group(
MenuAttribute,
[UseLiteral("name", "icon"), value],
[UseLiteral("name", "icon"), VALUE_HOOKS],
),
])
])
@ -143,7 +140,7 @@ menu = Group(
@completer(
applies_in=[ast.UI],
applies_in=[UI],
matches=new_statement_patterns,
)
def menu_completer(ast_node, match_variables):

View file

@ -18,13 +18,8 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .. import ast
from ..ast_utils import AstNode, validate
from ..completions_utils import *
from ..lsp_utils import Completion, CompletionItemKind
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class Widget(AstNode):
@ -58,7 +53,7 @@ class Widgets(AstNode):
@validate("widgets")
def container_is_size_group(self):
self.validate_parent_type("Gtk", "SizeGroup", "size group properties")
validate_parent_type(self, "Gtk", "SizeGroup", "size group properties")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("widgets")
@ -68,7 +63,7 @@ class Widgets(AstNode):
@completer(
applies_in=[ast.ObjectContent],
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "SizeGroup"),
matches=new_statement_patterns,
)

View file

@ -18,18 +18,14 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..ast import BaseTypedAttribute, Value, TranslatedStringValue
from ..ast_utils import AstNode, validate
from ..completions_utils import *
from ..gir import StringType
from ..lsp_utils import Completion, CompletionItemKind
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .values import Value, TranslatedStringValue
from .common import *
class Item(AstNode):
grammar = value
grammar = VALUE_HOOKS
@property
def value_type(self):
@ -53,7 +49,7 @@ class Strings(AstNode):
@validate("items")
def container_is_string_list(self):
self.validate_parent_type("Gtk", "StringList", "StringList items")
validate_parent_type(self, "Gtk", "StringList", "StringList items")
def emit_xml(self, xml: XmlEmitter):
@ -64,7 +60,7 @@ class Strings(AstNode):
@completer(
applies_in=[ast.ObjectContent],
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "StringList"),
matches=new_statement_patterns,
)

View file

@ -18,13 +18,8 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .. import ast
from ..ast_utils import AstNode, validate
from ..completions_utils import *
from ..lsp_utils import Completion, CompletionItemKind
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class StyleClass(AstNode):
@ -44,7 +39,7 @@ class Styles(AstNode):
@validate("styles")
def container_is_widget(self):
self.validate_parent_type("Gtk", "Widget", "style classes")
validate_parent_type(self, "Gtk", "Widget", "style classes")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("style")
@ -54,7 +49,7 @@ class Styles(AstNode):
@completer(
applies_in=[ast.ObjectContent],
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns,
)

View file

@ -0,0 +1,45 @@
# gtkbuilder_child.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 .gobject_object import Object
from .common import *
class Child(AstNode):
grammar = [
Optional([
"[",
Optional(["internal-child", UseLiteral("internal_child", True)]),
UseIdent("child_type").expected("a child type"),
"]",
]),
Object,
]
def emit_xml(self, xml: XmlEmitter):
child_type = internal_child = None
if self.tokens["internal_child"]:
internal_child = self.tokens["child_type"]
else:
child_type = self.tokens["child_type"]
xml.start_tag("child", type=child_type, internal_child=internal_child)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()

View file

@ -0,0 +1,46 @@
# gtkbuilder_template.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 .gobject_object import Object, ObjectContent
from .common import *
class Template(Object):
grammar = [
"template",
UseIdent("name").expected("template class name"),
Optional([
Match(":"),
class_name.expected("parent class"),
]),
ObjectContent,
]
def emit_xml(self, xml: XmlEmitter):
if self.gir_class:
parent = self.gir_class.glib_type_name
elif self.tokens["class_name"]:
parent = self.tokens["class_name"]
else:
parent = None
xml.start_tag("template", **{"class": self.tokens["name"]}, parent=parent)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()

View file

@ -18,6 +18,7 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .. import gir
from .common import *

View file

@ -0,0 +1,103 @@
# ui.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 .. import gir
from .imports import GtkDirective, Import
from .gtkbuilder_template import Template
from .common import *
class UI(AstNode):
""" The AST node for the entire file """
grammar = [
GtkDirective,
ZeroOrMore(Import),
Until(AnyOf(
Template,
OBJECT_HOOKS,
), Eof()),
]
@property
def gir(self):
gir_ctx = gir.GirContext()
self._gir_errors = []
try:
gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace)
except CompileError as e:
e.start = self.children[GtkDirective][0].group.start
e.end = self.children[GtkDirective][0].group.end
self._gir_errors.append(e)
for i in self.children[Import]:
try:
if i.gir_namespace is not None:
gir_ctx.add_namespace(i.gir_namespace)
except CompileError as e:
e.start = i.group.tokens["namespace"].start
e.end = i.group.tokens["version"].end
self._gir_errors.append(e)
return gir_ctx
@property
def objects_by_id(self):
return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None }
@validate()
def gir_errors(self):
# make sure gir is loaded
self.gir
if len(self._gir_errors):
raise MultipleErrors(self._gir_errors)
@validate()
def at_most_one_template(self):
if len(self.children[Template]) > 1:
for template in self.children[Template][1:]:
raise CompileError(
f"Only one template may be defined per file, but this file contains {len(self.children[Template])}",
template.group.tokens["name"].start, template.group.tokens["name"].end,
)
@validate()
def unique_ids(self):
passed = {}
for obj in self.iterate_children_recursive():
if obj.tokens["id"] is None:
continue
if obj.tokens["id"] in passed:
token = obj.group.tokens["id"]
raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end)
passed[obj.tokens["id"]] = obj
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("interface")
for x in self.children:
x.emit_xml(xml)
xml.end_tag()

View file

@ -0,0 +1,175 @@
# values.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 Value(AstNode):
pass
class TranslatedStringValue(Value):
grammar = AnyOf(
[
"_",
"(",
UseQuoted("value").expected("a quoted string"),
Match(")").expected(),
],
[
"C_",
"(",
UseQuoted("context").expected("a quoted string"),
",",
UseQuoted("value").expected("a quoted string"),
Optional(","),
Match(")").expected(),
],
)
@property
def attrs(self):
attrs = { "translatable": "true" }
if "context" in self.tokens:
attrs["context"] = self.tokens["context"]
return attrs
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
class LiteralValue(Value):
grammar = AnyOf(
UseNumber("value"),
UseQuoted("value"),
)
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
@validate()
def validate_for_type(self):
type = self.parent.value_type
if isinstance(type, gir.IntType):
try:
int(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer")
elif isinstance(type, gir.UIntType):
try:
int(self.tokens["value"])
if int(self.tokens["value"]) < 0:
raise Exception()
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer")
elif isinstance(type, gir.FloatType):
try:
float(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to float")
elif isinstance(type, gir.StringType):
pass
elif isinstance(type, gir.Class) or isinstance(type, gir.Interface):
parseable_types = [
"Gdk.Paintable",
"Gdk.Texture",
"Gdk.Pixbuf",
"GLib.File",
"Gtk.ShortcutTrigger",
"Gtk.ShortcutAction",
]
if type.full_name not in parseable_types:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
elif type is not None:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
class Flag(AstNode):
grammar = UseIdent("value")
class FlagsValue(Value):
grammar = [Flag, "|", Delimited(Flag, "|")]
def emit_xml(self, xml: XmlEmitter):
xml.put_text("|".join([flag.tokens["value"] for flag in self.children[Flag]]))
class IdentValue(Value):
grammar = UseIdent("value")
def emit_xml(self, xml: XmlEmitter):
if isinstance(self.parent.value_type, gir.Enumeration):
xml.put_text(self.parent.value_type.members[self.tokens["value"]].nick)
else:
xml.put_text(self.tokens["value"])
@validate()
def validate_for_type(self):
type = self.parent.value_type
if isinstance(type, gir.Enumeration):
if self.tokens["value"] not in type.members:
raise CompileError(
f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=(self.tokens['value'], type.members.keys()),
)
elif isinstance(type, gir.BoolType):
if self.tokens["value"] not in ["true", "false"]:
raise CompileError(
f"Expected 'true' or 'false' for boolean value",
did_you_mean=(self.tokens['value'], ["true", "false"]),
)
elif type is not None:
object = self.root.objects_by_id.get(self.tokens["value"])
if object is None:
raise CompileError(
f"Could not find object with ID {self.tokens['value']}",
did_you_mean=(self.tokens['value'], self.root.objects_by_id.keys()),
)
elif object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
@docs()
def docs(self):
type = self.parent.value_type
if isinstance(type, gir.Enumeration):
if member := type.members.get(self.tokens["value"]):
return member.doc
else:
return type.doc
elif isinstance(type, gir.GirNode):
return type.doc
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
if isinstance(self.parent.value_type, gir.Enumeration):
token = self.group.tokens["value"]
yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember)

View file

@ -530,5 +530,7 @@ def to_parse_node(value) -> ParseNode:
return Sequence(*value)
elif isinstance(value, type) and hasattr(value, "grammar"):
return Group(value, getattr(value, "grammar"))
else:
elif isinstance(value, ParseNode):
return value
else:
raise CompilerBugError()

View file

@ -18,113 +18,18 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from . import ast
from .errors import MultipleErrors
from .parse_tree import *
from .parser_utils import *
from .tokenizer import TokenType
from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS
from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI
def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors]]:
""" Parses a list of tokens into an abstract syntax tree. """
object = Group(
ast.Object,
None
)
property = Group(
ast.Property,
Statement(
UseIdent("name"),
":",
AnyOf(
OBJECT_HOOKS,
object,
value,
).expected("a value"),
)
)
binding = Group(
ast.Property,
Statement(
UseIdent("name"),
":",
"bind",
UseIdent("bind_source").expected("the ID of a source object to bind from"),
".",
UseIdent("bind_property").expected("a property name to bind from"),
ZeroOrMore(AnyOf(
["sync-create", UseLiteral("sync_create", True)],
["inverted", UseLiteral("inverted", True)],
["bidirectional", UseLiteral("bidirectional", True)],
)),
)
)
child = Group(
ast.Child,
[
Optional([
"[",
Optional(["internal-child", UseLiteral("internal_child", True)]),
UseIdent("child_type").expected("a child type"),
"]",
]),
object,
]
)
object_content = Group(
ast.ObjectContent,
[
"{",
Until(AnyOf(
OBJECT_CONTENT_HOOKS,
binding,
property,
child,
), "}"),
]
)
# work around the recursive reference
object.child = Sequence(
class_name,
Optional(UseIdent("id")),
object_content,
)
template = Group(
ast.Template,
[
"template",
UseIdent("name").expected("template class name"),
Optional([
Match(":"),
class_name.expected("parent class"),
]),
object_content.expected("block"),
]
)
ui = Group(
ast.UI,
[
ast.GtkDirective,
ZeroOrMore(ast.Import),
Until(AnyOf(
OBJECT_HOOKS,
template,
object,
), Eof()),
]
)
ctx = ParseContext(tokens)
ui.parse(ctx)
AnyOf(UI).parse(ctx)
ast_node = ctx.last_group.to_ast() if ctx.last_group else None
errors = MultipleErrors(ctx.errors) if len(ctx.errors) else None

View file

@ -18,7 +18,6 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from . import ast
from .parse_tree import *
@ -35,48 +34,3 @@ class_name = AnyOf(
],
UseIdent("class_name"),
)
literal = Group(
ast.LiteralValue,
AnyOf(
UseNumber("value"),
UseQuoted("value"),
)
)
ident_value = Group(
ast.IdentValue,
UseIdent("value"),
)
flags_value = Group(
ast.FlagsValue,
[
Group(ast.Flag, UseIdent("value")),
"|",
Delimited(Group(ast.Flag, UseIdent("value")), "|"),
],
)
translated_string = Group(
ast.TranslatedStringValue,
AnyOf(
[
"_",
"(",
UseQuoted("value").expected("a quoted string"),
Match(")").expected(),
],
[
"C_",
"(",
UseQuoted("context").expected("a quoted string"),
",",
UseQuoted("value").expected("a quoted string"),
Optional(","),
Match(")").expected(),
],
),
)
value = AnyOf(translated_string, literal, flags_value, ident_value)