mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
Format using black
This commit is contained in:
parent
6a36d92380
commit
8fee46ec68
40 changed files with 975 additions and 610 deletions
|
@ -26,11 +26,14 @@ from .lsp_utils import SemanticToken
|
|||
|
||||
|
||||
class Children:
|
||||
""" Allows accessing children by type using array syntax. """
|
||||
"""Allows accessing children by type using array syntax."""
|
||||
|
||||
def __init__(self, children):
|
||||
self._children = children
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._children)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, int):
|
||||
return self._children[key]
|
||||
|
@ -39,7 +42,7 @@ class Children:
|
|||
|
||||
|
||||
class AstNode:
|
||||
""" Base class for nodes in the abstract syntax tree. """
|
||||
"""Base class for nodes in the abstract syntax tree."""
|
||||
|
||||
completers: T.List = []
|
||||
|
||||
|
@ -55,8 +58,9 @@ class AstNode:
|
|||
|
||||
def __init_subclass__(cls):
|
||||
cls.completers = []
|
||||
cls.validators = [getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")]
|
||||
|
||||
cls.validators = [
|
||||
getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")
|
||||
]
|
||||
|
||||
@property
|
||||
def root(self):
|
||||
|
@ -116,13 +120,11 @@ class AstNode:
|
|||
for child in self.children:
|
||||
yield from child.get_semantic_tokens()
|
||||
|
||||
|
||||
def iterate_children_recursive(self) -> T.Iterator["AstNode"]:
|
||||
yield self
|
||||
for child in self.children:
|
||||
yield from child.iterate_children_recursive()
|
||||
|
||||
|
||||
def validate_unique_in_parent(self, error, check=None):
|
||||
for child in self.parent.children:
|
||||
if child is self:
|
||||
|
@ -132,13 +134,19 @@ class AstNode:
|
|||
if check is None or check(child):
|
||||
raise CompileError(
|
||||
error,
|
||||
references=[ErrorReference(child.group.start, child.group.end, "previous declaration was here")]
|
||||
references=[
|
||||
ErrorReference(
|
||||
child.group.start,
|
||||
child.group.end,
|
||||
"previous declaration was here",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def validate(token_name=None, end_token_name=None, skip_incomplete=False):
|
||||
""" Decorator for functions that validate an AST node. Exceptions raised
|
||||
during validation are marked with range information from the tokens. """
|
||||
"""Decorator for functions that validate an AST node. Exceptions raised
|
||||
during validation are marked with range information from the tokens."""
|
||||
|
||||
def decorator(func):
|
||||
def inner(self):
|
||||
|
@ -191,7 +199,7 @@ class Docs:
|
|||
|
||||
|
||||
def docs(*args, **kwargs):
|
||||
""" Decorator for functions that return documentation for tokens. """
|
||||
"""Decorator for functions that return documentation for tokens."""
|
||||
|
||||
def decorator(func):
|
||||
return Docs(func, *args, **kwargs)
|
||||
|
|
|
@ -30,9 +30,13 @@ from .tokenizer import TokenType, Token
|
|||
Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]]
|
||||
|
||||
|
||||
def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int) -> T.Iterator[Completion]:
|
||||
def _complete(
|
||||
ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int
|
||||
) -> T.Iterator[Completion]:
|
||||
for child in ast_node.children:
|
||||
if child.group.start <= idx and (idx < child.group.end or (idx == child.group.end and child.incomplete)):
|
||||
if child.group.start <= idx and (
|
||||
idx < child.group.end or (idx == child.group.end and child.incomplete)
|
||||
):
|
||||
yield from _complete(child, tokens, idx, token_idx)
|
||||
return
|
||||
|
||||
|
@ -49,7 +53,9 @@ def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int
|
|||
yield from completer(prev_tokens, ast_node)
|
||||
|
||||
|
||||
def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[Completion]:
|
||||
def complete(
|
||||
ast_node: AstNode, tokens: T.List[Token], idx: int
|
||||
) -> T.Iterator[Completion]:
|
||||
token_idx = 0
|
||||
# find the current token
|
||||
for i, token in enumerate(tokens):
|
||||
|
@ -71,13 +77,17 @@ def using_gtk(ast_node, match_variables):
|
|||
|
||||
@completer(
|
||||
applies_in=[language.UI, language.ObjectContent, language.Template],
|
||||
matches=new_statement_patterns
|
||||
matches=new_statement_patterns,
|
||||
)
|
||||
def namespace(ast_node, match_variables):
|
||||
yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.")
|
||||
for ns in ast_node.root.children[language.Import]:
|
||||
if ns.gir_namespace is not None:
|
||||
yield Completion(ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".")
|
||||
yield Completion(
|
||||
ns.gir_namespace.name,
|
||||
CompletionItemKind.Module,
|
||||
text=ns.gir_namespace.name + ".",
|
||||
)
|
||||
|
||||
|
||||
@completer(
|
||||
|
@ -85,7 +95,7 @@ def namespace(ast_node, match_variables):
|
|||
matches=[
|
||||
[(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)],
|
||||
[(TokenType.IDENT, None), (TokenType.OP, ".")],
|
||||
]
|
||||
],
|
||||
)
|
||||
def object_completer(ast_node, match_variables):
|
||||
ns = ast_node.root.gir.namespaces.get(match_variables[0])
|
||||
|
@ -117,9 +127,7 @@ def property_completer(ast_node, match_variables):
|
|||
|
||||
@completer(
|
||||
applies_in=[language.Property, language.BaseTypedAttribute],
|
||||
matches=[
|
||||
[(TokenType.IDENT, None), (TokenType.OP, ":")]
|
||||
],
|
||||
matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]],
|
||||
)
|
||||
def prop_value_completer(ast_node, match_variables):
|
||||
if isinstance(ast_node.value_type, gir.Enumeration):
|
||||
|
@ -141,16 +149,23 @@ def signal_completer(ast_node, match_variables):
|
|||
if not isinstance(ast_node.parent, language.Object):
|
||||
name = "on"
|
||||
else:
|
||||
name = "on_" + (ast_node.parent.children[ClassName][0].tokens["id"] or ast_node.parent.children[ClassName][0].tokens["class_name"].lower())
|
||||
yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;")
|
||||
name = "on_" + (
|
||||
ast_node.parent.children[ClassName][0].tokens["id"]
|
||||
or ast_node.parent.children[ClassName][0]
|
||||
.tokens["class_name"]
|
||||
.lower()
|
||||
)
|
||||
yield Completion(
|
||||
signal,
|
||||
CompletionItemKind.Property,
|
||||
snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;",
|
||||
)
|
||||
|
||||
|
||||
@completer(
|
||||
applies_in=[language.UI],
|
||||
matches=new_statement_patterns
|
||||
)
|
||||
@completer(applies_in=[language.UI], matches=new_statement_patterns)
|
||||
def template_completer(ast_node, match_variables):
|
||||
yield Completion(
|
||||
"template", CompletionItemKind.Snippet,
|
||||
snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}"
|
||||
"template",
|
||||
CompletionItemKind.Snippet,
|
||||
snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}",
|
||||
)
|
||||
|
|
|
@ -32,20 +32,25 @@ new_statement_patterns = [
|
|||
|
||||
|
||||
def applies_to(*ast_types):
|
||||
""" Decorator describing which AST nodes the completer should apply in. """
|
||||
"""Decorator describing which AST nodes the completer should apply in."""
|
||||
|
||||
def decorator(func):
|
||||
for c in ast_types:
|
||||
c.completers.append(func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None):
|
||||
|
||||
def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None):
|
||||
def decorator(func):
|
||||
def inner(prev_tokens: T.List[Token], ast_node):
|
||||
# For completers that apply in ObjectContent nodes, we can further
|
||||
# check that the object is the right class
|
||||
if applies_in_subclass is not None:
|
||||
type = ast_node.root.gir.get_type(applies_in_subclass[1], applies_in_subclass[0])
|
||||
type = ast_node.root.gir.get_type(
|
||||
applies_in_subclass[1], applies_in_subclass[0]
|
||||
)
|
||||
if ast_node.gir_class and not ast_node.gir_class.assignable_to(type):
|
||||
return
|
||||
|
||||
|
@ -59,7 +64,9 @@ def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None):
|
|||
for i in range(0, len(pattern)):
|
||||
type, value = pattern[i]
|
||||
token = prev_tokens[i - len(pattern)]
|
||||
if token.type != type or (value is not None and str(token) != value):
|
||||
if token.type != type or (
|
||||
value is not None and str(token) != value
|
||||
):
|
||||
break
|
||||
if value is None:
|
||||
match_variables.append(str(token))
|
||||
|
|
|
@ -60,16 +60,16 @@ class DecompileCtx:
|
|||
|
||||
self.gir.add_namespace(get_namespace("Gtk", "4.0"))
|
||||
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
imports = "\n".join([
|
||||
f"using {ns} {namespace.version};"
|
||||
for ns, namespace in self.gir.namespaces.items()
|
||||
])
|
||||
imports = "\n".join(
|
||||
[
|
||||
f"using {ns} {namespace.version};"
|
||||
for ns, namespace in self.gir.namespaces.items()
|
||||
]
|
||||
)
|
||||
return imports + "\n" + self._result
|
||||
|
||||
|
||||
def type_by_cname(self, cname):
|
||||
if type := self.gir.get_type_by_cname(cname):
|
||||
return type
|
||||
|
@ -83,7 +83,6 @@ class DecompileCtx:
|
|||
except:
|
||||
pass
|
||||
|
||||
|
||||
def start_block(self):
|
||||
self._blocks_need_end.append(None)
|
||||
|
||||
|
@ -94,7 +93,6 @@ class DecompileCtx:
|
|||
def end_block_with(self, text):
|
||||
self._blocks_need_end[-1] = text
|
||||
|
||||
|
||||
def print(self, line, newline=True):
|
||||
if line == "}" or line == "]":
|
||||
self._indent -= 1
|
||||
|
@ -109,7 +107,11 @@ class DecompileCtx:
|
|||
line_type = LineType.STMT
|
||||
else:
|
||||
line_type = LineType.NONE
|
||||
if line_type != self._last_line_type and self._last_line_type != LineType.BLOCK_START and line_type != LineType.BLOCK_END:
|
||||
if (
|
||||
line_type != self._last_line_type
|
||||
and self._last_line_type != LineType.BLOCK_START
|
||||
and line_type != LineType.BLOCK_END
|
||||
):
|
||||
self._result += "\n"
|
||||
self._last_line_type = line_type
|
||||
|
||||
|
@ -127,10 +129,10 @@ class DecompileCtx:
|
|||
for member in type.members.values():
|
||||
if member.nick == value or member.c_ident == value:
|
||||
return member.name
|
||||
return value.replace('-', '_')
|
||||
return value.replace("-", "_")
|
||||
|
||||
if type is None:
|
||||
self.print(f"{name}: \"{escape_quote(value)}\";")
|
||||
self.print(f'{name}: "{escape_quote(value)}";')
|
||||
elif type.assignable_to(FloatType()):
|
||||
self.print(f"{name}: {value};")
|
||||
elif type.assignable_to(BoolType()):
|
||||
|
@ -139,12 +141,20 @@ class DecompileCtx:
|
|||
elif (
|
||||
type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf"))
|
||||
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture"))
|
||||
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable"))
|
||||
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction"))
|
||||
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger"))
|
||||
or type.assignable_to(
|
||||
self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable")
|
||||
)
|
||||
or type.assignable_to(
|
||||
self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction")
|
||||
)
|
||||
or type.assignable_to(
|
||||
self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")
|
||||
)
|
||||
):
|
||||
self.print(f'{name}: "{escape_quote(value)}";')
|
||||
elif type.assignable_to(
|
||||
self.gir.namespaces["Gtk"].lookup_type("GObject.Object")
|
||||
):
|
||||
self.print(f"{name}: \"{escape_quote(value)}\";")
|
||||
elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")):
|
||||
self.print(f"{name}: {value};")
|
||||
elif isinstance(type, Bitfield):
|
||||
flags = [get_enum_name(flag) for flag in value.split("|")]
|
||||
|
@ -152,7 +162,7 @@ class DecompileCtx:
|
|||
elif isinstance(type, Enumeration):
|
||||
self.print(f"{name}: {get_enum_name(value)};")
|
||||
else:
|
||||
self.print(f"{name}: \"{escape_quote(value)}\";")
|
||||
self.print(f'{name}: "{escape_quote(value)}";')
|
||||
|
||||
|
||||
def _decompile_element(ctx: DecompileCtx, gir, xml):
|
||||
|
@ -191,19 +201,21 @@ def decompile(data):
|
|||
return ctx.result
|
||||
|
||||
|
||||
|
||||
def canon(string: str) -> str:
|
||||
if string == "class":
|
||||
return "klass"
|
||||
else:
|
||||
return string.replace("-", "_").lower()
|
||||
|
||||
|
||||
def truthy(string: str) -> bool:
|
||||
return string.lower() in ["yes", "true", "t", "y", "1"]
|
||||
|
||||
|
||||
def full_name(gir):
|
||||
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
|
||||
|
||||
|
||||
def lookup_by_cname(gir, cname: str):
|
||||
if isinstance(gir, GirContext):
|
||||
return gir.get_type_by_cname(cname)
|
||||
|
@ -216,15 +228,17 @@ def decompiler(tag, cdata=False):
|
|||
func._cdata = cdata
|
||||
_DECOMPILERS[tag] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def escape_quote(string: str) -> str:
|
||||
return (string
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\'", "\\'")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n"))
|
||||
return (
|
||||
string.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
)
|
||||
|
||||
|
||||
@decompiler("interface")
|
||||
|
@ -243,7 +257,18 @@ def decompile_placeholder(ctx, gir):
|
|||
|
||||
|
||||
@decompiler("property", cdata=True)
|
||||
def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None):
|
||||
def decompile_property(
|
||||
ctx,
|
||||
gir,
|
||||
name,
|
||||
cdata,
|
||||
bind_source=None,
|
||||
bind_property=None,
|
||||
bind_flags=None,
|
||||
translatable="false",
|
||||
comments=None,
|
||||
context=None,
|
||||
):
|
||||
name = name.replace("_", "-")
|
||||
if comments is not None:
|
||||
ctx.print(f"/* Translators: {comments} */")
|
||||
|
@ -263,18 +288,32 @@ def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=No
|
|||
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
|
||||
elif truthy(translatable):
|
||||
if context is not None:
|
||||
ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");")
|
||||
ctx.print(
|
||||
f'{name}: C_("{escape_quote(context)}", "{escape_quote(cdata)}");'
|
||||
)
|
||||
else:
|
||||
ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");")
|
||||
ctx.print(f'{name}: _("{escape_quote(cdata)}");')
|
||||
elif gir is None or gir.properties.get(name) is None:
|
||||
ctx.print(f"{name}: \"{escape_quote(cdata)}\";")
|
||||
ctx.print(f'{name}: "{escape_quote(cdata)}";')
|
||||
else:
|
||||
ctx.print_attribute(name, cdata, gir.properties.get(name).type)
|
||||
return gir
|
||||
|
||||
|
||||
@decompiler("attribute", cdata=True)
|
||||
def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None):
|
||||
decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context)
|
||||
def decompile_attribute(
|
||||
ctx, gir, name, cdata, translatable="false", comments=None, context=None
|
||||
):
|
||||
decompile_property(
|
||||
ctx,
|
||||
gir,
|
||||
name,
|
||||
cdata,
|
||||
translatable=translatable,
|
||||
comments=comments,
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
@decompiler("attributes")
|
||||
def decompile_attributes(ctx, gir):
|
||||
|
@ -291,5 +330,7 @@ class UnsupportedError(Exception):
|
|||
print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}")
|
||||
if self.tag:
|
||||
print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}")
|
||||
print(f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You
|
||||
probably need to port this file manually.{Colors.CLEAR}\n""")
|
||||
print(
|
||||
f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You
|
||||
probably need to port this file manually.{Colors.CLEAR}\n"""
|
||||
)
|
||||
|
|
|
@ -23,9 +23,10 @@ import sys, traceback
|
|||
from . import utils
|
||||
from .utils import Colors
|
||||
|
||||
|
||||
class PrintableError(Exception):
|
||||
""" Parent class for errors that can be pretty-printed for the user, e.g.
|
||||
compilation warnings and errors. """
|
||||
"""Parent class for errors that can be pretty-printed for the user, e.g.
|
||||
compilation warnings and errors."""
|
||||
|
||||
def pretty_print(self, filename, code):
|
||||
raise NotImplementedError()
|
||||
|
@ -39,12 +40,22 @@ class ErrorReference:
|
|||
|
||||
|
||||
class CompileError(PrintableError):
|
||||
""" A PrintableError with a start/end position and optional hints """
|
||||
"""A PrintableError with a start/end position and optional hints"""
|
||||
|
||||
category = "error"
|
||||
color = Colors.RED
|
||||
|
||||
def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None, actions=None, fatal=False, references=None):
|
||||
def __init__(
|
||||
self,
|
||||
message,
|
||||
start=None,
|
||||
end=None,
|
||||
did_you_mean=None,
|
||||
hints=None,
|
||||
actions=None,
|
||||
fatal=False,
|
||||
references=None,
|
||||
):
|
||||
super().__init__(message)
|
||||
|
||||
self.message = message
|
||||
|
@ -62,7 +73,6 @@ class CompileError(PrintableError):
|
|||
self.hints.append(hint)
|
||||
return self
|
||||
|
||||
|
||||
def _did_you_mean(self, word: str, options: T.List[str]):
|
||||
if word.replace("_", "-") in options:
|
||||
self.hint(f"use '-', not '_': `{word.replace('_', '-')}`")
|
||||
|
@ -86,9 +96,11 @@ class CompileError(PrintableError):
|
|||
# Display 1-based line numbers
|
||||
line_num += 1
|
||||
|
||||
stream.write(f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
|
||||
stream.write(
|
||||
f"""{self.color}{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}\n""")
|
||||
{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n"""
|
||||
)
|
||||
|
||||
for hint in self.hints:
|
||||
stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n")
|
||||
|
@ -98,9 +110,11 @@ at {filename} line {line_num} column {col_num}:
|
|||
line = code.splitlines(True)[line_num]
|
||||
line_num += 1
|
||||
|
||||
stream.write(f"""{Colors.FAINT}note: {ref.message}:
|
||||
stream.write(
|
||||
f"""{Colors.FAINT}note: {ref.message}:
|
||||
at {filename} line {line_num} column {col_num}:
|
||||
{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""")
|
||||
{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n"""
|
||||
)
|
||||
|
||||
stream.write("\n")
|
||||
|
||||
|
@ -122,9 +136,9 @@ class CodeAction:
|
|||
|
||||
|
||||
class MultipleErrors(PrintableError):
|
||||
""" If multiple errors occur during compilation, they can be collected into
|
||||
"""If multiple errors occur during compilation, they can be collected into
|
||||
a list and re-thrown using the MultipleErrors exception. It will
|
||||
pretty-print all of the errors and a count of how many errors there are. """
|
||||
pretty-print all of the errors and a count of how many errors there are."""
|
||||
|
||||
def __init__(self, errors: T.List[CompileError]):
|
||||
super().__init__()
|
||||
|
@ -138,24 +152,25 @@ class MultipleErrors(PrintableError):
|
|||
|
||||
|
||||
class CompilerBugError(Exception):
|
||||
""" Emitted on assertion errors """
|
||||
"""Emitted on assertion errors"""
|
||||
|
||||
|
||||
def assert_true(truth: bool, message: T.Optional[str]=None):
|
||||
def assert_true(truth: bool, message: T.Optional[str] = None):
|
||||
if not truth:
|
||||
raise CompilerBugError(message)
|
||||
|
||||
|
||||
def report_bug(): # pragma: no cover
|
||||
""" Report an error and ask people to report it. """
|
||||
def report_bug(): # pragma: no cover
|
||||
"""Report an error and ask people to report it."""
|
||||
|
||||
print(traceback.format_exc())
|
||||
print(f"Arguments: {sys.argv}\n")
|
||||
print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG *****
|
||||
print(
|
||||
f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG *****
|
||||
The blueprint-compiler program has crashed. Please report the above stacktrace,
|
||||
along with the input file(s) if possible, on GitLab:
|
||||
{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue
|
||||
{Colors.CLEAR}""")
|
||||
{Colors.CLEAR}"""
|
||||
)
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
|
|
|
@ -21,9 +21,10 @@ from functools import cached_property
|
|||
import typing as T
|
||||
import os, sys
|
||||
|
||||
import gi # type: ignore
|
||||
import gi # type: ignore
|
||||
|
||||
gi.require_version("GIRepository", "2.0")
|
||||
from gi.repository import GIRepository # type: ignore
|
||||
from gi.repository import GIRepository # type: ignore
|
||||
|
||||
from .errors import CompileError, CompilerBugError
|
||||
from . import typelib, xml_reader
|
||||
|
@ -60,10 +61,13 @@ def get_namespace(namespace, version) -> "Namespace":
|
|||
def get_xml(namespace, version):
|
||||
from .main import VERSION
|
||||
from xml.etree import ElementTree
|
||||
|
||||
search_paths = []
|
||||
|
||||
if data_paths := os.environ.get("XDG_DATA_DIRS"):
|
||||
search_paths += [os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep)]
|
||||
search_paths += [
|
||||
os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep)
|
||||
]
|
||||
|
||||
filename = f"{namespace}-{version}.gir"
|
||||
|
||||
|
@ -104,36 +108,57 @@ class BasicType(GirType):
|
|||
def full_name(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class BoolType(BasicType):
|
||||
name = "bool"
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
return isinstance(other, BoolType)
|
||||
|
||||
|
||||
class IntType(BasicType):
|
||||
name = "int"
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType)
|
||||
return (
|
||||
isinstance(other, IntType)
|
||||
or isinstance(other, UIntType)
|
||||
or isinstance(other, FloatType)
|
||||
)
|
||||
|
||||
|
||||
class UIntType(BasicType):
|
||||
name = "uint"
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType)
|
||||
return (
|
||||
isinstance(other, IntType)
|
||||
or isinstance(other, UIntType)
|
||||
or isinstance(other, FloatType)
|
||||
)
|
||||
|
||||
|
||||
class FloatType(BasicType):
|
||||
name = "float"
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
return isinstance(other, FloatType)
|
||||
|
||||
|
||||
class StringType(BasicType):
|
||||
name = "string"
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
return isinstance(other, StringType)
|
||||
|
||||
|
||||
class TypeType(BasicType):
|
||||
name = "GType"
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
return isinstance(other, TypeType)
|
||||
|
||||
|
||||
_BASIC_TYPES = {
|
||||
"gboolean": BoolType,
|
||||
"int": IntType,
|
||||
|
@ -150,6 +175,7 @@ _BASIC_TYPES = {
|
|||
"type": TypeType,
|
||||
}
|
||||
|
||||
|
||||
class GirNode:
|
||||
def __init__(self, container, tl):
|
||||
self.container = container
|
||||
|
@ -291,7 +317,9 @@ class Interface(GirNode, GirType):
|
|||
n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES
|
||||
offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE
|
||||
offset += (n_prerequisites + n_prerequisites % 2) * 2
|
||||
offset += self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE
|
||||
offset += (
|
||||
self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE
|
||||
)
|
||||
offset += self.tl.INTERFACE_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE
|
||||
n_signals = self.tl.INTERFACE_N_SIGNALS
|
||||
property_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE
|
||||
|
@ -342,7 +370,9 @@ class Class(GirNode, GirType):
|
|||
offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE
|
||||
offset += (n_interfaces + n_interfaces % 2) * 2
|
||||
offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE
|
||||
offset += self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE
|
||||
offset += (
|
||||
self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE
|
||||
)
|
||||
n_properties = self.tl.OBJ_N_PROPERTIES
|
||||
property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE
|
||||
result = {}
|
||||
|
@ -357,7 +387,9 @@ class Class(GirNode, GirType):
|
|||
offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE
|
||||
offset += (n_interfaces + n_interfaces % 2) * 2
|
||||
offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE
|
||||
offset += self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE
|
||||
offset += (
|
||||
self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE
|
||||
)
|
||||
offset += self.tl.OBJ_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE
|
||||
offset += self.tl.OBJ_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE
|
||||
n_signals = self.tl.OBJ_N_SIGNALS
|
||||
|
@ -381,16 +413,18 @@ class Class(GirNode, GirType):
|
|||
if self.parent is not None:
|
||||
result += f" : {self.parent.container.name}.{self.parent.name}"
|
||||
if len(self.implements):
|
||||
result += " implements " + ", ".join([impl.full_name for impl in self.implements])
|
||||
result += " implements " + ", ".join(
|
||||
[impl.full_name for impl in self.implements]
|
||||
)
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def properties(self):
|
||||
return { p.name: p for p in self._enum_properties() }
|
||||
return {p.name: p for p in self._enum_properties()}
|
||||
|
||||
@cached_property
|
||||
def signals(self):
|
||||
return { s.name: s for s in self._enum_signals() }
|
||||
return {s.name: s for s in self._enum_signals()}
|
||||
|
||||
def assignable_to(self, other) -> bool:
|
||||
if self == other:
|
||||
|
@ -509,9 +543,12 @@ class Namespace(GirNode):
|
|||
elif entry_type == typelib.BLOB_TYPE_OBJECT:
|
||||
self.entries[entry_name] = Class(self, entry_blob)
|
||||
elif entry_type == typelib.BLOB_TYPE_INTERFACE:
|
||||
self.entries[entry_name] = Interface(self, entry_blob)
|
||||
elif entry_type == typelib.BLOB_TYPE_BOXED or entry_type == typelib.BLOB_TYPE_STRUCT:
|
||||
self.entries[entry_name] = Boxed(self, entry_blob)
|
||||
self.entries[entry_name] = Interface(self, entry_blob)
|
||||
elif (
|
||||
entry_type == typelib.BLOB_TYPE_BOXED
|
||||
or entry_type == typelib.BLOB_TYPE_STRUCT
|
||||
):
|
||||
self.entries[entry_name] = Boxed(self, entry_blob)
|
||||
|
||||
@cached_property
|
||||
def xml(self):
|
||||
|
@ -531,25 +568,33 @@ class Namespace(GirNode):
|
|||
|
||||
@cached_property
|
||||
def classes(self):
|
||||
return { name: entry for name, entry in self.entries.items() if isinstance(entry, Class) }
|
||||
return {
|
||||
name: entry
|
||||
for name, entry in self.entries.items()
|
||||
if isinstance(entry, Class)
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def interfaces(self):
|
||||
return { name: entry for name, entry in self.entries.items() if isinstance(entry, Interface) }
|
||||
return {
|
||||
name: entry
|
||||
for name, entry in self.entries.items()
|
||||
if isinstance(entry, Interface)
|
||||
}
|
||||
|
||||
def get_type(self, name):
|
||||
""" Gets a type (class, interface, enum, etc.) from this namespace. """
|
||||
"""Gets a type (class, interface, enum, etc.) from this namespace."""
|
||||
return self.entries.get(name)
|
||||
|
||||
def get_type_by_cname(self, cname: str):
|
||||
""" Gets a type from this namespace by its C name. """
|
||||
"""Gets a type from this namespace by its C name."""
|
||||
for item in self.entries.values():
|
||||
if hasattr(item, "cname") and item.cname == cname:
|
||||
return item
|
||||
|
||||
def lookup_type(self, type_name: str):
|
||||
""" Looks up a type in the scope of this namespace (including in the
|
||||
namespace's dependencies). """
|
||||
"""Looks up a type in the scope of this namespace (including in the
|
||||
namespace's dependencies)."""
|
||||
|
||||
if type_name in _BASIC_TYPES:
|
||||
return _BASIC_TYPES[type_name]()
|
||||
|
@ -569,7 +614,9 @@ class Repository(GirNode):
|
|||
if dependencies := tl[0x24].string:
|
||||
deps = [tuple(dep.split("-", 1)) for dep in dependencies.split("|")]
|
||||
try:
|
||||
self.includes = { name: get_namespace(name, version) for name, version in deps }
|
||||
self.includes = {
|
||||
name: get_namespace(name, version) for name, version in deps
|
||||
}
|
||||
except:
|
||||
raise CompilerBugError(f"Failed to load dependencies.")
|
||||
else:
|
||||
|
@ -578,16 +625,14 @@ class Repository(GirNode):
|
|||
def get_type(self, name: str, ns: str) -> T.Optional[GirNode]:
|
||||
return self.lookup_namespace(ns).get_type(name)
|
||||
|
||||
|
||||
def get_type_by_cname(self, name: str) -> T.Optional[GirNode]:
|
||||
for ns in [self.namespace, *self.includes.values()]:
|
||||
if type := ns.get_type_by_cname(name):
|
||||
return type
|
||||
return None
|
||||
|
||||
|
||||
def lookup_namespace(self, ns: str):
|
||||
""" Finds a namespace among this namespace's dependencies. """
|
||||
"""Finds a namespace among this namespace's dependencies."""
|
||||
if ns == self.namespace.name:
|
||||
return self.namespace
|
||||
else:
|
||||
|
@ -610,9 +655,19 @@ class Repository(GirNode):
|
|||
return BoolType()
|
||||
elif type_id in [typelib.TYPE_FLOAT, typelib.TYPE_DOUBLE]:
|
||||
return FloatType()
|
||||
elif type_id in [typelib.TYPE_INT8, typelib.TYPE_INT16, typelib.TYPE_INT32, typelib.TYPE_INT64]:
|
||||
elif type_id in [
|
||||
typelib.TYPE_INT8,
|
||||
typelib.TYPE_INT16,
|
||||
typelib.TYPE_INT32,
|
||||
typelib.TYPE_INT64,
|
||||
]:
|
||||
return IntType()
|
||||
elif type_id in [typelib.TYPE_UINT8, typelib.TYPE_UINT16, typelib.TYPE_UINT32, typelib.TYPE_UINT64]:
|
||||
elif type_id in [
|
||||
typelib.TYPE_UINT8,
|
||||
typelib.TYPE_UINT16,
|
||||
typelib.TYPE_UINT32,
|
||||
typelib.TYPE_UINT64,
|
||||
]:
|
||||
return UIntType()
|
||||
elif type_id == typelib.TYPE_UTF8:
|
||||
return StringType()
|
||||
|
@ -621,30 +676,30 @@ class Repository(GirNode):
|
|||
else:
|
||||
raise CompilerBugError("Unknown type ID", type_id)
|
||||
else:
|
||||
return self._resolve_dir_entry(self.tl.header[type_id].INTERFACE_TYPE_INTERFACE)
|
||||
|
||||
return self._resolve_dir_entry(
|
||||
self.tl.header[type_id].INTERFACE_TYPE_INTERFACE
|
||||
)
|
||||
|
||||
|
||||
class GirContext:
|
||||
def __init__(self):
|
||||
self.namespaces = {}
|
||||
|
||||
|
||||
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.name}-{namespace.version} can't be imported because version {other.version} was imported earlier")
|
||||
raise CompileError(
|
||||
f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier"
|
||||
)
|
||||
|
||||
self.namespaces[namespace.name] = namespace
|
||||
|
||||
|
||||
def get_type_by_cname(self, name: str) -> T.Optional[GirNode]:
|
||||
for ns in self.namespaces.values():
|
||||
if type := ns.get_type_by_cname(name):
|
||||
return type
|
||||
return None
|
||||
|
||||
|
||||
def get_type(self, name: str, ns: str) -> T.Optional[GirNode]:
|
||||
ns = ns or "Gtk"
|
||||
|
||||
|
@ -653,7 +708,6 @@ class GirContext:
|
|||
|
||||
return self.namespaces[ns].get_type(name)
|
||||
|
||||
|
||||
def get_class(self, name: str, ns: str) -> T.Optional[Class]:
|
||||
type = self.get_type(name, ns)
|
||||
if isinstance(type, Class):
|
||||
|
@ -661,10 +715,9 @@ class GirContext:
|
|||
else:
|
||||
return None
|
||||
|
||||
|
||||
def validate_ns(self, ns: str):
|
||||
""" Raises an exception if there is a problem looking up the given
|
||||
namespace. """
|
||||
"""Raises an exception if there is a problem looking up the given
|
||||
namespace."""
|
||||
|
||||
ns = ns or "Gtk"
|
||||
|
||||
|
@ -675,7 +728,7 @@ class GirContext:
|
|||
)
|
||||
|
||||
def validate_type(self, name: str, ns: str):
|
||||
""" Raises an exception if there is a problem looking up the given type. """
|
||||
"""Raises an exception if there is a problem looking up the given type."""
|
||||
|
||||
self.validate_ns(ns)
|
||||
|
||||
|
|
|
@ -35,9 +35,11 @@ class CouldNotPort:
|
|||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
|
||||
def change_suffix(f):
|
||||
return f.removesuffix(".ui") + ".blp"
|
||||
|
||||
|
||||
def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
|
||||
if os.path.exists(out_file):
|
||||
return CouldNotPort("already exists")
|
||||
|
@ -63,12 +65,15 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
|
|||
except PrintableError as e:
|
||||
e.pretty_print(out_file, decompiled)
|
||||
|
||||
print(f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}")
|
||||
print(
|
||||
f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}"
|
||||
)
|
||||
print(f"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}")
|
||||
print(
|
||||
f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the
|
||||
f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the
|
||||
porting tool. If you think it's a bug (which is likely), please file an issue on GitLab:
|
||||
{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""")
|
||||
{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n"""
|
||||
)
|
||||
|
||||
return CouldNotPort("does not compile")
|
||||
|
||||
|
@ -108,7 +113,9 @@ def enter():
|
|||
|
||||
|
||||
def step1():
|
||||
print(f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}")
|
||||
print(
|
||||
f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}"
|
||||
)
|
||||
|
||||
if os.path.exists("subprojects/blueprint-compiler.wrap"):
|
||||
print("subprojects/blueprint-compiler.wrap already exists, skipping\n")
|
||||
|
@ -121,17 +128,20 @@ def step1():
|
|||
pass
|
||||
|
||||
from .main import VERSION
|
||||
|
||||
VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION
|
||||
|
||||
with open("subprojects/blueprint-compiler.wrap", "w") as wrap:
|
||||
wrap.write(f"""[wrap-git]
|
||||
wrap.write(
|
||||
f"""[wrap-git]
|
||||
directory = blueprint-compiler
|
||||
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
|
||||
revision = {VERSION}
|
||||
depth = 1
|
||||
|
||||
[provide]
|
||||
program_names = blueprint-compiler""")
|
||||
program_names = blueprint-compiler"""
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
|
@ -146,7 +156,9 @@ def step2():
|
|||
if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"):
|
||||
gitignore.write("\n/subprojects/blueprint-compiler\n")
|
||||
else:
|
||||
print("'/subprojects/blueprint-compiler' already in .gitignore, skipping")
|
||||
print(
|
||||
"'/subprojects/blueprint-compiler' already in .gitignore, skipping"
|
||||
)
|
||||
else:
|
||||
if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"):
|
||||
with open(".gitignore", "w") as gitignore:
|
||||
|
@ -169,9 +181,13 @@ def step3():
|
|||
if isinstance(result, CouldNotPort):
|
||||
if result.message == "already exists":
|
||||
print(Colors.FAINT, end="")
|
||||
print(f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}")
|
||||
print(
|
||||
f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}"
|
||||
)
|
||||
else:
|
||||
print(f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}")
|
||||
print(
|
||||
f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}"
|
||||
)
|
||||
success += 1
|
||||
|
||||
print()
|
||||
|
@ -180,7 +196,9 @@ def step3():
|
|||
elif success == len(files):
|
||||
print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}")
|
||||
elif success > 0:
|
||||
print(f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}")
|
||||
print(
|
||||
f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}"
|
||||
)
|
||||
else:
|
||||
print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}")
|
||||
|
||||
|
@ -204,22 +222,33 @@ def step3():
|
|||
|
||||
def step4(ported):
|
||||
print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}")
|
||||
print(f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}")
|
||||
print(
|
||||
f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}"
|
||||
)
|
||||
|
||||
meson_files = [file for file in listdir_recursive(".") if os.path.basename(file) == "meson.build"]
|
||||
meson_files = [
|
||||
file
|
||||
for file in listdir_recursive(".")
|
||||
if os.path.basename(file) == "meson.build"
|
||||
]
|
||||
for meson_file in meson_files:
|
||||
with open(meson_file, "r") as f:
|
||||
if "gnome.compile_resources" in f.read():
|
||||
parent = os.path.dirname(meson_file)
|
||||
file_list = "\n ".join([
|
||||
f"'{os.path.relpath(file, parent)}',"
|
||||
for file in ported
|
||||
if file.startswith(parent)
|
||||
])
|
||||
file_list = "\n ".join(
|
||||
[
|
||||
f"'{os.path.relpath(file, parent)}',"
|
||||
for file in ported
|
||||
if file.startswith(parent)
|
||||
]
|
||||
)
|
||||
|
||||
if len(file_list):
|
||||
print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}")
|
||||
print(f"""
|
||||
print(
|
||||
f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}"
|
||||
)
|
||||
print(
|
||||
f"""
|
||||
blueprints = custom_target('blueprints',
|
||||
input: files(
|
||||
{file_list}
|
||||
|
@ -227,14 +256,17 @@ blueprints = custom_target('blueprints',
|
|||
output: '.',
|
||||
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
enter()
|
||||
|
||||
print(f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()'
|
||||
print(
|
||||
f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()'
|
||||
arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}
|
||||
|
||||
dependencies: blueprints,
|
||||
""")
|
||||
"""
|
||||
)
|
||||
enter()
|
||||
|
||||
print()
|
||||
|
@ -244,7 +276,9 @@ def step5(in_files):
|
|||
print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}")
|
||||
|
||||
if not os.path.exists("po/POTFILES.in"):
|
||||
print(f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n")
|
||||
print(
|
||||
f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n"
|
||||
)
|
||||
return
|
||||
|
||||
with open("po/POTFILES.in", "r") as potfiles:
|
||||
|
@ -257,12 +291,24 @@ def step5(in_files):
|
|||
|
||||
new_data = "".join(lines)
|
||||
|
||||
print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}")
|
||||
print(
|
||||
"".join([
|
||||
(Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + Colors.CLEAR
|
||||
for line in difflib.unified_diff(old_lines, lines)
|
||||
])
|
||||
f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}"
|
||||
)
|
||||
print(
|
||||
"".join(
|
||||
[
|
||||
(
|
||||
Colors.GREEN
|
||||
if line.startswith("+")
|
||||
else Colors.RED + Colors.FAINT
|
||||
if line.startswith("-")
|
||||
else ""
|
||||
)
|
||||
+ line
|
||||
+ Colors.CLEAR
|
||||
for line in difflib.unified_diff(old_lines, lines)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if yesno("Is this ok?"):
|
||||
|
@ -291,5 +337,6 @@ def run(opts):
|
|||
step5(in_files)
|
||||
step6(in_files)
|
||||
|
||||
print(f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}")
|
||||
|
||||
print(
|
||||
f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}"
|
||||
)
|
||||
|
|
|
@ -16,7 +16,16 @@ from .gtkbuilder_template import Template
|
|||
from .imports import GtkDirective, Import
|
||||
from .ui import UI
|
||||
from .types import ClassName
|
||||
from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, Flag, QuotedValue, NumberValue, Value
|
||||
from .values import (
|
||||
TypeValue,
|
||||
IdentValue,
|
||||
TranslatedStringValue,
|
||||
FlagsValue,
|
||||
Flag,
|
||||
QuotedValue,
|
||||
NumberValue,
|
||||
Value,
|
||||
)
|
||||
|
||||
from .common import *
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ from .common import *
|
|||
|
||||
|
||||
class BaseAttribute(AstNode):
|
||||
""" A helper class for attribute syntax of the form `name: literal_value;`"""
|
||||
"""A helper class for attribute syntax of the form `name: literal_value;`"""
|
||||
|
||||
tag_name: str = ""
|
||||
attr_name: str = "name"
|
||||
|
@ -34,5 +34,5 @@ class BaseAttribute(AstNode):
|
|||
|
||||
|
||||
class BaseTypedAttribute(BaseAttribute):
|
||||
""" A BaseAttribute whose parent has a value_type property that can assist
|
||||
in validation. """
|
||||
"""A BaseAttribute whose parent has a value_type property that can assist
|
||||
in validation."""
|
||||
|
|
|
@ -33,6 +33,7 @@ class ObjectContent(AstNode):
|
|||
def gir_class(self):
|
||||
return self.parent.gir_class
|
||||
|
||||
|
||||
class Object(AstNode):
|
||||
grammar: T.Any = [
|
||||
ConcreteClassName,
|
||||
|
@ -75,13 +76,17 @@ 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}")
|
||||
raise CompileError(
|
||||
f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}"
|
||||
)
|
||||
|
||||
|
||||
@decompiler("object")
|
||||
def decompile_object(ctx, gir, klass, id=None):
|
||||
gir_class = ctx.type_by_cname(klass)
|
||||
klass_name = decompile.full_name(gir_class) if gir_class is not None else "." + klass
|
||||
klass_name = (
|
||||
decompile.full_name(gir_class) if gir_class is not None else "." + klass
|
||||
)
|
||||
if id is None:
|
||||
ctx.print(f"{klass_name} {{")
|
||||
else:
|
||||
|
|
|
@ -34,12 +34,16 @@ class Property(AstNode):
|
|||
UseIdent("bind_source"),
|
||||
".",
|
||||
UseIdent("bind_property"),
|
||||
ZeroOrMore(AnyOf(
|
||||
["no-sync-create", UseLiteral("no_sync_create", True)],
|
||||
["inverted", UseLiteral("inverted", True)],
|
||||
["bidirectional", UseLiteral("bidirectional", True)],
|
||||
Match("sync-create").warn("sync-create is deprecated in favor of no-sync-create"),
|
||||
)),
|
||||
ZeroOrMore(
|
||||
AnyOf(
|
||||
["no-sync-create", UseLiteral("no_sync_create", True)],
|
||||
["inverted", UseLiteral("inverted", True)],
|
||||
["bidirectional", UseLiteral("bidirectional", True)],
|
||||
Match("sync-create").warn(
|
||||
"sync-create is deprecated in favor of no-sync-create"
|
||||
),
|
||||
)
|
||||
),
|
||||
";",
|
||||
],
|
||||
Statement(
|
||||
|
@ -63,19 +67,16 @@ class Property(AstNode):
|
|||
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:
|
||||
|
@ -91,15 +92,19 @@ class Property(AstNode):
|
|||
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())
|
||||
did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()),
|
||||
)
|
||||
|
||||
@validate("bind")
|
||||
def property_bindable(self):
|
||||
if self.tokens["bind"] and self.gir_property is not None and self.gir_property.construct_only:
|
||||
if (
|
||||
self.tokens["bind"]
|
||||
and self.gir_property is not None
|
||||
and self.gir_property.construct_only
|
||||
):
|
||||
raise CompileError(
|
||||
f"{self.gir_property.full_name} can't be bound because it is construct-only",
|
||||
hints=["construct-only properties may only be set to a static value"]
|
||||
hints=["construct-only properties may only be set to a static value"],
|
||||
)
|
||||
|
||||
@validate("name")
|
||||
|
@ -107,7 +112,6 @@ class Property(AstNode):
|
|||
if self.gir_property is not None and not self.gir_property.writable:
|
||||
raise CompileError(f"{self.gir_property.full_name} is not writable")
|
||||
|
||||
|
||||
@validate()
|
||||
def obj_property_type(self):
|
||||
if len(self.children[Object]) == 0:
|
||||
|
@ -115,20 +119,23 @@ class Property(AstNode):
|
|||
|
||||
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):
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
@validate("name")
|
||||
def unique_in_parent(self):
|
||||
self.validate_unique_in_parent(
|
||||
f"Duplicate property '{self.tokens['name']}'",
|
||||
check=lambda child: child.tokens["name"] == self.tokens["name"]
|
||||
check=lambda child: child.tokens["name"] == self.tokens["name"],
|
||||
)
|
||||
|
||||
|
||||
@docs("name")
|
||||
def property_docs(self):
|
||||
if self.gir_property is not None:
|
||||
|
|
|
@ -26,19 +26,23 @@ from .common import *
|
|||
class Signal(AstNode):
|
||||
grammar = Statement(
|
||||
UseIdent("name"),
|
||||
Optional([
|
||||
"::",
|
||||
UseIdent("detail_name").expected("a signal detail name"),
|
||||
]),
|
||||
Optional(
|
||||
[
|
||||
"::",
|
||||
UseIdent("detail_name").expected("a signal detail name"),
|
||||
]
|
||||
),
|
||||
"=>",
|
||||
UseIdent("handler").expected("the name of a function to handle the signal"),
|
||||
Match("(").expected("argument list"),
|
||||
Optional(UseIdent("object")).expected("object identifier"),
|
||||
Match(")").expected(),
|
||||
ZeroOrMore(AnyOf(
|
||||
[Keyword("swapped"), UseLiteral("swapped", True)],
|
||||
[Keyword("after"), UseLiteral("after", True)],
|
||||
)),
|
||||
ZeroOrMore(
|
||||
AnyOf(
|
||||
[Keyword("swapped"), UseLiteral("swapped", True)],
|
||||
[Keyword("after"), UseLiteral("after", True)],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -65,18 +69,15 @@ class Signal(AstNode):
|
|||
def is_after(self) -> bool:
|
||||
return self.tokens["after"] or False
|
||||
|
||||
|
||||
@property
|
||||
def gir_signal(self):
|
||||
if self.gir_class is not None:
|
||||
return self.gir_class.signals.get(self.tokens["name"])
|
||||
|
||||
|
||||
@property
|
||||
def gir_class(self):
|
||||
return self.parent.parent.gir_class
|
||||
|
||||
|
||||
@validate("name")
|
||||
def signal_exists(self):
|
||||
if self.gir_class is None:
|
||||
|
@ -92,10 +93,9 @@ class Signal(AstNode):
|
|||
if self.gir_signal is None:
|
||||
raise CompileError(
|
||||
f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}",
|
||||
did_you_mean=(self.tokens["name"], self.gir_class.signals.keys())
|
||||
did_you_mean=(self.tokens["name"], self.gir_class.signals.keys()),
|
||||
)
|
||||
|
||||
|
||||
@validate("object")
|
||||
def object_exists(self):
|
||||
object_id = self.tokens["object"]
|
||||
|
@ -103,10 +103,7 @@ class Signal(AstNode):
|
|||
return
|
||||
|
||||
if self.root.objects_by_id.get(object_id) is None:
|
||||
raise CompileError(
|
||||
f"Could not find object with ID '{object_id}'"
|
||||
)
|
||||
|
||||
raise CompileError(f"Could not find object with ID '{object_id}'")
|
||||
|
||||
@docs("name")
|
||||
def signal_docs(self):
|
||||
|
|
|
@ -86,6 +86,7 @@ def get_state_types(gir):
|
|||
"selected": BoolType(),
|
||||
}
|
||||
|
||||
|
||||
def get_types(gir):
|
||||
return {
|
||||
**get_property_types(gir),
|
||||
|
@ -93,6 +94,7 @@ def get_types(gir):
|
|||
**get_state_types(gir),
|
||||
}
|
||||
|
||||
|
||||
def _get_docs(gir, name):
|
||||
if gir_type := (
|
||||
gir.get_type("AccessibleProperty", "Gtk").members.get(name)
|
||||
|
@ -174,8 +176,7 @@ class A11y(AstNode):
|
|||
)
|
||||
def a11y_completer(ast_node, match_variables):
|
||||
yield Completion(
|
||||
"accessibility", CompletionItemKind.Snippet,
|
||||
snippet="accessibility {\n $0\n}"
|
||||
"accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}"
|
||||
)
|
||||
|
||||
|
||||
|
@ -185,20 +186,24 @@ def a11y_completer(ast_node, match_variables):
|
|||
)
|
||||
def a11y_name_completer(ast_node, match_variables):
|
||||
for name, type in get_types(ast_node.root.gir).items():
|
||||
yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type))
|
||||
yield Completion(
|
||||
name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type)
|
||||
)
|
||||
|
||||
|
||||
@decompiler("relation", cdata=True)
|
||||
def decompile_relation(ctx, gir, name, cdata):
|
||||
ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name))
|
||||
|
||||
|
||||
@decompiler("state", cdata=True)
|
||||
def decompile_state(ctx, gir, name, cdata, translatable="false"):
|
||||
if decompile.truthy(translatable):
|
||||
ctx.print(f"{name}: _(\"{_escape_quote(cdata)}\");")
|
||||
ctx.print(f'{name}: _("{_escape_quote(cdata)}");')
|
||||
else:
|
||||
ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name))
|
||||
|
||||
|
||||
@decompiler("accessibility")
|
||||
def decompile_accessibility(ctx, gir):
|
||||
ctx.print("accessibility {")
|
||||
|
|
|
@ -35,12 +35,14 @@ class Item(BaseTypedAttribute):
|
|||
item = Group(
|
||||
Item,
|
||||
[
|
||||
Optional([
|
||||
UseIdent("name"),
|
||||
":",
|
||||
]),
|
||||
Optional(
|
||||
[
|
||||
UseIdent("name"),
|
||||
":",
|
||||
]
|
||||
),
|
||||
VALUE_HOOKS,
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
@ -67,7 +69,4 @@ class Items(AstNode):
|
|||
matches=new_statement_patterns,
|
||||
)
|
||||
def items_completer(ast_node, match_variables):
|
||||
yield Completion(
|
||||
"items", CompletionItemKind.Snippet,
|
||||
snippet="items [$0]"
|
||||
)
|
||||
yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]")
|
||||
|
|
|
@ -37,6 +37,7 @@ class Filters(AstNode):
|
|||
f"Duplicate {self.tokens['tag_name']} block",
|
||||
check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"],
|
||||
)
|
||||
|
||||
wrapped_validator(self)
|
||||
|
||||
|
||||
|
@ -57,12 +58,12 @@ def create_node(tag_name: str, singular: str):
|
|||
[
|
||||
UseQuoted("name"),
|
||||
UseLiteral("tag_name", singular),
|
||||
]
|
||||
],
|
||||
),
|
||||
",",
|
||||
),
|
||||
"]",
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
@ -77,31 +78,38 @@ suffixes = create_node("suffixes", "suffix")
|
|||
matches=new_statement_patterns,
|
||||
)
|
||||
def file_filter_completer(ast_node, match_variables):
|
||||
yield Completion("mime-types", CompletionItemKind.Snippet, snippet="mime-types [\"$0\"]")
|
||||
yield Completion("patterns", CompletionItemKind.Snippet, snippet="patterns [\"$0\"]")
|
||||
yield Completion("suffixes", CompletionItemKind.Snippet, snippet="suffixes [\"$0\"]")
|
||||
yield Completion(
|
||||
"mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]'
|
||||
)
|
||||
yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]')
|
||||
yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]')
|
||||
|
||||
|
||||
@decompiler("mime-types")
|
||||
def decompile_mime_types(ctx, gir):
|
||||
ctx.print("mime-types [")
|
||||
|
||||
|
||||
@decompiler("mime-type", cdata=True)
|
||||
def decompile_mime_type(ctx, gir, cdata):
|
||||
ctx.print(f'"{cdata}",')
|
||||
|
||||
|
||||
@decompiler("patterns")
|
||||
def decompile_patterns(ctx, gir):
|
||||
ctx.print("patterns [")
|
||||
|
||||
|
||||
@decompiler("pattern", cdata=True)
|
||||
def decompile_pattern(ctx, gir, cdata):
|
||||
ctx.print(f'"{cdata}",')
|
||||
|
||||
|
||||
@decompiler("suffixes")
|
||||
def decompile_suffixes(ctx, gir):
|
||||
ctx.print("suffixes [")
|
||||
|
||||
|
||||
@decompiler("suffix", cdata=True)
|
||||
def decompile_suffix(ctx, gir, cdata):
|
||||
ctx.print(f'"{cdata}",')
|
||||
|
|
|
@ -45,7 +45,7 @@ layout_prop = Group(
|
|||
UseIdent("name"),
|
||||
":",
|
||||
VALUE_HOOKS.expected("a value"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -71,10 +71,7 @@ class Layout(AstNode):
|
|||
matches=new_statement_patterns,
|
||||
)
|
||||
def layout_completer(ast_node, match_variables):
|
||||
yield Completion(
|
||||
"layout", CompletionItemKind.Snippet,
|
||||
snippet="layout {\n $0\n}"
|
||||
)
|
||||
yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}")
|
||||
|
||||
|
||||
@decompiler("layout")
|
||||
|
|
|
@ -56,22 +56,12 @@ menu_contents = Sequence()
|
|||
|
||||
menu_section = Group(
|
||||
Menu,
|
||||
[
|
||||
"section",
|
||||
UseLiteral("tag", "section"),
|
||||
Optional(UseIdent("id")),
|
||||
menu_contents
|
||||
]
|
||||
["section", UseLiteral("tag", "section"), Optional(UseIdent("id")), menu_contents],
|
||||
)
|
||||
|
||||
menu_submenu = Group(
|
||||
Menu,
|
||||
[
|
||||
"submenu",
|
||||
UseLiteral("tag", "submenu"),
|
||||
Optional(UseIdent("id")),
|
||||
menu_contents
|
||||
]
|
||||
["submenu", UseLiteral("tag", "submenu"), Optional(UseIdent("id")), menu_contents],
|
||||
)
|
||||
|
||||
menu_attribute = Group(
|
||||
|
@ -81,7 +71,7 @@ menu_attribute = Group(
|
|||
":",
|
||||
VALUE_HOOKS.expected("a value"),
|
||||
Match(";").expected(),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
menu_item = Group(
|
||||
|
@ -92,7 +82,7 @@ menu_item = Group(
|
|||
Optional(UseIdent("id")),
|
||||
Match("{").expected(),
|
||||
Until(menu_attribute, "}"),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
menu_item_shorthand = Group(
|
||||
|
@ -105,58 +95,60 @@ menu_item_shorthand = Group(
|
|||
MenuAttribute,
|
||||
[UseLiteral("name", "label"), VALUE_HOOKS],
|
||||
),
|
||||
Optional([
|
||||
",",
|
||||
Optional([
|
||||
Group(
|
||||
MenuAttribute,
|
||||
[UseLiteral("name", "action"), VALUE_HOOKS],
|
||||
Optional(
|
||||
[
|
||||
",",
|
||||
Optional(
|
||||
[
|
||||
Group(
|
||||
MenuAttribute,
|
||||
[UseLiteral("name", "action"), VALUE_HOOKS],
|
||||
),
|
||||
Optional(
|
||||
[
|
||||
",",
|
||||
Group(
|
||||
MenuAttribute,
|
||||
[UseLiteral("name", "icon"), VALUE_HOOKS],
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
Optional([
|
||||
",",
|
||||
Group(
|
||||
MenuAttribute,
|
||||
[UseLiteral("name", "icon"), VALUE_HOOKS],
|
||||
),
|
||||
])
|
||||
])
|
||||
]),
|
||||
]
|
||||
),
|
||||
Match(")").expected(),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
menu_contents.children = [
|
||||
Match("{"),
|
||||
Until(AnyOf(
|
||||
menu_section,
|
||||
menu_submenu,
|
||||
menu_item_shorthand,
|
||||
menu_item,
|
||||
menu_attribute,
|
||||
), "}"),
|
||||
Until(
|
||||
AnyOf(
|
||||
menu_section,
|
||||
menu_submenu,
|
||||
menu_item_shorthand,
|
||||
menu_item,
|
||||
menu_attribute,
|
||||
),
|
||||
"}",
|
||||
),
|
||||
]
|
||||
|
||||
menu: Group = Group(
|
||||
Menu,
|
||||
[
|
||||
"menu",
|
||||
UseLiteral("tag", "menu"),
|
||||
Optional(UseIdent("id")),
|
||||
menu_contents
|
||||
],
|
||||
["menu", UseLiteral("tag", "menu"), Optional(UseIdent("id")), menu_contents],
|
||||
)
|
||||
|
||||
from .ui import UI
|
||||
|
||||
|
||||
@completer(
|
||||
applies_in=[UI],
|
||||
matches=new_statement_patterns,
|
||||
)
|
||||
def menu_completer(ast_node, match_variables):
|
||||
yield Completion(
|
||||
"menu", CompletionItemKind.Snippet,
|
||||
snippet="menu {\n $0\n}"
|
||||
)
|
||||
yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}")
|
||||
|
||||
|
||||
@completer(
|
||||
|
@ -165,34 +157,21 @@ def menu_completer(ast_node, match_variables):
|
|||
)
|
||||
def menu_content_completer(ast_node, match_variables):
|
||||
yield Completion(
|
||||
"submenu", CompletionItemKind.Snippet,
|
||||
snippet="submenu {\n $0\n}"
|
||||
"submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}"
|
||||
)
|
||||
yield Completion(
|
||||
"section", CompletionItemKind.Snippet,
|
||||
snippet="section {\n $0\n}"
|
||||
"section", CompletionItemKind.Snippet, snippet="section {\n $0\n}"
|
||||
)
|
||||
yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}")
|
||||
yield Completion(
|
||||
"item", CompletionItemKind.Snippet,
|
||||
snippet="item {\n $0\n}"
|
||||
)
|
||||
yield Completion(
|
||||
"item (shorthand)", CompletionItemKind.Snippet,
|
||||
snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")'
|
||||
"item (shorthand)",
|
||||
CompletionItemKind.Snippet,
|
||||
snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")',
|
||||
)
|
||||
|
||||
yield Completion(
|
||||
"label", CompletionItemKind.Snippet,
|
||||
snippet='label: $0;'
|
||||
)
|
||||
yield Completion(
|
||||
"action", CompletionItemKind.Snippet,
|
||||
snippet='action: "$0";'
|
||||
)
|
||||
yield Completion(
|
||||
"icon", CompletionItemKind.Snippet,
|
||||
snippet='icon: "$0";'
|
||||
)
|
||||
yield Completion("label", CompletionItemKind.Snippet, snippet="label: $0;")
|
||||
yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";')
|
||||
yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";')
|
||||
|
||||
|
||||
@decompiler("menu")
|
||||
|
@ -202,6 +181,7 @@ def decompile_menu(ctx, gir, id=None):
|
|||
else:
|
||||
ctx.print("menu {")
|
||||
|
||||
|
||||
@decompiler("submenu")
|
||||
def decompile_submenu(ctx, gir, id=None):
|
||||
if id:
|
||||
|
@ -209,6 +189,7 @@ def decompile_submenu(ctx, gir, id=None):
|
|||
else:
|
||||
ctx.print("submenu {")
|
||||
|
||||
|
||||
@decompiler("item")
|
||||
def decompile_item(ctx, gir, id=None):
|
||||
if id:
|
||||
|
@ -216,6 +197,7 @@ def decompile_item(ctx, gir, id=None):
|
|||
else:
|
||||
ctx.print("item {")
|
||||
|
||||
|
||||
@decompiler("section")
|
||||
def decompile_section(ctx, gir, id=None):
|
||||
if id:
|
||||
|
|
|
@ -32,7 +32,7 @@ class Widget(AstNode):
|
|||
if object is None:
|
||||
raise CompileError(
|
||||
f"Could not find object with ID {self.tokens['name']}",
|
||||
did_you_mean=(self.tokens['name'], self.root.objects_by_id.keys()),
|
||||
did_you_mean=(self.tokens["name"], self.root.objects_by_id.keys()),
|
||||
)
|
||||
elif object.gir_class and not object.gir_class.assignable_to(type):
|
||||
raise CompileError(
|
||||
|
|
|
@ -55,7 +55,4 @@ class Strings(AstNode):
|
|||
matches=new_statement_patterns,
|
||||
)
|
||||
def strings_completer(ast_node, match_variables):
|
||||
yield Completion(
|
||||
"strings", CompletionItemKind.Snippet,
|
||||
snippet="strings [$0]"
|
||||
)
|
||||
yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]")
|
||||
|
|
|
@ -49,13 +49,14 @@ class Styles(AstNode):
|
|||
matches=new_statement_patterns,
|
||||
)
|
||||
def style_completer(ast_node, match_variables):
|
||||
yield Completion("styles", CompletionItemKind.Keyword, snippet="styles [\"$0\"]")
|
||||
yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]')
|
||||
|
||||
|
||||
@decompiler("style")
|
||||
def decompile_style(ctx, gir):
|
||||
ctx.print(f"styles [")
|
||||
|
||||
|
||||
@decompiler("class")
|
||||
def decompile_style_class(ctx, gir, name):
|
||||
ctx.print(f'"{name}",')
|
||||
|
|
|
@ -26,18 +26,21 @@ from .common import *
|
|||
|
||||
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
|
||||
("Gtk", "Buildable"),
|
||||
("Gio", "ListStore")
|
||||
("Gio", "ListStore"),
|
||||
]
|
||||
|
||||
|
||||
class Child(AstNode):
|
||||
grammar = [
|
||||
Optional([
|
||||
"[",
|
||||
Optional(["internal-child", UseLiteral("internal_child", True)]),
|
||||
UseIdent("child_type").expected("a child type"),
|
||||
Optional(ResponseId),
|
||||
"]",
|
||||
]),
|
||||
Optional(
|
||||
[
|
||||
"[",
|
||||
Optional(["internal-child", UseLiteral("internal_child", True)]),
|
||||
UseIdent("child_type").expected("a child type"),
|
||||
Optional(ResponseId),
|
||||
"]",
|
||||
]
|
||||
),
|
||||
Object,
|
||||
]
|
||||
|
||||
|
@ -53,9 +56,13 @@ class Child(AstNode):
|
|||
if gir_class.assignable_to(parent_type):
|
||||
break
|
||||
else:
|
||||
hints=["only Gio.ListStore or Gtk.Buildable implementors can have children"]
|
||||
hints = [
|
||||
"only Gio.ListStore or Gtk.Buildable implementors can have children"
|
||||
]
|
||||
if "child" in gir_class.properties:
|
||||
hints.append("did you mean to assign this object to the 'child' property?")
|
||||
hints.append(
|
||||
"did you mean to assign this object to the 'child' property?"
|
||||
)
|
||||
raise CompileError(
|
||||
f"{gir_class.full_name} doesn't have children",
|
||||
hints=hints,
|
||||
|
|
|
@ -28,10 +28,12 @@ class Template(Object):
|
|||
grammar = [
|
||||
"template",
|
||||
UseIdent("id").expected("template class name"),
|
||||
Optional([
|
||||
Match(":"),
|
||||
to_parse_node(ClassName).expected("parent class"),
|
||||
]),
|
||||
Optional(
|
||||
[
|
||||
Match(":"),
|
||||
to_parse_node(ClassName).expected("parent class"),
|
||||
]
|
||||
),
|
||||
ObjectContent,
|
||||
]
|
||||
|
||||
|
@ -54,7 +56,9 @@ class Template(Object):
|
|||
|
||||
@validate("id")
|
||||
def unique_in_parent(self):
|
||||
self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",)
|
||||
self.validate_unique_in_parent(
|
||||
f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",
|
||||
)
|
||||
|
||||
|
||||
@decompiler("template")
|
||||
|
|
|
@ -24,8 +24,12 @@ from .common import *
|
|||
|
||||
class GtkDirective(AstNode):
|
||||
grammar = Statement(
|
||||
Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
|
||||
Match("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
|
||||
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"),
|
||||
)
|
||||
|
||||
|
@ -35,7 +39,9 @@ class GtkDirective(AstNode):
|
|||
if version not in ["4.0"]:
|
||||
err = CompileError("Only GTK 4 is supported")
|
||||
if version and version.startswith("4"):
|
||||
err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.")
|
||||
err.hint(
|
||||
"Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'."
|
||||
)
|
||||
else:
|
||||
err.hint("Expected 'using Gtk 4.0;'")
|
||||
raise err
|
||||
|
@ -51,7 +57,6 @@ class GtkDirective(AstNode):
|
|||
hints=e.hints,
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def gir_namespace(self):
|
||||
# validate the GTK version first to make sure the more specific error
|
||||
|
|
|
@ -26,21 +26,13 @@ from .common import *
|
|||
class ResponseId(AstNode):
|
||||
"""Response ID of action widget."""
|
||||
|
||||
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
|
||||
("Gtk", "Dialog"),
|
||||
("Gtk", "InfoBar")
|
||||
]
|
||||
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")]
|
||||
|
||||
grammar = [
|
||||
Keyword("response"),
|
||||
"=",
|
||||
AnyOf(
|
||||
UseIdent("response_id"),
|
||||
UseNumber("response_id")
|
||||
),
|
||||
Optional([
|
||||
Keyword("default"), UseLiteral("is_default", True)
|
||||
])
|
||||
AnyOf(UseIdent("response_id"), UseNumber("response_id")),
|
||||
Optional([Keyword("default"), UseLiteral("is_default", True)]),
|
||||
]
|
||||
|
||||
@validate()
|
||||
|
@ -91,18 +83,15 @@ class ResponseId(AstNode):
|
|||
|
||||
if isinstance(response, int):
|
||||
if response < 0:
|
||||
raise CompileError(
|
||||
"Numeric response type can't be negative")
|
||||
raise CompileError("Numeric response type can't be negative")
|
||||
elif isinstance(response, float):
|
||||
raise CompileError(
|
||||
"Response type must be GtkResponseType member or integer,"
|
||||
" not float"
|
||||
"Response type must be GtkResponseType member or integer," " not float"
|
||||
)
|
||||
else:
|
||||
responses = gir.get_type("ResponseType", "Gtk").members.keys()
|
||||
if response not in responses:
|
||||
raise CompileError(
|
||||
f"Response type \"{response}\" doesn't exist")
|
||||
raise CompileError(f'Response type "{response}" doesn\'t exist')
|
||||
|
||||
@validate("default")
|
||||
def no_multiple_default(self) -> None:
|
||||
|
@ -135,4 +124,3 @@ class ResponseId(AstNode):
|
|||
|
||||
_object: Object = self.parent.children[Object][0]
|
||||
return _object.tokens["id"]
|
||||
|
||||
|
|
|
@ -41,7 +41,9 @@ class TypeName(AstNode):
|
|||
@validate("class_name")
|
||||
def type_exists(self):
|
||||
if not self.tokens["ignore_gir"] and self.gir_ns is not None:
|
||||
self.root.gir.validate_type(self.tokens["class_name"], self.tokens["namespace"])
|
||||
self.root.gir.validate_type(
|
||||
self.tokens["class_name"], self.tokens["namespace"]
|
||||
)
|
||||
|
||||
@validate("namespace")
|
||||
def gir_ns_exists(self):
|
||||
|
@ -56,7 +58,9 @@ class TypeName(AstNode):
|
|||
@property
|
||||
def gir_type(self) -> T.Optional[gir.Class]:
|
||||
if self.tokens["class_name"] and not self.tokens["ignore_gir"]:
|
||||
return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"])
|
||||
return self.root.gir.get_type(
|
||||
self.tokens["class_name"], self.tokens["namespace"]
|
||||
)
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -82,7 +86,9 @@ class ClassName(TypeName):
|
|||
def gir_class_exists(self):
|
||||
if self.gir_type is not None and not isinstance(self.gir_type, Class):
|
||||
if isinstance(self.gir_type, Interface):
|
||||
raise CompileError(f"{self.gir_type.full_name} is an interface, not a class")
|
||||
raise CompileError(
|
||||
f"{self.gir_type.full_name} is an interface, not a class"
|
||||
)
|
||||
else:
|
||||
raise CompileError(f"{self.gir_type.full_name} is not a class")
|
||||
|
||||
|
@ -93,6 +99,5 @@ class ConcreteClassName(ClassName):
|
|||
if isinstance(self.gir_type, Class) and self.gir_type.abstract:
|
||||
raise CompileError(
|
||||
f"{self.gir_type.full_name} can't be instantiated because it's abstract",
|
||||
hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"]
|
||||
hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"],
|
||||
)
|
||||
|
||||
|
|
|
@ -27,16 +27,19 @@ from .common import *
|
|||
|
||||
|
||||
class UI(AstNode):
|
||||
""" The AST node for the entire file """
|
||||
"""The AST node for the entire file"""
|
||||
|
||||
grammar = [
|
||||
GtkDirective,
|
||||
ZeroOrMore(Import),
|
||||
Until(AnyOf(
|
||||
Template,
|
||||
menu,
|
||||
Object,
|
||||
), Eof()),
|
||||
Until(
|
||||
AnyOf(
|
||||
Template,
|
||||
menu,
|
||||
Object,
|
||||
),
|
||||
Eof(),
|
||||
),
|
||||
]
|
||||
|
||||
@property
|
||||
|
@ -61,11 +64,13 @@ class UI(AstNode):
|
|||
|
||||
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 }
|
||||
|
||||
return {
|
||||
obj.tokens["id"]: obj
|
||||
for obj in self.iterate_children_recursive()
|
||||
if obj.tokens["id"] is not None
|
||||
}
|
||||
|
||||
@validate()
|
||||
def gir_errors(self):
|
||||
|
@ -74,7 +79,6 @@ class UI(AstNode):
|
|||
if len(self._gir_errors):
|
||||
raise MultipleErrors(self._gir_errors)
|
||||
|
||||
|
||||
@validate()
|
||||
def unique_ids(self):
|
||||
passed = {}
|
||||
|
@ -84,5 +88,7 @@ class UI(AstNode):
|
|||
|
||||
if obj.tokens["id"] in passed:
|
||||
token = obj.group.tokens["id"]
|
||||
raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end)
|
||||
raise CompileError(
|
||||
f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end
|
||||
)
|
||||
passed[obj.tokens["id"]] = obj
|
||||
|
|
|
@ -84,13 +84,21 @@ class QuotedValue(Value):
|
|||
@validate()
|
||||
def validate_for_type(self):
|
||||
type = self.parent.value_type
|
||||
if isinstance(type, gir.IntType) or isinstance(type, gir.UIntType) or isinstance(type, gir.FloatType):
|
||||
if (
|
||||
isinstance(type, gir.IntType)
|
||||
or isinstance(type, gir.UIntType)
|
||||
or isinstance(type, gir.FloatType)
|
||||
):
|
||||
raise CompileError(f"Cannot convert string to number")
|
||||
|
||||
elif isinstance(type, gir.StringType):
|
||||
pass
|
||||
|
||||
elif isinstance(type, gir.Class) or isinstance(type, gir.Interface) or isinstance(type, gir.Boxed):
|
||||
elif (
|
||||
isinstance(type, gir.Class)
|
||||
or isinstance(type, gir.Interface)
|
||||
or isinstance(type, gir.Boxed)
|
||||
):
|
||||
parseable_types = [
|
||||
"Gdk.Paintable",
|
||||
"Gdk.Texture",
|
||||
|
@ -106,8 +114,12 @@ class QuotedValue(Value):
|
|||
if type.full_name not in parseable_types:
|
||||
hints = []
|
||||
if isinstance(type, gir.TypeType):
|
||||
hints.append(f"use the typeof operator: 'typeof({self.tokens('value')})'")
|
||||
raise CompileError(f"Cannot convert string to {type.full_name}", hints=hints)
|
||||
hints.append(
|
||||
f"use the typeof operator: 'typeof({self.tokens('value')})'"
|
||||
)
|
||||
raise CompileError(
|
||||
f"Cannot convert string to {type.full_name}", hints=hints
|
||||
)
|
||||
|
||||
elif type is not None:
|
||||
raise CompileError(f"Cannot convert string to {type.full_name}")
|
||||
|
@ -127,7 +139,9 @@ class NumberValue(Value):
|
|||
try:
|
||||
int(self.tokens["value"])
|
||||
except:
|
||||
raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer")
|
||||
raise CompileError(
|
||||
f"Cannot convert {self.group.tokens['value']} to integer"
|
||||
)
|
||||
|
||||
elif isinstance(type, gir.UIntType):
|
||||
try:
|
||||
|
@ -135,13 +149,17 @@ class NumberValue(Value):
|
|||
if int(self.tokens["value"]) < 0:
|
||||
raise Exception()
|
||||
except:
|
||||
raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer")
|
||||
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")
|
||||
raise CompileError(
|
||||
f"Cannot convert {self.group.tokens['value']} to float"
|
||||
)
|
||||
|
||||
elif type is not None:
|
||||
raise CompileError(f"Cannot convert number to {type.full_name}")
|
||||
|
@ -164,7 +182,7 @@ class Flag(AstNode):
|
|||
if isinstance(type, gir.Bitfield) and 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()),
|
||||
did_you_mean=(self.tokens["value"], type.members.keys()),
|
||||
)
|
||||
|
||||
|
||||
|
@ -189,14 +207,14 @@ class IdentValue(Value):
|
|||
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()),
|
||||
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"]),
|
||||
did_you_mean=(self.tokens["value"], ["true", "false"]),
|
||||
)
|
||||
|
||||
elif type is not None:
|
||||
|
@ -204,14 +222,13 @@ class IdentValue(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()),
|
||||
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
|
||||
|
@ -223,9 +240,7 @@ class IdentValue(Value):
|
|||
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)
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ def command(json_method):
|
|||
def decorator(func):
|
||||
func._json_method = json_method
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
|
@ -50,8 +51,16 @@ class OpenFile:
|
|||
|
||||
def apply_changes(self, changes):
|
||||
for change in changes:
|
||||
start = utils.pos_to_idx(change["range"]["start"]["line"], change["range"]["start"]["character"], self.text)
|
||||
end = utils.pos_to_idx(change["range"]["end"]["line"], change["range"]["end"]["character"], self.text)
|
||||
start = utils.pos_to_idx(
|
||||
change["range"]["start"]["line"],
|
||||
change["range"]["start"]["character"],
|
||||
self.text,
|
||||
)
|
||||
end = utils.pos_to_idx(
|
||||
change["range"]["end"]["line"],
|
||||
change["range"]["end"]["character"],
|
||||
self.text,
|
||||
)
|
||||
self.text = self.text[:start] + change["text"] + self.text[end:]
|
||||
self._update()
|
||||
|
||||
|
@ -69,16 +78,17 @@ class OpenFile:
|
|||
except CompileError as e:
|
||||
self.diagnostics.append(e)
|
||||
|
||||
|
||||
def calc_semantic_tokens(self) -> T.List[int]:
|
||||
tokens = list(self.ast.get_semantic_tokens())
|
||||
token_lists = [
|
||||
[
|
||||
*utils.idx_to_pos(token.start, self.text), # line and column
|
||||
token.end - token.start, # length
|
||||
*utils.idx_to_pos(token.start, self.text), # line and column
|
||||
token.end - token.start, # length
|
||||
token.type,
|
||||
0, # token modifiers
|
||||
] for token in tokens]
|
||||
0, # token modifiers
|
||||
]
|
||||
for token in tokens
|
||||
]
|
||||
|
||||
# convert line, column numbers to deltas
|
||||
for i, token_list in enumerate(token_lists[1:]):
|
||||
|
@ -125,53 +135,60 @@ class LanguageServer:
|
|||
except Exception as e:
|
||||
printerr(traceback.format_exc())
|
||||
|
||||
|
||||
def _send(self, data):
|
||||
data["jsonrpc"] = "2.0"
|
||||
line = json.dumps(data, separators=(",", ":")) + "\r\n"
|
||||
printerr("output: " + line)
|
||||
sys.stdout.write(f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}")
|
||||
sys.stdout.write(
|
||||
f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
|
||||
def _send_response(self, id, result):
|
||||
self._send({
|
||||
"id": id,
|
||||
"result": result,
|
||||
})
|
||||
self._send(
|
||||
{
|
||||
"id": id,
|
||||
"result": result,
|
||||
}
|
||||
)
|
||||
|
||||
def _send_notification(self, method, params):
|
||||
self._send({
|
||||
"method": method,
|
||||
"params": params,
|
||||
})
|
||||
|
||||
self._send(
|
||||
{
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
)
|
||||
|
||||
@command("initialize")
|
||||
def initialize(self, id, params):
|
||||
from . import main
|
||||
|
||||
self.client_capabilities = params.get("capabilities")
|
||||
self._send_response(id, {
|
||||
"capabilities": {
|
||||
"textDocumentSync": {
|
||||
"openClose": True,
|
||||
"change": TextDocumentSyncKind.Incremental,
|
||||
},
|
||||
"semanticTokensProvider": {
|
||||
"legend": {
|
||||
"tokenTypes": ["enumMember"],
|
||||
self._send_response(
|
||||
id,
|
||||
{
|
||||
"capabilities": {
|
||||
"textDocumentSync": {
|
||||
"openClose": True,
|
||||
"change": TextDocumentSyncKind.Incremental,
|
||||
},
|
||||
"full": True,
|
||||
"semanticTokensProvider": {
|
||||
"legend": {
|
||||
"tokenTypes": ["enumMember"],
|
||||
},
|
||||
"full": True,
|
||||
},
|
||||
"completionProvider": {},
|
||||
"codeActionProvider": {},
|
||||
"hoverProvider": True,
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "Blueprint",
|
||||
"version": main.VERSION,
|
||||
},
|
||||
"completionProvider": {},
|
||||
"codeActionProvider": {},
|
||||
"hoverProvider": True,
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "Blueprint",
|
||||
"version": main.VERSION,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
@command("textDocument/didOpen")
|
||||
def didOpen(self, id, params):
|
||||
|
@ -198,14 +215,23 @@ class LanguageServer:
|
|||
@command("textDocument/hover")
|
||||
def hover(self, id, params):
|
||||
open_file = self._open_files[params["textDocument"]["uri"]]
|
||||
docs = open_file.ast and open_file.ast.get_docs(utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text))
|
||||
docs = open_file.ast and open_file.ast.get_docs(
|
||||
utils.pos_to_idx(
|
||||
params["position"]["line"],
|
||||
params["position"]["character"],
|
||||
open_file.text,
|
||||
)
|
||||
)
|
||||
if docs:
|
||||
self._send_response(id, {
|
||||
"contents": {
|
||||
"kind": "markdown",
|
||||
"value": docs,
|
||||
}
|
||||
})
|
||||
self._send_response(
|
||||
id,
|
||||
{
|
||||
"contents": {
|
||||
"kind": "markdown",
|
||||
"value": docs,
|
||||
}
|
||||
},
|
||||
)
|
||||
else:
|
||||
self._send_response(id, None)
|
||||
|
||||
|
@ -217,40 +243,59 @@ class LanguageServer:
|
|||
self._send_response(id, [])
|
||||
return
|
||||
|
||||
idx = utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text)
|
||||
idx = utils.pos_to_idx(
|
||||
params["position"]["line"], params["position"]["character"], open_file.text
|
||||
)
|
||||
completions = complete(open_file.ast, open_file.tokens, idx)
|
||||
self._send_response(id, [completion.to_json(True) for completion in completions])
|
||||
|
||||
self._send_response(
|
||||
id, [completion.to_json(True) for completion in completions]
|
||||
)
|
||||
|
||||
@command("textDocument/semanticTokens/full")
|
||||
def semantic_tokens(self, id, params):
|
||||
open_file = self._open_files[params["textDocument"]["uri"]]
|
||||
|
||||
self._send_response(id, {
|
||||
"data": open_file.calc_semantic_tokens(),
|
||||
})
|
||||
|
||||
self._send_response(
|
||||
id,
|
||||
{
|
||||
"data": open_file.calc_semantic_tokens(),
|
||||
},
|
||||
)
|
||||
|
||||
@command("textDocument/codeAction")
|
||||
def code_actions(self, id, params):
|
||||
open_file = self._open_files[params["textDocument"]["uri"]]
|
||||
|
||||
range_start = utils.pos_to_idx(params["range"]["start"]["line"], params["range"]["start"]["character"], open_file.text)
|
||||
range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text)
|
||||
range_start = utils.pos_to_idx(
|
||||
params["range"]["start"]["line"],
|
||||
params["range"]["start"]["character"],
|
||||
open_file.text,
|
||||
)
|
||||
range_end = utils.pos_to_idx(
|
||||
params["range"]["end"]["line"],
|
||||
params["range"]["end"]["character"],
|
||||
open_file.text,
|
||||
)
|
||||
|
||||
actions = [
|
||||
{
|
||||
"title": action.title,
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, diagnostic)],
|
||||
"diagnostics": [
|
||||
self._create_diagnostic(open_file.text, open_file.uri, diagnostic)
|
||||
],
|
||||
"edit": {
|
||||
"changes": {
|
||||
open_file.uri: [{
|
||||
"range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text),
|
||||
"newText": action.replace_with
|
||||
}]
|
||||
open_file.uri: [
|
||||
{
|
||||
"range": utils.idxs_to_range(
|
||||
diagnostic.start, diagnostic.end, open_file.text
|
||||
),
|
||||
"newText": action.replace_with,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
for diagnostic in open_file.diagnostics
|
||||
if not (diagnostic.end < range_start or diagnostic.start > range_end)
|
||||
|
@ -259,23 +304,30 @@ class LanguageServer:
|
|||
|
||||
self._send_response(id, actions)
|
||||
|
||||
|
||||
def _send_file_updates(self, open_file: OpenFile):
|
||||
self._send_notification("textDocument/publishDiagnostics", {
|
||||
"uri": open_file.uri,
|
||||
"diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, err) for err in open_file.diagnostics],
|
||||
})
|
||||
self._send_notification(
|
||||
"textDocument/publishDiagnostics",
|
||||
{
|
||||
"uri": open_file.uri,
|
||||
"diagnostics": [
|
||||
self._create_diagnostic(open_file.text, open_file.uri, err)
|
||||
for err in open_file.diagnostics
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
def _create_diagnostic(self, text, uri, err):
|
||||
message = err.message
|
||||
|
||||
for hint in err.hints:
|
||||
message += '\nhint: ' + hint
|
||||
message += "\nhint: " + hint
|
||||
|
||||
result = {
|
||||
"range": utils.idxs_to_range(err.start, err.end, text),
|
||||
"message": message,
|
||||
"severity": DiagnosticSeverity.Warning if isinstance(err, CompileWarning) else DiagnosticSeverity.Error,
|
||||
"severity": DiagnosticSeverity.Warning
|
||||
if isinstance(err, CompileWarning)
|
||||
else DiagnosticSeverity.Error,
|
||||
}
|
||||
|
||||
if len(err.references) > 0:
|
||||
|
@ -285,7 +337,7 @@ class LanguageServer:
|
|||
"uri": uri,
|
||||
"range": utils.idxs_to_range(ref.start, ref.end, text),
|
||||
},
|
||||
"message": ref.message
|
||||
"message": ref.message,
|
||||
}
|
||||
for ref in err.references
|
||||
]
|
||||
|
@ -297,4 +349,3 @@ for name in dir(LanguageServer):
|
|||
item = getattr(LanguageServer, name)
|
||||
if callable(item) and hasattr(item, "_json_method"):
|
||||
LanguageServer.commands[item._json_method] = item
|
||||
|
||||
|
|
|
@ -31,13 +31,16 @@ class TextDocumentSyncKind(enum.IntEnum):
|
|||
Full = 1
|
||||
Incremental = 2
|
||||
|
||||
|
||||
class CompletionItemTag(enum.IntEnum):
|
||||
Deprecated = 1
|
||||
|
||||
|
||||
class InsertTextFormat(enum.IntEnum):
|
||||
PlainText = 1
|
||||
Snippet = 2
|
||||
|
||||
|
||||
class CompletionItemKind(enum.IntEnum):
|
||||
Text = 1
|
||||
Method = 2
|
||||
|
@ -91,12 +94,14 @@ class Completion:
|
|||
"documentation": {
|
||||
"kind": "markdown",
|
||||
"value": self.docs,
|
||||
} if self.docs else None,
|
||||
}
|
||||
if self.docs
|
||||
else None,
|
||||
"deprecated": self.deprecated,
|
||||
"insertText": insert_text,
|
||||
"insertTextFormat": insert_text_format,
|
||||
}
|
||||
return { k: v for k, v in result.items() if v is not None }
|
||||
return {k: v for k, v in result.items() if v is not None}
|
||||
|
||||
|
||||
class SemanticTokenType(enum.IntEnum):
|
||||
|
@ -110,7 +115,6 @@ class DiagnosticSeverity(enum.IntEnum):
|
|||
Hint = 4
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class SemanticToken:
|
||||
start: int
|
||||
|
|
|
@ -30,24 +30,41 @@ from .outputs import XmlOutput
|
|||
VERSION = "uninstalled"
|
||||
LIBDIR = None
|
||||
|
||||
|
||||
class BlueprintApp:
|
||||
def main(self):
|
||||
self.parser = argparse.ArgumentParser()
|
||||
self.subparsers = self.parser.add_subparsers(metavar="command")
|
||||
self.parser.set_defaults(func=self.cmd_help)
|
||||
|
||||
compile = self.add_subcommand("compile", "Compile blueprint files", self.cmd_compile)
|
||||
compile = self.add_subcommand(
|
||||
"compile", "Compile blueprint files", self.cmd_compile
|
||||
)
|
||||
compile.add_argument("--output", dest="output", default="-")
|
||||
compile.add_argument("input", metavar="filename", default=sys.stdin, type=argparse.FileType('r'))
|
||||
compile.add_argument(
|
||||
"input", metavar="filename", default=sys.stdin, type=argparse.FileType("r")
|
||||
)
|
||||
|
||||
batch_compile = self.add_subcommand("batch-compile", "Compile many blueprint files at once", self.cmd_batch_compile)
|
||||
batch_compile = self.add_subcommand(
|
||||
"batch-compile",
|
||||
"Compile many blueprint files at once",
|
||||
self.cmd_batch_compile,
|
||||
)
|
||||
batch_compile.add_argument("output_dir", metavar="output-dir")
|
||||
batch_compile.add_argument("input_dir", metavar="input-dir")
|
||||
batch_compile.add_argument("inputs", nargs="+", metavar="filenames", default=sys.stdin, type=argparse.FileType('r'))
|
||||
batch_compile.add_argument(
|
||||
"inputs",
|
||||
nargs="+",
|
||||
metavar="filenames",
|
||||
default=sys.stdin,
|
||||
type=argparse.FileType("r"),
|
||||
)
|
||||
|
||||
port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port)
|
||||
|
||||
lsp = self.add_subcommand("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp)
|
||||
lsp = self.add_subcommand(
|
||||
"lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp
|
||||
)
|
||||
|
||||
self.add_subcommand("help", "Show this message", self.cmd_help)
|
||||
|
||||
|
@ -65,17 +82,14 @@ class BlueprintApp:
|
|||
except:
|
||||
report_bug()
|
||||
|
||||
|
||||
def add_subcommand(self, name, help, func):
|
||||
parser = self.subparsers.add_parser(name, help=help)
|
||||
parser.set_defaults(func=func)
|
||||
return parser
|
||||
|
||||
|
||||
def cmd_help(self, opts):
|
||||
self.parser.print_help()
|
||||
|
||||
|
||||
def cmd_compile(self, opts):
|
||||
data = opts.input.read()
|
||||
try:
|
||||
|
@ -93,14 +107,15 @@ class BlueprintApp:
|
|||
e.pretty_print(opts.input.name, data)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_batch_compile(self, opts):
|
||||
for file in opts.inputs:
|
||||
data = file.read()
|
||||
|
||||
try:
|
||||
if not os.path.commonpath([file.name, opts.input_dir]):
|
||||
print(f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}")
|
||||
print(
|
||||
f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
xml, warnings = self._compile(data)
|
||||
|
@ -111,9 +126,8 @@ class BlueprintApp:
|
|||
path = os.path.join(
|
||||
opts.output_dir,
|
||||
os.path.relpath(
|
||||
os.path.splitext(file.name)[0] + ".ui",
|
||||
opts.input_dir
|
||||
)
|
||||
os.path.splitext(file.name)[0] + ".ui", opts.input_dir
|
||||
),
|
||||
)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w") as file:
|
||||
|
@ -122,16 +136,13 @@ class BlueprintApp:
|
|||
e.pretty_print(file.name, data)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_lsp(self, opts):
|
||||
langserv = LanguageServer()
|
||||
langserv.run()
|
||||
|
||||
|
||||
def cmd_port(self, opts):
|
||||
interactive_port.run(opts)
|
||||
|
||||
|
||||
def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]:
|
||||
tokens = tokenizer.tokenize(data)
|
||||
ast, errors, warnings = parser.parse(tokens)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from ..language import UI
|
||||
|
||||
|
||||
class OutputFormat:
|
||||
def emit(self, ui: UI) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
from .xml import XmlOutput
|
||||
|
|
|
@ -24,7 +24,13 @@ import typing as T
|
|||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
|
||||
from .errors import assert_true, CompilerBugError, CompileError, CompileWarning, UnexpectedTokenError
|
||||
from .errors import (
|
||||
assert_true,
|
||||
CompilerBugError,
|
||||
CompileError,
|
||||
CompileWarning,
|
||||
UnexpectedTokenError,
|
||||
)
|
||||
from .tokenizer import Token, TokenType
|
||||
|
||||
|
||||
|
@ -32,15 +38,15 @@ SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE]
|
|||
|
||||
|
||||
class ParseResult(Enum):
|
||||
""" Represents the result of parsing. The extra EMPTY result is necessary
|
||||
"""Represents the result of parsing. The extra EMPTY result is necessary
|
||||
to avoid freezing the parser: imagine a ZeroOrMore node containing a node
|
||||
that can match empty. It will repeatedly match empty and never advance
|
||||
the parser. So, ZeroOrMore stops when a failed *or empty* match is
|
||||
made. """
|
||||
made."""
|
||||
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
EMPTY = 2
|
||||
EMPTY = 2
|
||||
|
||||
def matched(self):
|
||||
return self == ParseResult.SUCCESS
|
||||
|
@ -53,10 +59,10 @@ class ParseResult(Enum):
|
|||
|
||||
|
||||
class ParseGroup:
|
||||
""" A matching group. Match groups have an AST type, children grouped by
|
||||
"""A matching group. Match groups have an AST type, children grouped by
|
||||
type, and key=value pairs. At the end of parsing, the match groups will
|
||||
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, start: int):
|
||||
self.ast_type = ast_type
|
||||
|
@ -77,23 +83,27 @@ class ParseGroup:
|
|||
self.tokens[key] = token
|
||||
|
||||
def to_ast(self):
|
||||
""" Creates an AST node from the match group. """
|
||||
"""Creates an AST node from the match group."""
|
||||
children = [child.to_ast() for child in self.children]
|
||||
|
||||
try:
|
||||
return self.ast_type(self, children, self.keys, incomplete=self.incomplete)
|
||||
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."
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
result = str(self.ast_type.__name__)
|
||||
result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n"
|
||||
result += "\n".join([str(child) for children in self.children.values() for child in children])
|
||||
result += "\n".join(
|
||||
[str(child) for children in self.children.values() for child in children]
|
||||
)
|
||||
return result.replace("\n", "\n ")
|
||||
|
||||
|
||||
class ParseContext:
|
||||
""" Contains the state of the parser. """
|
||||
"""Contains the state of the parser."""
|
||||
|
||||
def __init__(self, tokens, index=0):
|
||||
self.tokens = list(tokens)
|
||||
|
@ -110,12 +120,11 @@ class ParseContext:
|
|||
self.errors = []
|
||||
self.warnings = []
|
||||
|
||||
|
||||
def create_child(self):
|
||||
""" Creates a new ParseContext at this context's position. The new
|
||||
"""Creates a new ParseContext at this context's position. The new
|
||||
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. """
|
||||
context will be discarded."""
|
||||
ctx = ParseContext(self.tokens, self.index)
|
||||
ctx.errors = self.errors
|
||||
ctx.warnings = self.warnings
|
||||
|
@ -123,7 +132,7 @@ class ParseContext:
|
|||
return ctx
|
||||
|
||||
def apply_child(self, other):
|
||||
""" Applies a child context to this context. """
|
||||
"""Applies a child context to this context."""
|
||||
|
||||
if other.group is not None:
|
||||
# If the other context had a match group, collect all the matched
|
||||
|
@ -150,43 +159,44 @@ class ParseContext:
|
|||
elif other.last_group:
|
||||
self.last_group = other.last_group
|
||||
|
||||
|
||||
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)
|
||||
self.group = ParseGroup(ast_type, self.tokens[self.index].start)
|
||||
|
||||
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)
|
||||
self.group_keys[key] = (value, token)
|
||||
|
||||
def set_group_incomplete(self):
|
||||
""" Marks the current match group as incomplete (it could not be fully
|
||||
parsed, but the parser recovered). """
|
||||
"""Marks the current match group as incomplete (it could not be fully
|
||||
parsed, but the parser recovered)."""
|
||||
self.group_incomplete = True
|
||||
|
||||
|
||||
def skip(self):
|
||||
""" Skips whitespace and comments. """
|
||||
while self.index < len(self.tokens) and self.tokens[self.index].type in SKIP_TOKENS:
|
||||
"""Skips whitespace and comments."""
|
||||
while (
|
||||
self.index < len(self.tokens)
|
||||
and self.tokens[self.index].type in SKIP_TOKENS
|
||||
):
|
||||
self.index += 1
|
||||
|
||||
def next_token(self) -> Token:
|
||||
""" Advances the token iterator and returns the next token. """
|
||||
"""Advances the token iterator and returns the next token."""
|
||||
self.skip()
|
||||
token = self.tokens[self.index]
|
||||
self.index += 1
|
||||
return token
|
||||
|
||||
def peek_token(self) -> Token:
|
||||
""" Returns the next token without advancing the iterator. """
|
||||
"""Returns the next token without advancing the iterator."""
|
||||
self.skip()
|
||||
token = self.tokens[self.index]
|
||||
return token
|
||||
|
||||
def skip_unexpected_token(self):
|
||||
""" Skips a token and logs an "unexpected token" error. """
|
||||
"""Skips a token and logs an "unexpected token" error."""
|
||||
|
||||
self.skip()
|
||||
start = self.tokens[self.index].start
|
||||
|
@ -194,9 +204,11 @@ class ParseContext:
|
|||
self.skip()
|
||||
end = self.tokens[self.index - 1].end
|
||||
|
||||
if (len(self.errors)
|
||||
and isinstance((err := self.errors[-1]), UnexpectedTokenError)
|
||||
and err.end == start):
|
||||
if (
|
||||
len(self.errors)
|
||||
and isinstance((err := self.errors[-1]), UnexpectedTokenError)
|
||||
and err.end == start
|
||||
):
|
||||
err.end = end
|
||||
else:
|
||||
self.errors.append(UnexpectedTokenError(start, end))
|
||||
|
@ -206,10 +218,10 @@ class ParseContext:
|
|||
|
||||
|
||||
class ParseNode:
|
||||
""" Base class for the nodes in the parser tree. """
|
||||
"""Base class for the nodes in the parser tree."""
|
||||
|
||||
def parse(self, ctx: ParseContext) -> ParseResult:
|
||||
""" Attempts to match the ParseNode at the context's current location. """
|
||||
"""Attempts to match the ParseNode at the context's current location."""
|
||||
start_idx = ctx.index
|
||||
inner_ctx = ctx.create_child()
|
||||
|
||||
|
@ -226,22 +238,22 @@ class ParseNode:
|
|||
raise NotImplementedError()
|
||||
|
||||
def err(self, message):
|
||||
""" Causes this ParseNode to raise an exception if it fails to parse.
|
||||
"""Causes this ParseNode to raise an exception if it fails to parse.
|
||||
This prevents the parser from backtracking, so you should understand
|
||||
what it does and how the parser works before using it. """
|
||||
what it does and how the parser works before using it."""
|
||||
return Err(self, message)
|
||||
|
||||
def expected(self, expect):
|
||||
""" Convenience method for err(). """
|
||||
"""Convenience method for err()."""
|
||||
return self.err("Expected " + expect)
|
||||
|
||||
def warn(self, message):
|
||||
""" Causes this ParseNode to emit a warning if it parses successfully. """
|
||||
"""Causes this ParseNode to emit a warning if it parses successfully."""
|
||||
return Warning(self, message)
|
||||
|
||||
|
||||
class Err(ParseNode):
|
||||
""" ParseNode that emits a compile error if it fails to parse. """
|
||||
"""ParseNode that emits a compile error if it fails to parse."""
|
||||
|
||||
def __init__(self, child, message):
|
||||
self.child = to_parse_node(child)
|
||||
|
@ -260,7 +272,7 @@ class Err(ParseNode):
|
|||
|
||||
|
||||
class Warning(ParseNode):
|
||||
""" ParseNode that emits a compile warning if it parses successfully. """
|
||||
"""ParseNode that emits a compile warning if it parses successfully."""
|
||||
|
||||
def __init__(self, child, message):
|
||||
self.child = to_parse_node(child)
|
||||
|
@ -272,12 +284,14 @@ class Warning(ParseNode):
|
|||
if self.child.parse(ctx).succeeded():
|
||||
start_token = ctx.tokens[start_idx]
|
||||
end_token = ctx.tokens[ctx.index]
|
||||
ctx.warnings.append(CompileWarning(self.message, start_token.start, end_token.end))
|
||||
ctx.warnings.append(
|
||||
CompileWarning(self.message, start_token.start, end_token.end)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class Fail(ParseNode):
|
||||
""" ParseNode that emits a compile error if it parses successfully. """
|
||||
"""ParseNode that emits a compile error if it parses successfully."""
|
||||
|
||||
def __init__(self, child, message):
|
||||
self.child = to_parse_node(child)
|
||||
|
@ -296,7 +310,8 @@ class Fail(ParseNode):
|
|||
|
||||
|
||||
class Group(ParseNode):
|
||||
""" ParseNode that creates a match group. """
|
||||
"""ParseNode that creates a match group."""
|
||||
|
||||
def __init__(self, ast_type, child):
|
||||
self.ast_type = ast_type
|
||||
self.child = to_parse_node(child)
|
||||
|
@ -308,7 +323,8 @@ class Group(ParseNode):
|
|||
|
||||
|
||||
class Sequence(ParseNode):
|
||||
""" ParseNode that attempts to match all of its children in sequence. """
|
||||
"""ParseNode that attempts to match all of its children in sequence."""
|
||||
|
||||
def __init__(self, *children):
|
||||
self.children = [to_parse_node(child) for child in children]
|
||||
|
||||
|
@ -320,8 +336,9 @@ class Sequence(ParseNode):
|
|||
|
||||
|
||||
class Statement(ParseNode):
|
||||
""" ParseNode that attempts to match all of its children in sequence. If any
|
||||
child raises an error, the error will be logged but parsing will continue. """
|
||||
"""ParseNode that attempts to match all of its children in sequence. If any
|
||||
child raises an error, the error will be logged but parsing will continue."""
|
||||
|
||||
def __init__(self, *children):
|
||||
self.children = [to_parse_node(child) for child in children]
|
||||
|
||||
|
@ -344,14 +361,16 @@ class Statement(ParseNode):
|
|||
|
||||
|
||||
class AnyOf(ParseNode):
|
||||
""" ParseNode that attempts to match exactly one of its children. Child
|
||||
nodes are attempted in order. """
|
||||
"""ParseNode that attempts to match exactly one of its children. Child
|
||||
nodes are attempted in order."""
|
||||
|
||||
def __init__(self, *children):
|
||||
self.children = children
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
@children.setter
|
||||
def children(self, children):
|
||||
self._children = [to_parse_node(child) for child in children]
|
||||
|
@ -364,9 +383,10 @@ class AnyOf(ParseNode):
|
|||
|
||||
|
||||
class Until(ParseNode):
|
||||
""" ParseNode that repeats its child until a delimiting token is found. If
|
||||
"""ParseNode that repeats its child until a delimiting token is found. If
|
||||
the child does not match, one token is skipped and the match is attempted
|
||||
again. """
|
||||
again."""
|
||||
|
||||
def __init__(self, child, delimiter):
|
||||
self.child = to_parse_node(child)
|
||||
self.delimiter = to_parse_node(delimiter)
|
||||
|
@ -387,13 +407,13 @@ class Until(ParseNode):
|
|||
|
||||
|
||||
class ZeroOrMore(ParseNode):
|
||||
""" ParseNode that matches its child any number of times (including zero
|
||||
"""ParseNode that matches its child any number of times (including zero
|
||||
times). It cannot fail to parse. If its child raises an exception, one token
|
||||
will be skipped and parsing will continue. """
|
||||
will be skipped and parsing will continue."""
|
||||
|
||||
def __init__(self, child):
|
||||
self.child = to_parse_node(child)
|
||||
|
||||
|
||||
def _parse(self, ctx):
|
||||
while True:
|
||||
try:
|
||||
|
@ -405,8 +425,9 @@ class ZeroOrMore(ParseNode):
|
|||
|
||||
|
||||
class Delimited(ParseNode):
|
||||
""" ParseNode that matches its first child any number of times (including zero
|
||||
times) with its second child in between and optionally at the end. """
|
||||
"""ParseNode that matches its first child any number of times (including zero
|
||||
times) with its second child in between and optionally at the end."""
|
||||
|
||||
def __init__(self, child, delimiter):
|
||||
self.child = to_parse_node(child)
|
||||
self.delimiter = to_parse_node(delimiter)
|
||||
|
@ -418,8 +439,9 @@ class Delimited(ParseNode):
|
|||
|
||||
|
||||
class Optional(ParseNode):
|
||||
""" ParseNode that matches its child zero or one times. It cannot fail to
|
||||
parse. """
|
||||
"""ParseNode that matches its child zero or one times. It cannot fail to
|
||||
parse."""
|
||||
|
||||
def __init__(self, child):
|
||||
self.child = to_parse_node(child)
|
||||
|
||||
|
@ -429,14 +451,16 @@ class Optional(ParseNode):
|
|||
|
||||
|
||||
class Eof(ParseNode):
|
||||
""" ParseNode that matches an EOF token. """
|
||||
"""ParseNode that matches an EOF token."""
|
||||
|
||||
def _parse(self, ctx: ParseContext) -> bool:
|
||||
token = ctx.next_token()
|
||||
return token.type == TokenType.EOF
|
||||
|
||||
|
||||
class Match(ParseNode):
|
||||
""" ParseNode that matches the given literal token. """
|
||||
"""ParseNode that matches the given literal token."""
|
||||
|
||||
def __init__(self, op):
|
||||
self.op = op
|
||||
|
||||
|
@ -445,7 +469,7 @@ class Match(ParseNode):
|
|||
return str(token) == self.op
|
||||
|
||||
def expected(self, expect: T.Optional[str] = None):
|
||||
""" Convenience method for err(). """
|
||||
"""Convenience method for err()."""
|
||||
if expect is None:
|
||||
return self.err(f"Expected '{self.op}'")
|
||||
else:
|
||||
|
@ -453,8 +477,9 @@ class Match(ParseNode):
|
|||
|
||||
|
||||
class UseIdent(ParseNode):
|
||||
""" ParseNode that matches any identifier and sets it in a key=value pair on
|
||||
the containing match group. """
|
||||
"""ParseNode that matches any identifier and sets it in a key=value pair on
|
||||
the containing match group."""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
|
@ -468,8 +493,9 @@ class UseIdent(ParseNode):
|
|||
|
||||
|
||||
class UseNumber(ParseNode):
|
||||
""" ParseNode that matches a number and sets it in a key=value pair on
|
||||
the containing match group. """
|
||||
"""ParseNode that matches a number and sets it in a key=value pair on
|
||||
the containing match group."""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
|
@ -486,8 +512,9 @@ class UseNumber(ParseNode):
|
|||
|
||||
|
||||
class UseNumberText(ParseNode):
|
||||
""" ParseNode that matches a number, but sets its *original text* it in a
|
||||
key=value pair on the containing match group. """
|
||||
"""ParseNode that matches a number, but sets its *original text* it in a
|
||||
key=value pair on the containing match group."""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
|
@ -501,8 +528,9 @@ class UseNumberText(ParseNode):
|
|||
|
||||
|
||||
class UseQuoted(ParseNode):
|
||||
""" ParseNode that matches a quoted string and sets it in a key=value pair
|
||||
on the containing match group. """
|
||||
"""ParseNode that matches a quoted string and sets it in a key=value pair
|
||||
on the containing match group."""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
|
@ -511,19 +539,22 @@ class UseQuoted(ParseNode):
|
|||
if token.type != TokenType.QUOTED:
|
||||
return False
|
||||
|
||||
string = (str(token)[1:-1]
|
||||
string = (
|
||||
str(token)[1:-1]
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\\"", "\"")
|
||||
.replace('\\"', '"')
|
||||
.replace("\\\\", "\\")
|
||||
.replace("\\'", "\'"))
|
||||
.replace("\\'", "'")
|
||||
)
|
||||
ctx.set_group_val(self.key, string, token)
|
||||
return True
|
||||
|
||||
|
||||
class UseLiteral(ParseNode):
|
||||
""" ParseNode that doesn't match anything, but rather sets a static key=value
|
||||
"""ParseNode that doesn't match anything, but rather sets a static key=value
|
||||
pair on the containing group. Useful for, e.g., property and signal flags:
|
||||
`Sequence(Keyword("swapped"), UseLiteral("swapped", True))` """
|
||||
`Sequence(Keyword("swapped"), UseLiteral("swapped", True))`"""
|
||||
|
||||
def __init__(self, key, literal):
|
||||
self.key = key
|
||||
self.literal = literal
|
||||
|
@ -534,8 +565,9 @@ class UseLiteral(ParseNode):
|
|||
|
||||
|
||||
class Keyword(ParseNode):
|
||||
""" Matches the given identifier and sets it as a named token, with the name
|
||||
being the identifier itself. """
|
||||
"""Matches the given identifier and sets it as a named token, with the name
|
||||
being the identifier itself."""
|
||||
|
||||
def __init__(self, kw):
|
||||
self.kw = kw
|
||||
self.set_token = True
|
||||
|
@ -565,12 +597,13 @@ class Infix(ParseNode):
|
|||
|
||||
def __lt__(self, other):
|
||||
return self.binding_power < other.binding_power
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.binding_power == other.binding_power
|
||||
|
||||
|
||||
class Pratt(ParseNode):
|
||||
""" Basic Pratt parser implementation. """
|
||||
"""Basic Pratt parser implementation."""
|
||||
|
||||
def __init__(self, *children):
|
||||
self.children = children
|
||||
|
@ -578,11 +611,14 @@ class Pratt(ParseNode):
|
|||
@property
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
@children.setter
|
||||
def children(self, children):
|
||||
self._children = children
|
||||
self.prefixes = [child for child in children if isinstance(child, Prefix)]
|
||||
self.infixes = sorted([child for child in children if isinstance(child, Infix)], reverse=True)
|
||||
self.infixes = sorted(
|
||||
[child for child in children if isinstance(child, Infix)], reverse=True
|
||||
)
|
||||
|
||||
def _parse(self, ctx: ParseContext) -> bool:
|
||||
for prefix in self.prefixes:
|
||||
|
|
|
@ -25,7 +25,7 @@ from .language import OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI
|
|||
|
||||
|
||||
def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]:
|
||||
""" Parses a list of tokens into an abstract syntax tree. """
|
||||
"""Parses a list of tokens into an abstract syntax tree."""
|
||||
|
||||
ctx = ParseContext(tokens)
|
||||
AnyOf(UI).parse(ctx)
|
||||
|
|
|
@ -26,28 +26,28 @@ from .errors import CompileError
|
|||
|
||||
|
||||
class TokenType(Enum):
|
||||
EOF = 0
|
||||
IDENT = 1
|
||||
QUOTED = 2
|
||||
NUMBER = 3
|
||||
OP = 4
|
||||
WHITESPACE = 5
|
||||
COMMENT = 6
|
||||
PUNCTUATION = 7
|
||||
EOF = 0
|
||||
IDENT = 1
|
||||
QUOTED = 2
|
||||
NUMBER = 3
|
||||
OP = 4
|
||||
WHITESPACE = 5
|
||||
COMMENT = 6
|
||||
PUNCTUATION = 7
|
||||
|
||||
|
||||
_tokens = [
|
||||
(TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"),
|
||||
(TokenType.QUOTED, r'"(\\"|[^"\n])*"'),
|
||||
(TokenType.QUOTED, r"'(\\'|[^'\n])*'"),
|
||||
(TokenType.NUMBER, r"0x[A-Za-z0-9_]+"),
|
||||
(TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"),
|
||||
(TokenType.NUMBER, r"[-+]?\.[\d_]+"),
|
||||
(TokenType.WHITESPACE, r"\s+"),
|
||||
(TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"),
|
||||
(TokenType.COMMENT, r"\/\/[^\n]*"),
|
||||
(TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"),
|
||||
(TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"),
|
||||
(TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"),
|
||||
(TokenType.QUOTED, r'"(\\"|[^"\n])*"'),
|
||||
(TokenType.QUOTED, r"'(\\'|[^'\n])*'"),
|
||||
(TokenType.NUMBER, r"0x[A-Za-z0-9_]+"),
|
||||
(TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"),
|
||||
(TokenType.NUMBER, r"[-+]?\.[\d_]+"),
|
||||
(TokenType.WHITESPACE, r"\s+"),
|
||||
(TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"),
|
||||
(TokenType.COMMENT, r"\/\/[^\n]*"),
|
||||
(TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"),
|
||||
(TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"),
|
||||
]
|
||||
_TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens]
|
||||
|
||||
|
@ -60,7 +60,7 @@ class Token:
|
|||
self.string = string
|
||||
|
||||
def __str__(self):
|
||||
return self.string[self.start:self.end]
|
||||
return self.string[self.start : self.end]
|
||||
|
||||
def get_number(self):
|
||||
if self.type != TokenType.NUMBER:
|
||||
|
@ -73,7 +73,9 @@ class Token:
|
|||
else:
|
||||
return float(string.replace("_", ""))
|
||||
except:
|
||||
raise CompileError(f"{str(self)} is not a valid number literal", self.start, self.end)
|
||||
raise CompileError(
|
||||
f"{str(self)} is not a valid number literal", self.start, self.end
|
||||
)
|
||||
|
||||
|
||||
def _tokenize(ui_ml: str):
|
||||
|
@ -90,7 +92,9 @@ def _tokenize(ui_ml: str):
|
|||
break
|
||||
|
||||
if not matched:
|
||||
raise CompileError("Could not determine what kind of syntax is meant here", i, i)
|
||||
raise CompileError(
|
||||
"Could not determine what kind of syntax is meant here", i, i
|
||||
)
|
||||
|
||||
yield Token(TokenType.EOF, i, i, ui_ml)
|
||||
|
||||
|
|
|
@ -241,7 +241,9 @@ class Typelib:
|
|||
return self._typelib_file[loc:end].decode("utf-8")
|
||||
|
||||
def _int(self, size, signed):
|
||||
return int.from_bytes(self._typelib_file[self._offset:self._offset + size], sys.byteorder)
|
||||
return int.from_bytes(
|
||||
self._typelib_file[self._offset : self._offset + size], sys.byteorder
|
||||
)
|
||||
|
||||
|
||||
class TypelibHeader(Typelib):
|
||||
|
|
|
@ -21,15 +21,15 @@ import typing as T
|
|||
|
||||
|
||||
class Colors:
|
||||
RED = '\033[91m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[33m'
|
||||
FAINT = '\033[2m'
|
||||
BOLD = '\033[1m'
|
||||
BLUE = '\033[34m'
|
||||
UNDERLINE = '\033[4m'
|
||||
NO_UNDERLINE = '\033[24m'
|
||||
CLEAR = '\033[0m'
|
||||
RED = "\033[91m"
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[33m"
|
||||
FAINT = "\033[2m"
|
||||
BOLD = "\033[1m"
|
||||
BLUE = "\033[34m"
|
||||
UNDERLINE = "\033[4m"
|
||||
NO_UNDERLINE = "\033[24m"
|
||||
CLEAR = "\033[0m"
|
||||
|
||||
|
||||
def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
|
||||
|
@ -56,12 +56,16 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
|
|||
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)
|
||||
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]
|
||||
return distances[m - 1][n - 1]
|
||||
|
||||
distances = [(option, levenshtein(word, option)) for option in options]
|
||||
closest = min(distances, key=lambda item:item[1])
|
||||
closest = min(distances, key=lambda item: item[1])
|
||||
if closest[1] <= 5:
|
||||
return closest[0]
|
||||
return None
|
||||
|
@ -75,10 +79,12 @@ def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]:
|
|||
col_num = len(sp[-1])
|
||||
return (line_num - 1, col_num)
|
||||
|
||||
|
||||
def pos_to_idx(line: int, col: int, text: str) -> int:
|
||||
lines = text.splitlines(keepends=True)
|
||||
return sum([len(line) for line in lines[:line]]) + col
|
||||
|
||||
|
||||
def idxs_to_range(start: int, end: int, text: str):
|
||||
start_l, start_c = idx_to_pos(start, text)
|
||||
end_l, end_c = idx_to_pos(end, text)
|
||||
|
|
|
@ -25,11 +25,24 @@ from xml import sax
|
|||
|
||||
|
||||
# To speed up parsing, we ignore all tags except these
|
||||
PARSE_GIR = set([
|
||||
"repository", "namespace", "class", "interface", "property", "glib:signal",
|
||||
"include", "implements", "type", "parameter", "parameters", "enumeration",
|
||||
"member", "bitfield",
|
||||
])
|
||||
PARSE_GIR = set(
|
||||
[
|
||||
"repository",
|
||||
"namespace",
|
||||
"class",
|
||||
"interface",
|
||||
"property",
|
||||
"glib:signal",
|
||||
"include",
|
||||
"implements",
|
||||
"type",
|
||||
"parameter",
|
||||
"parameters",
|
||||
"enumeration",
|
||||
"member",
|
||||
"bitfield",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class Element:
|
||||
|
@ -41,14 +54,10 @@ class Element:
|
|||
|
||||
@cached_property
|
||||
def cdata(self):
|
||||
return ''.join(self.cdata_chunks)
|
||||
return "".join(self.cdata_chunks)
|
||||
|
||||
def get_elements(self, name) -> T.List["Element"]:
|
||||
return [
|
||||
child
|
||||
for child in self.children
|
||||
if child.tag == name
|
||||
]
|
||||
return [child for child in self.children if child.tag == name]
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.attrs.get(key)
|
||||
|
|
|
@ -7,10 +7,16 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|||
|
||||
from blueprintcompiler import tokenizer, parser, decompiler, gir
|
||||
from blueprintcompiler.completions import complete
|
||||
from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError, CompilerBugError
|
||||
from blueprintcompiler.errors import (
|
||||
PrintableError,
|
||||
MultipleErrors,
|
||||
CompileError,
|
||||
CompilerBugError,
|
||||
)
|
||||
from blueprintcompiler.tokenizer import Token, TokenType, tokenize
|
||||
from blueprintcompiler import utils
|
||||
|
||||
|
||||
@PythonFuzz
|
||||
def fuzz(buf):
|
||||
try:
|
||||
|
@ -29,6 +35,7 @@ def fuzz(buf):
|
|||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Make sure Gtk 4.0 is accessible, otherwise every test will fail on that
|
||||
# and nothing interesting will be tested
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
|
||||
import difflib # I love Python
|
||||
import difflib # I love Python
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
import unittest
|
||||
|
@ -59,23 +59,26 @@ class TestSamples(unittest.TestCase):
|
|||
|
||||
xml = XmlOutput()
|
||||
actual = xml.emit(ast)
|
||||
if actual.strip() != expected.strip(): # pragma: no cover
|
||||
if actual.strip() != expected.strip(): # pragma: no cover
|
||||
diff = difflib.unified_diff(expected.splitlines(), actual.splitlines())
|
||||
print("\n".join(diff))
|
||||
raise AssertionError()
|
||||
|
||||
self.assert_docs_dont_crash(blueprint, ast)
|
||||
self.assert_completions_dont_crash(blueprint, ast, tokens)
|
||||
except PrintableError as e: # pragma: no cover
|
||||
except PrintableError as e: # pragma: no cover
|
||||
e.pretty_print(name + ".blp", blueprint)
|
||||
raise AssertionError()
|
||||
|
||||
|
||||
def assert_sample_error(self, name):
|
||||
try:
|
||||
with open((Path(__file__).parent / f"sample_errors/{name}.blp").resolve()) as f:
|
||||
with open(
|
||||
(Path(__file__).parent / f"sample_errors/{name}.blp").resolve()
|
||||
) as f:
|
||||
blueprint = f.read()
|
||||
with open((Path(__file__).parent / f"sample_errors/{name}.err").resolve()) as f:
|
||||
with open(
|
||||
(Path(__file__).parent / f"sample_errors/{name}.err").resolve()
|
||||
) as f:
|
||||
expected = f.read()
|
||||
|
||||
tokens = tokenizer.tokenize(blueprint)
|
||||
|
@ -91,6 +94,7 @@ class TestSamples(unittest.TestCase):
|
|||
if len(warnings):
|
||||
raise MultipleErrors(warnings)
|
||||
except PrintableError as e:
|
||||
|
||||
def error_str(error):
|
||||
line, col = utils.idx_to_pos(error.start + 1, blueprint)
|
||||
len = error.end - error.start
|
||||
|
@ -100,17 +104,16 @@ class TestSamples(unittest.TestCase):
|
|||
actual = error_str(e)
|
||||
elif isinstance(e, MultipleErrors):
|
||||
actual = "\n".join([error_str(error) for error in e.errors])
|
||||
else: # pragma: no cover
|
||||
else: # pragma: no cover
|
||||
raise AssertionError()
|
||||
|
||||
if actual.strip() != expected.strip(): # pragma: no cover
|
||||
if actual.strip() != expected.strip(): # pragma: no cover
|
||||
diff = difflib.unified_diff(expected.splitlines(), actual.splitlines())
|
||||
print("\n".join(diff))
|
||||
raise AssertionError()
|
||||
else: # pragma: no cover
|
||||
else: # pragma: no cover
|
||||
raise AssertionError("Expected a compiler error, but none was emitted")
|
||||
|
||||
|
||||
def assert_decompile(self, name):
|
||||
try:
|
||||
with open((Path(__file__).parent / f"samples/{name}.blp").resolve()) as f:
|
||||
|
@ -121,15 +124,14 @@ class TestSamples(unittest.TestCase):
|
|||
|
||||
actual = decompiler.decompile(ui_path)
|
||||
|
||||
if actual.strip() != expected.strip(): # pragma: no cover
|
||||
if actual.strip() != expected.strip(): # pragma: no cover
|
||||
diff = difflib.unified_diff(expected.splitlines(), actual.splitlines())
|
||||
print("\n".join(diff))
|
||||
raise AssertionError()
|
||||
except PrintableError as e: # pragma: no cover
|
||||
except PrintableError as e: # pragma: no cover
|
||||
e.pretty_print(name + ".blp", blueprint)
|
||||
raise AssertionError()
|
||||
|
||||
|
||||
def test_samples(self):
|
||||
self.assert_sample("accessibility")
|
||||
self.assert_sample("action_widgets")
|
||||
|
@ -161,7 +163,6 @@ class TestSamples(unittest.TestCase):
|
|||
self.assert_sample("unchecked_class")
|
||||
self.assert_sample("using")
|
||||
|
||||
|
||||
def test_sample_errors(self):
|
||||
self.assert_sample_error("a11y_in_non_widget")
|
||||
self.assert_sample_error("a11y_prop_dne")
|
||||
|
@ -209,7 +210,6 @@ class TestSamples(unittest.TestCase):
|
|||
self.assert_sample_error("using_invalid_namespace")
|
||||
self.assert_sample_error("widgets_in_non_size_group")
|
||||
|
||||
|
||||
def test_decompiler(self):
|
||||
self.assert_decompile("accessibility_dec")
|
||||
self.assert_decompile("binding")
|
||||
|
|
|
@ -32,47 +32,57 @@ class TestTokenizer(unittest.TestCase):
|
|||
for token, (type, token_str) in zip(tokens, expect):
|
||||
self.assertEqual(token.type, type)
|
||||
self.assertEqual(str(token), token_str)
|
||||
except PrintableError as e: # pragma: no cover
|
||||
except PrintableError as e: # pragma: no cover
|
||||
e.pretty_print("<test input>", string)
|
||||
raise e
|
||||
|
||||
|
||||
def test_basic(self):
|
||||
self.assert_tokenize("ident(){}; \n <<+>>*/=", [
|
||||
(TokenType.IDENT, "ident"),
|
||||
(TokenType.PUNCTUATION, "("),
|
||||
(TokenType.PUNCTUATION, ")"),
|
||||
(TokenType.PUNCTUATION, "{"),
|
||||
(TokenType.PUNCTUATION, "}"),
|
||||
(TokenType.PUNCTUATION, ";"),
|
||||
(TokenType.WHITESPACE, " \n "),
|
||||
(TokenType.OP, "<<"),
|
||||
(TokenType.OP, "+"),
|
||||
(TokenType.OP, ">>"),
|
||||
(TokenType.OP, "*"),
|
||||
(TokenType.OP, "/"),
|
||||
(TokenType.OP, "="),
|
||||
(TokenType.EOF, ""),
|
||||
])
|
||||
self.assert_tokenize(
|
||||
"ident(){}; \n <<+>>*/=",
|
||||
[
|
||||
(TokenType.IDENT, "ident"),
|
||||
(TokenType.PUNCTUATION, "("),
|
||||
(TokenType.PUNCTUATION, ")"),
|
||||
(TokenType.PUNCTUATION, "{"),
|
||||
(TokenType.PUNCTUATION, "}"),
|
||||
(TokenType.PUNCTUATION, ";"),
|
||||
(TokenType.WHITESPACE, " \n "),
|
||||
(TokenType.OP, "<<"),
|
||||
(TokenType.OP, "+"),
|
||||
(TokenType.OP, ">>"),
|
||||
(TokenType.OP, "*"),
|
||||
(TokenType.OP, "/"),
|
||||
(TokenType.OP, "="),
|
||||
(TokenType.EOF, ""),
|
||||
],
|
||||
)
|
||||
|
||||
def test_quotes(self):
|
||||
self.assert_tokenize(r'"this is a \n string""this is \\another \"string\""', [
|
||||
(TokenType.QUOTED, r'"this is a \n string"'),
|
||||
(TokenType.QUOTED, r'"this is \\another \"string\""'),
|
||||
(TokenType.EOF, ""),
|
||||
])
|
||||
self.assert_tokenize(
|
||||
r'"this is a \n string""this is \\another \"string\""',
|
||||
[
|
||||
(TokenType.QUOTED, r'"this is a \n string"'),
|
||||
(TokenType.QUOTED, r'"this is \\another \"string\""'),
|
||||
(TokenType.EOF, ""),
|
||||
],
|
||||
)
|
||||
|
||||
def test_comments(self):
|
||||
self.assert_tokenize('/* \n \\n COMMENT /* */', [
|
||||
(TokenType.COMMENT, '/* \n \\n COMMENT /* */'),
|
||||
(TokenType.EOF, ""),
|
||||
])
|
||||
self.assert_tokenize('line // comment\nline', [
|
||||
(TokenType.IDENT, 'line'),
|
||||
(TokenType.WHITESPACE, ' '),
|
||||
(TokenType.COMMENT, '// comment'),
|
||||
(TokenType.WHITESPACE, '\n'),
|
||||
(TokenType.IDENT, 'line'),
|
||||
(TokenType.EOF, ""),
|
||||
])
|
||||
|
||||
self.assert_tokenize(
|
||||
"/* \n \\n COMMENT /* */",
|
||||
[
|
||||
(TokenType.COMMENT, "/* \n \\n COMMENT /* */"),
|
||||
(TokenType.EOF, ""),
|
||||
],
|
||||
)
|
||||
self.assert_tokenize(
|
||||
"line // comment\nline",
|
||||
[
|
||||
(TokenType.IDENT, "line"),
|
||||
(TokenType.WHITESPACE, " "),
|
||||
(TokenType.COMMENT, "// comment"),
|
||||
(TokenType.WHITESPACE, "\n"),
|
||||
(TokenType.IDENT, "line"),
|
||||
(TokenType.EOF, ""),
|
||||
],
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue