mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
Add GObject Introspection integration
- Parse .gir files - Validate class, property, and signal names
This commit is contained in:
parent
2ad2f1c54a
commit
e553e5db29
10 changed files with 734 additions and 58 deletions
|
@ -22,4 +22,4 @@
|
|||
from gtkblueprinttool import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main.main()
|
||||
|
|
|
@ -18,13 +18,102 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
|
||||
from .errors import assert_true
|
||||
from .errors import assert_true, AlreadyCaughtError, CompileError, CompilerBugError, MultipleErrors
|
||||
from .gir import GirContext, get_namespace
|
||||
from .utils import lazy_prop
|
||||
from .xml_emitter import XmlEmitter
|
||||
|
||||
|
||||
class Validator():
|
||||
def __init__(self, func, token_name=None, end_token_name=None):
|
||||
self.func = func
|
||||
self.token_name = token_name
|
||||
self.end_token_name = end_token_name
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
key = "_validation_result_" + self.func.__name__
|
||||
|
||||
if key + "_err" in instance.__dict__:
|
||||
# If the validator has failed before, raise a generic Exception.
|
||||
# We want anything that depends on this validation result to
|
||||
# fail, but not report the exception twice.
|
||||
raise AlreadyCaughtError()
|
||||
|
||||
if key not in instance.__dict__:
|
||||
try:
|
||||
instance.__dict__[key] = self.func(instance)
|
||||
except CompileError as e:
|
||||
# Mark the validator as already failed so we don't print the
|
||||
# same message again
|
||||
instance.__dict__[key + "_err"] = True
|
||||
|
||||
# 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 self.token_name is not None and e.start is None:
|
||||
group = instance.group.tokens.get(self.token_name)
|
||||
if self.end_token_name is not None and group is None:
|
||||
group = instance.group.tokens[self.end_token_name]
|
||||
e.start = group.start
|
||||
if (self.token_name is not None or self.end_token_name is not None) and e.end is None:
|
||||
e.end = instance.group.tokens[self.end_token_name or self.token_name].end
|
||||
|
||||
# Re-raise the exception
|
||||
raise e
|
||||
|
||||
# Return the validation result (which other validators, or the code
|
||||
# generation phase, might depend on)
|
||||
return instance.__dict__[key]
|
||||
|
||||
|
||||
def validate(*args, **kwargs):
|
||||
""" Decorator for functions that validate an AST node. Exceptions raised
|
||||
during validation are marked with range information from the tokens. Also
|
||||
creates a cached property out of the function. """
|
||||
|
||||
def decorator(func):
|
||||
return Validator(func, *args, **kwargs)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class AstNode:
|
||||
""" Base class for nodes in the abstract syntax tree. """
|
||||
|
||||
def __init__(self):
|
||||
self.group = None
|
||||
self.parent = None
|
||||
self.child_nodes = None
|
||||
|
||||
@lazy_prop
|
||||
def root(self):
|
||||
if self.parent is None:
|
||||
return self
|
||||
else:
|
||||
return self.parent.root
|
||||
|
||||
@lazy_prop
|
||||
def errors(self):
|
||||
return list(self._get_errors())
|
||||
|
||||
def _get_errors(self):
|
||||
for name in dir(type(self)):
|
||||
item = getattr(type(self), name)
|
||||
if isinstance(item, Validator):
|
||||
try:
|
||||
getattr(self, name)
|
||||
except AlreadyCaughtError:
|
||||
pass
|
||||
except CompileError as e:
|
||||
yield e
|
||||
|
||||
for child in self.child_nodes:
|
||||
yield from child._get_errors()
|
||||
|
||||
|
||||
def generate(self) -> str:
|
||||
""" Generates an XML string from the node. """
|
||||
xml = XmlEmitter()
|
||||
|
@ -40,6 +129,7 @@ class UI(AstNode):
|
|||
""" The AST node for the entire file """
|
||||
|
||||
def __init__(self, gtk_directives=[], imports=[], objects=[], templates=[]):
|
||||
super().__init__()
|
||||
assert_true(len(gtk_directives) == 1)
|
||||
|
||||
self.gtk_directive = gtk_directives[0]
|
||||
|
@ -47,6 +137,22 @@ class UI(AstNode):
|
|||
self.objects = objects
|
||||
self.templates = templates
|
||||
|
||||
@validate()
|
||||
def gir(self):
|
||||
gir = GirContext()
|
||||
|
||||
gir.add_namespace(self.gtk_directive.gir_namespace)
|
||||
for i in self.imports:
|
||||
gir.add_namespace(i.gir_namespace)
|
||||
|
||||
return gir
|
||||
|
||||
@validate()
|
||||
def at_most_one_template(self):
|
||||
if len(self.templates) > 1:
|
||||
raise CompileError(f"Only one template may be defined per file, but this file contains {len(self.templates)}",
|
||||
self.templates[1].group.start)
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
xml.start_tag("interface")
|
||||
self.gtk_directive.emit_xml(xml)
|
||||
|
@ -60,8 +166,21 @@ class UI(AstNode):
|
|||
class GtkDirective(AstNode):
|
||||
child_type = "gtk_directives"
|
||||
def __init__(self, version):
|
||||
super().__init__()
|
||||
self.version = version
|
||||
|
||||
@validate("version")
|
||||
def gir_namespace(self):
|
||||
if self.version in ["4.0"]:
|
||||
return get_namespace("Gtk", self.version)
|
||||
else:
|
||||
err = CompileError("Only GTK 4 is supported")
|
||||
if self.version.startswith("4"):
|
||||
err.hint("Expected the GIR version, not an exact version number. Use `@gtk \"4.0\";`.")
|
||||
else:
|
||||
err.hint("Expected `@gtk \"4.0\";`")
|
||||
raise err
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
xml.put_self_closing("requires", lib="gtk", version=self.version)
|
||||
|
||||
|
@ -69,9 +188,14 @@ class GtkDirective(AstNode):
|
|||
class Import(AstNode):
|
||||
child_type = "imports"
|
||||
def __init__(self, namespace, version):
|
||||
super().__init__()
|
||||
self.namespace = namespace
|
||||
self.version = version
|
||||
|
||||
@validate("namespace", "version")
|
||||
def gir_namespace(self):
|
||||
return get_namespace(self.namespace, self.version)
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
pass
|
||||
|
||||
|
@ -79,6 +203,7 @@ class Import(AstNode):
|
|||
class Template(AstNode):
|
||||
child_type = "templates"
|
||||
def __init__(self, name, class_name, object_content, namespace=None):
|
||||
super().__init__()
|
||||
assert_true(len(object_content) == 1)
|
||||
|
||||
self.name = name
|
||||
|
@ -86,10 +211,16 @@ class Template(AstNode):
|
|||
self.parent_class = class_name
|
||||
self.object_content = object_content[0]
|
||||
|
||||
|
||||
@validate("namespace", "class_name")
|
||||
def gir_parent(self):
|
||||
return self.root.gir.get_class(self.parent_class, self.parent_namespace)
|
||||
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
xml.start_tag("template", **{
|
||||
"class": self.name,
|
||||
"parent": self.parent_namespace + self.parent_class,
|
||||
"parent": self.gir_parent.glib_type_name,
|
||||
})
|
||||
self.object_content.emit_xml(xml)
|
||||
xml.end_tag()
|
||||
|
@ -98,6 +229,7 @@ class Template(AstNode):
|
|||
class Object(AstNode):
|
||||
child_type = "objects"
|
||||
def __init__(self, class_name, object_content, namespace=None, id=None):
|
||||
super().__init__()
|
||||
assert_true(len(object_content) == 1)
|
||||
|
||||
self.namespace = namespace
|
||||
|
@ -105,9 +237,13 @@ class Object(AstNode):
|
|||
self.id = id
|
||||
self.object_content = object_content[0]
|
||||
|
||||
@validate("namespace", "class_name")
|
||||
def gir_class(self):
|
||||
return self.root.gir.get_class(self.class_name, self.namespace)
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
xml.start_tag("object", **{
|
||||
"class": self.namespace + self.class_name,
|
||||
"class": self.gir_class.glib_type_name,
|
||||
"id": self.id,
|
||||
})
|
||||
self.object_content.emit_xml(xml)
|
||||
|
@ -117,6 +253,7 @@ class Object(AstNode):
|
|||
class Child(AstNode):
|
||||
child_type = "children"
|
||||
def __init__(self, objects, child_type=None):
|
||||
super().__init__()
|
||||
assert_true(len(objects) == 1)
|
||||
self.object = objects[0]
|
||||
self.child_type = child_type
|
||||
|
@ -130,28 +267,62 @@ class Child(AstNode):
|
|||
class ObjectContent(AstNode):
|
||||
child_type = "object_content"
|
||||
def __init__(self, properties=[], signals=[], children=[]):
|
||||
super().__init__()
|
||||
self.properties = properties
|
||||
self.signals = signals
|
||||
self.children = children
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
for prop in self.properties:
|
||||
prop.emit_xml(xml)
|
||||
for signal in self.signals:
|
||||
signal.emit_xml(xml)
|
||||
for child in self.children:
|
||||
child.emit_xml(xml)
|
||||
for x in [*self.properties, *self.signals, *self.children]:
|
||||
x.emit_xml(xml)
|
||||
|
||||
|
||||
class Property(AstNode):
|
||||
child_type = "properties"
|
||||
def __init__(self, name, value=None, translatable=False, bind_source=None, bind_property=None):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.translatable = translatable
|
||||
self.bind_source = bind_source
|
||||
self.bind_property = bind_property
|
||||
|
||||
|
||||
@validate()
|
||||
def gir_property(self):
|
||||
if self.gir_class is not None:
|
||||
return self.gir_class.properties.get(self.name)
|
||||
|
||||
@validate()
|
||||
def gir_class(self):
|
||||
parent = self.parent.parent
|
||||
if isinstance(parent, Template):
|
||||
return parent.gir_parent
|
||||
elif isinstance(parent, Object):
|
||||
return parent.gir_class
|
||||
else:
|
||||
raise CompilerBugError()
|
||||
|
||||
|
||||
@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.name}",
|
||||
did_you_mean=(self.name, self.gir_class.properties.keys())
|
||||
)
|
||||
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
props = {
|
||||
"name": self.name,
|
||||
|
@ -170,6 +341,7 @@ class Property(AstNode):
|
|||
class Signal(AstNode):
|
||||
child_type = "signals"
|
||||
def __init__(self, name, handler, swapped=False, after=False, object=False, detail_name=None):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.handler = handler
|
||||
self.swapped = swapped
|
||||
|
@ -177,6 +349,42 @@ class Signal(AstNode):
|
|||
self.object = object
|
||||
self.detail_name = detail_name
|
||||
|
||||
|
||||
@validate()
|
||||
def gir_signal(self):
|
||||
if self.gir_class is not None:
|
||||
return self.gir_class.signals.get(self.name)
|
||||
|
||||
@validate()
|
||||
def gir_class(self):
|
||||
parent = self.parent.parent
|
||||
if isinstance(parent, Template):
|
||||
return parent.gir_parent
|
||||
elif isinstance(parent, Object):
|
||||
return parent.gir_class
|
||||
else:
|
||||
raise CompilerBugError()
|
||||
|
||||
|
||||
@validate("name")
|
||||
def signal_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 signal is part of a template, it might be defined by
|
||||
# the application and thus not in gir
|
||||
return
|
||||
|
||||
if self.gir_signal is None:
|
||||
print(self.gir_class.signals.keys())
|
||||
raise CompileError(
|
||||
f"Class {self.gir_class.full_name} does not contain a signal called {self.name}",
|
||||
did_you_mean=(self.name, self.gir_class.signals.keys())
|
||||
)
|
||||
|
||||
def emit_xml(self, xml: XmlEmitter):
|
||||
name = self.name
|
||||
if self.detail_name:
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
|
||||
import sys, traceback
|
||||
from . import utils
|
||||
|
||||
|
||||
class _colors:
|
||||
|
@ -37,34 +38,63 @@ class PrintableError(Exception):
|
|||
|
||||
|
||||
class CompileError(PrintableError):
|
||||
""" A PrintableError with a start/end position and optional hints """
|
||||
|
||||
category = "error"
|
||||
|
||||
def __init__(self, message, start, end=None):
|
||||
def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None):
|
||||
super().__init__(message)
|
||||
|
||||
self.message = message
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.hints = hints or []
|
||||
|
||||
def pretty_print(self, filename, code):
|
||||
sp = code[:self.start+1].splitlines(keepends=True)
|
||||
if did_you_mean is not None:
|
||||
self._did_you_mean(*did_you_mean)
|
||||
|
||||
def hint(self, hint: str):
|
||||
self.hints.append(hint)
|
||||
return self
|
||||
|
||||
|
||||
def _did_you_mean(self, word: str, options: [str]):
|
||||
if word.replace("_", "-") in options:
|
||||
self.hint(f"use '-', not '_': `{word.replace('_', '-')}`")
|
||||
return
|
||||
|
||||
recommend = utils.did_you_mean(word, options)
|
||||
if recommend is not None:
|
||||
if word.casefold() == recommend.casefold():
|
||||
self.hint(f"Did you mean `{recommend}` (note the capitalization)?")
|
||||
else:
|
||||
self.hint(f"Did you mean `{recommend}`?")
|
||||
else:
|
||||
self.hint("Did you check your spelling?")
|
||||
self.hint("Are your dependencies up to date?")
|
||||
|
||||
def line_col_from_index(self, code, index):
|
||||
sp = code[:index].splitlines(keepends=True)
|
||||
line_num = len(sp)
|
||||
col_num = len(sp[-1])
|
||||
return (line_num, col_num)
|
||||
|
||||
def pretty_print(self, filename, code):
|
||||
line_num, col_num = self.line_col_from_index(code, self.start + 1)
|
||||
line = code.splitlines(True)[line_num-1]
|
||||
|
||||
print(f"""{_colors.RED}{_colors.BOLD}{self.category}: {self.message}{_colors.CLEAR}
|
||||
at {filename} line {line_num} column {col_num}:
|
||||
{_colors.FAINT}{line_num :>4} |{_colors.CLEAR}{line.rstrip()}\n {_colors.FAINT}|{" "*(col_num-1)}^{_colors.CLEAR}
|
||||
""")
|
||||
{_colors.FAINT}{line_num :>4} |{_colors.CLEAR}{line.rstrip()}\n {_colors.FAINT}|{" "*(col_num-1)}^{_colors.CLEAR}""")
|
||||
|
||||
for hint in self.hints:
|
||||
print(f"{_colors.FAINT}hint: {hint}{_colors.CLEAR}")
|
||||
print()
|
||||
|
||||
|
||||
class TokenizeError(CompileError):
|
||||
def __init__(self, start):
|
||||
super().__init__("Could not determine what kind of syntax is meant here", start)
|
||||
|
||||
|
||||
class ParseError(CompileError):
|
||||
pass
|
||||
class AlreadyCaughtError(Exception):
|
||||
""" Emitted when a validation has already failed and its error message
|
||||
should not be repeated. """
|
||||
|
||||
|
||||
class MultipleErrors(PrintableError):
|
||||
|
|
249
gtkblueprinttool/gir.py
Normal file
249
gtkblueprinttool/gir.py
Normal file
|
@ -0,0 +1,249 @@
|
|||
# gir.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 os, sys
|
||||
|
||||
from .errors import CompileError, CompilerBugError
|
||||
from .utils import lazy_prop
|
||||
from . import xml_reader
|
||||
|
||||
|
||||
extra_search_paths = []
|
||||
_namespace_cache = {}
|
||||
|
||||
_search_paths = []
|
||||
xdg_data_home = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
||||
_search_paths.append(os.path.join(xdg_data_home, "gir-1.0"))
|
||||
xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":")
|
||||
_search_paths += [os.path.join(dir, "gir-1.0") for dir in xdg_data_dirs]
|
||||
|
||||
|
||||
def get_namespace(namespace, version):
|
||||
filename = f"{namespace}-{version}.gir"
|
||||
|
||||
if filename not in _namespace_cache:
|
||||
for search_path in _search_paths:
|
||||
path = os.path.join(search_path, filename)
|
||||
|
||||
if os.path.exists(path) and os.path.isfile(path):
|
||||
xml = xml_reader.parse(path, xml_reader.PARSE_GIR)
|
||||
repository = Repository(xml)
|
||||
|
||||
_namespace_cache[filename] = repository.namespaces.get(namespace)
|
||||
break
|
||||
|
||||
if filename not in _namespace_cache:
|
||||
raise CompileError(f"Namespace `{namespace}-{version}` could not be found.")
|
||||
|
||||
return _namespace_cache[filename]
|
||||
|
||||
|
||||
class GirNode:
|
||||
def __init__(self, xml):
|
||||
self.xml = xml
|
||||
|
||||
@lazy_prop
|
||||
def glib_type_name(self):
|
||||
return self.xml["glib:type-name"]
|
||||
|
||||
@lazy_prop
|
||||
def name(self) -> str:
|
||||
return self.xml["name"]
|
||||
|
||||
@lazy_prop
|
||||
def available_in(self) -> str:
|
||||
return self.xml.get("version")
|
||||
|
||||
@lazy_prop
|
||||
def doc(self) -> str:
|
||||
el = self.xml.find("doc")
|
||||
if el is None:
|
||||
return None
|
||||
return el.cdata
|
||||
|
||||
|
||||
class Property(GirNode):
|
||||
pass
|
||||
|
||||
|
||||
class Signal(GirNode):
|
||||
pass
|
||||
|
||||
|
||||
class Interface(GirNode):
|
||||
def __init__(self, ns, xml: xml_reader.Element):
|
||||
super().__init__(xml)
|
||||
self.ns = ns
|
||||
self.properties = {child["name"]: Property(child) for child in xml.get_elements("property")}
|
||||
self.signals = {child["name"]: Signal(child) for child in xml.get_elements("glib:signal")}
|
||||
|
||||
|
||||
class Class(GirNode):
|
||||
def __init__(self, ns, xml: xml_reader.Element):
|
||||
super().__init__(xml)
|
||||
self.ns = ns
|
||||
self._parent = xml["parent"]
|
||||
self.implements = [impl["name"] for impl in xml.get_elements("implements")]
|
||||
self.own_properties = {child["name"]: Property(child) for child in xml.get_elements("property")}
|
||||
self.own_signals = {child["name"]: Signal(child) for child in xml.get_elements("glib:signal")}
|
||||
|
||||
@lazy_prop
|
||||
def properties(self):
|
||||
return { p.name: p for p in self._enum_properties() }
|
||||
|
||||
@lazy_prop
|
||||
def signals(self):
|
||||
return { s.name: s for s in self._enum_signals() }
|
||||
|
||||
@lazy_prop
|
||||
def full_name(self):
|
||||
return f"{self.ns.name}.{self.name}"
|
||||
|
||||
@lazy_prop
|
||||
def parent(self):
|
||||
if self._parent is None:
|
||||
return None
|
||||
return self.ns.lookup_class(self._parent)
|
||||
|
||||
|
||||
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 self.ns.lookup_interface(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 self.ns.lookup_interface(impl).signals.values()
|
||||
|
||||
|
||||
class Namespace(GirNode):
|
||||
def __init__(self, repo, xml: xml_reader.Element):
|
||||
super().__init__(xml)
|
||||
self.repo = repo
|
||||
self.classes = { child["name"]: Class(self, child) for child in xml.get_elements("class") }
|
||||
self.interfaces = { child["name"]: Interface(self, child) for child in xml.get_elements("interface") }
|
||||
self.version = xml["version"]
|
||||
|
||||
def lookup_class(self, name: str):
|
||||
if "." in name:
|
||||
ns, cls = name.split(".")
|
||||
return self.repo.lookup_namespace(ns).lookup_class(cls)
|
||||
else:
|
||||
return self.classes.get(name)
|
||||
|
||||
def lookup_interface(self, name: str):
|
||||
if "." in name:
|
||||
ns, iface = name.split(".")
|
||||
return self.repo.lookup_namespace(ns).lookup_interface(iface)
|
||||
else:
|
||||
return self.interfaces.get(name)
|
||||
|
||||
def lookup_namespace(self, ns: str):
|
||||
return self.repo.lookup_namespace(ns)
|
||||
|
||||
|
||||
class Repository(GirNode):
|
||||
def __init__(self, xml: xml_reader.Element):
|
||||
super().__init__(xml)
|
||||
self.namespaces = { child["name"]: Namespace(self, child) for child in xml.get_elements("namespace") }
|
||||
|
||||
try:
|
||||
self.includes = { include["name"]: get_namespace(include["name"], include["version"]) for include in xml.get_elements("include") }
|
||||
except:
|
||||
raise CompilerBugError(f"Failed to load dependencies of {namespace}-{version}")
|
||||
|
||||
def lookup_namespace(self, name: str):
|
||||
ns = self.namespaces.get(name)
|
||||
if ns is not None:
|
||||
return ns
|
||||
for include in self.includes.values():
|
||||
ns = include.lookup_namespace(name)
|
||||
if ns is not None:
|
||||
return ns
|
||||
|
||||
|
||||
class GirContext:
|
||||
def __init__(self):
|
||||
self.namespaces = {}
|
||||
self.incomplete = set([])
|
||||
|
||||
|
||||
def add_namespace(self, namespace: Namespace):
|
||||
other = self.namespaces.get(namespace.name)
|
||||
if other is not None and other.version != namespace.version:
|
||||
raise CompileError(f"Namespace {namespace}-{version} can't be imported because version {other.version} was imported earlier")
|
||||
|
||||
self.namespaces[namespace.name] = namespace
|
||||
|
||||
|
||||
def add_incomplete(self, namespace: str):
|
||||
""" Adds an "incomplete" namespace for which missing items won't cause
|
||||
errors. """
|
||||
self.incomplete.add(namespace)
|
||||
|
||||
|
||||
def get_class(self, name: str, ns:str=None) -> Class:
|
||||
if ns is None:
|
||||
options = [namespace.classes[name]
|
||||
for namespace in self.namespaces.values()
|
||||
if name in namespace.classes]
|
||||
|
||||
if len(options) == 1:
|
||||
return options[0]
|
||||
elif len(options) == 0:
|
||||
raise CompileError(
|
||||
f"No imported namespace contains a class called {name}",
|
||||
hints=[
|
||||
"Did you forget to import a namespace?",
|
||||
"Did you check your spelling?",
|
||||
"Are your dependencies up to date?",
|
||||
],
|
||||
)
|
||||
else:
|
||||
raise CompileError(
|
||||
f"Class name {name} is ambiguous",
|
||||
hints=[
|
||||
f"Specify the namespace, e.g. `{options[0].ns.name}.{name}`",
|
||||
f"Namespaces with a class named {name}: {', '.join([cls.ns.name for cls in options])}",
|
||||
],
|
||||
)
|
||||
|
||||
else:
|
||||
if ns not in self.namespaces:
|
||||
raise CompileError(
|
||||
f"Namespace `{ns}` was not imported.",
|
||||
did_you_mean=(ns, self.namespaces.keys()),
|
||||
)
|
||||
|
||||
if name not in self.namespaces[ns].classes:
|
||||
raise CompileError(
|
||||
f"Namespace {ns} does not contain a class called {name}.",
|
||||
did_you_mean=(name, self.namespaces[ns].classes.keys()),
|
||||
)
|
||||
|
||||
return self.namespaces[ns].classes[name]
|
|
@ -18,9 +18,10 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
|
||||
import argparse, sys
|
||||
import argparse, json, os, sys
|
||||
|
||||
from .errors import PrintableError, report_compile_error
|
||||
from .errors import PrintableError, report_compile_error, MultipleErrors
|
||||
from .lsp import LanguageServer
|
||||
from .pipeline import Pipeline
|
||||
from . import parser, tokenizer
|
||||
|
||||
|
@ -43,6 +44,8 @@ class BlueprintApp:
|
|||
try:
|
||||
opts = self.parser.parse_args()
|
||||
opts.func(opts)
|
||||
except SystemExit as e:
|
||||
raise e
|
||||
except:
|
||||
report_compile_error()
|
||||
|
||||
|
@ -60,7 +63,12 @@ class BlueprintApp:
|
|||
try:
|
||||
tokens = tokenizer.tokenize(data)
|
||||
ast = parser.parse(tokens)
|
||||
|
||||
if len(ast.errors):
|
||||
raise MultipleErrors(ast.errors)
|
||||
|
||||
xml = ast.generate()
|
||||
|
||||
if opts.output == "-":
|
||||
print(xml)
|
||||
else:
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
from enum import Enum
|
||||
|
||||
from .ast import AstNode
|
||||
from .errors import assert_true, CompilerBugError, CompileError, ParseError
|
||||
from .errors import assert_true, CompilerBugError, CompileError
|
||||
from .tokenizer import Token, TokenType
|
||||
|
||||
|
||||
|
@ -57,10 +57,13 @@ class ParseGroup:
|
|||
be converted to AST nodes by passing the children and key=value pairs to
|
||||
the AST node constructor. """
|
||||
|
||||
def __init__(self, ast_type):
|
||||
def __init__(self, ast_type, start: int):
|
||||
self.ast_type = ast_type
|
||||
self.children = {}
|
||||
self.keys = {}
|
||||
self.tokens = {}
|
||||
self.start = start
|
||||
self.end = None
|
||||
|
||||
def add_child(self, child):
|
||||
child_type = child.ast_type.child_type
|
||||
|
@ -68,10 +71,11 @@ class ParseGroup:
|
|||
self.children[child_type] = []
|
||||
self.children[child_type].append(child)
|
||||
|
||||
def set_val(self, key, val):
|
||||
def set_val(self, key, val, token):
|
||||
assert_true(key not in self.keys)
|
||||
|
||||
self.keys[key] = val
|
||||
self.tokens[key] = token
|
||||
|
||||
def to_ast(self) -> AstNode:
|
||||
""" Creates an AST node from the match group. """
|
||||
|
@ -80,7 +84,12 @@ class ParseGroup:
|
|||
for child_type, children in self.children.items()
|
||||
}
|
||||
try:
|
||||
return self.ast_type(**children, **self.keys)
|
||||
ast = self.ast_type(**children, **self.keys)
|
||||
ast.group = self
|
||||
ast.child_nodes = [c for child_type in children.values() for c in child_type]
|
||||
for child in ast.child_nodes:
|
||||
child.parent = ast
|
||||
return ast
|
||||
except TypeError as e:
|
||||
raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.")
|
||||
|
||||
|
@ -113,7 +122,10 @@ class ParseContext:
|
|||
context will be used to parse one node. If parsing is successful, the
|
||||
new context will be applied to "self". If parsing fails, the new
|
||||
context will be discarded. """
|
||||
return ParseContext(self.tokens, self.index)
|
||||
ctx = ParseContext(self.tokens, self.index)
|
||||
ctx.errors = self.errors
|
||||
ctx.warnings = self.warnings
|
||||
return ctx
|
||||
|
||||
def apply_child(self, other):
|
||||
""" Applies a child context to this context. """
|
||||
|
@ -121,10 +133,11 @@ class ParseContext:
|
|||
if other.group is not None:
|
||||
# If the other context had a match group, collect all the matched
|
||||
# values into it and then add it to our own match group.
|
||||
for key, val in other.group_keys.items():
|
||||
other.group.set_val(key, val)
|
||||
for key, (val, token) in other.group_keys.items():
|
||||
other.group.set_val(key, val, token)
|
||||
for child in other.group_children:
|
||||
other.group.add_child(child)
|
||||
other.group.end = other.tokens[other.index - 1].end
|
||||
self.group_children.append(other.group)
|
||||
else:
|
||||
# If the other context had no match group of its own, collect all
|
||||
|
@ -144,23 +157,12 @@ class ParseContext:
|
|||
def start_group(self, ast_type):
|
||||
""" Sets this context to have its own match group. """
|
||||
assert_true(self.group is None)
|
||||
self.group = ParseGroup(ast_type)
|
||||
self.group = ParseGroup(ast_type, self.tokens[self.index].start)
|
||||
|
||||
def set_group_val(self, key, value):
|
||||
def set_group_val(self, key, value, token):
|
||||
""" Sets a matched key=value pair on the current match group. """
|
||||
assert_true(key not in self.group_keys)
|
||||
self.group_keys[key] = value
|
||||
|
||||
|
||||
def create_parse_error(self, message):
|
||||
""" Creates a ParseError identifying the current token index. """
|
||||
start_idx = self.start
|
||||
while self.tokens[start_idx].type in _SKIP_TOKENS:
|
||||
start_idx += 1
|
||||
|
||||
start_token = self.tokens[start_idx]
|
||||
end_token = self.tokens[self.index]
|
||||
return ParseError(message, start_token.start, end_token.end)
|
||||
self.group_keys[key] = (value, token)
|
||||
|
||||
|
||||
def skip(self):
|
||||
|
@ -224,7 +226,32 @@ class Err(ParseNode):
|
|||
|
||||
def _parse(self, ctx):
|
||||
if self.child.parse(ctx).failed():
|
||||
raise ctx.create_parse_error(self.message)
|
||||
start_idx = ctx.start
|
||||
while ctx.tokens[start_idx].type in _SKIP_TOKENS:
|
||||
start_idx += 1
|
||||
|
||||
start_token = ctx.tokens[start_idx]
|
||||
end_token = ctx.tokens[ctx.index]
|
||||
raise CompileError(self.message, start_token.start, end_token.end)
|
||||
return True
|
||||
|
||||
|
||||
class Fail(ParseNode):
|
||||
""" ParseNode that emits a compile error if it parses successfully. """
|
||||
|
||||
def __init__(self, child, message):
|
||||
self.child = child
|
||||
self.message = message
|
||||
|
||||
def _parse(self, ctx):
|
||||
if self.child.parse(ctx).succeeded():
|
||||
start_idx = ctx.start
|
||||
while ctx.tokens[start_idx].type in _SKIP_TOKENS:
|
||||
start_idx += 1
|
||||
|
||||
start_token = ctx.tokens[start_idx]
|
||||
end_token = ctx.tokens[ctx.index]
|
||||
raise CompileError(self.message, start_token.start, end_token.end)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -250,6 +277,7 @@ class Group(ParseNode):
|
|||
self.child = child
|
||||
|
||||
def _parse(self, ctx: ParseContext) -> bool:
|
||||
ctx.skip()
|
||||
ctx.start_group(self.ast_type)
|
||||
return self.child.parse(ctx).succeeded()
|
||||
|
||||
|
@ -367,16 +395,15 @@ class UseIdent(ParseNode):
|
|||
if token.type != TokenType.IDENT:
|
||||
return False
|
||||
|
||||
ctx.set_group_val(self.key, str(token))
|
||||
ctx.set_group_val(self.key, str(token), token)
|
||||
return True
|
||||
|
||||
|
||||
class UseNumber(ParseNode):
|
||||
""" ParseNode that matches a number and sets it in a key=value pair on
|
||||
the containing match group. """
|
||||
def __init__(self, key, keep_trailing_decimal=False):
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.keep_trailing_decimal = keep_trailing_decimal
|
||||
|
||||
def _parse(self, ctx: ParseContext):
|
||||
token = ctx.next_token()
|
||||
|
@ -384,9 +411,9 @@ class UseNumber(ParseNode):
|
|||
return False
|
||||
|
||||
number = token.get_number()
|
||||
if not self.keep_trailing_decimal and number % 1.0 == 0:
|
||||
if number % 1.0 == 0:
|
||||
number = int(number)
|
||||
ctx.set_group_val(self.key, number)
|
||||
ctx.set_group_val(self.key, number, token)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -405,7 +432,7 @@ class UseQuoted(ParseNode):
|
|||
.replace("\\n", "\n")
|
||||
.replace("\\\"", "\"")
|
||||
.replace("\\\\", "\\"))
|
||||
ctx.set_group_val(self.key, string)
|
||||
ctx.set_group_val(self.key, string, token)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -418,7 +445,7 @@ class UseLiteral(ParseNode):
|
|||
self.literal = literal
|
||||
|
||||
def _parse(self, ctx: ParseContext):
|
||||
ctx.set_group_val(self.key, self.literal)
|
||||
ctx.set_group_val(self.key, self.literal, None)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
|
||||
from . import ast
|
||||
from .errors import MultipleErrors, ParseError
|
||||
from .errors import MultipleErrors
|
||||
from .parse_tree import *
|
||||
from .tokenizer import TokenType
|
||||
|
||||
|
@ -31,7 +31,8 @@ def parse(tokens) -> ast.UI:
|
|||
ast.GtkDirective,
|
||||
Sequence(
|
||||
Directive("gtk"),
|
||||
UseNumber("version", True).expected("a version number for GTK"),
|
||||
Fail(UseNumber(None), "Version number must be in quotation marks"),
|
||||
UseQuoted("version").expected("a version number for GTK"),
|
||||
StmtEnd().expected("`;`"),
|
||||
)
|
||||
)
|
||||
|
@ -41,7 +42,8 @@ def parse(tokens) -> ast.UI:
|
|||
Sequence(
|
||||
Directive("import"),
|
||||
UseIdent("namespace").expected("a GIR namespace"),
|
||||
UseNumber("version", True).expected("a version number"),
|
||||
Fail(UseNumber(None), "Version number must be in quotation marks"),
|
||||
UseQuoted("version").expected("a version number"),
|
||||
StmtEnd().expected("`;`"),
|
||||
)
|
||||
).recover()
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
import re
|
||||
from enum import Enum
|
||||
|
||||
from .errors import TokenizeError
|
||||
from .errors import CompileError
|
||||
|
||||
|
||||
class TokenType(Enum):
|
||||
|
@ -106,7 +106,7 @@ def _tokenize(ui_ml: str):
|
|||
break
|
||||
|
||||
if not matched:
|
||||
raise TokenizeError(i)
|
||||
raise CompileError("Could not determine what kind of syntax is meant here", i)
|
||||
|
||||
yield Token(TokenType.EOF, i, i, ui_ml)
|
||||
|
||||
|
|
67
gtkblueprinttool/utils.py
Normal file
67
gtkblueprinttool/utils.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# utils.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
|
||||
|
||||
|
||||
def lazy_prop(func):
|
||||
key = "_lazy_prop_" + func.__name__
|
||||
|
||||
@property
|
||||
def real_func(self):
|
||||
if key not in self.__dict__:
|
||||
self.__dict__[key] = func(self)
|
||||
return self.__dict__[key]
|
||||
|
||||
return real_func
|
||||
|
||||
|
||||
def did_you_mean(word: str, options: [str]) -> T.Optional[str]:
|
||||
if len(options) == 0:
|
||||
return None
|
||||
|
||||
def levenshtein(a, b):
|
||||
# see https://en.wikipedia.org/wiki/Levenshtein_distance
|
||||
m = len(a)
|
||||
n = len(b)
|
||||
|
||||
distances = [[0 for j in range(n)] for i in range(m)]
|
||||
|
||||
for i in range(m):
|
||||
distances[i][0] = i
|
||||
for j in range(n):
|
||||
distances[0][j] = j
|
||||
|
||||
for j in range(1, n):
|
||||
for i in range(1, m):
|
||||
cost = 0
|
||||
if a[i] != b[j]:
|
||||
if a[i].casefold() == b[j].casefold():
|
||||
cost = 1
|
||||
else:
|
||||
cost = 2
|
||||
distances[i][j] = min(distances[i-1][j] + 2, distances[i][j-1] + 2, distances[i-1][j-1] + cost)
|
||||
|
||||
return distances[m-1][n-1]
|
||||
|
||||
distances = [(option, levenshtein(word, option)) for option in options]
|
||||
closest = min(distances, key=lambda item:item[1])
|
||||
if closest[1] <= 5:
|
||||
return closest[0]
|
||||
return None
|
85
gtkblueprinttool/xml_reader.py
Normal file
85
gtkblueprinttool/xml_reader.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
# xml_reader.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
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
from xml import sax
|
||||
|
||||
from .utils import lazy_prop
|
||||
|
||||
|
||||
PARSE_GIR = set([
|
||||
"repository", "namespace", "class", "interface", "property", "glib:signal",
|
||||
"include",
|
||||
])
|
||||
|
||||
|
||||
class Element:
|
||||
def __init__(self, tag, attrs):
|
||||
self.tag = tag
|
||||
self.attrs = attrs
|
||||
self.children = defaultdict(list)
|
||||
self.cdata_chunks = []
|
||||
|
||||
@lazy_prop
|
||||
def cdata(self):
|
||||
return ''.join(self.cdata_chunks)
|
||||
|
||||
def get_elements(self, name):
|
||||
return self.children.get(name, [])
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.attrs.get(key)
|
||||
|
||||
|
||||
class Handler(sax.handler.ContentHandler):
|
||||
def __init__(self, parse_type):
|
||||
self.root = None
|
||||
self.stack = []
|
||||
self._interesting_elements = parse_type
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
if name not in self._interesting_elements:
|
||||
return
|
||||
|
||||
element = Element(name, attrs.copy())
|
||||
|
||||
if len(self.stack):
|
||||
last = self.stack[-1]
|
||||
last.children[name].append(element)
|
||||
else:
|
||||
self.root = element
|
||||
|
||||
self.stack.append(element)
|
||||
|
||||
|
||||
def endElement(self, name):
|
||||
if name in self._interesting_elements:
|
||||
self.stack.pop()
|
||||
|
||||
def characters(self, content):
|
||||
self.stack[-1].cdata_chunks.append(content)
|
||||
|
||||
|
||||
def parse(filename, parse_type):
|
||||
parser = sax.make_parser()
|
||||
handler = Handler(parse_type)
|
||||
parser.setContentHandler(handler)
|
||||
parser.parse(filename)
|
||||
return handler.root
|
Loading…
Add table
Add a link
Reference in a new issue