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