UNPKG

@ts-for-gir/lib

Version:

Typescript .d.ts generator from GIR for gjs

489 lines (401 loc) 13.7 kB
import { ConsoleReporter } from "@ts-for-gir/reporter"; import type { FormatGenerator } from "../generators/generator.ts"; import { ArrayType, ClassStructTypeIdentifier, NativeType, type TypeExpression, TypeIdentifier } from "../gir.ts"; import type { GirRecordElement, GirUnionElement, OptionsLoad, RecordResolution } from "../types/index.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 { IntrospectedBaseClass, type IntrospectedClassCallback, IntrospectedClassFunction, IntrospectedStaticClassFunction, } from "./introspected-classes.ts"; import type { IntrospectedNamespace } from "./namespace.ts"; import { IntrospectedField, type IntrospectedProperty } from "./property.ts"; import type { IntrospectedSignal } from "./signal.ts"; const log = new ConsoleReporter(true, "gir/class", true); export class IntrospectedRecord extends IntrospectedBaseClass { private _isForeign: boolean = false; private _structFor: ClassStructTypeIdentifier | null = null; private _isSimple: boolean | null = null; private _isSimpleWithoutPointers: string | null = null; private _overriddenGType: string | null = null; /** * Returns all signals for this record (records typically don't have signals) */ getAllSignals(): Array<{ name: string; signal?: IntrospectedSignal; isNotifySignal?: boolean; isDetailSignal?: boolean; parameterTypes?: string[]; returnType?: string; }> { // Records typically don't have signals, but we provide a consistent API return []; } isForeign(): boolean { return this._isForeign; } get structFor() { return this._structFor; } get gtype() { return this._overriddenGType ?? this.name; } overrideGType(gtype: string) { this._overriddenGType = gtype; } getType(): TypeIdentifier { if (this._structFor) { return this._structFor; } return new TypeIdentifier(this.name, this.namespace.namespace); } someParent(predicate: (p: IntrospectedRecord) => boolean): boolean { const resolution = this.resolveParents(); const parent = resolution.extends(); return !!parent && (predicate(parent.node) || parent.node.someParent(predicate)); } findParent(predicate: (p: IntrospectedRecord) => boolean): IntrospectedRecord | undefined { 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<K>(predicate: (p: IntrospectedRecord) => 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; } accept(visitor: GirVisitor): IntrospectedRecord { 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(): RecordResolution { 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: { parent?: undefined; constructors?: IntrospectedConstructor[]; members?: IntrospectedClassFunction[]; props?: IntrospectedProperty[]; fields?: IntrospectedField[]; callbacks?: IntrospectedClassCallback[]; } = {}, ): IntrospectedRecord { const { name, namespace, superType, members, constructors, _isForeign, _structFor, _overriddenGType, 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._overriddenGType = _overriddenGType; 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: string, namespace: IntrospectedNamespace): IntrospectedRecord { const foreignRecord = new IntrospectedRecord({ name, namespace }); foreignRecord._isForeign = true; return foreignRecord; } static fromXML( element: GirRecordElement | GirUnionElement, namespace: IntrospectedNamespace, options: OptionsLoad, ): IntrospectedRecord { 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 }); IntrospectedRecord.configureRecordProperties(element, clazz); IntrospectedRecord.setupTypeStructOrResolveNames(element, clazz, namespace, name); IntrospectedRecord.parseRecordDocumentation(element, clazz, options); IntrospectedRecord.parseRecordMembers(element, clazz, options); return clazz; } private static configureRecordProperties( element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord, ): void { const isPrivate = IntrospectedRecord.isRecordPrivate(element); clazz.setPrivate(isPrivate); const isForeign = "foreign" in element.$ && element.$.foreign === "1"; clazz._isForeign = isForeign; } private static isRecordPrivate(element: GirRecordElement | GirUnionElement): boolean { if (!element.$.name) { return false; } return ( element.$.name.startsWith("_") || ("disguised" in element.$ && element.$.disguised === "1") || ("opaque" in element.$ && element.$.opaque === "1") ); } private static setupTypeStructOrResolveNames( element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord, namespace: IntrospectedNamespace, name: string, ): void { const gtypeStructFor = "glib:is-gtype-struct-for" in element.$ ? element.$["glib:is-gtype-struct-for"] : undefined; if (typeof gtypeStructFor === "string" && gtypeStructFor) { const structFor = parseTypeIdentifier(namespace.namespace, gtypeStructFor); clazz._structFor = new ClassStructTypeIdentifier(structFor.name, structFor.namespace); } else { IntrospectedRecord.registerResolveNames(element, clazz, namespace, name); } } private static registerResolveNames( element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord, namespace: IntrospectedNamespace, name: string, ): void { 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); } } private static parseRecordDocumentation( element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord, options: OptionsLoad, ): void { if (options.loadDocs) { clazz.doc = parseDoc(element); clazz.metadata = parseMetadata(element); } } private static parseRecordMembers( element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord, options: OptionsLoad, ): void { try { IntrospectedRecord.parseRecordMethods(element, clazz, options); IntrospectedRecord.parseRecordConstructors(element, clazz, options); IntrospectedRecord.parseRecordStaticMethods(element, clazz, options); IntrospectedRecord.parseRecordFields(element, clazz); } catch (e) { log.reportParsingFailure(clazz.name, "record", clazz.namespace.namespace, e as Error); } } private static parseRecordMethods( element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord, options: OptionsLoad, ): void { if (element.method) { clazz.members.push(...element.method.map((method) => IntrospectedClassFunction.fromXML(method, clazz, options))); } } private static parseRecordConstructors( element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord, options: OptionsLoad, ): void { if (Array.isArray(element.constructor)) { element.constructor.forEach((ctor) => { const c = IntrospectedConstructor.fromXML(ctor, clazz, options); clazz.constructors.push(c); }); } } private static parseRecordStaticMethods( element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord, options: OptionsLoad, ): void { if (element.function) { clazz.members.push( ...element.function.map((func) => IntrospectedStaticClassFunction.fromXML(func, clazz, options)), ); } } private static parseRecordFields(element: GirRecordElement | GirUnionElement, clazz: IntrospectedRecord): void { if (element.field) { clazz.fields.push( ...element.field .filter((field) => !("callback" in field)) .map((field) => IntrospectedField.fromXML(field, clazz)), ); } } /** * Calculate if a type expression is "simple" without pointers */ isSimpleTypeWithoutPointers(typeContainer: TypeExpression): boolean { 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: TypeExpression): boolean { // 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<T extends FormatGenerator<unknown>>(generator: T): ReturnType<T["generateRecord"]> { return generator.generateRecord(this) as ReturnType<T["generateRecord"]>; } }