@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
561 lines • 18.3 kB
JavaScript
import { deprecatedVersion, introducedVersion, isDeprecated } from "./namespace.js";
import { GirDirection } from "@gi.ts/parser";
import { TypeIdentifier, ThisType, ArrayType, ClosureType, BinaryType, ObjectType, NullableType, StringType, NumberType, BooleanType, Uint8ArrayType, AnyType, UnknownType, NeverType, VoidType, GenerifiedTypeIdentifier, GenericType, NativeType } from "../gir.js";
import { TwoKeyMap } from "../util.js";
const reservedWords = [
// For now, at least, the typescript compiler doesn't throw on numerical types like int, float, etc.
"abstract",
"arguments",
"await",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"constructor", // This isn't technically reserved, but it's problematic.
"debugger",
"default",
"delete",
"do",
// "double",
"else",
"enum",
"eval",
"export",
"extends",
"false",
"final",
"finally",
// "float",
"for",
"function",
"goto",
"if",
"implements",
"import",
"in",
"instanceof",
// "int",
"interface",
"let",
// "long",
"native",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"true",
"try",
"typeof",
"var",
"void",
"volatile",
"while",
"with",
"yield"
];
export function getAliasType(namespace, ns, parameter) {
let name = parameter.type?.[0].$["name"] || "unknown";
const nameParts = name.split(" ");
if (nameParts.length === 1) {
name = nameParts[0];
}
else {
name = nameParts[1];
}
return parseTypeExpression(namespace, name);
}
/**
* This function determines whether a given type is a "pointer type"...
*
* Any type where the c:type ends with *
*/
function isPointerType(types) {
const type = types?.[0];
if (!type)
return false;
const ctype = type.$["c:type"];
if (!ctype)
return false;
const typeName = type.$.name;
if (!typeName)
return false;
if (isPrimitiveType(typeName))
return false;
return ctype.endsWith("*");
}
/* Decode the type */
export function getType(ns, param) {
const modName = ns.namespace;
if (!param)
return VoidType;
let name = "";
let arrayDepth = null;
let length = null;
let isPointer = false;
const parameter = param;
if (parameter.array && parameter.array[0]) {
arrayDepth = 1;
const [array] = parameter.array;
if (array.$ && array.$.length != null) {
length = Number.parseInt(array.$.length, 10);
if (Number.isNaN(length)) {
throw new Error(`Error parsing array length: ${array.$.length}`);
}
}
if (array.type && array.type[0].$ && array.type[0].$["name"]) {
name = array.type[0].$["name"];
}
else if (array.array) {
let arr = array;
let depth = 1;
while (Array.isArray(arr.array)) {
arr = arr.array[0];
depth++;
}
const possibleName = arr.type?.[0].$["name"];
if (possibleName) {
name = possibleName;
}
else {
name = "unknown";
console.log(`Failed to find array type in ${ns.packageName}: `, JSON.stringify(parameter.$, null, 4), "\nMarking as unknown!");
}
arrayDepth = depth;
isPointer = isPointerType(array.type);
}
else {
name = "unknown";
}
}
else if (parameter.type && parameter.type[0] && parameter.type[0].$) {
const possibleName = parameter.type[0].$["name"];
if (possibleName) {
name = possibleName;
}
else {
name = "unknown";
console.log(`Failed to find type in ${modName}: `, JSON.stringify(parameter.type[0].$, null, 4), "\nMarking as unknown!");
}
isPointer = isPointerType(parameter.type);
}
else if (parameter.varargs || (parameter.$ && parameter.$.name === "...")) {
arrayDepth = 1;
name = "any";
}
else {
name = "unknown";
console.log(`Unknown varargs type in ${modName}: `, JSON.stringify(parameter.$, null, 4), "\nMarking as unknown!");
}
let closure = null;
if (parameter.$ && parameter.$.closure) {
closure = Number.parseInt(parameter.$.closure, 10);
if (Number.isNaN(closure)) {
throw new Error(`Error parsing closure data position: ${parameter.$.closure}`);
}
}
const nullable = parameter.$ && parameter.$["nullable"] === "1";
const allowNone = parameter.$ && parameter.$["allow-none"] === "1";
const x = name.split(" ");
if (x.length === 1) {
name = x[0];
}
else {
name = x[1];
}
const baseType = parseTypeString(name);
if (!baseType) {
throw new Error(`Un-parsable type: ${name}`);
}
let variableType = parseTypeExpression(ns.namespace, name);
if (variableType instanceof TypeIdentifier) {
if (variableType.is("GLib", "List") || variableType.is("GLib", "SList")) {
// TODO: $?.name was not necessary in gi.ts, but TelepathyLogger
// fails to generate now.
const listType = parameter?.type?.[0].type?.[0]?.$?.name;
if (listType) {
name = listType;
variableType = parseTypeExpression(ns.namespace, name);
arrayDepth = 1;
}
}
else if (variableType.is("GLib", "HashTable")) {
const keyType = parameter?.type?.[0]?.type?.[0]?.$.name;
const valueType = parameter?.type?.[0]?.type?.[1]?.$.name;
if (keyType && valueType) {
const key = parseTypeExpression(ns.namespace, keyType);
const value = parseTypeExpression(ns.namespace, valueType);
variableType = new GenerifiedTypeIdentifier("HashTable", "GLib", [key, value]);
}
}
}
if (arrayDepth != null) {
const primitiveArrayType = resolvePrimitiveArrayType(name, arrayDepth);
if (primitiveArrayType) {
const [primitiveName, primitiveArrayDepth] = primitiveArrayType;
variableType = ArrayType.new({
type: primitiveName,
arrayDepth: primitiveArrayDepth,
length
});
}
else {
variableType = ArrayType.new({ type: variableType, arrayDepth, length });
}
}
else if (closure != null) {
variableType = ClosureType.new({ type: variableType, user_data: closure });
}
if (parameter.$ &&
(parameter.$.direction === GirDirection.Inout || parameter.$.direction === GirDirection.Out) &&
(nullable || allowNone) &&
!(variableType instanceof NativeType)) {
return new NullableType(variableType);
}
if ((!parameter.$?.direction || parameter.$.direction === GirDirection.In) && nullable) {
return new NullableType(variableType);
}
variableType.isPointer = isPointer;
return variableType;
}
export const SanitizedIdentifiers = new Map();
export function sanitizeIdentifierName(namespace, name) {
// This is a unique case where the name is "empty".
if (name === "") {
return "''";
}
let sanitized_name = name.replace(/[^A-z0-9_]/gi, "_");
if (reservedWords.includes(sanitized_name)) {
sanitized_name = `__${sanitized_name}`;
}
if (sanitized_name.match(/^[^A-z_]/) != null) {
sanitized_name = `__${sanitized_name}`;
}
if (namespace && sanitized_name !== name) {
SanitizedIdentifiers.set(`${namespace}.${name}`, `${namespace}.${sanitized_name}`);
}
return sanitized_name;
}
// TODO: Until we support resolving via c:type, fix GIRs with
// broken namespacing...
export function sanitizeNamespace(namespace) {
if (namespace === "Tracker_Vala") {
return "Tracker";
}
return namespace;
}
export function sanitizeMemberName(name) {
// This is a unique case where the name is "empty".
if (name === "") {
return "''";
}
return name.replace(/[^A-z0-9_]/gi, "_");
}
export function isInvalid(name) {
if (reservedWords.includes(name)) {
return true;
}
const sanitized = sanitizeMemberName(name);
if (sanitized.match(/^[^A-z_]/) != null) {
return true;
}
return false;
}
export function parseDoc(element) {
const el = element.doc?.[0]?._;
return el ? `${el}` : null;
}
export function parseDeprecatedDoc(element) {
return element["doc-deprecated"]?.[0]?._ ?? null;
}
export function parseMetadata(element) {
const version = introducedVersion(element);
const deprecatedIn = deprecatedVersion(element);
const deprecated = isDeprecated(element);
const doc = parseDeprecatedDoc(element);
if (!version && !deprecated && !deprecatedIn && !doc) {
return undefined;
}
return {
...(deprecated ? { deprecated } : {}),
...(doc ? { deprecatedDoc: doc } : {}),
...(deprecatedIn ? { deprecatedVersion: deprecatedIn } : {}),
...(version ? { introducedVersion: version } : {})
};
}
export function parseTypeString(type) {
if (type.includes(".")) {
const parts = type.split(".");
if (parts.length > 2) {
throw new Error(`Invalid type string ${type} passed.`);
}
const [namespace, name] = parts;
return { name, namespace };
}
else {
return { name: type, namespace: null };
}
}
export function parseTypeIdentifier(namespace, type) {
const baseType = parseTypeString(type);
if (baseType.namespace) {
return new TypeIdentifier(baseType.name, baseType.namespace);
}
else {
return new TypeIdentifier(baseType.name, namespace);
}
}
export function parseTypeExpression(namespace, type) {
const baseType = parseTypeString(type);
if (baseType.namespace) {
if (baseType.namespace === namespace) {
return new TypeIdentifier(baseType.name, namespace);
}
return new TypeIdentifier(baseType.name, baseType.namespace).sanitize();
}
else {
const primitiveType = resolvePrimitiveType(baseType.name);
if (primitiveType !== null) {
return primitiveType;
}
else {
return new TypeIdentifier(baseType.name, namespace);
}
}
}
export function resolvePrimitiveArrayType(name, arrayDepth) {
if (arrayDepth > 0) {
switch (name) {
case "gint8":
case "guint8":
return [Uint8ArrayType, arrayDepth - 1];
case "gunichar":
return [StringType, arrayDepth - 1];
}
}
const resolvedName = resolvePrimitiveType(name);
if (resolvedName) {
return [resolvedName, arrayDepth];
}
else {
return null;
}
}
export function isPrimitiveType(name) {
return resolvePrimitiveType(name) !== null;
}
export function resolvePrimitiveType(name) {
switch (name) {
case "":
console.error("Resolving '' to any on " + name);
return AnyType;
case "filename":
return StringType;
// Pass this through
case "GType":
return new TypeIdentifier("GType", "GObject");
case "utf8":
return StringType;
case "void": // Support TS "void"
case "none":
return VoidType;
// TODO Some libraries are bad and don't use g-prefixed numerical types
case "uint":
case "int":
case "uint8":
case "int8":
case "uint16":
case "int16":
case "uint32":
case "int32":
case "int64":
case "uint64":
case "double":
case "long":
case "float":
// Most libraries will use these though:
case "gshort":
case "guint32":
case "guint16":
case "gint16":
case "gint8":
case "gint32":
case "gushort":
case "gfloat":
case "gchar":
case "guint":
case "glong":
case "gulong":
case "gint":
case "guint8":
case "guint64":
case "gint64":
case "gdouble":
case "gssize":
case "gsize":
return NumberType;
case "gboolean":
return BooleanType;
case "gpointer": // This is typically used in callbacks to pass data, so we'll allow anything.
return AnyType;
case "object":
return ObjectType;
case "va_list":
return AnyType;
case "guintptr": // You can't use pointers in GJS! (at least that I'm aware of)
return NeverType;
case "never": // Support TS "never"
return NeverType;
case "unknown": // Support TS "unknown"
return UnknownType;
case "any": // Support TS "any"
return AnyType;
case "this": // Support TS "this"
return ThisType;
case "number": // Support TS "number"
return NumberType;
case "gunichar":
case "string": // Support TS "string"
return StringType;
case "boolean": // Support TS "boolean"
return BooleanType;
case "object": // Support TS "object"
return ObjectType;
}
return null;
}
export function resolveDirectedType(type, direction) {
if (type instanceof ArrayType) {
if ((direction === GirDirection.In || direction === GirDirection.Inout) &&
type.type.equals(Uint8ArrayType) &&
type.arrayDepth === 0) {
return new BinaryType(type, StringType);
}
else {
// Rewrap arrays if they have directional types
return type.rewrap(resolveDirectedType(type.type, direction) ?? type.type);
}
}
else if (type instanceof TypeIdentifier) {
if ((direction === GirDirection.In || direction === GirDirection.Inout) && type.is("GLib", "Bytes")) {
return new BinaryType(type, Uint8ArrayType);
}
else if (type.is("GObject", "Value")) {
if (direction === GirDirection.In || direction === GirDirection.Inout) {
return new BinaryType(type, AnyType);
}
else {
// GJS converts GObject.Value out parameters to their unboxed type, which we don't know,
// so type as `unknown`
return UnknownType;
}
}
else if (type.is("GLib", "HashTable")) {
if (direction === GirDirection.In) {
return new BinaryType(new NativeType("{ [key: string]: any }"), type);
}
else {
return type;
}
}
}
return null;
}
/**
* Resolves a class identifier.
*
* If the identifier is a class type it is returned,
* otherwise `null`.
*
* @param namespace
* @param type
*/
export function resolveTypeIdentifier(namespace, type) {
const ns = type.namespace;
const name = type.name;
const resolved_ns = namespace.assertInstalledImport(ns);
const pclass = resolved_ns.getClass(name);
if (pclass) {
return pclass;
}
else {
return null;
}
}
/**
*
* @param a
* @param b
*/
function isTypeConflict(a, b) {
return !a.equals(b) || !b.equals(a);
}
/**
* Checks if a given type expression in the context of a given this type
* is a subtype (compatible with) of another type expression in the context
* of a parent type.
*
* @param namespace
* @param thisType
* @param parentThisType
* @param potentialSubtype
* @param parentType
*/
export function isSubtypeOf(namespace, thisType, parentThisType, potentialSubtype, parentType) {
if (!isTypeConflict(potentialSubtype, parentType)) {
return true;
}
const unwrappedSubtype = potentialSubtype.unwrap();
let unwrappedParent = parentType.unwrap();
if ((potentialSubtype.equals(ThisType) && unwrappedParent.equals(thisType)) ||
(parentType.equals(ThisType) && unwrappedSubtype.equals(parentThisType))) {
return true;
}
if (unwrappedParent instanceof GenericType && unwrappedParent.identifier !== "T") {
// Technically there could be a conflicting generic, but most generic types should specify a replacement for type checking.
// "T" denotes a local function generic in the current implementation, those can't be ignored.
if (!unwrappedParent.replacedType) {
return true;
}
// Use the generic replaced type as a stand-in.
unwrappedParent = unwrappedParent.replacedType;
}
if (!(unwrappedSubtype instanceof TypeIdentifier) || !(unwrappedParent instanceof TypeIdentifier)) {
return false;
}
const resolutions = namespace.parent.subtypes.get(unwrappedSubtype.name, unwrappedSubtype.namespace) ??
new TwoKeyMap();
const resolution = resolutions.get(unwrappedParent.name, unwrappedParent.namespace);
if (typeof resolution === "boolean") {
return resolution;
}
const resolved = resolveTypeIdentifier(namespace, unwrappedSubtype);
if (!resolved) {
return false;
}
const parentResolution = resolved.resolveParents();
// This checks that the two types have the same form, regardless of identifier (e.g. A | null and B | null)
const isStructurallySubtype = potentialSubtype.rewrap(AnyType).equals(parentType.rewrap(AnyType));
const isSubtype = isStructurallySubtype &&
parentResolution.node.someParent((t) => t.getType().equals(unwrappedParent));
resolutions.set(unwrappedParent.name, unwrappedParent.namespace, isSubtype);
namespace.parent.subtypes.set(unwrappedSubtype.name, unwrappedSubtype.namespace, resolutions);
return isSubtype;
}
//# sourceMappingURL=util.js.map