@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
348 lines (282 loc) • 9.99 kB
text/typescript
import { IntrospectedDirectAllocationConstructor } from "../gir/direct-allocation-constructor.ts";
import { IntrospectedError } from "../gir/error.ts";
import type { IntrospectedBaseClass, IntrospectedClass, IntrospectedInterface } from "../gir/introspected-classes.ts";
import { IntrospectedClassFunction, IntrospectedStaticClassFunction } from "../gir/introspected-classes.ts";
import { IntrospectedRecord } from "../gir/record.ts";
import { AnyType, ArrayType, NativeType, TypeIdentifier } from "../gir.ts";
import { resolveTypeIdentifier } from "../utils/type-resolution.ts";
import { GirVisitor } from "../visitor.ts";
// These methods are often marked introspectable="0" in GIR because the C signatures
// involve gpointer types, but GJS exposes them at runtime regardless. Their signatures are
// always trivial (no params, return self or void) so they are safe to keep.
// Only exempt on records — classes have deep inheritance hierarchies where return types
// at different levels conflict (e.g. GObject.Object.ref() vs Gtk.Widget.ref()).
const INTROSPECTABLE_EXEMPT_METHODS = new Set(["ref", "unref", "copy", "free", "destroy"]);
const filterIntrospectableClassMembers = <T extends IntrospectedBaseClass>(node: T): T => {
node.fields = node.fields.filter((field) => field.isIntrospectable);
node.props = node.props.filter((prop) => prop.isIntrospectable);
node.callbacks = node.callbacks.filter((prop) => prop.isIntrospectable);
node.constructors = node.constructors.filter((prop) => prop.isIntrospectable);
const isRecord = node instanceof IntrospectedRecord;
node.members = node.members.filter(
(member) => member.isIntrospectable || (isRecord && INTROSPECTABLE_EXEMPT_METHODS.has(member.name)),
);
return node;
};
const PROTECTED_FIELD_NAMES = ["parent_instance", "parent", "parent_class", "object_class"];
const filterProtectedFields = <T extends IntrospectedBaseClass>(node: T): T => {
const set = new Set(PROTECTED_FIELD_NAMES);
node.fields = node.fields.filter((f) => {
return !set.has(f.name);
});
return node;
};
/**
* Filters fields, properties, and members to ensure they are not named
* after reserved object keys (prototype, constructor)
*/
const filterReservedProperties = <T extends IntrospectedBaseClass>(node: T): T => {
const set = new Set(["prototype", "constructor"]);
node.fields = node.fields.filter((f) => {
return !set.has(f.name);
});
node.props = node.props.filter((p) => {
return !set.has(p.name);
});
node.members = node.members.filter((m) => {
return !set.has(m.name);
});
return node;
};
const filterConflictingNamedClassMembers = <T extends IntrospectedBaseClass>(node: T): T => {
// Props shadow members
node.members = node.members.filter((m) => {
return !node.props.some((prop) => prop.name === m.name && !(m instanceof IntrospectedStaticClassFunction));
});
// Props and members shadow fields
node.fields = node.fields.filter(
(f) =>
!node.members.some((n) => n.name === f.name && !(n instanceof IntrospectedStaticClassFunction)) &&
!node.props.some((n) => n.name === f.name),
);
return node;
};
/**
* Subtypes of ParamSpec are not supported (e.g. a subtype of ParamSpecString).
*
* First, we transform the node to use ParamSpec as a parent and then flag it
* to not emit.
*
* If a generator doesn't follow the emit() standard, the parent type will at least
* be valid.
*
* @param node
* @returns
*/
const fixParamSpecSubtypes = <T extends IntrospectedBaseClass>(node: T): T => {
if (node.superType?.namespace === "GObject" && node.superType.name.startsWith("ParamSpec")) {
// We don't assert this import because we don't want to force libraries
// to unnecessarily import GObject.
node.superType = new TypeIdentifier("ParamSpec", "GObject");
node.noEmit();
}
return node;
};
/**
* Checks if a class implements a GObject.Object-based interface
* If the class is missing a direct parent we inject GObject.Object
* as a stand-in considering it already indirectly inherits
* from it.
*
* @param node
*/
const fixMissingParent = <T extends IntrospectedBaseClass>(node: T): T => {
const { namespace } = node;
if (node.superType == null) {
const isGObject = node.someParent((p) => p.getType().is("GObject", "Object"));
if (isGObject) {
node.superType = namespace.assertInstalledImport("GObject").assertClass("Object").getType();
}
}
return node;
};
/**
* Removes fields with types that GJS cannot directly expose on a struct instance.
* Error types and non-simple non-pointer struct fields are removed.
* Array fields (zero-terminated pointer arrays, T**) are always safe in GJS and are only
* rejected if the element type is private or disguised.
*
* @param node
*/
const removeComplexFields = <T extends IntrospectedBaseClass>(node: T): T => {
const { namespace } = node;
node.fields = node.fields.filter((f) => {
// Array fields (T**) are marshalled by GJS for any GBoxed element type.
// Only reject arrays of private/disguised element types.
if (f.type instanceof ArrayType) {
const elementType = f.type.deepUnwrap();
if (elementType instanceof TypeIdentifier) {
const classNode = resolveTypeIdentifier(namespace, elementType);
return !classNode?.isPrivate;
}
return true;
}
const type = f.type.deepUnwrap();
if (type instanceof NativeType) {
return true;
}
if (type instanceof TypeIdentifier) {
// Find the type for the identifier
const classNode = resolveTypeIdentifier(namespace, type);
// Don't allow private or disguised fields
if (classNode?.isPrivate) {
return false;
}
// Only allow fields pointing to simple structs.
if (classNode && classNode instanceof IntrospectedRecord && !classNode.isSimple()) {
return false;
}
const en = namespace.assertInstalledImport(type.namespace).getEnum(type.name);
if (!(en instanceof IntrospectedError)) {
return true;
}
return false;
}
return true;
});
return node;
};
/**
* TODO: Consider making this transformation optional.
*
* If we are referencing an unknown library, any-ify the type.
*
* @param node
*/
const removeReferencesToMissingLibraries = <T extends IntrospectedBaseClass>(node: T): T => {
const { namespace } = node;
node.fields = node.fields.map((f) => {
const type = f.type.deepUnwrap();
if (type instanceof TypeIdentifier) {
// Find the type for the identifier
const nsNode = namespace.getInstalledImport(type.namespace);
// Don't allow private or disguised fields
if (!nsNode) {
return f.copy({ type: AnyType });
}
}
return f;
});
return node;
};
const removePrivateFields = <T extends IntrospectedBaseClass>(node: T): T => {
node.fields = node.fields.filter((f) => {
return !f.isPrivate && !f.name.startsWith("_");
});
return node;
};
/**
* Boxed types have a specific order of preference for
* which constructor will be used...
*
* 1. Zero Args Constructor
* 2. Direct Allocation (if the type is simple)
* 3. The new() constructor
* 4. The first constructor
*
* @param node
*/
const resolveMainConstructor = (node: IntrospectedRecord): IntrospectedRecord => {
const newConstructor = node.constructors.find((c) => c.name === "new");
const zeroArgsConstructor = node.constructors.find((c) => c.parameters.length === 0);
const firstConstructor = node.constructors?.[0];
if (node.isForeign()) {
node.mainConstructor = null;
return node;
}
if (zeroArgsConstructor || node.isSimpleWithoutPointers()) {
node.mainConstructor = IntrospectedDirectAllocationConstructor.fromFields(node.fields, node);
return node;
}
const resolvedConstructor = newConstructor ?? firstConstructor;
if (resolvedConstructor) {
node.mainConstructor = resolvedConstructor.copy();
}
if (node.isSimple()) {
node.mainConstructor = IntrospectedDirectAllocationConstructor.fromFields(node.fields, node);
return node;
}
return node;
};
const mergeStaticDefinitions = (node: IntrospectedClass): IntrospectedClass => {
if (!node.staticDefinition) {
return node;
}
const { namespace } = node;
const staticDefinition = namespace.getClass(node.staticDefinition);
if (!(staticDefinition instanceof IntrospectedRecord)) {
return node;
}
const staticMethods = staticDefinition.members
.filter((m) => m instanceof IntrospectedClassFunction)
.map((m) => {
// Convert the class function to a static class function
const { name, parameters, output_parameters, isIntrospectable } = m;
return new IntrospectedStaticClassFunction({
name,
parameters,
output_parameters,
return_type: m.return(),
parent: node,
isIntrospectable,
});
});
for (const staticMethod of staticMethods) {
if (
!node.members.some(
(member) => member.name === staticMethod.name && member instanceof IntrospectedStaticClassFunction,
)
) {
node.members.push(staticMethod);
}
}
return node;
};
function chainVisitors<T>(node: T, ...args: ((node: T) => T)[]) {
let currentNode = node;
for (const visitor of args) {
currentNode = visitor(currentNode);
}
return currentNode;
}
export class ClassVisitor extends GirVisitor {
visitClass = (node: IntrospectedClass) =>
chainVisitors(
node,
removeReferencesToMissingLibraries,
fixMissingParent,
fixParamSpecSubtypes,
removeComplexFields,
removePrivateFields,
mergeStaticDefinitions,
filterConflictingNamedClassMembers,
filterIntrospectableClassMembers,
filterProtectedFields,
filterReservedProperties,
);
visitInterface = (node: IntrospectedInterface) =>
chainVisitors(node, filterIntrospectableClassMembers, filterReservedProperties);
visitRecord = (node: IntrospectedRecord) =>
chainVisitors(
node,
fixMissingParent,
fixParamSpecSubtypes,
resolveMainConstructor,
removeComplexFields,
removePrivateFields,
filterConflictingNamedClassMembers,
filterIntrospectableClassMembers,
filterProtectedFields,
filterReservedProperties,
);
}