blueprint-compiler/blueprintcompiler/decompiler.py
2024-06-29 13:32:34 +00:00

346 lines
9.8 KiB
Python

# decompiler.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import re
import typing as T
from dataclasses import dataclass
from enum import Enum
from . import formatter
from .gir import *
from .utils import Colors, escape_quote
from .xml_reader import Element, parse, parse_string
__all__ = ["decompile"]
_DECOMPILERS: T.Dict = {}
_CLOSING = {
"{": "}",
"[": "]",
}
_NAMESPACES = [
("GLib", "2.0"),
("GObject", "2.0"),
("Gio", "2.0"),
("Adw", "1"),
]
class LineType(Enum):
NONE = 1
STMT = 2
BLOCK_START = 3
BLOCK_END = 4
class DecompileCtx:
def __init__(self) -> None:
self._result: str = ""
self.gir = GirContext()
self._indent: int = 0
self._blocks_need_end: T.List[str] = []
self._last_line_type: LineType = LineType.NONE
self.template_class: T.Optional[str] = None
self.gir.add_namespace(get_namespace("Gtk", "4.0"))
@property
def result(self) -> str:
imports = "\n".join(
[
f"using {ns} {namespace.version};"
for ns, namespace in self.gir.namespaces.items()
]
)
full_string = imports + "\n" + self._result
formatted_string = formatter.format(full_string)
return formatted_string
def type_by_cname(self, cname: str) -> T.Optional[GirType]:
if type := self.gir.get_type_by_cname(cname):
return type
for ns, version in _NAMESPACES:
try:
namespace = get_namespace(ns, version)
if type := namespace.get_type_by_cname(cname):
self.gir.add_namespace(namespace)
return type
except:
pass
return None
def start_block(self) -> None:
self._blocks_need_end.append("")
def end_block(self) -> None:
if close := self._blocks_need_end.pop():
self.print(close)
def end_block_with(self, text: str) -> None:
self._blocks_need_end[-1] = text
def print(self, line: str, newline: bool = True) -> None:
self._result += line
if line.endswith("{") or line.endswith("["):
if len(self._blocks_need_end):
self._blocks_need_end[-1] = _CLOSING[line[-1]]
def print_attribute(self, name: str, value: str, type: GirType) -> None:
def get_enum_name(value):
for member in type.members.values():
if (
member.nick == value
or member.c_ident == value
or str(member.value) == value
):
return member.name
return value.replace("-", "_")
if type is None:
self.print(f"{name}: {escape_quote(value)};")
elif type.assignable_to(FloatType()):
self.print(f"{name}: {value};")
elif type.assignable_to(BoolType()):
val = truthy(value)
self.print(f"{name}: {'true' if val else 'false'};")
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")
)
):
self.print(f"{name}: {escape_quote(value)};")
elif value == self.template_class:
self.print(f"{name}: template;")
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("|")]
self.print(f"{name}: {' | '.join(flags)};")
elif isinstance(type, Enumeration):
self.print(f"{name}: {get_enum_name(value)};")
else:
self.print(f"{name}: {escape_quote(value)};")
def _decompile_element(
ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element
) -> None:
try:
decompiler = _DECOMPILERS.get(xml.tag)
if decompiler is None:
raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>")
args: T.Dict[str, T.Optional[str]] = {
canon(name): value for name, value in xml.attrs.items()
}
if decompiler._cdata:
if len(xml.children):
args["cdata"] = None
else:
args["cdata"] = xml.cdata
ctx.start_block()
gir = decompiler(ctx, gir, **args)
for child in xml.children:
_decompile_element(ctx, gir, child)
ctx.end_block()
except UnsupportedError as e:
raise e
except TypeError as e:
raise UnsupportedError(tag=xml.tag)
def decompile(data: str) -> str:
ctx = DecompileCtx()
xml = parse(data)
_decompile_element(ctx, None, xml)
return ctx.result
def decompile_string(data):
ctx = DecompileCtx()
xml = parse_string(data)
_decompile_element(ctx, None, xml)
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) -> str:
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]:
if isinstance(gir, GirContext):
return gir.get_type_by_cname(cname)
else:
return gir.get_containing(Repository).get_type_by_cname(cname)
def decompiler(tag, cdata=False):
def decorator(func):
func._cdata = cdata
_DECOMPILERS[tag] = func
return func
return decorator
@decompiler("interface")
def decompile_interface(ctx, gir):
return gir
@decompiler("requires")
def decompile_requires(ctx, gir, lib=None, version=None):
return gir
@decompiler("placeholder")
def decompile_placeholder(ctx, gir):
pass
def decompile_translatable(
string: str,
translatable: T.Optional[str],
context: T.Optional[str],
comments: T.Optional[str],
) -> T.Tuple[T.Optional[str], str]:
if translatable is not None and truthy(translatable):
if comments is not None:
comments = comments.replace("/*", " ").replace("*/", " ")
comments = f"/* Translators: {comments} */"
if context is not None:
return comments, f"C_({escape_quote(context)}, {escape_quote(string)})"
else:
return comments, f"_({escape_quote(string)})"
else:
return comments, f"{escape_quote(string)}"
@decompiler("property", cdata=True)
def decompile_property(
ctx: DecompileCtx,
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} */")
if cdata is None:
ctx.print(f"{name}: ", False)
ctx.end_block_with(";")
elif bind_source:
flags = ""
bind_flags = bind_flags or []
if "sync-create" not in bind_flags:
flags += " no-sync-create"
if "invert-boolean" in bind_flags:
flags += " inverted"
if "bidirectional" in bind_flags:
flags += " bidirectional"
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
elif truthy(translatable):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
)
if comments is not None:
ctx.print(comments)
ctx.print(f"{name}: {translatable};")
elif gir is None or gir.properties.get(name) is None:
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,
)
@decompiler("attributes")
def decompile_attributes(ctx, gir):
ctx.print("attributes {")
@dataclass
class UnsupportedError(Exception):
message: str = "unsupported feature"
tag: T.Optional[str] = None
def print(self, filename: str):
print(f"\n{Colors.RED}{Colors.BOLD}error: {self.message}{Colors.CLEAR}")
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"""
)