@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
489 lines (401 loc) • 13.7 kB
text/typescript
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"]>;
}
}