UNPKG

@ts-for-gir/lib

Version:

Typescript .d.ts generator from GIR for gjs

1,567 lines (1,372 loc) 47.2 kB
import { ConsoleReporter } from "@ts-for-gir/reporter"; import type { FormatGenerator } from "../generators/generator.ts"; import { FunctionType, Generic, GenericType, GenerifiedTypeIdentifier, makeNullable, NullableType, type TypeExpression, TypeIdentifier, UnknownType, } from "../gir.ts"; import type { GirCallbackElement, GirClassElement, GirFunctionElement, GirInterfaceElement, GirMethodElement, GirRecordElement, GirVirtualMethodElement, } from "../index.ts"; import type { ClassDefinition, ClassMember, ClassResolution, InterfaceResolution, IntrospectedOptions, OptionsLoad, RecordResolution, } from "../types/index.ts"; import { findMap } from "../util.ts"; import { parseDoc, parseMetadata } from "../utils/gir-parsing.ts"; import { sanitizeIdentifierName } from "../utils/naming.ts"; import { resolveTypeIdentifier } from "../utils/type-resolution.ts"; import { parseTypeIdentifier } from "../utils/types.ts"; import type { GirVisitor } from "../visitor.ts"; import { IntrospectedConstructor } from "./constructor.ts"; import type { IntrospectedDirectAllocationConstructor } from "./direct-allocation-constructor.ts"; import type { IntrospectedEnum } from "./enum.ts"; import { IntrospectedFunction } from "./function.ts"; import { createGenericNameGenerator, createGenericNameGeneratorAt } from "./generics.ts"; import { IntrospectedBase } from "./introspected-base.ts"; import { IntrospectedNamespaceMember } from "./introspected-namespace-member.ts"; import type { IntrospectedNamespace } from "./namespace.ts"; import type { IntrospectedFunctionParameter } from "./parameter.ts"; import { IntrospectedField, IntrospectedProperty } from "./property.ts"; import { IntrospectedSignal } from "./signal.ts"; const log = new ConsoleReporter(true, "gir/introspected-classes", true); /** * Walks the implementing class's `extends` chain (skipping the prerequisite class * itself) and reports whether any ancestor *shadows* a member by declaring its * own member of the given name. A shadow indicates the parent chain has * overridden the prerequisite's member with a possibly incompatible type, which * would otherwise leave the implementing class unable to satisfy the * interface's contract. */ function hasExtendsShadowOf(cls: IntrospectedBaseClass, prerequisite: IntrospectedBaseClass, name: string): boolean { let current = cls.resolveParents().extends(); while (current) { const node = current.node; if (node !== prerequisite) { const hasOwn = [...node.props, ...node.fields, ...node.members].some((m) => m.name === name); if (hasOwn) return true; } current = current.extends(); } return false; } function resolveNullableProperties(cls: IntrospectedBaseClass): void { for (const prop of cls.props) { if (prop.type instanceof NullableType) continue; const getterName = prop.getter ?? `get_${prop.name}`; const getter = cls.members.find((m) => m.name === getterName && !(m instanceof IntrospectedStaticClassFunction)); if (getter instanceof IntrospectedClassFunction && getter.return() instanceof NullableType) { prop.type = makeNullable(prop.type); } } } /** * Represents a signal with metadata */ export interface SignalDescriptor { name: string; signal?: IntrospectedSignal; isNotifySignal?: boolean; isDetailSignal?: boolean; isTemplateLiteral?: boolean; parameterTypes?: string[]; returnType?: string; } /** * Base class for all class functions */ export class IntrospectedClassFunction< Parent extends IntrospectedBaseClass | IntrospectedEnum = IntrospectedBaseClass | IntrospectedEnum, > extends IntrospectedBase<Parent> { readonly parameters: IntrospectedFunctionParameter[]; protected readonly return_type: TypeExpression; readonly output_parameters: IntrospectedFunctionParameter[]; protected _anyify: boolean = false; protected _generify: boolean = false; interfaceParent: IntrospectedBaseClass | IntrospectedEnum | null = null; returnTypeDoc?: string | null; /** If this function was generated from a signal, stores the signal name. */ signalOrigin?: string; /** GIR glib:finish-func attribute: name of the function that finishes this async operation. */ finishFuncName?: string; generics: Generic[] = []; constructor({ name, parameters = [], output_parameters = [], return_type = UnknownType, parent, doc, ...args }: IntrospectedOptions<{ name: string; parameters?: IntrospectedFunctionParameter[]; output_parameters?: IntrospectedFunctionParameter[]; return_type?: TypeExpression; parent: Parent; originalParent?: Parent | null; doc?: string | null; }>) { super(name, parent, { ...args }); this.parameters = parameters.map((p) => p.copy({ parent: this })); this.output_parameters = output_parameters.map((p) => p.copy({ parent: this })); this.return_type = return_type; this.doc = doc; } get namespace() { return this.parent.namespace; } getCallbackParameters() { const { name, parent, output_parameters, parameters, return_type } = this; return { name, parent, output_parameters, parameters, return_type, }; } copy({ parent = this.parent, name, interfaceParent, parameters, outputParameters, returnType, }: { parent?: Parent; name?: string; interfaceParent?: IntrospectedBaseClass | IntrospectedEnum; parameters?: IntrospectedFunctionParameter[]; outputParameters?: IntrospectedFunctionParameter[]; returnType?: TypeExpression; } = {}): IntrospectedClassFunction<Parent> { const fn = new IntrospectedClassFunction<Parent>({ name: name ?? this.name, parent, output_parameters: outputParameters ?? this.output_parameters, parameters: parameters ?? this.parameters, return_type: returnType ?? this.return_type, }); fn.generics = [...this.generics]; fn.returnTypeDoc = this.returnTypeDoc; fn.finishFuncName = this.finishFuncName; if (interfaceParent) { fn.interfaceParent = interfaceParent; } return fn._copyBaseProperties(this); } accept(visitor: GirVisitor): IntrospectedClassFunction<Parent> { const node = this.copy({ parameters: this.parameters.map((p) => { return p.accept(visitor); }), outputParameters: this.output_parameters.map((p) => { return p.accept(visitor); }), returnType: visitor.visitType?.(this.return_type), }); const fn = visitor.visitClassFunction?.(node); return fn ?? node; } static fromXML( element: GirFunctionElement | GirMethodElement, parent: IntrospectedBaseClass | IntrospectedEnum, options: OptionsLoad, ): IntrospectedClassFunction { const fn = IntrospectedFunction.fromXML(element, parent.namespace, options); // Convert the function to a class function const { raw_name: name, output_parameters, parameters, return_type, doc, isIntrospectable } = fn; // A function with shadowed-by is superseded by the shadowing function (which uses `shadows`) // and takes the original name. Do not emit the shadowed function to avoid duplicate declarations. const isShadowedBy = element.$["shadowed-by"] != null; const classFn = new IntrospectedClassFunction({ parent, name, output_parameters, parameters, return_type, doc, isIntrospectable: isIntrospectable && !isShadowedBy, }); classFn.returnTypeDoc = fn.returnTypeDoc; classFn.generics = [...fn.generics]; classFn.finishFuncName = element.$["glib:finish-func"]; return classFn; } anyify(): this { this._anyify = true; return this; } shouldAnyify() { return this._anyify; } return(): TypeExpression { return this.return_type; } asString<T extends FormatGenerator<unknown>>(generator: T): ReturnType<T["generateClassFunction"]> { return generator.generateClassFunction(this) as ReturnType<T["generateClassFunction"]>; } } /** * Virtual class function */ export class IntrospectedVirtualClassFunction extends IntrospectedClassFunction<IntrospectedBaseClass> { constructor({ name, parameters = [], output_parameters = [], return_type = UnknownType, parent, doc, ...args }: IntrospectedOptions<{ name: string; parameters: IntrospectedFunctionParameter[]; output_parameters?: IntrospectedFunctionParameter[]; return_type?: TypeExpression; parent: IntrospectedBaseClass; doc?: string | null; }>) { super({ parent, name: name.startsWith("vfunc_") ? name : `vfunc_${name}`, parameters, output_parameters, return_type, doc, ...args, }); } copy({ parent = this.parent, interfaceParent, parameters, outputParameters, returnType, }: { parent?: IntrospectedBaseClass; interfaceParent?: IntrospectedBaseClass | IntrospectedEnum | undefined; parameters?: IntrospectedFunctionParameter[] | undefined; outputParameters?: IntrospectedFunctionParameter[] | undefined; returnType?: TypeExpression | undefined; }): IntrospectedVirtualClassFunction { const fn = new IntrospectedVirtualClassFunction({ name: this.name, parent, output_parameters: outputParameters ?? this.output_parameters, parameters: parameters ?? this.parameters, return_type: returnType ?? this.return_type, }); fn.generics = [...this.generics]; fn.returnTypeDoc = this.returnTypeDoc; if (interfaceParent) { fn.interfaceParent = interfaceParent; } return fn._copyBaseProperties(this); } accept(visitor: GirVisitor): IntrospectedVirtualClassFunction { const node = this.copy({ parameters: this.parameters.map((p) => { return p.accept(visitor); }), outputParameters: this.output_parameters.map((p) => { return p.accept(visitor); }), returnType: visitor.visitType?.(this.return_type), }); return visitor.visitVirtualClassFunction?.(node) ?? node; } static fromXML( m: GirVirtualMethodElement, parent: IntrospectedBaseClass, options: OptionsLoad, ): IntrospectedVirtualClassFunction { const fn = IntrospectedFunction.fromXML(m, parent.namespace, options); // Convert the function to a virtual class function const { raw_name: name, output_parameters, parameters, return_type, doc, isIntrospectable } = fn; return new IntrospectedVirtualClassFunction({ parent, name, output_parameters, parameters, return_type, doc, isIntrospectable, }); } asString<T extends FormatGenerator<unknown>>(generator: T): ReturnType<T["generateVirtualClassFunction"]> { return generator.generateVirtualClassFunction(this) as ReturnType<T["generateVirtualClassFunction"]>; } } /** * Static class function */ export class IntrospectedStaticClassFunction extends IntrospectedClassFunction { asString<T extends FormatGenerator<unknown>>(generator: T): ReturnType<T["generateStaticClassFunction"]> { return generator.generateStaticClassFunction(this) as ReturnType<T["generateStaticClassFunction"]>; } copy({ parent = this.parent, interfaceParent, parameters, outputParameters, returnType, }: { parent?: IntrospectedBaseClass | IntrospectedEnum; interfaceParent?: IntrospectedBaseClass | IntrospectedEnum | undefined; parameters?: IntrospectedFunctionParameter[] | undefined; outputParameters?: IntrospectedFunctionParameter[] | undefined; returnType?: TypeExpression | undefined; } = {}): IntrospectedStaticClassFunction { const fn = new IntrospectedStaticClassFunction({ name: this.name, parent, output_parameters: outputParameters ?? this.output_parameters, parameters: parameters ?? this.parameters, return_type: returnType ?? this.return_type, }); fn.generics = [...this.generics]; fn.returnTypeDoc = this.returnTypeDoc; if (interfaceParent) { fn.interfaceParent = interfaceParent; } return fn._copyBaseProperties(this); } accept(visitor: GirVisitor): IntrospectedStaticClassFunction { const node = this.copy({ parent: this.parent, parameters: this.parameters.map((p) => { return p.accept(visitor); }), outputParameters: this.output_parameters.map((p) => { return p.accept(visitor); }), returnType: visitor.visitType?.(this.return_type), }); return visitor.visitStaticClassFunction?.(node) ?? node; } asClassFunction(parent: IntrospectedBaseClass): IntrospectedClassFunction { const { name, output_parameters, parameters, return_type, doc, isIntrospectable } = this; const fn = new IntrospectedClassFunction({ parent, name, output_parameters, parameters, return_type, doc, isIntrospectable, }); fn.returnTypeDoc = this.returnTypeDoc; fn.generics = [...this.generics]; return fn; } static fromXML( m: GirFunctionElement, parent: IntrospectedBaseClass | IntrospectedEnum, options: OptionsLoad, ): IntrospectedStaticClassFunction { const fn = IntrospectedFunction.fromXML(m, parent.namespace, options); // Convert the function to a static class function const { raw_name: name, output_parameters, parameters, return_type, doc, isIntrospectable } = fn; const isShadowedBy = m.$["shadowed-by"] != null; return new IntrospectedStaticClassFunction({ parent, name, output_parameters, parameters, return_type, doc, isIntrospectable: isIntrospectable && !isShadowedBy, }); } } /** * Class callback function */ export class IntrospectedClassCallback extends IntrospectedClassFunction { asFunctionType(): FunctionType { return new FunctionType( Object.fromEntries(this.parameters.map((p) => [p.name, p.type] as const)), this.return_type, ); } copy({ parameters, returnType, outputParameters, parent, }: { parent?: IntrospectedBaseClass; parameters?: IntrospectedFunctionParameter[]; outputParameters?: IntrospectedFunctionParameter[]; returnType?: TypeExpression; } = {}): IntrospectedClassCallback { const cb = new IntrospectedClassCallback({ name: this.name, return_type: returnType ?? this.return_type, parameters: parameters ?? this.parameters, output_parameters: outputParameters ?? this.output_parameters, parent: parent ?? this.parent, })._copyBaseProperties(this); cb.generics = [...this.generics]; return cb; } accept(visitor: GirVisitor): IntrospectedClassCallback { const node = this.copy({ parameters: this.parameters.map((p) => { return p.accept(visitor); }), outputParameters: this.output_parameters.map((p) => { return p.accept(visitor); }), returnType: visitor.visitType?.(this.return_type), }); return visitor.visitClassCallback?.(node) ?? node; } static fromXML( element: GirCallbackElement, parent: IntrospectedBaseClass, options: OptionsLoad, ): IntrospectedClassCallback { const ns = parent.namespace; const cb = new IntrospectedClassCallback( IntrospectedClassFunction.fromXML(element, parent, options).getCallbackParameters(), ); const glibTypeName = element.$["glib:type-name"]; if (typeof glibTypeName === "string" && element.$["glib:type-name"]) { cb.resolve_names.push(glibTypeName); ns.registerResolveName(glibTypeName, ns.namespace, cb.name); } if (element.$["c:type"]) { cb.resolve_names.push(element.$["c:type"]); ns.registerResolveName(element.$["c:type"], ns.namespace, cb.name); } return cb; } asString<T extends FormatGenerator<unknown>>(generator: T): ReturnType<T["generateClassCallback"]> { return generator.generateClassCallback(this) as ReturnType<T["generateClassCallback"]>; } } /** * Abstract base class for classes and interfaces */ export abstract 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?: string; superType: TypeIdentifier | null; mainConstructor: null | IntrospectedConstructor | IntrospectedDirectAllocationConstructor; constructors: IntrospectedConstructor[]; members: IntrospectedClassFunction[]; props: IntrospectedProperty[]; fields: IntrospectedField[]; callbacks: IntrospectedClassCallback[]; // Generics support generics: Generic[] = []; constructor( options: IntrospectedOptions<{ name: string; namespace: IntrospectedNamespace; }> & Partial<ClassDefinition>, ) { 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 }))]; } abstract accept(visitor: GirVisitor): IntrospectedBaseClass; abstract copy(options?: { parent?: undefined; constructors?: IntrospectedConstructor[]; members?: IntrospectedClassFunction[]; props?: IntrospectedProperty[]; fields?: IntrospectedField[]; callbacks?: IntrospectedClassCallback[]; }): IntrospectedBaseClass; getGenericName = createGenericNameGenerator(); abstract resolveParents(): RecordResolution | InterfaceResolution | ClassResolution; abstract someParent(predicate: (b: IntrospectedBaseClass) => boolean): boolean; abstract findParent(predicate: (b: IntrospectedBaseClass) => boolean): IntrospectedBaseClass | undefined; abstract findParentMap<K>(predicate: (b: IntrospectedBaseClass) => K | undefined): K | undefined; addGeneric(definition: { deriveFrom?: TypeIdentifier; default?: TypeExpression; constraint?: TypeExpression; propagate?: boolean; }) { const param = new Generic( new GenericType(this.getGenericName(), definition.constraint), definition.default, definition.deriveFrom, definition.constraint, definition.propagate, ); this.generics.push(param); } getType(): TypeIdentifier { return new TypeIdentifier(this.name, this.namespace.namespace); } static fromXML( _element: GirClassElement | GirInterfaceElement | GirRecordElement, _ns: IntrospectedNamespace, _options: OptionsLoad, ): IntrospectedBaseClass { throw new Error("fromXML is not implemented on GirBaseClass"); } abstract asString<T = string>(generator: FormatGenerator<T>): T; } /** * Represents a GIR class */ export class IntrospectedClass extends IntrospectedBaseClass { signals: IntrospectedSignal[] = []; interfaces: TypeIdentifier[] = []; isAbstract: boolean = false; mainConstructor: null | IntrospectedConstructor = null; private _staticDefinition: string | null = null; constructor(name: string, namespace: IntrospectedNamespace) { super({ name, namespace }); } get gtype() { return this.name; } getAllSignals(): SignalDescriptor[] { const allSignals = this.getBaseSignals(); if (this.hasGObjectSupport()) { this.addNotifySignals(allSignals); this.addDetailedSignals(allSignals); } return allSignals; } private getBaseSignals() { return this.signals.map((signal) => ({ name: signal.name, signal: signal, isNotifySignal: false, isDetailSignal: false, })); } private hasGObjectSupport(): boolean { const isGObjectObject = this.isGObjectObject(); const hasNotifySignal = this.hasExplicitNotifySignal(); const hasGObjectParent = this.hasGObjectParent(); return isGObjectObject || hasNotifySignal || hasGObjectParent; } private isGObjectObject(): boolean { return this.name === "Object" && this.namespace.namespace === "GObject"; } private hasExplicitNotifySignal(): boolean { return this.signals.some((signal) => signal.name === "notify"); } private hasGObjectParent(): boolean { return this.someParent( (p: IntrospectedClass | IntrospectedInterface) => p.namespace.namespace === "GObject" && p.name === "Object", ); } private addNotifySignals(allSignals: SignalDescriptor[]): void { const propertyNames = this.getUniquePropertyNames(); propertyNames.forEach((propertyName) => { allSignals.push({ name: `notify::${propertyName}`, isNotifySignal: true, isDetailSignal: false, parameterTypes: ["GObject.ParamSpec"], returnType: "void", }); }); // Add template literal catch-all for arbitrary property names. // Only on GObject.Object itself — children inherit it via SignalSignatures extends. if (this.isGObjectObject()) { allSignals.push({ name: `notify::\${string}`, isNotifySignal: true, isDetailSignal: false, isTemplateLiteral: true, parameterTypes: ["GObject.ParamSpec"], returnType: "void", }); } } private addDetailedSignals(allSignals: SignalDescriptor[]): void { const propertyNames = this.getUniquePropertyNames(); this.signals.forEach((signal) => { if (signal.detailed) { // Skip "notify" signal — already handled by addNotifySignals if (signal.name === "notify") return; propertyNames.forEach((propertyName) => { allSignals.push({ name: `${signal.name}::${propertyName}`, signal: signal, isNotifySignal: false, isDetailSignal: true, parameterTypes: signal.parameters.map((p) => this.getPropertyTypeString(p.type)), returnType: this.getPropertyTypeString(signal.return_type), }); }); // Add template literal catch-all for arbitrary detail strings allSignals.push({ name: `${signal.name}::\${string}`, signal: signal, isNotifySignal: false, isDetailSignal: true, isTemplateLiteral: true, parameterTypes: signal.parameters.map((p) => this.getPropertyTypeString(p.type)), returnType: this.getPropertyTypeString(signal.return_type), }); } }); } private getUniquePropertyNames(): Set<string> { const allProperties = this.getAllProperties(); return new Set( allProperties.map((prop) => prop.name .replace(/_/g, "-") .replace(/([a-z0-9])([A-Z])/g, "$1-$2") .toLowerCase(), ), ); } private getPropertyTypeString(type: TypeExpression): string { // Simple type conversion - this might need to be adjusted based on actual type structure if (typeof type === "string") return type; if (type?.toString) return type.toString(); return "any"; } private getAllProperties(): IntrospectedProperty[] { const allProperties = [...this.props]; let currentClass = this as IntrospectedClass; while (currentClass) { const parentResolution = currentClass.resolveParents().extends(); if (parentResolution && parentResolution.node instanceof IntrospectedClass) { const parentClass = parentResolution.node as IntrospectedClass; allProperties.push(...parentClass.props); currentClass = parentClass; } else { break; } } const implementedProps = this.implementedProperties(); allProperties.push(...implementedProps); return allProperties; } accept(visitor: GirVisitor): IntrospectedClass { 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 extends { name: string }>(s: S): boolean { 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: (p: IntrospectedClass | IntrospectedInterface) => boolean): boolean { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { if (predicate(parent.node as IntrospectedClass | IntrospectedInterface)) return true; const some = parent.node.someParent(predicate); if (some) return some; } return resolution .implements() .map((i) => i.node) .some( (i) => predicate(i) || i.someParent( (p: IntrospectedBaseClass) => (p instanceof IntrospectedClass || p instanceof IntrospectedInterface) && predicate(p), ), ); } findParent( predicate: (p: IntrospectedClass | IntrospectedInterface) => boolean, ): IntrospectedClass | IntrospectedInterface | undefined { return this.findParentMap((parent) => { if (predicate(parent)) { return parent; } return undefined; }); } findParentMap<K>(predicate: (p: IntrospectedClass | IntrospectedInterface) => K | undefined): K | undefined { const resolution = this.resolveParents(); const parent = resolution.extends(); if (parent) { const value = predicate(parent.node as IntrospectedClass | IntrospectedInterface); if (value) return value; const parentMap = parent.node.findParentMap(predicate); if (parentMap) return parentMap; } return findMap( resolution.implements().map((i) => i.node), (i) => predicate(i) || i.findParentMap((p: IntrospectedBaseClass) => p instanceof IntrospectedClass || p instanceof IntrospectedInterface ? predicate(p) : undefined, ), ); } private collectImplementedItems<T extends { name: string }>( getItems: (node: IntrospectedBaseClass) => T[], validate: (item: T) => boolean, ): Map<string, T> { const resolution = this.resolveParents(); const implementedOnParent = [...(resolution.extends() ?? [])].flatMap((r) => r.implements()); const items = new Map<string, T>(); 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 item of getItems(implemented.node)) { if (items.has(item.name) || !validate(item)) continue; items.set(item.name, item); } } 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 item of getItems(e.node)) { if (items.has(item.name) || !validate(item)) continue; items.set(item.name, item); } }); } // An interface's <prerequisite> of class type is always satisfied by the // implementing class's actual parent chain — those members are already // inherited via TS class inheritance, so we don't re-emit them. The one // case we still need to handle: when the parent chain has a same-named // member with an incompatible signature/type, we re-emit the interface's // version so `filterConflicts` / `filterFunctionConflict` can broaden or // override it to satisfy both `extends` and `implements` simultaneously. for (const implemented of resolution.implements()) { const extended = implemented.extends(); if (extended?.node instanceof IntrospectedClass) { for (const item of getItems(extended.node)) { if (items.has(item.name) || !validate(item)) continue; if (!hasExtendsShadowOf(this, extended.node, item.name)) continue; items.set(item.name, item); } } } return items; } implementedProperties(potentialConflicts: IntrospectedBase<never>[] = []) { const properties = this.collectImplementedItems( (node) => node.props, (prop) => !this.hasInstanceSymbol(prop) && potentialConflicts.every((p) => prop.name !== p.name), ); return [...properties.values()]; } implementedMethods(potentialConflicts: ClassMember[] = []) { const methods = this.collectImplementedItems( (node) => node.members, (method) => !(method instanceof IntrospectedStaticClassFunction) && !this.hasInstanceSymbol(method) && potentialConflicts.every((m) => method.name !== m.name), ); return [...methods.values()].map((f) => { const mapping = new Map<string, TypeExpression>(); 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(): ClassResolution { const self = this; return { *[Symbol.iterator]() { let current = this.extends(); while (current !== undefined) { yield current; current = current.extends(); } }, extends(): ClassResolution | undefined { const resolved_parent = self.superType ? resolveTypeIdentifier(self.namespace, self.superType) : undefined; if (resolved_parent instanceof IntrospectedClass) { return resolved_parent.resolveParents(); } return undefined; }, implements(): InterfaceResolution[] { return self.interfaces .map((iface) => { const resolved = resolveTypeIdentifier(self.namespace, iface); if (resolved instanceof IntrospectedInterface) { return resolved.resolveParents(); } throw new Error(`Interface ${iface.name} not found`); }) .filter((res): res is InterfaceResolution => res !== undefined); }, node: self, identifier: self.getType(), }; } copy( options: { parent?: undefined; signals?: IntrospectedSignal[]; constructors?: IntrospectedConstructor[]; members?: IntrospectedClassFunction[]; props?: IntrospectedProperty[]; fields?: IntrospectedField[]; callbacks?: IntrospectedClassCallback[]; } = {}, ): IntrospectedClass { const klass = new IntrospectedClass(this.name, this.namespace); klass.interfaces = [...this.interfaces]; klass.superType = this.superType; klass.isAbstract = this.isAbstract; klass.doc = this.doc; klass.generics = [...this.generics]; klass._staticDefinition = this._staticDefinition; const { signals = this.signals, constructors = this.constructors, members = this.members, props = this.props, fields = this.fields, callbacks = this.callbacks, } = options; klass.signals = [...signals.map((s) => s.copy({ parent: klass }))]; klass.constructors = [...constructors.map((c) => c.copy({ parent: klass }))]; klass.members = [...members.map((m) => m.copy({ parent: klass }))]; klass.props = [...props.map((p) => p.copy({ parent: klass }))]; klass.fields = [...fields.map((f) => f.copy({ parent: klass }))]; klass.callbacks = [...callbacks.map((c) => c.copy({ parent: klass }))]; if (this.mainConstructor) { klass.mainConstructor = this.mainConstructor.copy({ parent: klass }); } klass.getGenericName = createGenericNameGeneratorAt(this.getGenericName()); return klass._copyBaseProperties(this); } get staticDefinition() { return this._staticDefinition; } static fromXML(element: GirClassElement, ns: IntrospectedNamespace, options: OptionsLoad): IntrospectedClass { const name = sanitizeIdentifierName(ns.namespace, element.$.name); if (options.verbose) { log.debug(` >> GirClass: Parsing definition ${element.$.name} (${name})...`); } const clazz = new IntrospectedClass(name, ns); IntrospectedClass.parseBasicProperties(element, clazz, ns, options); IntrospectedClass.parseResolveNames(element, clazz, ns, name); IntrospectedClass.parseInheritanceAndMembers(element, clazz, ns, options); resolveNullableProperties(clazz); return clazz; } private static parseBasicProperties( element: GirClassElement, clazz: IntrospectedClass, ns: IntrospectedNamespace, options: OptionsLoad, ): void { if (options.loadDocs) { clazz.doc = parseDoc(element); clazz.metadata = parseMetadata(element); } if (element.$.parent) { clazz.superType = parseTypeIdentifier(ns.namespace, element.$.parent); } if (element.$.abstract) { clazz.isAbstract = true; } } private static parseResolveNames( element: GirClassElement, clazz: IntrospectedClass, ns: IntrospectedNamespace, name: string, ): void { if (element.$["glib:type-name"]) { clazz.resolve_names.push(element.$["glib:type-name"]); ns.registerResolveName(element.$["glib:type-name"], 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); } } private static parseInheritanceAndMembers( element: GirClassElement, clazz: IntrospectedClass, ns: IntrospectedNamespace, options: OptionsLoad, ): void { try { IntrospectedClass.parseConstructors(element, clazz, options); IntrospectedClass.parseSignals(element, clazz, options); IntrospectedClass.parseProperties(element, clazz, options); IntrospectedClass.parseMethods(element, clazz, options); IntrospectedClass.parseFields(element, clazz); IntrospectedClass.parseInterfaces(element, clazz, ns); IntrospectedClass.parseCallbacks(element, clazz, ns, options); IntrospectedClass.parseVirtualMethods(element, clazz, options); IntrospectedClass.parseStaticFunctions(element, clazz, options); } catch (e) { log.reportParsingFailure(clazz.name, "class", ns.namespace, e as Error); } } private static parseConstructors(element: GirClassElement, clazz: IntrospectedClass, options: OptionsLoad): void { if (Array.isArray(element.constructor)) { clazz.constructors.push( ...element.constructor.map((constructorElement) => IntrospectedConstructor.fromXML(constructorElement, clazz, options), ), ); } } private static parseSignals(element: GirClassElement, clazz: IntrospectedClass, options: OptionsLoad): void { if (element["glib:signal"]) { clazz.signals.push(...element["glib:signal"].map((signal) => IntrospectedSignal.fromXML(signal, clazz, options))); } } private static parseProperties(element: GirClassElement, clazz: IntrospectedClass, options: OptionsLoad): void { if (!element.property) return; element.property.forEach((prop) => { const property = IntrospectedProperty.fromXML(prop, clazz, options); switch (options.propertyCase) { case "both": { clazz.props.push(property); const camelCase = property.toCamelCase(); 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; } }); } private static parseMethods(element: GirClassElement, clazz: IntrospectedClass, options: OptionsLoad): void { if (element.method) { clazz.members.push(...element.method.map((method) => IntrospectedClassFunction.fromXML(method, clazz, options))); } } private static parseFields(element: GirClassElement, clazz: IntrospectedClass): void { if (element.field) { element.field .filter((field) => !("callback" in field)) .forEach((field) => { const f = IntrospectedField.fromXML(field, clazz); clazz.fields.push(f); }); } } private static parseInterfaces(element: GirClassElement, clazz: IntrospectedClass, ns: IntrospectedNamespace): void { if (element.implements) { element.implements.forEach((implementee) => { const name = implementee.$.name; const type = parseTypeIdentifier(ns.namespace, name); if (type) { clazz.interfaces.push(type); } }); } } private static parseCallbacks( element: GirClassElement, clazz: IntrospectedClass, ns: IntrospectedNamespace, options: OptionsLoad, ): void { if (element.callback) { if (options.verbose) { element.callback.forEach((callback) => { log.debug(`Adding callback ${callback.$.name} for ${ns.namespace}`); }); } clazz.callbacks.push( ...element.callback.map((callback) => IntrospectedClassCallback.fromXML(callback, clazz, options)), ); } } private static parseVirtualMethods(element: GirClassElement, clazz: IntrospectedClass, options: OptionsLoad): void { if (element["virtual-method"]) { clazz.members.push( ...element["virtual-method"].map((method) => IntrospectedVirtualClassFunction.fromXML(method, clazz, options)), ); } } private static parseStaticFunctions(element: GirClassElement, clazz: IntrospectedClass, options: OptionsLoad): void { if (element.function) { clazz.members.push( ...element.function.map((func) => IntrospectedStaticClassFunction.fromXML(func, clazz, options)), ); } } registerStaticDefinition(typeStruct: string) { this._staticDefinition = typeStruct; } asString<T extends FormatGenerator<unknown>>(generator: T): ReturnType<T["generateClass"]> { return generator.generateClass(this) as ReturnType<T["generateClass"]>; } } /** * Represents a GIR interface */ export class IntrospectedInterface extends IntrospectedBaseClass { interfaces: TypeIdentifier[] = []; noParent: boolean = false; accept(visitor: GirVisitor): IntrospectedInterface { 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; } someParent(predicate: (b: IntrospectedBaseClass) => boolean): boolean { const resolution = this.resolveParents(); const parent = resolution.extends(); return !!parent && (predicate(parent.node) || parent.node.someParent(predicate)); } findParent(predicate: (b: IntrospectedBaseClass) => boolean): IntrospectedBaseClass | undefined { return this.findParentMap((parent) => { if (predicate(parent)) { return parent; } return undefined; }); } findParentMap<K>(predicate: (b: IntrospectedBaseClass) => K | undefined): K | undefined { 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(): InterfaceResolution { const self = this; return { *[Symbol.iterator]() { const parentResolution = this.extends(); if (parentResolution) { yield parentResolution; yield* parentResolution; } }, extends(): InterfaceResolution | ClassResolution | undefined { // For interfaces, superType would be the prerequisite const resolved_parent = self.superType ? resolveTypeIdentifier(self.namespace, self.superType) : undefined; if (resolved_parent instanceof IntrospectedInterface) { return resolved_parent.resolveParents(); } if (resolved_parent instanceof IntrospectedClass) { return resolved_parent.resolveParents(); } return undefined; }, node: self, identifier: self.getType(), }; } copy( options: { parent?: undefined; constructors?: IntrospectedConstructor[]; members?: IntrospectedClassFunction[]; props?: IntrospectedProperty[]; fields?: IntrospectedField[]; callbacks?: IntrospectedClassCallback[]; } = {}, ): IntrospectedInterface { const iface = new IntrospectedInterface({ name: this.name, namespace: this.namespace }); iface.interfaces = [...this.interfaces]; iface.superType = this.superType; iface.doc = this.doc; iface.generics = [...this.generics]; const { constructors = this.constructors, members = this.members, props = this.props, fields = this.fields, callbacks = this.callbacks, } = options; iface.constructors = [...constructors.map((c) => c.copy({ parent: iface }))]; iface.members = [...members.map((m) => m.copy({ parent: iface }))]; iface.props = [...props.map((p) => p.copy({ parent: iface }))]; iface.fields = [...fields.map((f) => f.copy({ parent: iface }))]; iface.callbacks = [...callbacks.map((c) => c.copy({ parent: iface }))]; if (this.mainConstructor) { iface.mainConstructor = this.mainConstructor.copy({ parent: iface }); } return iface._copyBaseProperties(this); } static fromXML( element: GirInterfaceElement, namespace: IntrospectedNamespace, options: OptionsLoad, ): IntrospectedInterface { const name = sanitizeIdentifierName(namespace.namespace, element.$.name); if (options.verbose) { log.debug(` >> GirInterface: Parsing definition ${element.$.name} (${name})...`); } const iface = new IntrospectedInterface({ name, namespace }); IntrospectedInterface.parseInterfaceBasicProperties(element, iface, namespace, options); IntrospectedInterface.parseInterfaceResolveNames(element, iface, namespace, name); IntrospectedInterface.parseInterfaceMembers(element, iface, namespace, options); resolveNullableProperties(iface); return iface; } private static parseInterfaceBasicProperties( element: GirInterfaceElement, iface: IntrospectedInterface, namespace: IntrospectedNamespace, options: OptionsLoad, ): void { if (options.loadDocs) { iface.doc = parseDoc(element); iface.metadata = parseMetadata(element); } if (element.prerequisite?.[0]) { const [prerequisite] = element.prerequisite; if (prerequisite.$.name) { iface.superType = parseTypeIdentifier(namespace.namespace, prerequisite.$.name); } } } private static parseInterfaceResolveNames( element: GirInterfaceElement, iface: IntrospectedInterface, namespace: IntrospectedNamespace, name: string, ): void { if (element.$["glib:type-name"]) { iface.resolve_names.push(element.$["glib:type-name"]); namespace.registerResolveName(element.$["glib:type-name"], namespace.namespace, name); } if (element.$["c:type"]) { iface.resolve_names.push(element.$["c:type"]); namespace.registerResolveName(element.$["c:type"], namespace.namespace, name); } } private static parseInterfaceMembers( element: GirInterfaceElement, iface: IntrospectedInterface, namespace: IntrospectedNamespace, options: OptionsLoad, ): void { try { IntrospectedInterface.parseInterfaceConstructors(element, iface, options); IntrospectedInterface.parseInterfaceProperties(element, iface, options); IntrospectedInterface.parseInterfaceMethods(element, iface, options); IntrospectedInterface.parseInterfaceFields(element, iface); IntrospectedInterface.parseInterfaceCallbacks(element, iface, namespace, options); IntrospectedInterface.parseInterfaceVirtualMethods(element, iface, options); IntrospectedInterface.parseInterfaceStaticFunctions(element, iface, options); } catch (e) { log.reportParsingFailure(iface.name, "interface", namespace.namespace, e as Error); } } private static parseInterfaceConstructors( element: GirInterfaceElement, iface: IntrospectedInterface, options: OptionsLoad, ): void { if (Array.isArray(element.constructor)) { iface.constructors.push( ...element.constructor.map((constructorElement) => IntrospectedConstructor.fromXML(constructorElement, iface, options), ), ); } } private static parseInterfaceProperties( element: GirInterfaceElement, iface: IntrospectedInterface, options: OptionsLoad, ): void { if (!element.property) return; element.property.forEach((prop) => { const property = IntrospectedProperty.fromXML(prop, iface, options); switch (options.propertyCase) { case "both": { iface.props.push(property); const camelCase = property.toCamelCase(); if (property.name !== camelCase.name) { iface.props.push(camelCase); } break; } case "camel": iface.props.push(property.toCamelCase()); break; case "underscore": iface.props.push(property); break; } }); } private static parseInterfaceMethods( element: GirInterfaceElement, iface: IntrospectedInterface, options: OptionsLoad, ): void { if (element.method) { iface.members.push(...element.method.map((method) => IntrospectedClassFunction.fromXML(method, iface, options))); } } private static parseInterfaceFields(element: GirInterfaceElement, iface: IntrospectedInterface): void { if (element.field) { element.field .filter((field) => !("callback" in field)) .forEach((field) => { const f = IntrospectedField.fromXML(field, iface); iface.fields.push(f); }); } } private static parseInterfaceCallbacks( element: GirInterfaceElement, iface: IntrospectedInterface, namespace: IntrospectedNamespace, options: OptionsLoad, ): void { if (element.callback) { if (options.verbose) { element.callback.forEach((callback) => { log.debug(`Adding callback ${callback.$.name} for ${namespace.namespace}`); }); } iface.callbacks.push( ...element.callback.map((callback) => IntrospectedClassCallback.fromXML(callback, iface, options)), ); } } private static parseInterfaceVirtualMethods( element: GirInterfaceElement, iface: IntrospectedInterface, options: OptionsLoad, ): void { if (element["virtual-method"]) { iface.members.push( ...element["virtual-method"].map((method) => IntrospectedVirtualClassFunction.fromXML(method, iface, options)), ); } } private static parseInterfaceStaticFunctions( element: GirInterfaceElement, iface: IntrospectedInterface, options: OptionsLoad, ): void { if (element.function) { iface.members.push( ...element.function.map((func) => IntrospectedStaticClassFunction.fromXML(func, iface, options)), ); } } asString<T extends FormatGenerator<unknown>>(generator: T): ReturnType<T["generateInterface"]> { return generator.generateInterface(this) as ReturnType<T["generateInterface"]>; } }