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
|
from gtkblueprinttool import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main.main()
|
||||||
|
|
|
@ -18,13 +18,102 @@
|
||||||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
# 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
|
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:
|
class AstNode:
|
||||||
""" Base class for nodes in the abstract syntax tree. """
|
""" 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:
|
def generate(self) -> str:
|
||||||
""" Generates an XML string from the node. """
|
""" Generates an XML string from the node. """
|
||||||
xml = XmlEmitter()
|
xml = XmlEmitter()
|
||||||
|
@ -40,6 +129,7 @@ class UI(AstNode):
|
||||||
""" The AST node for the entire file """
|
""" The AST node for the entire file """
|
||||||
|
|
||||||
def __init__(self, gtk_directives=[], imports=[], objects=[], templates=[]):
|
def __init__(self, gtk_directives=[], imports=[], objects=[], templates=[]):
|
||||||
|
super().__init__()
|
||||||
assert_true(len(gtk_directives) == 1)
|
assert_true(len(gtk_directives) == 1)
|
||||||
|
|
||||||
self.gtk_directive = gtk_directives[0]
|
self.gtk_directive = gtk_directives[0]
|
||||||
|
@ -47,6 +137,22 @@ class UI(AstNode):
|
||||||
self.objects = objects
|
self.objects = objects
|
||||||
self.templates = templates
|
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):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
xml.start_tag("interface")
|
xml.start_tag("interface")
|
||||||
self.gtk_directive.emit_xml(xml)
|
self.gtk_directive.emit_xml(xml)
|
||||||
|
@ -60,8 +166,21 @@ class UI(AstNode):
|
||||||
class GtkDirective(AstNode):
|
class GtkDirective(AstNode):
|
||||||
child_type = "gtk_directives"
|
child_type = "gtk_directives"
|
||||||
def __init__(self, version):
|
def __init__(self, version):
|
||||||
|
super().__init__()
|
||||||
self.version = version
|
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):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
xml.put_self_closing("requires", lib="gtk", version=self.version)
|
xml.put_self_closing("requires", lib="gtk", version=self.version)
|
||||||
|
|
||||||
|
@ -69,9 +188,14 @@ class GtkDirective(AstNode):
|
||||||
class Import(AstNode):
|
class Import(AstNode):
|
||||||
child_type = "imports"
|
child_type = "imports"
|
||||||
def __init__(self, namespace, version):
|
def __init__(self, namespace, version):
|
||||||
|
super().__init__()
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
self.version = version
|
self.version = version
|
||||||
|
|
||||||
|
@validate("namespace", "version")
|
||||||
|
def gir_namespace(self):
|
||||||
|
return get_namespace(self.namespace, self.version)
|
||||||
|
|
||||||
def emit_xml(self, xml: XmlEmitter):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -79,6 +203,7 @@ class Import(AstNode):
|
||||||
class Template(AstNode):
|
class Template(AstNode):
|
||||||
child_type = "templates"
|
child_type = "templates"
|
||||||
def __init__(self, name, class_name, object_content, namespace=None):
|
def __init__(self, name, class_name, object_content, namespace=None):
|
||||||
|
super().__init__()
|
||||||
assert_true(len(object_content) == 1)
|
assert_true(len(object_content) == 1)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -86,10 +211,16 @@ class Template(AstNode):
|
||||||
self.parent_class = class_name
|
self.parent_class = class_name
|
||||||
self.object_content = object_content[0]
|
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):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
xml.start_tag("template", **{
|
xml.start_tag("template", **{
|
||||||
"class": self.name,
|
"class": self.name,
|
||||||
"parent": self.parent_namespace + self.parent_class,
|
"parent": self.gir_parent.glib_type_name,
|
||||||
})
|
})
|
||||||
self.object_content.emit_xml(xml)
|
self.object_content.emit_xml(xml)
|
||||||
xml.end_tag()
|
xml.end_tag()
|
||||||
|
@ -98,6 +229,7 @@ class Template(AstNode):
|
||||||
class Object(AstNode):
|
class Object(AstNode):
|
||||||
child_type = "objects"
|
child_type = "objects"
|
||||||
def __init__(self, class_name, object_content, namespace=None, id=None):
|
def __init__(self, class_name, object_content, namespace=None, id=None):
|
||||||
|
super().__init__()
|
||||||
assert_true(len(object_content) == 1)
|
assert_true(len(object_content) == 1)
|
||||||
|
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
|
@ -105,9 +237,13 @@ class Object(AstNode):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.object_content = object_content[0]
|
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):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
xml.start_tag("object", **{
|
xml.start_tag("object", **{
|
||||||
"class": self.namespace + self.class_name,
|
"class": self.gir_class.glib_type_name,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
})
|
})
|
||||||
self.object_content.emit_xml(xml)
|
self.object_content.emit_xml(xml)
|
||||||
|
@ -117,6 +253,7 @@ class Object(AstNode):
|
||||||
class Child(AstNode):
|
class Child(AstNode):
|
||||||
child_type = "children"
|
child_type = "children"
|
||||||
def __init__(self, objects, child_type=None):
|
def __init__(self, objects, child_type=None):
|
||||||
|
super().__init__()
|
||||||
assert_true(len(objects) == 1)
|
assert_true(len(objects) == 1)
|
||||||
self.object = objects[0]
|
self.object = objects[0]
|
||||||
self.child_type = child_type
|
self.child_type = child_type
|
||||||
|
@ -130,28 +267,62 @@ class Child(AstNode):
|
||||||
class ObjectContent(AstNode):
|
class ObjectContent(AstNode):
|
||||||
child_type = "object_content"
|
child_type = "object_content"
|
||||||
def __init__(self, properties=[], signals=[], children=[]):
|
def __init__(self, properties=[], signals=[], children=[]):
|
||||||
|
super().__init__()
|
||||||
self.properties = properties
|
self.properties = properties
|
||||||
self.signals = signals
|
self.signals = signals
|
||||||
self.children = children
|
self.children = children
|
||||||
|
|
||||||
def emit_xml(self, xml: XmlEmitter):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
for prop in self.properties:
|
for x in [*self.properties, *self.signals, *self.children]:
|
||||||
prop.emit_xml(xml)
|
x.emit_xml(xml)
|
||||||
for signal in self.signals:
|
|
||||||
signal.emit_xml(xml)
|
|
||||||
for child in self.children:
|
|
||||||
child.emit_xml(xml)
|
|
||||||
|
|
||||||
|
|
||||||
class Property(AstNode):
|
class Property(AstNode):
|
||||||
child_type = "properties"
|
child_type = "properties"
|
||||||
def __init__(self, name, value=None, translatable=False, bind_source=None, bind_property=None):
|
def __init__(self, name, value=None, translatable=False, bind_source=None, bind_property=None):
|
||||||
|
super().__init__()
|
||||||
self.name = name
|
self.name = name
|
||||||
self.value = value
|
self.value = value
|
||||||
self.translatable = translatable
|
self.translatable = translatable
|
||||||
self.bind_source = bind_source
|
self.bind_source = bind_source
|
||||||
self.bind_property = bind_property
|
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):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
props = {
|
props = {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
|
@ -170,6 +341,7 @@ class Property(AstNode):
|
||||||
class Signal(AstNode):
|
class Signal(AstNode):
|
||||||
child_type = "signals"
|
child_type = "signals"
|
||||||
def __init__(self, name, handler, swapped=False, after=False, object=False, detail_name=None):
|
def __init__(self, name, handler, swapped=False, after=False, object=False, detail_name=None):
|
||||||
|
super().__init__()
|
||||||
self.name = name
|
self.name = name
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
self.swapped = swapped
|
self.swapped = swapped
|
||||||
|
@ -177,6 +349,42 @@ class Signal(AstNode):
|
||||||
self.object = object
|
self.object = object
|
||||||
self.detail_name = detail_name
|
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):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
name = self.name
|
name = self.name
|
||||||
if self.detail_name:
|
if self.detail_name:
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
|
|
||||||
import sys, traceback
|
import sys, traceback
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
class _colors:
|
class _colors:
|
||||||
|
@ -37,34 +38,63 @@ class PrintableError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class CompileError(PrintableError):
|
class CompileError(PrintableError):
|
||||||
|
""" A PrintableError with a start/end position and optional hints """
|
||||||
|
|
||||||
category = "error"
|
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)
|
super().__init__(message)
|
||||||
|
|
||||||
self.message = message
|
self.message = message
|
||||||
self.start = start
|
self.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
|
self.hints = hints or []
|
||||||
|
|
||||||
def pretty_print(self, filename, code):
|
if did_you_mean is not None:
|
||||||
sp = code[:self.start+1].splitlines(keepends=True)
|
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)
|
line_num = len(sp)
|
||||||
col_num = len(sp[-1])
|
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]
|
line = code.splitlines(True)[line_num-1]
|
||||||
|
|
||||||
print(f"""{_colors.RED}{_colors.BOLD}{self.category}: {self.message}{_colors.CLEAR}
|
print(f"""{_colors.RED}{_colors.BOLD}{self.category}: {self.message}{_colors.CLEAR}
|
||||||
at {filename} line {line_num} column {col_num}:
|
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):
|
class AlreadyCaughtError(Exception):
|
||||||
def __init__(self, start):
|
""" Emitted when a validation has already failed and its error message
|
||||||
super().__init__("Could not determine what kind of syntax is meant here", start)
|
should not be repeated. """
|
||||||
|
|
||||||
|
|
||||||
class ParseError(CompileError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleErrors(PrintableError):
|
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
|
# 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 .pipeline import Pipeline
|
||||||
from . import parser, tokenizer
|
from . import parser, tokenizer
|
||||||
|
|
||||||
|
@ -43,6 +44,8 @@ class BlueprintApp:
|
||||||
try:
|
try:
|
||||||
opts = self.parser.parse_args()
|
opts = self.parser.parse_args()
|
||||||
opts.func(opts)
|
opts.func(opts)
|
||||||
|
except SystemExit as e:
|
||||||
|
raise e
|
||||||
except:
|
except:
|
||||||
report_compile_error()
|
report_compile_error()
|
||||||
|
|
||||||
|
@ -60,7 +63,12 @@ class BlueprintApp:
|
||||||
try:
|
try:
|
||||||
tokens = tokenizer.tokenize(data)
|
tokens = tokenizer.tokenize(data)
|
||||||
ast = parser.parse(tokens)
|
ast = parser.parse(tokens)
|
||||||
|
|
||||||
|
if len(ast.errors):
|
||||||
|
raise MultipleErrors(ast.errors)
|
||||||
|
|
||||||
xml = ast.generate()
|
xml = ast.generate()
|
||||||
|
|
||||||
if opts.output == "-":
|
if opts.output == "-":
|
||||||
print(xml)
|
print(xml)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from .ast import AstNode
|
from .ast import AstNode
|
||||||
from .errors import assert_true, CompilerBugError, CompileError, ParseError
|
from .errors import assert_true, CompilerBugError, CompileError
|
||||||
from .tokenizer import Token, TokenType
|
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
|
be converted to AST nodes by passing the children and key=value pairs to
|
||||||
the AST node constructor. """
|
the AST node constructor. """
|
||||||
|
|
||||||
def __init__(self, ast_type):
|
def __init__(self, ast_type, start: int):
|
||||||
self.ast_type = ast_type
|
self.ast_type = ast_type
|
||||||
self.children = {}
|
self.children = {}
|
||||||
self.keys = {}
|
self.keys = {}
|
||||||
|
self.tokens = {}
|
||||||
|
self.start = start
|
||||||
|
self.end = None
|
||||||
|
|
||||||
def add_child(self, child):
|
def add_child(self, child):
|
||||||
child_type = child.ast_type.child_type
|
child_type = child.ast_type.child_type
|
||||||
|
@ -68,10 +71,11 @@ class ParseGroup:
|
||||||
self.children[child_type] = []
|
self.children[child_type] = []
|
||||||
self.children[child_type].append(child)
|
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)
|
assert_true(key not in self.keys)
|
||||||
|
|
||||||
self.keys[key] = val
|
self.keys[key] = val
|
||||||
|
self.tokens[key] = token
|
||||||
|
|
||||||
def to_ast(self) -> AstNode:
|
def to_ast(self) -> AstNode:
|
||||||
""" Creates an AST node from the match group. """
|
""" Creates an AST node from the match group. """
|
||||||
|
@ -80,7 +84,12 @@ class ParseGroup:
|
||||||
for child_type, children in self.children.items()
|
for child_type, children in self.children.items()
|
||||||
}
|
}
|
||||||
try:
|
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:
|
except TypeError as e:
|
||||||
raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.")
|
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
|
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
|
new context will be applied to "self". If parsing fails, the new
|
||||||
context will be discarded. """
|
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):
|
def apply_child(self, other):
|
||||||
""" Applies a child context to this context. """
|
""" Applies a child context to this context. """
|
||||||
|
@ -121,10 +133,11 @@ class ParseContext:
|
||||||
if other.group is not None:
|
if other.group is not None:
|
||||||
# If the other context had a match group, collect all the matched
|
# If the other context had a match group, collect all the matched
|
||||||
# values into it and then add it to our own match group.
|
# values into it and then add it to our own match group.
|
||||||
for key, val in other.group_keys.items():
|
for key, (val, token) in other.group_keys.items():
|
||||||
other.group.set_val(key, val)
|
other.group.set_val(key, val, token)
|
||||||
for child in other.group_children:
|
for child in other.group_children:
|
||||||
other.group.add_child(child)
|
other.group.add_child(child)
|
||||||
|
other.group.end = other.tokens[other.index - 1].end
|
||||||
self.group_children.append(other.group)
|
self.group_children.append(other.group)
|
||||||
else:
|
else:
|
||||||
# If the other context had no match group of its own, collect all
|
# 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):
|
def start_group(self, ast_type):
|
||||||
""" Sets this context to have its own match group. """
|
""" Sets this context to have its own match group. """
|
||||||
assert_true(self.group is None)
|
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. """
|
""" Sets a matched key=value pair on the current match group. """
|
||||||
assert_true(key not in self.group_keys)
|
assert_true(key not in self.group_keys)
|
||||||
self.group_keys[key] = value
|
self.group_keys[key] = (value, token)
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def skip(self):
|
def skip(self):
|
||||||
|
@ -224,7 +226,32 @@ class Err(ParseNode):
|
||||||
|
|
||||||
def _parse(self, ctx):
|
def _parse(self, ctx):
|
||||||
if self.child.parse(ctx).failed():
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -250,6 +277,7 @@ class Group(ParseNode):
|
||||||
self.child = child
|
self.child = child
|
||||||
|
|
||||||
def _parse(self, ctx: ParseContext) -> bool:
|
def _parse(self, ctx: ParseContext) -> bool:
|
||||||
|
ctx.skip()
|
||||||
ctx.start_group(self.ast_type)
|
ctx.start_group(self.ast_type)
|
||||||
return self.child.parse(ctx).succeeded()
|
return self.child.parse(ctx).succeeded()
|
||||||
|
|
||||||
|
@ -367,16 +395,15 @@ class UseIdent(ParseNode):
|
||||||
if token.type != TokenType.IDENT:
|
if token.type != TokenType.IDENT:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
ctx.set_group_val(self.key, str(token))
|
ctx.set_group_val(self.key, str(token), token)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class UseNumber(ParseNode):
|
class UseNumber(ParseNode):
|
||||||
""" ParseNode that matches a number and sets it in a key=value pair on
|
""" ParseNode that matches a number and sets it in a key=value pair on
|
||||||
the containing match group. """
|
the containing match group. """
|
||||||
def __init__(self, key, keep_trailing_decimal=False):
|
def __init__(self, key):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.keep_trailing_decimal = keep_trailing_decimal
|
|
||||||
|
|
||||||
def _parse(self, ctx: ParseContext):
|
def _parse(self, ctx: ParseContext):
|
||||||
token = ctx.next_token()
|
token = ctx.next_token()
|
||||||
|
@ -384,9 +411,9 @@ class UseNumber(ParseNode):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
number = token.get_number()
|
number = token.get_number()
|
||||||
if not self.keep_trailing_decimal and number % 1.0 == 0:
|
if number % 1.0 == 0:
|
||||||
number = int(number)
|
number = int(number)
|
||||||
ctx.set_group_val(self.key, number)
|
ctx.set_group_val(self.key, number, token)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -405,7 +432,7 @@ class UseQuoted(ParseNode):
|
||||||
.replace("\\n", "\n")
|
.replace("\\n", "\n")
|
||||||
.replace("\\\"", "\"")
|
.replace("\\\"", "\"")
|
||||||
.replace("\\\\", "\\"))
|
.replace("\\\\", "\\"))
|
||||||
ctx.set_group_val(self.key, string)
|
ctx.set_group_val(self.key, string, token)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -418,7 +445,7 @@ class UseLiteral(ParseNode):
|
||||||
self.literal = literal
|
self.literal = literal
|
||||||
|
|
||||||
def _parse(self, ctx: ParseContext):
|
def _parse(self, ctx: ParseContext):
|
||||||
ctx.set_group_val(self.key, self.literal)
|
ctx.set_group_val(self.key, self.literal, None)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
|
|
||||||
from . import ast
|
from . import ast
|
||||||
from .errors import MultipleErrors, ParseError
|
from .errors import MultipleErrors
|
||||||
from .parse_tree import *
|
from .parse_tree import *
|
||||||
from .tokenizer import TokenType
|
from .tokenizer import TokenType
|
||||||
|
|
||||||
|
@ -31,7 +31,8 @@ def parse(tokens) -> ast.UI:
|
||||||
ast.GtkDirective,
|
ast.GtkDirective,
|
||||||
Sequence(
|
Sequence(
|
||||||
Directive("gtk"),
|
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("`;`"),
|
StmtEnd().expected("`;`"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -41,7 +42,8 @@ def parse(tokens) -> ast.UI:
|
||||||
Sequence(
|
Sequence(
|
||||||
Directive("import"),
|
Directive("import"),
|
||||||
UseIdent("namespace").expected("a GIR namespace"),
|
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("`;`"),
|
StmtEnd().expected("`;`"),
|
||||||
)
|
)
|
||||||
).recover()
|
).recover()
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
import re
|
import re
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from .errors import TokenizeError
|
from .errors import CompileError
|
||||||
|
|
||||||
|
|
||||||
class TokenType(Enum):
|
class TokenType(Enum):
|
||||||
|
@ -106,7 +106,7 @@ def _tokenize(ui_ml: str):
|
||||||
break
|
break
|
||||||
|
|
||||||
if not matched:
|
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)
|
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