UNPKG

@ts-for-gir/lib

Version:

Typescript .d.ts generator from GIR for gjs

720 lines (605 loc) 21.9 kB
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); } }