nihilqui
Version:
Typescript .d.ts generator from GIR for gjs and node-gtk
1,459 lines (1,263 loc) • 107 kB
text/typescript
// TODO move this class into a web-worker? https://www.npmjs.com/package/web-worker
import {
Transformation,
FULL_TYPE_MAP,
PRIMITIVE_TYPE_MAP,
ARRAY_TYPE_MAP,
IGNORE_GIR_TYPE_TS_DOC_TYPES,
} from './transformation.js'
import { STATIC_NAME_ALREADY_EXISTS, MAX_CLASS_PARENT_DEPTH } from './constants.js'
import { Logger } from './logger.js'
import { Injector } from './injection/injector.js'
import { GirFactory } from './gir-factory.js'
import { ConflictResolver } from './conflict-resolver.js'
import { DependencyManager } from './dependency-manager.js'
import {
NO_TSDATA,
WARN_NOT_FOUND_TYPE,
WARN_CONSTANT_ALREADY_EXPORTED,
WARN_DUPLICATE_SYMBOL,
WARN_DUPLICATE_PARAMETER,
WARN_DUPLICATE_ENUM_IDENTIFIER,
} from './messages.js'
import { isEqual, find, clone, girBool, removeNamespace, addNamespace, girElementIsIntrospectable } from './utils.js'
import { SymTable } from './symtable.js'
import { LibraryVersion } from './library-version.js'
import { GirDirection } from './types/index.js'
import type {
Dependency,
GirRepository,
GirNamespace,
GirAliasElement,
GirEnumElement,
GirMemberElement,
GirFunctionElement,
GirClassElement,
GirArrayType,
GirType,
GirCallableParams,
GirCallableParamElement,
GirVirtualMethodElement,
GirSignalElement,
GirCallableReturn,
GirRecordElement,
GirCallbackElement,
GirConstantElement,
GirBitfieldElement,
GirFieldElement,
GirMethodElement,
GirPropertyElement,
GirAnyElement,
GirUnionElement,
GirInstanceParameter,
GirInterfaceElement,
GirConstructorElement,
GirDocElement,
TypeGirVariable,
TypeGirClass,
TypeGirEnumerationMember,
LocalNameCheck,
LocalNameType,
LocalName,
LocalNames,
TsDoc,
TsDocTag,
TsClass,
TsMethod,
TsFunction,
TsProperty,
TsVar,
TsParameter,
TsInstanceParameter,
TsCallbackInterface,
TsMember,
TsEnum,
TsAlias,
TsType,
TsGenericParameter,
TsCallback,
InheritanceTable,
ParsedGir,
GenerateConfig,
ClassParent,
InjectionParameter,
TsSignal,
PromisifyFunc,
} from './types/index.js'
export class GirModule {
/**
* Array of all gir modules
*/
static allGirModules: GirModule[] = []
/**
* E.g. 'Gtk'
*/
namespace: string
/**
* E.g. '4.0'
*/
version = '0.0'
/**
* E.g. 'Gtk-4.0'
*/
packageName: string
/**
* E.g. 'Gtk40'
* Is used in the generated index.d.ts, for example: `import * as Gtk40 from "./Gtk-4.0.js";`
*/
importNamespace: string
importName: string
/**
* The version of the library as an object.
* E.g. `{ major: 4, minor: 0, patch: 0 }` or as string `4.0.0`'
*/
libraryVersion: LibraryVersion
dependencies: Dependency[] = []
private _transitiveDependencies: Dependency[] = []
set transitiveDependencies(deps: Dependency[]) {
this._transitiveDependencies = this.checkTransitiveDependencies(deps)
}
get transitiveDependencies(): Dependency[] {
return this._transitiveDependencies
}
get allDependencies(): Dependency[] {
return [...new Set([...this.dependencies, ...this.transitiveDependencies])]
}
repo: GirRepository
ns: GirNamespace = { $: { name: '', version: '0.0' } }
/**
* Used to find namespaces that are used in other modules
*/
symTable: SymTable
transformation: Transformation
girFactory = new GirFactory()
dependencyManager: DependencyManager
conflictResolver: ConflictResolver
log: Logger
inject: Injector
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 } = {}
constructor(xml: ParsedGir, private readonly config: GenerateConfig) {
this.repo = xml.repository
if (!this.repo.namespace || !this.repo.namespace.length) {
throw new Error(`Namespace not found!`)
}
this.dependencyManager = DependencyManager.getInstance(this.config)
this.dependencies = this.dependencyManager.fromGirIncludes(this.repo.include || [])
this.ns = this.repo.namespace[0]
this.namespace = this.ns.$.name
this.version = this.ns.$.version
this.packageName = `${this.namespace}-${this.version}`
this.libraryVersion = new LibraryVersion(this.ns.constant, this.version)
this.transformation = new Transformation(config)
this.log = new Logger(config.environment, config.verbose, this.packageName || 'GirModule')
this.conflictResolver = new ConflictResolver(config.environment, config.verbose)
this.inject = new Injector(this.config.environment)
this.importNamespace = this.transformation.transformModuleNamespaceName(this.packageName)
this.importName = this.transformation.transformImportName(this.packageName)
this.symTable = new SymTable(this.config, this.packageName, this.namespace)
}
private 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(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(this.dependencyManager.get('Unity', '7.0'))
}
}
if (this.packageName === 'UnityExtras-6.0') {
if (!find(transitiveDependencies, (x) => x.packageName === 'Unity-6.0')) {
transitiveDependencies.push(this.dependencyManager.get('Unity', '6.0'))
}
}
if (this.packageName === 'GTop-2.0') {
if (!find(transitiveDependencies, (x) => x.packageName === 'GLib-2.0')) {
transitiveDependencies.push(this.dependencyManager.get('GLib', '2.0'))
}
}
return transitiveDependencies
}
private getTsDoc(girDoc: GirDocElement) {
const tsDoc: TsDoc = {
text: '',
tags: [],
}
if (girDoc.doc?.[0]?._) {
let text = girDoc.doc?.[0]?._ || ''
text = this.transformation.transformGirDocText(text)
tsDoc.text = text
}
return tsDoc
}
private getTsDocGirElementTags(tsTypeName?: string, girTypeName?: string): TsDocTag[] {
const tags: TsDocTag[] = []
if (!girTypeName || IGNORE_GIR_TYPE_TS_DOC_TYPES.includes(girTypeName)) {
return tags
}
tags.push({
tagName: girTypeName,
paramName: '',
text: '',
})
return tags
}
private getTsDocReturnTags(
girElement?:
| GirCallbackElement
| GirConstructorElement
| GirFunctionElement
| GirMethodElement
| GirSignalElement
| GirVirtualMethodElement,
): TsDocTag[] {
const girReturnValue = girElement?.['return-value']?.[0]
if (!girReturnValue || !girReturnValue.doc?.[0]?._) {
return []
}
const returnTag: TsDocTag = {
tagName: 'returns',
paramName: '',
text: this.transformation.transformGirDocTagText(girReturnValue.doc[0]._),
}
return [returnTag]
}
private getTsDocInParamTags(inParams?: GirCallableParamElement[]): TsDocTag[] {
const tags: TsDocTag[] = []
if (!inParams?.length) {
return tags
}
for (const inParam of inParams) {
if (!inParam._tsData) {
throw new Error(NO_TSDATA('getTsDocInParamTags'))
}
if (!inParam._tsData?.doc) {
inParam._tsData.doc = this.getTsDoc(inParam)
}
if (inParam._tsData?.name) {
tags.push({
paramName: inParam._tsData.name,
tagName: 'param',
text: inParam._tsData.doc.text
? this.transformation.transformGirDocTagText(inParam._tsData.doc.text)
: '',
})
}
}
return tags
}
private annotateFunctionArguments(
girFunc:
| GirMethodElement
| GirFunctionElement
| GirConstructorElement
| GirVirtualMethodElement
| GirCallbackElement
| GirSignalElement,
): void {
const funcName = girFunc._fullSymName
if (funcName && girFunc.parameters) {
for (const girParams of girFunc.parameters) {
if (girParams.parameter) {
for (const girParam of girParams.parameter) {
girParam._module = this
if (girParam.$ && girParam.$.name) {
girParam._fullSymName = `${funcName}.${girParam.$.name}`
}
}
}
}
}
}
private annotateFunctionReturn(
girFunc:
| GirMethodElement
| GirFunctionElement
| GirConstructorElement
| GirVirtualMethodElement
| GirCallbackElement
| GirSignalElement,
): void {
const retVals = girFunc['return-value']
if (retVals && girFunc._fullSymName)
for (const retVal of retVals) {
retVal._module = this
retVal.girTypeName = 'callable-return'
if (retVal.$ && retVal.$.name) {
retVal._fullSymName = `${girFunc._fullSymName}.${retVal.$.name}`
}
}
}
/**
* Functions which are not part of a class
* @param girFuncs
*/
private annotateFunctions(girFuncs: GirFunctionElement[] | GirCallbackElement[]): void {
if (Array.isArray(girFuncs))
for (const girFunc of girFuncs) {
if (girFunc?.$?.name) {
// girFunc._girType = girType
girFunc._fullSymName = `${this.namespace}.${girFunc.$.name}`
this.annotateFunctionArguments(girFunc)
this.annotateFunctionReturn(girFunc)
}
}
}
/**
* Functions and methods of a class
*/
private annotateMethods(
girClass: GirClassElement | GirRecordElement | GirInterfaceElement,
girFuncs:
| GirMethodElement[]
| GirFunctionElement[]
| GirConstructorElement[]
| GirVirtualMethodElement[]
| GirSignalElement[],
): void {
if (Array.isArray(girFuncs))
for (const girFunc of girFuncs) {
if (girFunc?.$?.name) {
// girFunc._girType = girType
// girFunc._tsType = tsType
girFunc._class = girClass
const nsName = girClass ? girClass._fullSymName : this.namespace
if (nsName) girFunc._fullSymName = `${nsName}.${girFunc.$.name}`
this.annotateFunctionArguments(girFunc)
this.annotateFunctionReturn(girFunc)
}
}
}
/**
* Variables which are not part of a class
*/
private annotateVariables(girVars: GirConstantElement[]): void {
for (const girVar of girVars) {
girVar._module = this
if (girVar.$ && girVar.$.name) {
girVar._fullSymName = `${this.namespace}.${girVar.$.name}`
}
}
}
private annotateFields(
girClass: GirClassElement | GirRecordElement | GirInterfaceElement | null,
girVars: GirPropertyElement[],
): void
private annotateFields(
girClass: GirClassElement | GirRecordElement | GirInterfaceElement | null,
girVars: GirFieldElement[],
): void
/**
* Fields are variables which are part of a class
* @see https://www.typescriptlang.org/docs/handbook/2/classes.html#fields
*/
private annotateFields(
girClass: GirClassElement | GirRecordElement | GirInterfaceElement,
girVars: GirPropertyElement[] | GirFieldElement[],
): void {
for (const girVar of girVars) {
const nsName = girClass ? girClass._fullSymName : this.namespace
girVar._module = this
if (girClass) {
girVar._class = girClass
}
if (girVar.$ && girVar.$.name && nsName) {
girVar._fullSymName = `${nsName}.${girVar.$.name}`
}
}
}
private annotateClass(girClass: GirClassElement, girTypeName: 'class'): void
private annotateClass(girClass: GirRecordElement, girTypeName: 'record'): void
private annotateClass(girClass: GirInterfaceElement, girTypeName: 'interface'): void
private annotateClass(girClass: GirClassElement | GirRecordElement | GirInterfaceElement) {
girClass._module = this
girClass._fullSymName = `${this.namespace}.${girClass.$.name}`
const constructors = Array.isArray(girClass.constructor) ? girClass.constructor : []
const signals = ((girClass as GirClassElement | GirInterfaceElement).signal ||
girClass['glib:signal'] ||
[]) as GirSignalElement[]
const functions = girClass.function || []
const methods = girClass.method || []
const vMethods = (girClass as GirClassElement)['virtual-method'] || new Array<GirVirtualMethodElement>()
const properties = girClass.property
const fields = girClass.field
this.annotateMethods(girClass, constructors)
this.annotateMethods(girClass, functions)
this.annotateMethods(girClass, methods)
this.annotateMethods(girClass, vMethods)
this.annotateMethods(girClass, signals)
if (properties) this.annotateFields(girClass, properties)
if (fields) this.annotateFields(girClass, fields)
}
/**
* Annotates the custom `_module`, `_fullSymName` and `_girType` properties.
* Also registers the element on the `symTable`.
* @param girElements
* @param girType
*/
private annotateAndRegisterGirElement(girElements: GirAnyElement[]): void {
for (const girElement of girElements) {
if (girElement?.$ && girElement.$.name) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
if (!girElementIsIntrospectable(girElement as any)) continue
girElement._module = this
girElement._fullSymName = `${this.namespace}.${girElement.$.name}`
if (this.symTable.get(this.allDependencies, girElement._fullSymName)) {
this.log.warn(WARN_DUPLICATE_SYMBOL(girElement._fullSymName))
}
this.symTable.set(this.allDependencies, girElement._fullSymName, girElement)
}
}
}
/**
* TODO: find better name for this method
* @param girVar
* @param fullTypeName
* @returns e.g.
* ```ts
* {
* namespace: "Gtk",
* resValue: "Gtk.Widget"
* }
*
*/
private fullTypeLookup(
girVar:
| GirCallableParamElement
| GirCallableReturn
| GirFieldElement
| GirAliasElement
| GirFieldElement
| GirPropertyElement
| GirConstantElement,
fullTypeName: string | null,
) {
let resValue = ''
let namespace = ''
if (!fullTypeName) {
return {
value: resValue,
fullTypeName,
namespace,
}
}
// Fully qualify our type name
if (!fullTypeName.includes('.')) {
let tryFullTypeName = ''
if (!resValue && girVar._module && girVar._module !== this) {
tryFullTypeName = `${girVar._module.namespace}.${fullTypeName}`
resValue = this.fullTypeLookupWithNamespace(tryFullTypeName).value
if (resValue) {
fullTypeName = tryFullTypeName
namespace = girVar._module.namespace
}
}
if (!resValue) {
tryFullTypeName = `${this.namespace}.${fullTypeName}`
resValue = this.fullTypeLookupWithNamespace(tryFullTypeName).value
if (resValue) {
fullTypeName = tryFullTypeName
namespace = this.namespace
}
}
const girClass = (
girVar as
| GirCallableParamElement
| GirCallableReturn
| GirFieldElement
| GirAliasElement
| GirFieldElement
| GirPropertyElement
)._class as GirClassElement | undefined
if (!resValue && girClass?._module?.namespace && girClass._module !== this) {
tryFullTypeName = `${girClass._module.namespace}.${fullTypeName}`
resValue = this.fullTypeLookupWithNamespace(tryFullTypeName).value
if (resValue) {
fullTypeName = tryFullTypeName
namespace = girClass?._module?.namespace
}
}
}
if (!resValue && fullTypeName) {
resValue = this.fullTypeLookupWithNamespace(fullTypeName).value
}
return {
value: resValue,
namespace,
}
}
/**
* this method needs to be refactored, an array can also be an array of an array for example
* @param girVar
* @returns
*/
getArrayData(
girVar:
| GirCallableReturn
| GirAliasElement
| GirFieldElement
| GirCallableParamElement
| GirPropertyElement
| GirConstantElement,
) {
let arrayType: GirType | null = null
let arrCType: string | undefined
let isArray = false
let overrideTypeName: string | undefined
let typeArray: GirType[] | undefined
let collection: GirArrayType[] | GirType[] | undefined
if ((girVar as GirCallableReturn | GirFieldElement).array) {
collection = (girVar as GirCallableReturn | GirFieldElement).array
} else if (/^GLib.S?List$/.test(girVar.type?.[0].$?.name || '')) {
// This converts GLib.List<T> / GLib.SList<T> to T[]
collection = girVar.type
}
if (collection && collection.length > 0) {
isArray = true
typeArray = collection[0].type
if (collection[0].$) {
const ea = collection[0].$
arrCType = ea['c:type']
}
}
if (typeArray && typeArray?.length > 0) {
arrayType = typeArray[0]
}
if (isArray && arrayType?.$?.name && ARRAY_TYPE_MAP[arrayType.$.name]) {
isArray = false
overrideTypeName = ARRAY_TYPE_MAP[arrayType.$.name] as string | undefined
}
return {
arrCType,
arrayType,
isArray,
overrideTypeName,
}
}
/**
* Get the typescript type of a GirElement like a `GirPropertyElement` or `GirCallableReturn`
* @param girVar
*/
private getTsType(
girVar:
| GirCallableReturn
| GirAliasElement
| GirFieldElement
| GirCallableParamElement
| GirPropertyElement
| GirConstantElement,
tsClass: TsClass | null,
defaults: Partial<TsType> = {},
) {
let type: GirType | undefined = girVar.type?.[0]
let fullTypeName: string | null = null
let typeName = defaults.type || ''
let isFunction = defaults.isFunction || false
let isCallback = defaults.isCallback || false
const nullable = this.typeIsNullable(girVar) || defaults.nullable || false
const optional = this.typeIsOptional(girVar) || defaults.optional || false
const girCallbacks: GirCallbackElement[] = []
const array = this.getArrayData(girVar)
if (array.overrideTypeName) {
typeName = array.overrideTypeName
}
if (array.arrayType) {
type = array.arrayType
}
const cType = type?.$ ? type.$['c:type'] : array.arrCType
fullTypeName = type?.$?.name || null
const callbacks = (girVar as GirFieldElement).callback
if (!typeName && callbacks?.length) {
for (const girCallback of callbacks) {
if (!girElementIsIntrospectable(girCallback)) continue
if (!girCallback._tsData) {
const tsCallback = this.getFunctionTsData(girCallback, 'callback', tsClass, {
isStatic: false,
isArrowType: true,
isGlobal: false,
isVirtual: false,
returnType: null,
generics: [],
})
if (!tsCallback) continue
girCallback._tsData = {
...tsCallback,
girTypeName: 'callback',
tsTypeName: this.girFactory.girTypeNameToTsTypeName('callback', false),
tsCallbackInterface: this.getCallbackInterfaceTsData(girCallback),
doc: this.getTsDoc(girCallback),
overloads: [],
}
}
if (girCallback._tsData) {
girCallbacks.push(girCallback)
isFunction = true
isCallback = true
}
}
}
if (!isFunction) {
const res = this.fullTypeLookup(girVar, fullTypeName)
if (res.value) {
typeName = res.value
fullTypeName = typeName
}
}
if (!typeName && type?.$?.name && PRIMITIVE_TYPE_MAP[type.$.name]) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
typeName = PRIMITIVE_TYPE_MAP[type.$.name]
}
if (cType) {
const parsedCType = PRIMITIVE_TYPE_MAP[cType] as string | undefined
if (!typeName && parsedCType) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
typeName = parsedCType
}
}
if (!typeName) {
typeName = 'any'
const logName = cType || fullTypeName || girVar.$.name || ''
this.log.warn(WARN_NOT_FOUND_TYPE(logName))
}
// TODO: transform array to type with generics?
const tsType = this.girFactory.newTsType({
...defaults,
type: typeName,
optional,
nullable,
callbacks: girCallbacks,
isArray: array.isArray,
isFunction,
isCallback,
})
return tsType
}
/**
*
* @param girFunc
* @returns
*/
private getReturnType(
girFunc:
| GirMethodElement
| GirFunctionElement
| GirConstructorElement
| GirCallbackElement
| GirSignalElement
| GirVirtualMethodElement,
tsClass: TsClass | null,
generics: TsGenericParameter[] = [],
) {
let outArrayLengthIndex = -1
if (girFunc['return-value'] && girFunc['return-value'].length > 1) {
throw new Error('Several return values found!')
}
// There are no multiple return values, so we always use index 0
const girVar = girFunc['return-value']?.[0] || null
// We still use an array to allow multiple return values for later
const returnTypes: TsType[] = []
if (girVar) {
returnTypes.push(this.getTsType(girVar, tsClass, { generics }))
outArrayLengthIndex = girVar.array && girVar.array[0].$?.length ? Number(girVar.array[0].$.length) : -1
} else {
returnTypes.push(this.girFactory.newTsType({ type: 'void', generics }))
}
const retTypeIsVoid = returnTypes.length === 1 && returnTypes[0].type === 'void'
return { returnTypes, outArrayLengthIndex, retTypeIsVoid }
}
private arrayLengthIndexLookup(girVar: GirCallableParamElement): number {
if (!girVar.array) return -1
const arr: GirArrayType = girVar.array[0]
if (!arr.$) return -1
if (arr.$.length) {
return parseInt(arr.$.length)
}
return -1
}
private closureDataIndexLookup(girVar: GirCallableParamElement): number {
if (!girVar.$.closure) return -1
return parseInt(girVar.$.closure)
}
private destroyDataIndexLookup(girVar: GirCallableParamElement): number {
if (!girVar.$.destroy) return -1
return parseInt(girVar.$.destroy)
}
private processParams(
params: GirCallableParamElement[],
skip: GirCallableParamElement[],
getIndex: (param: GirCallableParamElement) => number,
): void {
for (const param of params) {
const index = getIndex(param)
if (index < 0) continue
if (index >= params.length) continue
skip.push(params[index])
}
}
/**
* Checks if the parameter is nullable (which results in ` | null`).
*
* @param girVar girVar to test
*/
private typeIsNullable(
girVar:
| GirCallableParamElement
| GirCallableReturn
| GirAliasElement
| GirFieldElement
| GirConstantElement
| GirPropertyElement,
): boolean {
const a = (girVar as GirCallableParamElement).$
if (!a) return false
const type: GirType | undefined = girVar.type?.[0]
const cType = type?.$?.['c:type']
// UTF-8 string pointers can be null, e.g. `gchar*`, see https://github.com/gjsify/ts-for-gir/issues/108
if (type?.$?.name === 'utf8' && cType?.endsWith('*')) {
return true
}
// If the default value is NULL, handle this as nullable
if (a['default-value'] === 'NULL') return true
// Ignore depreciated `allow-none` if one of the new implementation `optional` or `nullable` is set
if (a.optional || a.nullable) {
return girBool(a.nullable)
} else {
return girBool(a.nullable) || girBool(a['allow-none']) || girBool(a['null-ok'])
}
}
/**
* Checks if the parameter is optional (which results in `foo?: bar`).
* @param girVar girVar to test
*/
private typeIsOptional(
girVar:
| GirCallableParamElement
| GirCallableReturn
| GirAliasElement
| GirFieldElement
| GirConstantElement
| GirPropertyElement,
): boolean {
const a = (girVar as GirCallableParamElement).$
if (!a) return false
// Ignore depreciated `allow-none` if one of the new implementation `optional` or `nullable` is set
if (a.optional || a.nullable) {
return girBool(a.optional)
} else {
return girBool(a.optional) || girBool(a['allow-none']) || girBool(a['null-ok'])
}
}
private setParameterTsData(
girParam: GirCallableParamElement,
girParams: GirCallableParamElement[],
paramNames: string[],
skip: GirCallableParamElement[],
parent: TsFunction | TsSignal,
) {
// I think it's safest to force inout params to have the
// same type for in and out
const tsType = this.getTsType(girParam, parent.parent)
// const optDirection = girParam.$.direction
if (girParam._tsData) {
// this.log.warn('[setParameterTsData] _tsData already set!')
return girParam._tsData
}
if (tsType.isCallback) {
throw new Error('Callback type is not implemented here')
}
let paramName = this.transformation.transformParameterName(girParam, false)
if (paramNames.includes(paramName)) {
this.log.warn(WARN_DUPLICATE_PARAMETER(paramName, girParam._fullSymName))
paramName += '_'
}
paramNames.push(paramName)
// In Typescript no optional parameters are allowed if the following ones are not optional
if (tsType.optional) {
const index = girParams.indexOf(girParam)
const following = girParams
.slice(index)
.filter(() => skip.indexOf(girParam) === -1)
.filter((p) => p.$.direction !== GirDirection.Out)
if (following.some((p) => !this.typeIsOptional(p))) {
tsType.optional = false
}
}
const tsParam: TsParameter = {
name: paramName,
type: [tsType],
isRest: false,
girTypeName: 'callable-param',
doc: this.getTsDoc(girParam),
parent,
}
girParam._tsData = tsParam
// // TODO: remove this, wee need a special solution for Gio.AsyncReadyCallback instead
girParam = this.inject.toParameterType(girParam)
return girParam._tsData
}
private getInstanceParameterTsData(instanceParameter: GirInstanceParameter): TsInstanceParameter | undefined {
const typeName = instanceParameter.type?.[0]?.$?.name || undefined
const rec = typeName ? this.ns.record?.find((r) => r.$.name == typeName) : undefined
const structFor = rec?.$['glib:is-gtype-struct-for']
if (structFor && instanceParameter.$.name) {
// TODO: Should use of a constructor, and even of an instance, be discouraged?
return {
name: instanceParameter.$.name,
structFor,
}
}
return undefined
}
private setParametersTsData(
outArrayLengthIndex: number,
girParams: GirCallableParams[] = [],
parent: TsFunction | TsCallback,
) {
const outParams: GirCallableParamElement[] = []
const inParams: GirCallableParamElement[] = []
const paramNames: string[] = []
const instanceParameters: GirInstanceParameter[] = []
if (girParams && girParams.length > 0) {
for (const girParam of girParams) {
const params = girParam?.parameter || []
// Instance parameter needs to be exposed for class methods (see comment above getClassMethods())
const instanceParameter = girParams[0]['instance-parameter']?.[0]
if (instanceParameter) {
instanceParameter._tsData = this.getInstanceParameterTsData(instanceParameter)
if (instanceParameter._tsData) {
instanceParameters.push(instanceParameter)
}
}
if (params.length) {
const skip = outArrayLengthIndex === -1 ? [] : [params[outArrayLengthIndex]]
this.processParams(params, skip, (girVar) => this.arrayLengthIndexLookup(girVar))
this.processParams(params, skip, (girVar) => this.closureDataIndexLookup(girVar))
this.processParams(params, skip, (girVar) => this.destroyDataIndexLookup(girVar))
for (const param of params) {
if (skip.includes(param)) {
continue
}
param._tsData = this.setParameterTsData(param, params, paramNames, skip, parent)
const optDirection = param.$.direction
if (
optDirection === GirDirection.Out ||
optDirection === GirDirection.Inout ||
optDirection === GirDirection.InOut
) {
outParams.push(param)
if (optDirection === GirDirection.Out) continue
}
inParams.push(param)
}
}
}
}
return { outParams, paramNames, inParams, instanceParameters }
}
private getVariableTsData(
girVar: GirPropertyElement,
girTypeName: 'property',
tsTypeName: 'property' | 'constructor-property' | 'static-property',
tsClass: TsClass | null,
optional: boolean,
nullable: boolean,
allowQuotes: boolean,
): GirPropertyElement['_tsData']
private getVariableTsData(
girVar: GirConstantElement,
girTypeName: 'constant',
tsTypeName: 'constant',
tsClass: TsClass | null,
optional: boolean,
nullable: boolean,
allowQuotes: boolean,
): GirConstantElement['_tsData']
private getVariableTsData(
girVar: GirFieldElement,
girTypeName: 'field',
tsTypeName: 'property' | 'static-property',
tsClass: TsClass | null,
optional: boolean,
nullable: boolean,
allowQuotes: boolean,
): GirFieldElement['_tsData']
private getVariableTsData(
girVar: GirPropertyElement | GirFieldElement | GirConstantElement,
girTypeName: 'property' | TypeGirVariable | 'field',
tsTypeName: 'constant' | 'property' | 'constructor-property' | 'static-property',
tsClass: TsClass | null,
optional = false,
nullable = false,
allowQuotes = false,
generics: TsGenericParameter[] = [],
) {
if (!girVar.$.name) return undefined
if (
!girVar ||
!girVar.$ ||
!girBool(girVar.$.introspectable, true) ||
girBool((girVar as GirFieldElement).$.private)
) {
return undefined
}
if (girVar._tsData) {
// this.log.warn('[getVariableTsData] _tsData already set!')
return girVar._tsData
}
let name = girVar.$.name
switch (girTypeName) {
case 'property':
name = this.transformation.transformPropertyName(girVar.$.name, allowQuotes)
break
case 'constant':
name = this.transformation.transformConstantName(girVar.$.name, allowQuotes)
break
case 'field':
name = this.transformation.transformFieldName(girVar.$.name, allowQuotes)
break
}
// Use the out type because it can be a union which isn't appropriate
// for a property
const tsType = this.getTsType(girVar, tsClass, { optional, nullable, generics })
const tsData: TsProperty | TsVar = {
name,
type: [tsType],
isInjected: false,
girTypeName,
tsTypeName,
doc: this.getTsDoc(girVar),
}
tsData.doc.tags.push(...this.getTsDocGirElementTags(tsData.tsTypeName, tsData.girTypeName))
return tsData
}
private getPropertyTsData(
girProp: GirPropertyElement,
girTypeName: 'property',
tsTypeName: 'property' | 'constructor-property' | 'static-property',
tsClass: TsClass,
construct?: boolean,
optional?: boolean,
nullable?: boolean,
indentCount?: number,
): TsProperty | undefined
private getPropertyTsData(
girProp: GirFieldElement,
girTypeName: 'field',
tsTypeName: 'property' | 'static-property',
tsClass: TsClass,
construct?: boolean,
optional?: boolean,
nullable?: boolean,
indentCount?: number,
): TsVar | undefined
/**
*
* @param girProp
* @param girTypeName
* @param tsTypeName
* @param construct construct means include the property even if it's construct-only,
* @param optional optional means if it's construct-only it will also be marked optional (?)
* @param nullable
* @returns
*/
private getPropertyTsData(
girProp: GirPropertyElement | GirFieldElement,
girTypeName: 'property' | 'field',
tsTypeName: 'constructor-property' | 'property' | 'static-property',
tsClass: TsClass,
construct = false,
optional?: boolean,
nullable?: boolean,
): TsProperty | undefined {
if (!girBool(girProp.$.writable) && construct) return undefined
if (girBool((girProp as GirFieldElement).$.private)) return undefined
if (girProp._tsData) {
// this.log.warn('[getPropertyTsData] _tsData already set!')
return girProp._tsData as TsProperty
}
if (optional === undefined) optional = this.typeIsOptional(girProp)
if (nullable === undefined) nullable = this.typeIsNullable(girProp)
const readonly =
!girBool(girProp.$.writable) || (!construct && girBool((girProp as GirPropertyElement).$['construct-only']))
let tsData: TsProperty | undefined
switch (girTypeName) {
case 'property':
tsData = this.getVariableTsData(
girProp as GirPropertyElement,
girTypeName,
tsTypeName,
tsClass,
construct && optional,
construct && nullable,
true,
) as TsProperty
break
case 'field':
if (tsTypeName !== 'property') {
throw new Error(`Wrong tsType: "${tsTypeName}" for girType: "${girTypeName}`)
}
tsData = this.getVariableTsData(
girProp as GirFieldElement,
girTypeName,
tsTypeName,
tsClass,
construct && optional,
construct && nullable,
true,
) as TsProperty
break
default:
throw new Error(`Unknown property type: ${girTypeName as string}`)
}
if (!tsData?.name) {
return undefined
}
tsData = {
...tsData,
readonly,
}
return tsData
}
/**
*
* @param girFunc
* @param prefix E.g. vfunc
* @param overrideReturnType
* @param isArrowType
* @param indentCount
*/
private getFunctionTsData(
girFunc:
| GirMethodElement
| GirFunctionElement
| GirConstructorElement
| GirCallbackElement
| GirVirtualMethodElement,
girTypeName: 'virtual' | 'method' | 'constructor' | 'function' | 'callback' | 'static-function',
tsClass: TsClass | null,
overwrite: {
isStatic: boolean
isArrowType: boolean
isGlobal: boolean
isVirtual: boolean
isInjected?: boolean
returnType: string | null
generics: TsGenericParameter[]
},
): TsFunction | undefined {
if (!girFunc || !girFunc.$ || !girBool(girFunc.$.introspectable, true) || girFunc.$['shadowed-by']) {
return undefined
}
let hasUnresolvedConflict: boolean | undefined
// TODO: Fix that we overwrite tsData every time seems wrong to me, but if I just return the already defined `_tsData` leads to problems with the overload methods
if (girFunc._tsData) {
hasUnresolvedConflict = girFunc._tsData?.hasUnresolvedConflict // WORKAROUND do not overwrite conflicts
}
let name = girFunc.$.name
const { returnTypes, outArrayLengthIndex, retTypeIsVoid } = this.getReturnType(girFunc, tsClass)
const shadows = (girFunc as GirMethodElement).$.shadows
if (shadows) {
name = shadows
}
// Overwrites
overwrite.isStatic = overwrite.isStatic || girTypeName === 'static-function' || girTypeName === 'constructor'
overwrite.isGlobal = overwrite.isGlobal || girTypeName === 'function'
overwrite.isVirtual = overwrite.isVirtual || girTypeName === 'virtual'
overwrite.isInjected = overwrite.isInjected || false
// Function name transformation by environment
name = this.transformation.transformFunctionName(name, overwrite.isVirtual)
const tsData: TsFunction = {
isArrowType: overwrite.isArrowType,
isStatic: overwrite.isStatic,
isGlobal: overwrite.isGlobal,
isVirtual: overwrite.isVirtual,
isInjected: overwrite.isInjected,
returnTypes,
retTypeIsVoid,
name,
overrideReturnType: overwrite.returnType || undefined,
inParams: [],
outParams: [],
instanceParameters: [],
generics: overwrite.generics,
hasUnresolvedConflict,
girTypeName,
tsTypeName: this.girFactory.girTypeNameToTsTypeName(girTypeName, overwrite.isStatic),
doc: this.getTsDoc(girFunc as GirDocElement),
overloads: [],
parent: tsClass,
}
const { inParams, outParams, instanceParameters } = this.setParametersTsData(
outArrayLengthIndex,
girFunc.parameters,
tsData,
)
tsData.inParams.push(...inParams)
tsData.outParams.push(...outParams)
tsData.instanceParameters.push(...instanceParameters)
tsData.doc.tags.push(...this.getTsDocGirElementTags(tsData.tsTypeName, tsData.girTypeName))
tsData.doc.tags.push(...this.getTsDocInParamTags(tsData?.inParams))
tsData.doc.tags.push(...this.getTsDocReturnTags(girFunc))
return tsData
}
overloadPromisifiedFunctions(girFunctions: GirFunctionElement[]): void {
if (!this.config.promisify) return
const promisifyAsyncReturn = ['Gio.AsyncReadyCallback', 'AsyncReadyCallback']
const promisifyFuncMap = {} as { [name: string]: PromisifyFunc }
// Find the functions that can be promisified
for (const girFunction of girFunctions) {
const tsFunction = girFunction._tsData
if (!tsFunction) continue
// Check if function name satisfies async,finish scheme
const isAsync = tsFunction.name.endsWith('_async') || tsFunction.name.endsWith('_begin')
const isFinish = tsFunction.name.endsWith('_finish')
if (!isAsync && !isFinish) continue
// Handle async functions
if (isAsync) {
if (tsFunction.inParams.length === 0) continue
const lastParam = tsFunction.inParams[tsFunction.inParams.length - 1]
if (lastParam.type && lastParam.type.length > 0) {
const type = lastParam.type[0].$.name
if (type && promisifyAsyncReturn.includes(type)) {
if (!(tsFunction.name in promisifyFuncMap)) promisifyFuncMap[tsFunction.name] = {}
promisifyFuncMap[tsFunction.name].asyncFn = tsFunction
}
}
}
// Handle finish functions
if (isFinish) {
if (tsFunction.returnTypes.length === 0) continue
let name = `${tsFunction.name.replace(/(_finish)$/, '')}_async`
if (!(name in promisifyFuncMap)) name = `${tsFunction.name.replace(/(_finish)$/, '')}_begin`
if (!(name in promisifyFuncMap)) promisifyFuncMap[name] = {}
promisifyFuncMap[name].finishFn = tsFunction
}
}
// Generate TsFunctions for promisify-able functions and add to the array
for (const [, func] of Object.entries(promisifyFuncMap)) {
if (!func.asyncFn || !func.finishFn) continue
const inParams = this.girFactory.newGirCallableParamElements(
func.asyncFn.inParams.slice(0, -1),
func.asyncFn,
)
const outParams = this.girFactory.newGirCallableParamElements(func.finishFn.outParams, func.asyncFn)
const returnTypes = this.girFactory.newTsTypes(outParams.length > 0 ? [] : func.finishFn.returnTypes)
let docReturnText = func.finishFn.doc.tags.find((tag) => tag.tagName === 'returns')?.text || ''
if (docReturnText) {
docReturnText = `A Promise of: ${docReturnText}`
} else {
docReturnText = `A Promise of the result of {@link ${func.asyncFn.name}}`
}
const docText = `Promisified version of {@link ${func.asyncFn.name}}\n\n${func.asyncFn.doc.text}`
const docTags = func.asyncFn.doc.tags.filter(
(tag) => tag.paramName !== 'callback' && tag.paramName !== 'returns',
)
docTags.push({
tagName: 'returns',
text: docReturnText,
paramName: '',
})
const doc = this.girFactory.newTsDoc({
text: docText,
tags: docTags,
})
const promisifyFn = this.girFactory.newTsFunction(
{
...func.asyncFn,
inParams,
outParams,
returnTypes,
isPromise: true,
doc,
},
func.asyncFn.parent,
)
func.asyncFn.overloads.push(promisifyFn)
}
}
private getCallbackInterfaceTsData(girCallback: GirCallbackElement | GirConstructorElement) {
if (!girElementIsIntrospectable(girCallback)) return undefined
const namespace = this.namespace
const tsDataInterface: TsCallbackInterface = {
name: `${namespace}.${girCallback.$.name}`,
generics: [],
}
return tsDataInterface
}
private setCallbackTsData(girCallback: GirCallbackElement, tsClass: TsClass | null) {
const tsFunction = this.getFunctionTsData(girCallback, 'callback', tsClass, {
isStatic: false,
isArrowType: true,
isGlobal: false,
isVirtual: false,
returnType: null,
generics: [],
})
if (tsFunction) {
const tsCallback: TsCallback = {
...tsFunction,
girTypeName: 'callback',
tsTypeName: this.girFactory.girTypeNameToTsTypeName('callback', false),
tsCallbackInterface: this.getCallbackInterfaceTsData(girCallback),
overloads: [],
}
girCallback._tsData = tsCallback
this.inject.toCallback(girCallback)
}
return girCallback._tsData
}
private getSignalCallbackInterfaceTsData(
girCallback: GirSignalElement,
girClass: GirClassElement | GirUnionElement | GirInterfaceElement | GirRecordElement,
) {
if (!girElementIsIntrospectable(girCallback)) return undefined
if (!girClass._tsData || !girClass._module) {
throw new Error(NO_TSDATA('getSignalCallbackTsData'))
}
const className = girClass._tsData.name
const signalName = girCallback.$.name
const signalInterfaceName = this.transformation.transformSignalInterfaceName(signalName)
const namespace = girClass._module.namespace
const tsDataInterface: TsCallbackInterface = {
name: `${namespace}.${className}.${signalInterfaceName}SignalCallback`,
generics: [],
overwriteDoc: {
text: `Signal callback interface for \`${signalName}\``,
tags: [],
},
}
return tsDataInterface
}
private getConstructorFunctionTsData(parentClass: TsClass, girConstructor: GirConstructorElement) {
if (!girElementIsIntrospectable(girConstructor)) return
const constructorTypeName = addNamespace(parentClass.name, parentClass.namespace)
return this.getFunctionTsData(girConstructor, 'constructor', parentClass, {
isStatic: true,
isArrowType: false,
isGlobal: false,
isVirtual: false,
returnType: constructorTypeName,
generics: [],
})
}
private getSignalCallbackTsData(
girSignalFunc: GirSignalElement,
girClass: GirClassElement | GirUnionElement | GirInterfaceElement | GirRecordElement,
) {
if (!girClass._tsData) {
throw new Error(NO_TSDATA('getSignalCallbackTsData'))
}
if (girSignalFunc._tsData) {
return girSignalFunc._tsData
}
// Leads to errors here
// if (!girElementIsIntrospectable(girSignalFunc)) return undefined
con