UNPKG

@ts-for-gir/lib

Version:

Typescript .d.ts generator from GIR for gjs

1,136 lines 50.5 kB
import { Logger } from "../logger.js"; import { NativeType, TypeIdentifier, NeverType, ArrayType, Generic, GenericType, GenerifiedTypeIdentifier, AnyType, ConflictType, TypeConflict } from "../gir.js"; import { IntrospectedNamespaceMember } from "./base.js"; import { GirDirection, ClassStructTypeIdentifier } from "../index.js"; import { IntrospectedClassFunction, IntrospectedVirtualClassFunction, IntrospectedStaticClassFunction, IntrospectedConstructor, IntrospectedFunctionParameter, IntrospectedClassCallback } from "./function.js"; import { IntrospectedProperty, IntrospectedField } from "./property.js"; import { sanitizeIdentifierName, parseTypeIdentifier, isSubtypeOf, resolveTypeIdentifier, parseDoc, parseMetadata } from "./util.js"; import { IntrospectedSignal } from "./signal.js"; import { GenericNameGenerator } from "./generics.js"; import { findMap } from "../util.js"; export var FilterBehavior; (function (FilterBehavior) { FilterBehavior[FilterBehavior["DELETE"] = 0] = "DELETE"; FilterBehavior[FilterBehavior["PRESERVE"] = 1] = "PRESERVE"; })(FilterBehavior || (FilterBehavior = {})); const log = new Logger(true, "gir/class"); export function filterConflicts(ns, c, elements, behavior = FilterBehavior.PRESERVE) { const filtered = [...elements.filter(p => p && p.name)]; const prev = []; const thisType = c.getType(); for (const next of filtered) { const field_conflicts = c.findParentMap(resolved_parent => { return findMap([...resolved_parent.fields], p => { if (p.name && p.name == next.name) { if (next instanceof IntrospectedProperty) { return ConflictType.ACCESSOR_PROPERTY_CONFLICT; } if (next instanceof IntrospectedField && !isSubtypeOf(ns, thisType, resolved_parent.getType(), next.type, p.type)) { return ConflictType.FIELD_NAME_CONFLICT; } } return undefined; }); }); const prop_conflicts = !field_conflicts ? c.findParentMap(resolved_parent => { return findMap([...resolved_parent.props], p => { if (p.name && p.name == next.name) { // TODO: This is very TypeScript-specific but until we include which parent // a conflict originates from in the return, we have to handle this here // and not in the generator... // // Classes can override parent interface accessors with properties _but_ // classes cannot override parent class accessors with properties without // an error occuring. if (p.parent instanceof IntrospectedClass && next instanceof IntrospectedField) { return ConflictType.PROPERTY_ACCESSOR_CONFLICT; } if (next instanceof IntrospectedProperty && !isSubtypeOf(ns, thisType, resolved_parent.getType(), next.type, p.type)) { log.warn(`>> Conflict in ${next.parent?.name}.${next.name} with ${p.parent?.name}.${p.name}.`); return ConflictType.PROPERTY_NAME_CONFLICT; } } return undefined; }); }) : undefined; const function_conflicts = !field_conflicts && !prop_conflicts ? c.findParentMap(resolved_parent => findMap([...resolved_parent.constructors, ...resolved_parent.members], p => { if (p.name && p.name == next.name) { if (!(next instanceof IntrospectedClassFunction) || isConflictingFunction(ns, thisType, next, resolved_parent.getType(), p)) { return ConflictType.FUNCTION_NAME_CONFLICT; } } return undefined; })) : undefined; const conflict = field_conflicts || prop_conflicts || function_conflicts; if (conflict) { if (behavior === FilterBehavior.PRESERVE) { if (next instanceof IntrospectedField || next instanceof IntrospectedProperty) { prev.push(next.copy({ type: new TypeConflict(next.type, conflict) })); } else { prev.push(next); } } } else { prev.push(next); } } return prev; } function isConflictingFunction(namespace, childThis, child, parentThis, parent) { if (!parent.isIntrospectable || !child.isIntrospectable) return false; if (child instanceof IntrospectedConstructor && parent instanceof IntrospectedConstructor) { return (child.parameters.length > parent.parameters.length || !isSubtypeOf(namespace, childThis, parentThis, child.return(), parent.return()) || child.parameters.some((p, i) => !isSubtypeOf(namespace, childThis, parentThis, p.type, parent.parameters[i].type))); } else if (child instanceof IntrospectedConstructor || parent instanceof IntrospectedConstructor) { return true; } // This occurs if two functions of the same name are passed but they // are different types (e.g. GirStaticClassFunction vs GirClassFunction) if (Object.getPrototypeOf(child) !== Object.getPrototypeOf(parent)) { return false; } return (child.parameters.length > parent.parameters.length || child.output_parameters.length !== parent.output_parameters.length || !isSubtypeOf(namespace, childThis, parentThis, child.return(), parent.return()) || child.parameters.some((np, i) => !isSubtypeOf(namespace, childThis, parentThis, np.type, parent.parameters[i].type)) || child.output_parameters.some((np, i) => !isSubtypeOf(namespace, childThis, parentThis, np.type, parent.output_parameters[i].type))); } export function filterFunctionConflict(ns, base, elements, conflict_ids) { const nextType = base.getType(); return elements .filter(m => m.name) .reduce((prev, next) => { // TODO This should catch most of them. let msg = null; let conflicts = conflict_ids.includes(next.name) || base.someParent(resolved_parent => { const parentType = resolved_parent.getType(); return [...resolved_parent.constructors, ...resolved_parent.members].some(p => { if (p.name && p.name == next.name) { const conflicting = isConflictingFunction(ns, nextType, next, parentType, p); if (conflicting) { msg = `// Conflicted with ${resolved_parent.namespace.namespace}.${resolved_parent.name}.${p.name}`; return true; } return conflicting; } return false; }); }); // Check if the method name conflicts with any props or fields either on // the class or in the parent... const field_conflicts = [...base.props, ...base.fields].some(p => p.name && p.name === next.name) || base.someParent(resolved_parent => [...resolved_parent.props, ...resolved_parent.fields].some(p => p.name && p.name === next.name)); const isGObject = base.someParent(p => p.namespace.namespace === "GObject" && p.name === "Object"); if (isGObject) { conflicts = conflicts || ["connect", "connect_after", "emit"].includes(next.name); } if (conflicts) { let never; const never_param = new IntrospectedFunctionParameter({ name: "args", direction: GirDirection.In, isVarArgs: true, type: new ArrayType(NeverType) }); const neverOptions = { name: next.name, parent: base, parameters: [never_param], return_type: AnyType }; if (next instanceof IntrospectedConstructor) { never = new IntrospectedConstructor(neverOptions); } else if (next instanceof IntrospectedStaticClassFunction) { never = new IntrospectedStaticClassFunction({ ...neverOptions, parent: next.parent }); } else if (next instanceof IntrospectedVirtualClassFunction && next.parent instanceof IntrospectedClass) { never = new IntrospectedVirtualClassFunction({ ...neverOptions, parent: next.parent }); } else if (next instanceof IntrospectedClassFunction) { never = new IntrospectedClassFunction({ ...neverOptions, parent: next.parent }); } else { const parent = Object.getPrototypeOf(next); throw new Error(`Unknown function type ${parent?.name} encountered.`); } if (msg) never.setWarning(msg); prev.push(next, never); } else if (field_conflicts) { log.warn(`Omitting ${next.name} due to field or property conflict.`); } else { prev.push(next); } return prev; }, []); } export class IntrospectedBaseClass extends IntrospectedNamespaceMember { /** * Used to add a TypeScript index signature to a class * * NOTE: This should probably be migrated into the TypeScript generator itself. */ __ts__indexSignature; superType; mainConstructor; constructors; members; props; fields; callbacks; // Generics support generics = []; constructor(options) { const { name, namespace, superType = null, mainConstructor = null, constructors = [], members = [], props = [], fields = [], callbacks = [], ...args } = options; super(name, namespace, { ...args }); this.superType = superType; this.mainConstructor = mainConstructor?.copy({ parent: this }) ?? null; this.constructors = [...constructors.map(c => c.copy({ parent: this }))]; this.members = [...members.map(m => m.copy({ parent: this }))]; this.props = [...props.map(p => p.copy({ parent: this }))]; this.fields = [...fields.map(f => f.copy({ parent: this }))]; this.callbacks = [...callbacks.map(c => c.copy({ parent: this }))]; } getGenericName = GenericNameGenerator.new(); addGeneric(definition) { const param = new Generic(new GenericType(this.getGenericName(), definition.constraint), definition.default, definition.deriveFrom, definition.constraint, definition.propagate); this.generics.push(param); } getType() { return new TypeIdentifier(this.name, this.namespace.namespace); } static fromXML(element, ns, options) { throw new Error("fromXML is not implemented on GirBaseClass"); } } export class IntrospectedClass extends IntrospectedBaseClass { signals = []; interfaces = []; isAbstract = false; mainConstructor = null; _staticDefinition = null; constructor(name, namespace) { super({ name, namespace }); } /** * Returns all signals for this class including: * - Explicit signals defined on the class * - Property notification signals (notify::property-name) for GObject-derived classes * - Detailed signal variants for properties (signal-name::property-name) */ getAllSignals() { const allSignals = []; // Add explicit signals defined on the class this.signals.forEach(signal => { allSignals.push({ name: signal.name, signal: signal, isNotifySignal: false, isDetailSignal: false }); }); // Check if this class supports GObject notifications const isGObjectObject = this.name === 'Object' && this.namespace.namespace === 'GObject'; const hasNotifySignal = this.signals.some(signal => signal.name === 'notify'); const hasGObjectParent = this.someParent((p) => p.namespace.namespace === 'GObject' && p.name === 'Object'); if (isGObjectObject || hasNotifySignal || hasGObjectParent) { // Collect all properties (own + inherited + implemented) const allProperties = this.getAllProperties(); // Generate property notification signals (notify::property-name) const uniquePropertyNames = new Set(allProperties.map(prop => prop.name .replace(/_/g, '-') .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .toLowerCase())); uniquePropertyNames.forEach(propertyName => { allSignals.push({ name: `notify::${propertyName}`, isNotifySignal: true, isDetailSignal: false, parameterTypes: ['GObject.ParamSpec'], returnType: 'void' }); }); // Generate detailed signal variants for properties const detailSignals = this.signals.filter(signal => signal.detailed); if (detailSignals.length > 0) { detailSignals.forEach(detailSignal => { uniquePropertyNames.forEach(propertyName => { allSignals.push({ name: `${detailSignal.name}::${propertyName}`, signal: detailSignal, isNotifySignal: false, isDetailSignal: true }); }); }); } } return allSignals; } /** * Returns all properties for this class including inherited and implemented properties */ getAllProperties() { const allProperties = [...this.props]; // Add properties from parent classes let currentClass = this; while (currentClass) { const parentResolution = currentClass.resolveParents().extends(); if (parentResolution && parentResolution.node instanceof IntrospectedClass) { const parentClass = parentResolution.node; allProperties.push(...parentClass.props); currentClass = parentClass; } else { break; } } // Add properties from implemented interfaces const implementedProps = this.implementedProperties(); allProperties.push(...implementedProps); return allProperties; } accept(visitor) { const node = this.copy({ signals: this.signals.map(s => s.accept(visitor)), constructors: this.constructors.map(c => c.accept(visitor)), members: this.members.map(m => m.accept(visitor)), props: this.props.map(p => p.accept(visitor)), fields: this.fields.map(f => f.accept(visitor)), callbacks: this.callbacks.map(c => c.accept(visitor)) }); return visitor.visitClass?.(node) ?? node; } hasInstanceSymbol(s) { return (this.members.some(p => s.name === p.name && !(p instanceof IntrospectedStaticClassFunction)) || this.props.some(p => s.name === p.name) || this.fields.some(p => s.name === p.name)); } someParent(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { if (predicate(parent.node)) return true; const some = parent.node.someParent(predicate); if (some) return some; } return (resolution .implements() .map(i => i.node) .some(n => predicate(n)) || resolution.implements().some(i => i.node.someParent(predicate))); } findParent(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { if (predicate(parent.node)) return this; const found = parent.node.findParent(predicate); if (found) return found; } const interfaces = resolution.implements().map(i => i.node); return interfaces.find(n => predicate(n)) || interfaces.find(n => n.findParent(predicate)); } findParentMap(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { let found = predicate(parent.node); if (found !== undefined) return found; found = parent.node.findParentMap(predicate); if (found !== undefined) return found; } const interfaces = resolution.implements().map(i => i.node); const result = findMap(interfaces, i => predicate(i)); if (result !== undefined) return result; return findMap(interfaces, i => i.findParentMap(predicate)); } implementedProperties(potentialConflicts = []) { const resolution = this.resolveParents(); const implemented_on_parent = [...(resolution.extends() ?? [])] .map(r => r.implements()) .flat() .map(i => i.identifier); const properties = new Map(); const validateProp = (prop) => !this.hasInstanceSymbol(prop) && !properties.has(prop.name) && potentialConflicts.every(p => prop.name !== p.name); for (const implemented of resolution.implements()) { if (implemented.node instanceof IntrospectedClass) continue; if (implemented_on_parent.some(p => p.equals(implemented.identifier))) continue; for (const prop of implemented.node.props) { if (!validateProp(prop)) { continue; } properties.set(prop.name, prop.copy({ parent: this })); } } for (const implemented of resolution.implements()) { [...implemented].forEach(e => { if (e.node instanceof IntrospectedClass) return; if (implemented_on_parent.some(p => p.equals(e.identifier))) return; for (const prop of e.node.props) { if (!validateProp(prop)) { continue; } properties.set(prop.name, prop.copy({ parent: this })); } }); } // If an interface inherits from a class (such as Gtk.Widget) // we need to pull in every method from that class... for (const implemented of resolution.implements()) { const extended = implemented.extends(); if (extended?.node instanceof IntrospectedClass) { for (const prop of extended.node.props) { if (!validateProp(prop)) continue; properties.set(prop.name, prop); } } } return [...properties.values()]; } implementedMethods(potentialConflicts = []) { const resolution = this.resolveParents(); const implementedOnParent = [...(resolution.extends() ?? [])].map(r => r.implements()).flat(); const methods = new Map(); const validateMethod = (method) => !(method instanceof IntrospectedStaticClassFunction) && !this.hasInstanceSymbol(method) && !methods.has(method.name) && potentialConflicts.every(m => method.name !== m.name); for (const implemented of resolution.implements()) { if (implemented.node instanceof IntrospectedClass) continue; if (implementedOnParent.find(p => p.identifier.equals(implemented.identifier))?.node?.generics?.length === 0) continue; for (const member of implemented.node.members) { if (!validateMethod(member)) continue; methods.set(member.name, member); } } for (const implemented of resolution.implements()) { [...implemented].forEach(e => { if (e.node instanceof IntrospectedClass) return; if (implementedOnParent.find(p => p.identifier.equals(e.identifier))?.node.generics.length === 0) return; for (const member of e.node.members) { if (!validateMethod(member)) continue; methods.set(member.name, member); } }); } // If an interface inherits from a class (such as Gtk.Widget) // we need to pull in every method from that class... for (const implemented of resolution.implements()) { const extended = implemented.extends(); if (extended?.node instanceof IntrospectedClass) { for (const member of extended.node.members) { if (!validateMethod(member)) continue; methods.set(member.name, member); } } } return [...methods.values()].map(f => { const mapping = new Map(); if (f.parent instanceof IntrospectedBaseClass) { const inter = this.interfaces.find(i => i.equals(f.parent.getType())); if (inter instanceof GenerifiedTypeIdentifier) { f.parent.generics.forEach((g, i) => { if (inter.generics.length > i) { mapping.set(g.type.identifier, inter.generics[i]); } }); } } const unwrapped = f.return().deepUnwrap(); let modifiedReturn = f.return(); if (unwrapped instanceof GenericType && mapping.has(unwrapped.identifier)) { const mapped = mapping.get(unwrapped.identifier); if (mapped) { modifiedReturn = f.return().rewrap(mapped); } // Handles the case where a class implements an interface and thus copies its virtual methods. } else if (unwrapped.equals(this.getType())) { modifiedReturn = f.return().rewrap(this.getType()); } return f.copy({ parent: this, interfaceParent: f.parent, parameters: f.parameters.map(p => { const t = p.type.deepUnwrap(); if (t instanceof GenericType && mapping.has(t.identifier)) { const iden = mapping.get(t.identifier); if (iden) { return p.copy({ type: p.type.rewrap(iden) }); } } return p; }), outputParameters: f.output_parameters.map(p => { const t = p.type.deepUnwrap(); if (t instanceof GenericType && mapping.has(t.identifier)) { const iden = mapping.get(t.identifier); if (iden) { return p.copy({ type: p.type.rewrap(iden) }); } } return p; }), returnType: modifiedReturn }); }); } resolveParents() { const { namespace, superType, interfaces } = this; return { *[Symbol.iterator]() { let current = this.extends(); while (current !== undefined) { yield current; current = current.extends(); } }, implements() { const z = interfaces .map(i => resolveTypeIdentifier(namespace, i)) .filter((i) => i instanceof IntrospectedInterface) .map(i => i.resolveParents()); return z; }, extends() { const parentType = superType; const resolved_parent = parentType && resolveTypeIdentifier(namespace, parentType); if (resolved_parent instanceof IntrospectedClass) return resolved_parent.resolveParents(); return undefined; }, node: this, identifier: this.getType() }; } copy(options = {}) { const { name, namespace, superType, interfaces, members, constructors, props, fields, callbacks, isAbstract, mainConstructor, signals, generics, _staticDefinition } = this; const clazz = new IntrospectedClass(name, namespace); clazz._copyBaseProperties(this); if (superType) { clazz.superType = superType; } clazz._staticDefinition = _staticDefinition; clazz.signals = (options.signals ?? signals).map(s => s.copy({ parent: clazz })); clazz.interfaces = [...interfaces]; clazz.props = (options.props ?? props).map(p => p.copy({ parent: clazz })); clazz.fields = (options.fields ?? fields).map(f => f.copy({ parent: clazz })); clazz.callbacks = (options.callbacks ?? callbacks).map(c => c.copy({ parent: clazz })); clazz.isAbstract = isAbstract; clazz.mainConstructor = mainConstructor; clazz.constructors = (options.constructors ?? constructors).map(c => c.copy({ parent: clazz })); clazz.members = (options.members ?? members).map(m => m.copy({ parent: clazz })); clazz.generics = [...generics]; // Ensure the generic iteration resumes clazz.getGenericName = GenericNameGenerator.at(this.getGenericName()); return clazz; } get staticDefinition() { return this._staticDefinition; } static fromXML(element, ns, options) { const name = sanitizeIdentifierName(ns.namespace, element.$.name); if (options.verbose) { log.debug(` >> GirClass: Parsing definition ${element.$.name} (${name})...`); } const clazz = new IntrospectedClass(name, ns); if (options.loadDocs) { clazz.doc = parseDoc(element); clazz.metadata = parseMetadata(element); } if (element.$["glib:type-name"]) { clazz.resolve_names.push(element.$["glib:type-name"]); ns.registerResolveName(element.$["glib:type-name"], ns.namespace, name); } if (element.$["glib:type-struct"]) { clazz.resolve_names.push(); ns.registerResolveName(element.$["glib:type-struct"], ns.namespace, name); } if (element.$["c:type"]) { clazz.resolve_names.push(element.$["c:type"]); ns.registerResolveName(element.$["c:type"], ns.namespace, name); } const typeStruct = element.$["glib:type-struct"]; if (typeStruct) { clazz.registerStaticDefinition(typeStruct); clazz.resolve_names.push(typeStruct); ns.registerResolveName(typeStruct, ns.namespace, name); } try { // Setup parent type if this is an interface or class. if (element.$.parent) { clazz.superType = parseTypeIdentifier(ns.namespace, element.$.parent); } if (element.$.abstract) { clazz.isAbstract = true; } if (Array.isArray(element.constructor)) { clazz.constructors.push(...element.constructor.map(constructor => IntrospectedConstructor.fromXML(constructor, clazz, options))); } if (element["glib:signal"]) { clazz.signals.push(...element["glib:signal"].map(signal => IntrospectedSignal.fromXML(signal, clazz, options))); } // Properties if (element.property) { element.property.forEach(prop => { const property = IntrospectedProperty.fromXML(prop, clazz, options); switch (options.propertyCase) { case "both": clazz.props.push(property); const camelCase = property.toCamelCase(); // Ensure we don't duplicate properties like 'show' if (property.name !== camelCase.name) { clazz.props.push(camelCase); } break; case "camel": clazz.props.push(property.toCamelCase()); break; case "underscore": clazz.props.push(property); break; } }); } // Instance Methods if (element.method) { clazz.members.push(...element.method.map(method => IntrospectedClassFunction.fromXML(method, clazz, options))); } // Fields if (element.field) { element.field .filter(field => !("callback" in field)) .forEach(field => { const f = IntrospectedField.fromXML(field, clazz); clazz.fields.push(f); }); } if (element.implements) { element.implements.forEach(implementee => { const name = implementee.$.name; const type = parseTypeIdentifier(ns.namespace, name); if (type) { clazz.interfaces.push(type); } }); } // Callback Types if (element.callback) { clazz.callbacks.push(...element.callback.map(callback => { if (options.verbose) { log.debug(`Adding callback ${callback.$.name} for ${ns.namespace}`); } return IntrospectedClassCallback.fromXML(callback, clazz, options); })); } // Virtual Methods if (element["virtual-method"]) { clazz.members.push(...element["virtual-method"].map(method => IntrospectedVirtualClassFunction.fromXML(method, clazz, options))); } // Static methods (functions) if (element.function) { clazz.members.push(...element.function.map(func => IntrospectedStaticClassFunction.fromXML(func, clazz, options))); } } catch (e) { log.error(`Failed to parse class: ${clazz.name} in ${ns.namespace}.`, e); } return clazz; } registerStaticDefinition(typeStruct) { this._staticDefinition = typeStruct; } asString(generator) { return generator.generateClass(this); } } export class IntrospectedRecord extends IntrospectedBaseClass { _isForeign = false; _structFor = null; _isSimple = null; _isSimpleWithoutPointers = null; /** * Returns all signals for this record (records typically don't have signals) */ getAllSignals() { // Records typically don't have signals, but we provide a consistent API return []; } isForeign() { return this._isForeign; } get structFor() { return this._structFor; } getType() { if (this._structFor) { return this._structFor; } return new TypeIdentifier(this.name, this.namespace.namespace); } someParent(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); return !!parent && (predicate(parent.node) || parent.node.someParent(predicate)); } findParent(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { if (predicate(parent.node)) return parent.node; const found = parent.node.findParent(predicate); if (found) return found; } return undefined; } findParentMap(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { const result = predicate(parent.node); if (result !== undefined) return result; return parent.node.findParentMap(predicate); } return undefined; } accept(visitor) { const node = this.copy({ constructors: this.constructors.map(c => c.accept(visitor)), members: this.members.map(m => m.accept(visitor)), props: this.props.map(p => p.accept(visitor)), fields: this.fields.map(f => f.accept(visitor)), callbacks: this.callbacks.map(c => c.accept(visitor)) }); return visitor.visitRecord?.(node) ?? node; } resolveParents() { const { namespace, superType } = this; return { *[Symbol.iterator]() { let current = this.extends(); while (current !== undefined) { yield current; current = current.extends(); } }, extends() { const resolved_parent = superType ? resolveTypeIdentifier(namespace, superType) : undefined; if (resolved_parent instanceof IntrospectedRecord) return resolved_parent.resolveParents(); return undefined; }, node: this, identifier: this.getType() }; } copy(options = {}) { const { name, namespace, superType, members, constructors, _isForeign, _structFor, props, fields, callbacks, generics, mainConstructor } = this; const clazz = new IntrospectedRecord({ name, namespace }); clazz._copyBaseProperties(this); if (superType) { clazz.superType = superType; } clazz._structFor = _structFor; clazz._isForeign = _isForeign; clazz.props = (options.props ?? props).map(p => p.copy({ parent: clazz })); clazz.fields = (options.fields ?? fields).map(f => f.copy({ parent: clazz })); clazz.callbacks = (options.callbacks ?? callbacks).map(c => c.copy({ parent: clazz })); clazz.mainConstructor = mainConstructor?.copy({ parent: clazz }) ?? null; clazz.constructors = (options.constructors ?? constructors).map(c => c.copy({ parent: clazz })); clazz.members = (options.members ?? members).map(m => m.copy({ parent: clazz })); clazz.generics = [...generics]; return clazz; } static foreign(name, namespace) { const foreignRecord = new IntrospectedRecord({ name, namespace }); foreignRecord._isForeign = true; return foreignRecord; } static fromXML(element, namespace, options) { if (!element.$.name) { throw new Error("Invalid GIR File: No name provided for union."); } const name = sanitizeIdentifierName(namespace.namespace, element.$.name); if (options.verbose) { log.debug(` >> GirRecord: Parsing definition ${element.$.name} (${name})...`); } const clazz = new IntrospectedRecord({ name, namespace }); clazz.setPrivate(element.$.name.startsWith("_") || ("disguised" in element.$ && element.$.disguised === "1") || ("opaque" in element.$ && element.$.opaque === "1")); if (typeof element.$["glib:is-gtype-struct-for"] === "string" && !!element.$["glib:is-gtype-struct-for"]) { const structFor = parseTypeIdentifier(namespace.namespace, element.$["glib:is-gtype-struct-for"]); // This let's replace these references when generating. clazz._structFor = new ClassStructTypeIdentifier(structFor.name, structFor.namespace); } else { if (element.$["glib:type-name"]) { clazz.resolve_names.push(element.$["glib:type-name"]); namespace.registerResolveName(element.$["glib:type-name"], namespace.namespace, name); } if (element.$["c:type"]) { clazz.resolve_names.push(element.$["c:type"]); namespace.registerResolveName(element.$["c:type"], namespace.namespace, name); } } if (options.loadDocs) { clazz.doc = parseDoc(element); clazz.metadata = parseMetadata(element); } try { // Instance Methods if (element.method) { clazz.members.push(...element.method.map(method => IntrospectedClassFunction.fromXML(method, clazz, options))); } // Constructors if (Array.isArray(element.constructor)) { element.constructor.forEach(constructor => { const c = IntrospectedConstructor.fromXML(constructor, clazz, options); clazz.constructors.push(c); }); } // Static methods (functions) if (element.function) { clazz.members.push(...element.function.map(func => IntrospectedStaticClassFunction.fromXML(func, clazz, options))); } // Is this a foreign type? (don't allow construction if foreign) clazz._isForeign = "foreign" in element.$ && element.$.foreign === "1"; // Fields (for "non-class" records) if (element.field) { clazz.fields.push(...element.field .filter(field => !("callback" in field)) .map(field => IntrospectedField.fromXML(field, clazz))); } } catch (e) { log.error(`Failed to parse record: ${clazz.name}.`, e); } return clazz; } /** * Calculate if a type expression is "simple" without pointers */ isSimpleTypeWithoutPointers(typeContainer) { if (!this.isSimpleType(typeContainer)) { return false; } if (typeContainer.isPointer) { return false; } // Primitive types can be directly allocated. if (typeContainer instanceof NativeType) { return true; } if (typeContainer instanceof ArrayType) { if (typeContainer.type.equals(this.getType())) { return true; } return this.isSimpleTypeWithoutPointers(typeContainer.type); } if (typeContainer instanceof TypeIdentifier) { const type = typeContainer; const child_ns = this.namespace.assertInstalledImport(type.namespace); const alias = child_ns.getAlias(type.name); if (alias) { return this.isSimpleTypeWithoutPointers(alias.type); } const child = child_ns.getClass(type.name); if (child === this) { return false; } if (child instanceof IntrospectedRecord) { return child.isSimpleWithoutPointers() !== null; } } return false; } /** * Calculate if a type expression is "simple" */ isSimpleType(typeContainer) { // Primitive types can be directly allocated. if (typeContainer instanceof NativeType) { return true; } if (typeContainer instanceof ArrayType) { if (typeContainer.type.equals(this.getType())) { return true; } return this.isSimpleType(typeContainer.type); } if (typeContainer instanceof TypeIdentifier) { const type = typeContainer; const child_ns = this.namespace.assertInstalledImport(type.namespace); const alias = child_ns.getAlias(type.name); if (alias) { return this.isSimpleType(alias.type); } const child = child_ns.getClass(type.name); if (child === this) { return false; } if (child instanceof IntrospectedRecord) { return child.isSimple(); } } return false; } /** * Check if a record is "simple" and can be constructed by GJS */ isSimple() { // Records with no fields are not // constructable. if (this.fields.length === 0) { return false; } // Because we may have to recursively check // if types are instantiable we cache whether // or not a given Record is simple. if (this._isSimple !== null) { return this._isSimple; } const isSimple = this.fields.every(f => this.isSimpleType(f.type)); this._isSimple = isSimple; return isSimple; } isSimpleWithoutPointers() { // Records which are "simple without pointers" is a subset of // "simple" records. if (!this.isSimple()) { return null; } // Because we may have to recursively check // if types are instantiable we cache whether // or not a given Record is simple. if (this._isSimpleWithoutPointers !== null) { return this._isSimpleWithoutPointers; } const isSimpleWithoutPointers = this.fields.find(f => { return !this.isSimpleTypeWithoutPointers(f.type); }); if (!isSimpleWithoutPointers) this._isSimpleWithoutPointers = "all fields good"; else this._isSimpleWithoutPointers = null; return this._isSimpleWithoutPointers; } asString(generator) { return generator.generateRecord(this); } } export class GirComplexRecord extends IntrospectedRecord { isSimple() { return false; } } export class IntrospectedInterface extends IntrospectedBaseClass { noParent = false; mainConstructor = null; /** * Returns all signals for this interface (most interfaces don't have signals, but some might) */ getAllSignals() { // Most interfaces don't have signals, but we provide a consistent API return []; } copy(options = {}) { const { name, namespace, superType, noParent, members, constructors, props, fields, callbacks, mainConstructor, generics } = this; const clazz = new IntrospectedInterface({ name, namespace }); clazz._copyBaseProperties(this); clazz.noParent = noParent; if (superType) { clazz.superType = superType; } clazz.props = (options.props ?? props).map(p => p.copy({ parent: clazz })); clazz.fields = (options.fields ?? fields).map(f => f.copy({ parent: clazz })); clazz.callbacks = (options.callbacks ?? callbacks).map(c => c.copy({ parent: clazz })); clazz.mainConstructor = mainConstructor?.copy({ parent: clazz }) ?? null; clazz.constructors = (options.constructors ?? constructors).map(c => c.copy({ parent: clazz })); clazz.members = (options.members ?? members).map(m => m.copy({ parent: clazz })); clazz.generics = [...generics]; return clazz; } someParent(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); return !!parent && (predicate(parent.node) || parent.node.someParent(predicate)); } findParent(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { if (predicate(parent.node)) return parent.node; const found = parent.node.findParent(predicate); if (found) return found; } return undefined; } findParentMap(predicate) { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { const result = predicate(parent.node); if (result !== undefined) return result; return parent.node.findParentMap(predicate); } return undefined; } resolveParents() { const { namespace, superType } = this; return { *[Symbol.iterator]() { let current = this.extends(); while (current !== undefined) { yield current; current = current.extends(); } }, extends() { if (!superType) return undefined; const resolved = resolveTypeIdentifier(namespace, superType); if (resolved && (resolved instanceof IntrospectedClass || resolved instanceof IntrospectedInterface)) return resolved.resolveParents(); return undefined; }, node: this, identifier: this.getType() }; } accept(visitor) { const node = this.copy({ constructors: this.constructors.map(c => c.accept(visitor)), members: this.members.map(m => m.accept(visitor)), props: this.props.map(p => p.accept(visitor)), fields: this.fields.map(f => f.accept(visitor)), callbacks: this.callbacks.map(c => c.accept(visitor)) }); return visitor.visitInterface?.(node) ?? node; } static fromXML(element, namespace, options) { const name = sanitizeIdentifierName(namespace.namespace, element.$.name); if (options.verbose) { log.debug(` >> GirInterface: Parsing definition ${element.$.name} (${name})...`); } const clazz = new IntrospectedInterface({ name, namespace }); if (options.loadDocs) { clazz.doc = parseDoc(element); clazz.metadata = parseMetadata(element); } if (element.$["glib:type-name"]) { clazz.resolve_names.push(element.$["glib:type-name"]); namespace.registerResolveName(element.$["glib:type-name"], namespace.namespace, name); } if (element.$["glib:type-struct"]) { clazz.resolve_names.push(); namespace.registerResolveName(element.$["glib:type-struct"], namespace.namespace, name); } if (element.$["c:type"]) { clazz.resolve_names.push(element.$["c:type"]); namespace.registerResolveName(element.$["c:type"], namespace.namespace, name); } try { // Setup the "parent" (prerequisite) for this interface. if (element.prerequisite && element.prerequisite[0]) { const [prerequisite] = element.prerequisite; if (prerequisite.$.name) { clazz.superType = parseTypeIdentifier(namespace.namespace, prerequisite.$.name); } } if (Array.isArray(element.constructor)) { for (const constructor of element.constructor) { clazz.constructors.push(IntrospectedConstructor.fromXML(constructor, clazz, options)); } } // Properties if (element.property) { clazz.props.push(...element.property .map(prop => IntrospectedProperty.fromXML(prop, clazz, options)) .map(prop => { switch (options.propertyCase) { case "both": const camelCase = prop.toCamelCase(); // Ensure we don't duplicate properties like 'show' if (prop.name !== camelCase.name) { return [prop, prop.toCamelCase()]; } return [prop]; case "camel": return [prop.toCamelCase()]; case "underscore": return [prop]; } }) .flat(1)); } // Instance Methods if (element.method) { for (const method of element.method) { const m = IntrospectedClassFunction.fromXML(method, clazz, options); clazz.members.push(m); } } // Virtual Methods if (element["virtual-method"]) { for (const method of element["virtual-method"]) { clazz.members.push(IntrospectedVirtualClassFunction.fromXML(method, clazz, options)); } } // Callback Types if (element.callback) { for (const callback of element.callback) { if (options.verbose) { log.debug(`Adding callback ${callback.$.name} for ${namespace.namespace}`); } clazz.callbacks.push(IntrospectedClassCallback.fromXML(callback, clazz, options)); } } // Static methods (functions) if (element.funct