@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
720 lines (605 loc) • 21.9 kB
text/typescript
import type { GirBoxedElement, GirInfoAttrs, GirType } from "@gi.ts/parser";
import { ConsoleReporter, ReporterService } from "@ts-for-gir/reporter";
import { DependencyManager } from "./dependency-manager.ts";
import { IntrospectedAlias } from "./gir/alias.ts";
import { IntrospectedCallback } from "./gir/callback.ts";
import { IntrospectedConstant } from "./gir/const.ts";
import { IntrospectedEnum } from "./gir/enum.ts";
import { IntrospectedError } from "./gir/error.ts";
import { IntrospectedFunction } from "./gir/function.ts";
import { IntrospectedBase } from "./gir/introspected-base.ts";
import type { IntrospectedClassCallback, IntrospectedClassFunction } from "./gir/introspected-classes.ts";
import { IntrospectedBaseClass, IntrospectedClass, IntrospectedInterface } from "./gir/introspected-classes.ts";
import type { IntrospectedNamespaceMember } from "./gir/introspected-namespace-member.ts";
import type { GirNSMember } from "./gir/namespace.ts";
import type { IntrospectedFunctionParameter } from "./gir/parameter.ts";
import { IntrospectedRecord } from "./gir/record.ts";
import type { NSRegistry } from "./gir/registry.ts";
import { NullableType, ObjectType, TypeIdentifier } from "./gir.ts";
import type { LibraryVersion } from "./library-version.ts";
import type {
Dependency,
GirAliasElement,
GirBitfieldElement,
GirConstantElement,
GirEnumElement,
GirInterfaceElement,
IGirModule,
IntrospectedMetadata,
OptionsGeneration,
OptionsLoad,
TsDocTag,
} from "./types/index.ts";
import { transformGirDocTagText } from "./utils/documentation.ts";
import { isIntrospectable } from "./utils/girs.ts";
import { find } from "./utils/objects.ts";
import { isPrimitiveType } from "./utils/types.ts";
import type { GirVisitor } from "./visitor.ts";
const logger = new ConsoleReporter(false, "GirModule", false);
export class GirModule implements IGirModule {
/**
* E.g. 'Gtk'
*/
get namespace(): string {
return this.dependency.namespace;
}
/**
* E.g. '4.0'
*/
get version(): string {
return this.dependency.version;
}
/**
* E.g. 'Gtk-4.0'
*/
get packageName(): string {
return this.dependency.packageName;
}
/**
* E.g. 'Gtk40'
* Is used in the generated index.d.ts, for example: `import * as Gtk40 from "./Gtk-4.0.ts";`
*/
get importNamespace(): string {
return this.dependency.importNamespace;
}
/**
* The NPM package name E.g. 'gtk-4.0'
*/
get importName(): string {
return this.dependency.importName;
}
/**
* Import path for the package E.g. './Gtk-4.0.ts' or '@girs/Gtk-4.0'
*/
get importPath(): string {
return this.dependency.importPath;
}
prefixes: string[] = [];
/**
* The version of the library as an object.
* E.g. `{ major: 4, minor: 0, patch: 0 }` or as string `4.0.0`'
*/
get libraryVersion(): LibraryVersion {
// GObject and Gio are following the version of GLib
if (this.namespace === "GObject" || this.namespace === "Gio") {
const dep = this.allDependencies.find((girModule) => girModule.namespace === "GLib");
if (dep) {
return dep.libraryVersion;
}
}
return this.dependency.libraryVersion;
}
protected _dependencies: Dependency[] | null = null;
protected _transitiveDependencies: Dependency[] | null = null;
get dependencies(): Dependency[] {
if (!this._dependencies) {
throw new Error("dependencies is not initialized, run initDependencies() first");
}
return this._dependencies;
}
get transitiveDependencies(): Dependency[] {
if (!this._transitiveDependencies) {
throw new Error("transitiveDependencies is not initialized, run initTransitiveDependencies() first");
}
return this._transitiveDependencies;
}
get allDependencies(): Dependency[] {
if (!this.dependencies) {
throw new Error("dependencies is not initialized, run init() first");
}
return [...new Set([...this.dependencies, ...this.transitiveDependencies])];
}
dependencyManager: DependencyManager;
log!: ConsoleReporter;
extends?: string;
/**
* To prevent constants from being exported twice, the names already exported are saved here for comparison.
* Please note: Such a case is only known for Zeitgeist-2.0 with the constant "ATTACHMENT"
*/
constNames: { [varName: string]: GirConstantElement } = {};
readonly c_prefixes: string[];
readonly dependency: Dependency;
private _members?: Map<string, GirNSMember | GirNSMember[]>;
private _enum_constants?: Map<string, readonly [string, string]>;
private _resolve_names: Map<string, TypeIdentifier> = new Map();
__dts__references?: string[];
package_version!: readonly [string, string] | readonly [string, string, string];
parent!: NSRegistry;
config: OptionsGeneration;
constructor(dependency: Dependency, prefixes: string[], config: OptionsGeneration) {
this.dependency = dependency;
this.c_prefixes = [...prefixes];
this.package_version = ["0", "0"];
this.config = config;
// TODO: Make this a singleton
this.dependencyManager = DependencyManager.getInstance(this.config);
}
public async initDependencies() {
this._dependencies = await this.dependencyManager.fromGirIncludes(
this.dependency.girXML?.repository[0]?.include || [],
);
}
public async initTransitiveDependencies(transitiveDependencies: Dependency[]) {
this._transitiveDependencies = await this.checkTransitiveDependencies(transitiveDependencies);
}
get ns() {
return this;
}
private async checkTransitiveDependencies(transitiveDependencies: Dependency[]) {
// Always pull in GObject-2.0, as we may need it for e.g. GObject-2.0.type
if (this.packageName !== "GObject-2.0") {
if (!find(transitiveDependencies, (x) => x.packageName === "GObject-2.0")) {
transitiveDependencies.push(await this.dependencyManager.get("GObject", "2.0"));
}
}
// Add missing dependencies
if (this.packageName === "UnityExtras-7.0") {
if (!find(transitiveDependencies, (x) => x.packageName === "Unity-7.0")) {
transitiveDependencies.push(await this.dependencyManager.get("Unity", "7.0"));
}
}
if (this.packageName === "UnityExtras-6.0") {
if (!find(transitiveDependencies, (x) => x.packageName === "Unity-6.0")) {
transitiveDependencies.push(await this.dependencyManager.get("Unity", "6.0"));
}
}
if (this.packageName === "GTop-2.0") {
if (!find(transitiveDependencies, (x) => x.packageName === "GLib-2.0")) {
transitiveDependencies.push(await this.dependencyManager.get("GLib", "2.0"));
}
}
// Gio
if (this.packageName === "GioUnix-2.0") {
if (!find(transitiveDependencies, (x) => x.packageName === "Gio-2.0")) {
transitiveDependencies.push(await this.dependencyManager.get("Gio", "2.0"));
}
if (!find(transitiveDependencies, (x) => x.packageName === "GLib-2.0")) {
transitiveDependencies.push(await this.dependencyManager.get("GLib", "2.0"));
}
}
// Filter out the dependency with the same namespace among each other
transitiveDependencies = transitiveDependencies.filter((dep, index, self) => {
const samePackage = self.findIndex((t) => t.namespace === dep.namespace);
this.log.debug(`Filtering out dependency with same namespace: ${dep.namespace} ${index} ${samePackage}`);
return index === samePackage;
});
return transitiveDependencies;
}
getTsDocReturnTags(girElement?: IntrospectedFunction | IntrospectedClassFunction): TsDocTag[] {
const girReturnValue = girElement?.returnTypeDoc;
if (!girReturnValue) {
return [];
}
const returnTag: TsDocTag = {
tagName: "returns",
paramName: "",
text: transformGirDocTagText(girReturnValue),
};
return [returnTag];
}
getTsDocInParamTags(inParams?: IntrospectedFunctionParameter[]): TsDocTag[] {
const tags: TsDocTag[] = [];
if (!inParams?.length) {
return tags;
}
for (const inParam of inParams) {
if (inParam.name) {
tags.push({
paramName: inParam.name,
tagName: "param",
text: typeof inParam.doc === "string" ? transformGirDocTagText(inParam.doc) : "",
});
}
}
return tags;
}
getTsDocMetadataTags(metadata?: IntrospectedMetadata): TsDocTag[] {
const tags: TsDocTag[] = [];
if (metadata?.introducedVersion) {
tags.push({ tagName: "since", paramName: "", text: metadata.introducedVersion });
}
if (metadata?.deprecated) {
const text = [
metadata.deprecatedVersion ? `since ${metadata.deprecatedVersion}` : "",
metadata.deprecatedDoc ?? "",
]
.filter(Boolean)
.join(": ");
tags.push({ tagName: "deprecated", paramName: "", text });
}
return tags;
}
registerResolveName(resolveName: string, namespace: string, name: string) {
this._resolve_names.set(resolveName, new TypeIdentifier(name, namespace));
}
get members(): Map<string, GirNSMember | GirNSMember[]> {
if (!this._members) {
this._members = new Map<string, GirNSMember | GirNSMember[]>();
}
return this._members;
}
get enum_constants(): Map<string, readonly [string, string]> {
if (!this._enum_constants) {
this._enum_constants = new Map();
}
return this._enum_constants;
}
accept(visitor: GirVisitor) {
for (const key of [...this.members.keys()]) {
const member = this.members.get(key);
if (!member) continue;
if (Array.isArray(member)) {
this.members.set(
key,
member.map((m) => {
return m.accept(visitor);
}),
);
} else {
this.members.set(key, member.accept(visitor));
}
}
return this;
}
getImportsForCPrefix(c_prefix: string): GirModule[] {
return this.parent.namespacesForPrefix(c_prefix);
}
// TODO: Move this into the generator
hasImport(name: string): boolean {
return this.dependencies.some((dep) => dep.importName === name);
}
private _getImport(namespace: string): GirModule | null {
if (namespace === this.namespace) {
return this;
}
const dep =
this.dependencies?.find((dep) => dep.namespace === namespace) ??
this.transitiveDependencies.find((dep) => dep.namespace === namespace);
// Handle finding imports via their other prefixes
if (!dep) {
this.log.info(`Failed to find namespace ${namespace} in dependencies, resolving via c:prefixes`);
// TODO: It might make more sense to move this conversion _before_
// the _getImport call.
const resolvedNamespaces = this.parent.namespacesForPrefix(namespace);
if (resolvedNamespaces.length > 0) {
this.log.info(
`Found namespaces for prefix ${namespace}: ${resolvedNamespaces.map((r) => `${r.namespace} (${r.version})`).join(", ")}`,
);
}
for (const resolvedNamespace of resolvedNamespaces) {
if (resolvedNamespace.namespace === this.namespace && resolvedNamespace.version === this.version) {
return this;
}
const dep =
this.dependencies?.find(
(dep) => dep.namespace === resolvedNamespace.namespace && dep.version === resolvedNamespace.version,
) ??
this.transitiveDependencies.find(
(dep) => dep.namespace === resolvedNamespace.namespace && dep.version === resolvedNamespace.version,
);
if (dep) {
return this.parent.namespace(resolvedNamespace.namespace, dep.version);
}
}
}
let version = dep?.version;
if (!version) {
version = this.parent.assertDefaultVersionOf(namespace);
}
return this.parent.namespace(namespace, version);
}
getInstalledImport(_namespace: string): GirModule | null {
if (_namespace === this.namespace) {
return this;
}
const dep =
this.dependencies?.find((dep) => dep.namespace === _namespace) ??
this.transitiveDependencies.find((dep) => dep.namespace === _namespace);
let version = dep?.version;
if (!version) {
version = this.parent.defaultVersionOf(_namespace) ?? undefined;
}
if (!version) {
return null;
}
const namespace = this.parent.namespace(_namespace, version);
return namespace;
}
assertInstalledImport(_namespace: string): GirModule {
const namespace = this._getImport(_namespace);
if (!namespace) {
throw new Error(`Failed to import ${_namespace} in ${this.namespace}, not installed or accessible.`);
}
return namespace;
}
getMembers(name: string): IntrospectedNamespaceMember[] {
const members = this.members.get(name);
if (Array.isArray(members)) {
return [...members];
}
return members ? [members] : [];
}
getMemberWithoutOverrides(name: string) {
if (this.members.has(name)) {
const member = this.members.get(name);
if (!Array.isArray(member)) {
return member;
}
return null;
}
const resolvedName = this._resolve_names.get(name);
if (resolvedName) {
const member = this.members.get(resolvedName.name);
if (!Array.isArray(member)) {
return member;
}
}
return null;
}
assertClass(name: string): IntrospectedBaseClass {
const clazz = this.getClass(name);
if (!clazz) {
throw new Error(`[${this.packageName}] Class ${name} does not exist in namespace ${this.namespace}.`);
}
return clazz;
}
getClass(name: string): IntrospectedBaseClass | null {
const member = this.getMemberWithoutOverrides(name);
if (member instanceof IntrospectedBaseClass) {
return member;
}
return null;
}
getEnum(name: string): IntrospectedEnum | null {
const member = this.getMemberWithoutOverrides(name);
if (member instanceof IntrospectedEnum) {
return member;
}
return null;
}
getAlias(name: string): IntrospectedAlias | null {
const member = this.getMemberWithoutOverrides(name);
if (member instanceof IntrospectedAlias) {
return member;
}
return null;
}
hasSymbol(name: string) {
return this.members.has(name);
}
resolveSymbolFromTypeName(name: string): string | null {
const resolvedName = this._resolve_names.get(name);
if (!resolvedName) {
return null;
}
const member = this.members.get(resolvedName.name);
if (member instanceof IntrospectedBase) {
return member.name;
}
return null;
}
findClassCallback(name: string): [string | null, string] {
const clazzes = Array.from(this.members.values()).filter(
(m): m is IntrospectedBaseClass => m instanceof IntrospectedBaseClass,
);
// First, try to handle compound names like "ResultFlatMapFunc" -> "Result.FlatMapFunc"
// This is the main fix for the Gpseq namespace collision issue
for (const clazz of clazzes) {
// Check if the name starts with the class name (compound name pattern)
if (name.startsWith(clazz.name)) {
const potentialCallbackName = name.slice(clazz.name.length);
const callback = clazz.callbacks.find((c) => c.name === potentialCallbackName);
if (callback) {
return [clazz.name, callback.name];
}
}
}
// Find all matches using the original logic
const allMatches = clazzes
.map<[IntrospectedBaseClass, IntrospectedClassCallback | undefined]>((m) => [
m,
m.callbacks.find((c) => c.name === name || c.resolve_names.includes(name)),
])
.filter((r): r is [IntrospectedBaseClass, IntrospectedClassCallback] => r[1] != null);
if (allMatches.length === 0) {
return [null, name];
}
// If there are multiple matches, prefer more specific ones
if (allMatches.length > 1) {
this.log.warn(`Found multiple matches for ${name}: ${allMatches.map((m) => m[0].name).join(", ")}`);
}
const res = allMatches[0];
return [res[0].name, res[1].name];
}
/**
* This is an internal method to add TypeScript <reference>
* comments when overrides use parts of the TypeScript standard
* libraries that are newer than default.
*/
___dts___addReference(reference: string) {
this.__dts__references ??= [];
this.__dts__references.push(reference);
}
static async load(dependency: Dependency, config: OptionsGeneration, registry: NSRegistry): Promise<GirModule> {
const girXML = dependency.girXML;
const ns = girXML?.repository[0]?.namespace?.[0];
if (!girXML) {
throw new Error(`Failed to load gir xml of ${dependency.packageName}`);
}
if (!ns) {
const packageName = girXML.repository[0].package?.[0]?.$.name || "unknown package";
throw new Error(`Missing namespace in ${packageName}`);
}
const modName = ns.$.name;
const version = ns.$.version;
if (!modName) {
throw new Error("Invalid GIR file: no namespace name specified.");
}
if (!version) {
throw new Error("Invalid GIR file: no version name specified.");
}
const c_prefix = ns.$?.["c:identifier-prefixes"]?.split(",") ?? [];
const building = new GirModule(dependency, c_prefix, config);
await building.initDependencies();
building.parent = registry;
// Set the namespace object here to prevent re-parsing the namespace if
// another namespace imports it.
registry.register(building);
const prefixes = girXML.repository[0]?.$?.["c:identifier-prefixes"]?.split(",");
const unknownPrefixes = prefixes?.filter((pre) => pre !== modName);
if (unknownPrefixes && unknownPrefixes.length > 0) {
logger.log(`Found additional prefixes for ${modName}: ${unknownPrefixes.join(", ")}`);
building.prefixes.push(...unknownPrefixes);
}
building.log = new ConsoleReporter(
config.verbose,
`GirModule(${building.packageName})`,
config.reporter,
config.reporterOutput,
);
// Register with reporter service if reporting is enabled
if (config.reporter) {
const reporterService = ReporterService.getInstance();
reporterService.registerReporter(`GirModule(${building.packageName})`, building.log);
}
return building;
}
/** Parse and store elements into this.members using a common pattern */
private parseAndStore<TXml, TResult extends GirNSMember>(
elements: TXml[] | undefined,
fromXML: (el: TXml) => TResult,
filter?: (el: TResult) => boolean,
): void {
if (!elements) return;
let items = (elements as Array<TXml & { $?: GirInfoAttrs }>).filter(isIntrospectable).map(fromXML);
if (filter) items = items.filter(filter);
for (const item of items) {
this.members.set(item.name, item);
}
}
/** Parse enumerations, which may be either enums or error domains */
private parseEnumerations(
enumerations: GirEnumElement[] | undefined,
options: OptionsLoad,
importConflicts: (el: { name: string }) => boolean,
): void {
this.parseAndStore(
enumerations,
(enumeration) => {
if (enumeration.$["glib:error-domain"]) {
return IntrospectedError.fromXML(enumeration, this, options);
}
return IntrospectedEnum.fromXML(enumeration, this, options);
},
importConflicts,
);
}
/** Parse glib:boxed elements into aliases */
private parseBoxed(boxed: GirBoxedElement[] | undefined): void {
if (!boxed) return;
const items = boxed.filter(isIntrospectable).map(
(b) =>
new IntrospectedAlias({
name: b.$["glib:name"],
namespace: this,
type: new NullableType(ObjectType),
}),
);
for (const item of items) {
this.members.set(item.name, item);
}
}
/** Parse aliases, filtering out non-introspectable symbol references */
private parseAliases(aliases: GirAliasElement[], options: OptionsLoad): void {
type NamedType = GirType & { $: { name: string } };
const parsed = aliases
.filter(isIntrospectable)
.map((b) => {
b.type = b.type
?.filter((t): t is NamedType => !!t?.$.name)
.map((t) => {
if (t.$.name && !this.hasSymbol(t.$.name) && !isPrimitiveType(t.$.name) && !t.$.name.includes(".")) {
return { $: { name: "unknown", "c:type": "unknown" } } as GirType;
}
return t;
});
return b;
})
.map((alias) => IntrospectedAlias.fromXML(alias, this, options))
.filter((alias): alias is IntrospectedAlias => alias != null);
for (const c of parsed) {
this.members.set(c.name, c);
}
}
/** Build the enum_constants map from parsed enum members */
private buildEnumConstantsMap(): void {
for (const m of this.members.values()) {
if (m instanceof IntrospectedEnum) {
for (const member of m.members.values()) {
this.enum_constants.set(member.c_identifier, [m.name, member.name] as const);
}
}
}
}
/** Start to parse all the data from the XML we need for the typescript generation */
public parse() {
this.log.debug(`Parsing ${this.dependency.packageName}...`);
const girXML = this.dependency.girXML;
const ns = girXML?.repository[0]?.namespace?.[0];
const options: OptionsLoad = {
loadDocs: !this.config.noComments,
propertyCase: "both",
verbose: this.config.verbose,
reporter: this.config.reporter,
reporterOutput: this.config.reporterOutput,
};
if (!girXML) {
throw new Error(`Failed to load gir xml of ${this.dependency.packageName}`);
}
if (!ns) {
const packageName = girXML.repository[0].package?.[0]?.$.name || "unknown package";
throw new Error(`Missing namespace in ${packageName}`);
}
const importConflicts = (el: { name: string }) => !this.hasImport(el.name);
this.parseEnumerations(ns.enumeration as GirEnumElement[] | undefined, options, importConflicts);
this.parseAndStore(
ns.constant,
(c) => IntrospectedConstant.fromXML(c as GirConstantElement, this, options),
importConflicts,
);
this.parseAndStore(ns.function, (f) => IntrospectedFunction.fromXML(f, this, options), importConflicts);
this.parseAndStore(ns.callback, (cb) => IntrospectedCallback.fromXML(cb, this, options), importConflicts);
this.parseBoxed(ns["glib:boxed"]);
this.parseAndStore(ns.bitfield, (f) => IntrospectedEnum.fromXML(f as GirBitfieldElement, this, options, true));
this.buildEnumConstantsMap();
this.parseAndStore(ns.class, (k) => IntrospectedClass.fromXML(k, this, options), importConflicts);
this.parseAndStore(ns.record, (r) => IntrospectedRecord.fromXML(r, this, options), importConflicts);
this.parseAndStore(ns.union, (u) => IntrospectedRecord.fromXML(u, this, options), importConflicts);
this.parseAndStore(
ns.interface,
(i) => IntrospectedInterface.fromXML(i as GirInterfaceElement, this, options),
importConflicts,
);
if (ns.alias) this.parseAliases(ns.alias, options);
}
}