# gir.py # # Copyright 2021 James Westman # # 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 . # # SPDX-License-Identifier: LGPL-3.0-or-later from functools import cached_property import typing as T import os, sys import gi # type: ignore gi.require_version("GIRepository", "2.0") from gi.repository import GIRepository # type: ignore from .errors import CompileError, CompilerBugError from . import typelib, xml_reader _namespace_cache = {} _xml_cache = {} def get_namespace(namespace, version): search_paths = GIRepository.Repository.get_search_path() filename = f"{namespace}-{version}.typelib" if filename not in _namespace_cache: for search_path in search_paths: path = os.path.join(search_path, filename) if os.path.exists(path) and os.path.isfile(path): tl = typelib.load_typelib(path) repository = Repository(tl) _namespace_cache[filename] = repository.namespace break if filename not in _namespace_cache: raise CompileError( f"Namespace {namespace}-{version} could not be found", hints=["search path: " + os.pathsep.join(search_paths)], ) return _namespace_cache[filename] 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)] filename = f"{namespace}-{version}.gir" if filename not in _xml_cache: for search_path in search_paths: path = os.path.join(search_path, filename) if os.path.exists(path) and os.path.isfile(path): _xml_cache[filename] = xml_reader.parse(path) break if filename not in _xml_cache: raise CompileError( f"GObject introspection file '{namespace}-{version}.gir' could not be found", hints=["search path: " + os.pathsep.join(search_paths)], ) return _xml_cache[filename] class GirType: @property def doc(self): return None def assignable_to(self, other) -> bool: raise NotImplementedError() @property def full_name(self) -> str: raise NotImplementedError() class BasicType(GirType): name: str = "unknown type" @property def full_name(self) -> str: return self.name class BoolType(BasicType): name = "bool" glib_type_name = "gboolean" def assignable_to(self, other) -> bool: return isinstance(other, BoolType) class IntType(BasicType): name = "int" glib_type_name = "gint" def assignable_to(self, other) -> bool: return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) class UIntType(BasicType): name = "uint" glib_type_name = "guint" def assignable_to(self, other) -> bool: return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) class FloatType(BasicType): name = "float" glib_type_name = "gfloat" def assignable_to(self, other) -> bool: return isinstance(other, FloatType) class StringType(BasicType): name = "string" glib_type_name = "gchararray" def assignable_to(self, other) -> bool: return isinstance(other, StringType) _BASIC_TYPES = { "bool": BoolType, "gboolean": BoolType, "int": IntType, "gint": IntType, "gint64": IntType, "guint": UIntType, "guint64": UIntType, "gfloat": FloatType, "gdouble": FloatType, "float": FloatType, "double": FloatType, "utf8": StringType, } class GirNode: def __init__(self, container, tl): self.container = container self.tl = tl def get_containing(self, container_type): if self.container is None: return None elif isinstance(self.container, container_type): return self.container else: return self.container.get_containing(container_type) @cached_property def xml(self): for el in self.container.xml.children: if el.attrs.get("name") == self.name: return el @cached_property def glib_type_name(self): return self.tl.OBJ_GTYPE_NAME @cached_property def full_name(self): if self.container is None: return self.name else: return f"{self.container.name}.{self.name}" @cached_property def name(self) -> str: return self.tl.BLOB_NAME @cached_property def cname(self) -> str: return self.tl.OBJ_GTYPE_NAME @cached_property def available_in(self) -> str: return self.xml.get("version") @cached_property def doc(self) -> T.Optional[str]: sections = [] if self.signature: sections.append("```\n" + self.signature + "\n```") try: el = self.xml.get_elements("doc") if len(el) == 1: sections.append(el[0].cdata.strip()) except: # Not a huge deal, but if you want docs in the language server you # should ensure .gir files are installed pass return "\n\n---\n\n".join(sections) @property def signature(self) -> T.Optional[str]: return None @property def type_name(self): return self.type.name @property def type(self): return self.get_containing(Namespace).lookup_type(self.type_name) class Property(GirNode): def __init__(self, klass, tl: typelib.Typelib): super().__init__(klass, tl) @cached_property def name(self): return self.tl.PROP_NAME @cached_property def type(self): return self.get_containing(Repository)._resolve_type_id(self.tl.PROP_TYPE) @cached_property def signature(self): return f"{self.type_name} {self.container.name}.{self.name}" @property def writable(self): return self.tl.PROP_WRITABLE == 1 @property def construct_only(self): return self.tl.PROP_CONSTRUCT_ONLY == 1 class Parameter(GirNode): def __init__(self, container: GirNode, tl: typelib.Typelib): super().__init__(container, tl) class Signal(GirNode): def __init__(self, klass, tl: typelib.Typelib): super().__init__(klass, tl) # if parameters := xml.get_elements('parameters'): # self.params = [Parameter(self, child) for child in parameters[0].get_elements('parameter')] # else: # self.params = [] @property def signature(self): # TODO: fix # args = ", ".join([f"{p.type_name} {p.name}" for p in self.params]) args = "" return f"signal {self.container.name}.{self.name} ({args})" class Interface(GirNode, GirType): def __init__(self, ns, tl: typelib.Typelib): super().__init__(ns, tl) @cached_property def properties(self): n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE offset += (n_prerequisites + n_prerequisites % 2) * 2 n_properties = self.tl.INTERFACE_N_PROPERTIES property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE result = {} for i in range(n_properties): property = Property(self, self.tl[offset + i * property_size]) result[property.name] = property return result @cached_property def signals(self): 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_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE n_signals = self.tl.INTERFACE_N_SIGNALS property_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE result = {} for i in range(n_signals): signal = Signal(self, self.tl[offset + i * property_size]) result[signal.name] = signal return result @cached_property def prerequisites(self): n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES result = [] for i in range(n_prerequisites): entry = self.tl.INTERFACE_PREREQUISITES[i * 2].AS_DIR_ENTRY result.append(self.get_containing(Repository)._resolve_dir_entry(entry)) return result def assignable_to(self, other) -> bool: if self == other: return True for pre in self.prerequisites: if pre.assignable_to(other): return True return False class Class(GirNode, GirType): def __init__(self, ns, tl: typelib.Typelib): super().__init__(ns, tl) @property def abstract(self): return self.tl.OBJ_ABSTRACT == 1 @cached_property def implements(self): n_interfaces = self.tl.OBJ_N_INTERFACES result = [] for i in range(n_interfaces): entry = self.tl[self.tl.header.HEADER_OBJECT_BLOB_SIZE + i * 2].AS_DIR_ENTRY result.append(self.get_containing(Repository)._resolve_dir_entry(entry)) return result @cached_property def own_properties(self): n_interfaces = self.tl.OBJ_N_INTERFACES 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 n_properties = self.tl.OBJ_N_PROPERTIES property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE result = {} for i in range(n_properties): property = Property(self, self.tl[offset + i * property_size]) result[property.name] = property return result @cached_property def own_signals(self): n_interfaces = self.tl.OBJ_N_INTERFACES 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_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 signal_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE result = {} for i in range(n_signals): signal = Signal(self, self.tl[offset][i * signal_size]) result[signal.name] = signal return result @cached_property def parent(self): if entry := self.tl.OBJ_PARENT: return self.get_containing(Repository)._resolve_dir_entry(entry) else: return None @cached_property def signature(self): result = f"class {self.container.name}.{self.name}" 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]) return result @cached_property def properties(self): 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() } def assignable_to(self, other) -> bool: if self == other: return True elif self.parent and self.parent.assignable_to(other): return True else: for iface in self.implements: if iface.assignable_to(other): return True return False def _enum_properties(self): yield from self.own_properties.values() if self.parent is not None: yield from self.parent.properties.values() for impl in self.implements: yield from impl.properties.values() def _enum_signals(self): yield from self.own_signals.values() if self.parent is not None: yield from self.parent.signals.values() for impl in self.implements: yield from impl.signals.values() class EnumMember(GirNode): def __init__(self, ns, tl: typelib.Typelib): super().__init__(ns, tl) @property def value(self): return self.tl.VALUE_VALUE @cached_property def name(self): return self.tl.VALUE_NAME @cached_property def nick(self): return self.name.replace("_", "-") @property def c_ident(self): return self.tl.attr("c:identifier") @property def signature(self): return f"enum member {self.full_name} = {self.value}" class Enumeration(GirNode, GirType): def __init__(self, ns, tl: typelib.Typelib): super().__init__(ns, tl) @cached_property def members(self): members = {} n_values = self.tl.ENUM_N_VALUES values = self.tl.ENUM_VALUES value_size = self.tl.header.HEADER_VALUE_BLOB_SIZE for i in range(n_values): member = EnumMember(self, values[i * value_size]) members[member.name] = member return members @property def signature(self): return f"enum {self.full_name}" def assignable_to(self, type): return type == self class Bitfield(Enumeration): def __init__(self, ns, tl: typelib.Typelib): super().__init__(ns, tl) class Namespace(GirNode): def __init__(self, repo, tl: typelib.Typelib): super().__init__(repo, tl) self.entries: T.Dict[str, GirNode] = {} n_local_entries = tl.HEADER_N_ENTRIES directory = tl.HEADER_DIRECTORY for i in range(n_local_entries): entry = directory[i * tl.HEADER_ENTRY_BLOB_SIZE] entry_name = entry.DIR_ENTRY_NAME entry_type = entry.DIR_ENTRY_BLOB_TYPE entry_blob = entry.DIR_ENTRY_OFFSET if entry_type == typelib.BLOB_TYPE_ENUM: self.entries[entry_name] = Enumeration(self, entry_blob) elif entry_type == typelib.BLOB_TYPE_FLAGS: self.entries[entry_name] = Bitfield(self, entry_blob) 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) @cached_property def xml(self): return get_xml(self.name, self.version).get_elements("namespace")[0] @cached_property def name(self): return self.tl.HEADER_NAMESPACE @cached_property def version(self): return self.tl.HEADER_NSVERSION @property def signature(self): return f"namespace {self.name} {self.version}" @cached_property def classes(self): 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) } def get_type(self, name): """ 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. """ 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). """ if type_name in _BASIC_TYPES: return _BASIC_TYPES[type_name]() elif "." in type_name: ns, name = type_name.split(".", 1) return self.get_containing(Repository).get_type(name, ns) else: return self.get_type(type_name) class Repository(GirNode): def __init__(self, tl: typelib.Typelib): super().__init__(None, tl) self.namespace = Namespace(self, tl) 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 } except: raise CompilerBugError(f"Failed to load dependencies.") else: self.includes = {} 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. """ if ns == self.namespace.name: return self.namespace else: for include in self.includes.values(): if namespace := include.get_containing(Repository).lookup_namespace(ns): return namespace def _resolve_dir_entry(self, dir_entry: typelib.Typelib): if dir_entry.DIR_ENTRY_LOCAL: return self.namespace.get_type(dir_entry.DIR_ENTRY_NAME) else: ns = dir_entry.DIR_ENTRY_NAMESPACE return self.lookup_namespace(ns).get_type(dir_entry.DIR_ENTRY_NAME) def _resolve_type_id(self, type_id: int): if type_id & 0xFFFFFF == 0: type_id = (type_id >> 27) & 0x1F # simple type if type_id == typelib.TYPE_BOOLEAN: 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]: return IntType() 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() else: raise CompilerBugError("Unknown type ID", type_id) else: 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") 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]: if ns is None and name in _BASIC_TYPES: return _BASIC_TYPES[name]() ns = ns or "Gtk" if ns not in self.namespaces: return None 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): return type else: return None def validate_ns(self, ns: str): """ Raises an exception if there is a problem looking up the given namespace. """ ns = ns or "Gtk" if ns not in self.namespaces: raise CompileError( f"Namespace {ns} was not imported", did_you_mean=(ns, self.namespaces.keys()), ) def validate_type(self, name: str, ns: str): """ Raises an exception if there is a problem looking up the given type. """ self.validate_ns(ns) type = self.get_type(name, ns) ns = ns or "Gtk" if type is None: raise CompileError( f"Namespace {ns} does not contain a type called {name}", did_you_mean=(name, self.namespaces[ns].classes.keys()), )