UNPKG

nihilqui

Version:

Typescript .d.ts generator from GIR for gjs and node-gtk

1,222 lines (1,070 loc) 55.3 kB
import { GirFactory } from './gir-factory.js' import { Logger } from './logger.js' import { NO_TSDATA } from './messages.js' import { isEqual, merge, clone, typesContainsOptional } from './utils.js' import { SIGNAL_METHOD_NAMES, MAX_CLASS_PARENT_DEPTH } from './constants.js' import { GirDirection } from './types/gir-direction.js' import type { Environment, GirClassElement, GirRecordElement, GirUnionElement, GirInterfaceElement, GirCallableParamElement, GirMethodElement, GirVirtualMethodElement, GirConstructorElement, GirFunctionElement, GirPropertyElement, GirFieldElement, TsSignal, TsFunction, TsProperty, TsVar, TsTypeSeparator, TsType, TsClass, TsParameter, TypeGirFunction, TypeGirProperty, ConflictChildElement, ConflictGroupedElements, ConflictGroupedElement, } from './types/index.js' /** * Resolve conflicts between types caused by overloads / inheritances and implementations * With multiple implementations or a inherit it can happen that the interfaces / parent have the same method and/or property names with incompatible types, * we are trying to resolve this conflicts by merging, overloading or removing this conflicts. */ export class ConflictResolver { private log: Logger private girFactory = new GirFactory() constructor(private readonly environment: Environment, private readonly verbose: boolean) { this.log = new Logger(environment, verbose, 'ConflictResolver') } private girElArrToChildArr<T = TsFunction | TsProperty | TsVar>( dataArr: Array< | GirMethodElement | GirVirtualMethodElement | GirConstructorElement | GirFunctionElement | GirPropertyElement | GirFieldElement >, depth: number, ): ConflictChildElement<T>[] { return dataArr .filter((data) => !!data?._tsData) .map((data) => { if (!data?._tsData) throw new Error('_tsData not set!') return { depth, data: data?._tsData as unknown as T, } }) } private tsElArrToChildArr<T extends ConflictChildElement>( dataArr: Array<TsFunction | TsProperty | TsVar>, depth: number, ): T[] { return dataArr .filter((m) => !!m) .map((data) => { return { depth, data, } as T }) } /** * Get class elements and parent elements (implementations and inherits) * @param tsClass * @param depth * @returns */ private getClassElements(tsClass: TsClass, depth: number, processedClasses: string[]) { const tsClassFullPackageSymName = `${tsClass.namespace}-${tsClass.version}.${tsClass.namespace}.${tsClass.name}` const signalMethods: ConflictChildElement<TsFunction>[] = [] const propertySignalMethods: ConflictChildElement<TsFunction>[] = [] const methods: ConflictChildElement<TsFunction>[] = [] const virtualMethods: ConflictChildElement<TsFunction>[] = [] const staticFunctions: ConflictChildElement<TsFunction>[] = [] const constructors: ConflictChildElement<TsFunction>[] = [] const properties: ConflictChildElement<TsProperty | TsVar>[] = [] const fields: ConflictChildElement<TsProperty | TsVar>[] = [] const constructProps: ConflictChildElement<TsProperty | TsVar>[] = [] const depthLimitReached = depth >= MAX_CLASS_PARENT_DEPTH const classAlreadyProcessed = processedClasses.includes(tsClassFullPackageSymName) if (depthLimitReached || classAlreadyProcessed) { if (depthLimitReached) { this.log.error(`[getClassElements] Maximum recursion depth reached (limit: ${MAX_CLASS_PARENT_DEPTH})!`) } return { signalMethods, propertySignalMethods, methods, virtualMethods, staticFunctions, constructors, properties, fields, constructProps, } } // Signals const _signals = tsClass.signals.map((s) => s._tsData).filter((s) => !!s) as TsSignal[] for (const tsSignal of _signals) { signalMethods.push(...this.tsElArrToChildArr<ConflictChildElement<TsFunction>>(tsSignal.tsMethods, depth)) } // Property signals propertySignalMethods.push( ...this.tsElArrToChildArr<ConflictChildElement<TsFunction>>(tsClass.propertySignalMethods, depth), ) // Methods if (tsClass.methods.length) methods.push(...this.girElArrToChildArr<TsFunction>(tsClass.methods, depth)) // Virtual methods if (tsClass.virtualMethods.length) virtualMethods.push(...this.girElArrToChildArr<TsFunction>(tsClass.virtualMethods, depth)) // Static functions if (tsClass.staticFunctions.length) staticFunctions.push(...this.girElArrToChildArr<TsFunction>(tsClass.staticFunctions, depth)) // Constructors if (tsClass.constructors.length) constructors.push(...this.girElArrToChildArr<TsFunction>(tsClass.constructors, depth)) // Properties if (tsClass.properties.length) properties.push(...this.girElArrToChildArr<TsProperty | TsVar>(tsClass.properties, depth)) // Fields if (tsClass.fields.length) fields.push(...this.girElArrToChildArr<TsProperty | TsVar>(tsClass.fields, depth)) // Constructor properties if (tsClass.constructProps.length) constructProps.push(...this.girElArrToChildArr<TsProperty | TsVar>(tsClass.constructProps, depth)) for (const ifacePackageFullSymName of Object.keys(tsClass.implements)) { if (tsClassFullPackageSymName === ifacePackageFullSymName) { this.log.warn("[getImplementedInterfaceElements] A interface can't implement itself") continue } const { interface: implementation, depth: parentDepth } = tsClass.implements[ifacePackageFullSymName] const implementationElements = this.getClassElements( implementation, parentDepth + depth + 1, processedClasses, ) signalMethods.push(...implementationElements.signalMethods) propertySignalMethods.push(...implementationElements.propertySignalMethods) methods.push(...implementationElements.methods) virtualMethods.push(...implementationElements.virtualMethods) staticFunctions.push(...implementationElements.staticFunctions) constructors.push(...implementationElements.constructors) properties.push(...implementationElements.properties) fields.push(...implementationElements.fields) constructProps.push(...implementationElements.constructProps) } for (const ifaceFullPackageSymName of Object.keys(tsClass.inherit)) { if (tsClassFullPackageSymName === ifaceFullPackageSymName) { this.log.warn("[getClassElements] A class can't inherit itself") continue } const { class: inherit, depth: parentDepth } = tsClass.inherit[ifaceFullPackageSymName] const inheritElements = this.getClassElements(inherit, parentDepth + depth + 1, processedClasses) signalMethods.push(...inheritElements.signalMethods) propertySignalMethods.push(...inheritElements.propertySignalMethods) methods.push(...inheritElements.methods) virtualMethods.push(...inheritElements.virtualMethods) staticFunctions.push(...inheritElements.staticFunctions) constructors.push(...inheritElements.constructors) properties.push(...inheritElements.properties) fields.push(...inheritElements.fields) constructProps.push(...inheritElements.constructProps) } processedClasses.push(tsClassFullPackageSymName) return { signalMethods, propertySignalMethods, methods, virtualMethods, staticFunctions, constructors, properties, fields, constructProps, } } private tsElementIsMethod(el: TsFunction | TsVar) { return !this.tsElementIsStatic(el) && this.tsElementIsMethodOrFunction(el) } private tsElementIsStaticFunction(el: TsFunction | TsVar) { return this.tsElementIsStatic(el) && this.tsElementIsMethodOrFunction(el) } private tsElementIsProperty(el: TsFunction | TsVar) { return !this.tsElementIsStatic(el) && this.tsElementIsPropertyOrVariable(el) } private tsElementIsStaticProperty(el: TsFunction | TsVar) { return this.tsElementIsStatic(el) && this.tsElementIsPropertyOrVariable(el) } private tsElementIsSignal(el: TsFunction | TsVar) { return el.tsTypeName === 'event-methods' } private tsElementIsConstructor(el: TsFunction | TsVar) { return el.tsTypeName === 'constructor' } private tsElementIsMethodOrFunction(el: TsFunction | TsVar) { return el.tsTypeName === 'function' || el.tsTypeName === 'method' || el.tsTypeName === 'static-function' } private tsElementIsPropertyOrVariable(el: TsFunction | TsVar | TsProperty) { return ( el.tsTypeName === 'constant' || el.tsTypeName === 'constructor-property' || el.tsTypeName === 'property' || el.tsTypeName === 'static-property' ) } private tsElementIsStatic(el: TsFunction | TsVar) { return ( // el.tsTypeName === 'constructor' || (el as TsFunction).isStatic || el.tsTypeName === 'static-property' || el.tsTypeName === 'static-function' ) } private typeIsString(type: TsType) { return ( type.type === 'string' || (type.type.startsWith("'") && type.type.endsWith("'")) || (type.type.startsWith('"') && type.type.endsWith('"')) ) } private tsTypeIsEqual(a: TsType, b: TsType) { return ( a && b && a.optional === b.optional && a.nullable === b.nullable && a.type === b.type && a.isArray === b.isArray && a.isFunction === b.isFunction && a.isCallback === b.isCallback && a.leftSeparator === b.leftSeparator && isEqual(a.callbacks, b.callbacks) && isEqual(a.generics, b.generics) ) } private mergeTypes(leftSeparator: TsTypeSeparator, ...types: Array<TsType | undefined>) { const dest: TsType[] = [] for (const type of types) { if (!type) continue if (!dest.find((destType) => this.tsTypeIsEqual(destType, type))) { dest.push({ ...type, leftSeparator }) } } return dest } private setTypesProperty(types: TsType[], property: 'optional', value: boolean) { for (const type of types) { type[property] = value } return types } /** * Merges two parameter name and type of two parameters * @param a * @param b * @returns */ private mergeParam(a: GirCallableParamElement | undefined, b: GirCallableParamElement | undefined) { if (!a?._tsData && !b?._tsData) { throw new Error('At least one parameter must be defined!') } let dest: GirCallableParamElement if (a?._tsData && b?._tsData) { dest = merge({}, clone(a), clone(b)) if (!dest._tsData) { throw new Error('Error on merge parameters!') } dest._tsData.type = [] dest._tsData.type = this.mergeTypes('|', ...a._tsData.type, ...b._tsData.type) if (a._tsData.name !== b._tsData.name) { dest._tsData.name = `${a._tsData.name}_or_${b._tsData.name}` } } else { dest = clone((a || b) as GirCallableParamElement) if (!dest._tsData) { throw new Error('Error on merge parameters!') } // If `a` or `b` is undefined make the types optional dest._tsData.type = this.setTypesProperty(dest._tsData.type, 'optional', true) } if (typesContainsOptional(dest._tsData.type)) { dest._tsData.type = this.setTypesProperty(dest._tsData.type, 'optional', true) } return dest } /** * Merges parameter names and types of multiple functions * @param params * @returns */ private mergeParams(...params: GirCallableParamElement[][]) { let dest: GirCallableParamElement[] = [] for (const a of params) { for (const b of params) { if (a === b) { continue } if (isEqual(a, b)) { dest = clone(a) } else { const length = Math.max(a.length, b.length) dest = new Array<GirCallableParamElement>(length) for (let i = 0; i < length; i++) { const aParam = a[i] as GirCallableParamElement | undefined const bParam = b[i] as GirCallableParamElement | undefined dest[i] = this.mergeParam(aParam, bParam) } } } } return dest } private paramCanBeOptional( girParam: GirCallableParamElement, girParams: GirCallableParamElement[], skip: GirCallableParamElement[] = [], ) { if (!girParam._tsData) return false let canBeOptional = true const index = girParams.indexOf(girParam) const following = girParams .slice(index) .filter((p) => !!p._tsData) .filter(() => !skip.includes(girParam)) .filter((p) => p.$.direction !== GirDirection.Out) .map((p) => p._tsData) if (following.some((p) => p && !typesContainsOptional(p.type))) { canBeOptional = false } return canBeOptional } /** * In Typescript no optional parameters are allowed if the following ones are not optional * @param girParams * @returns */ private fixOptionalParameters(girParams: GirCallableParamElement[]) { for (const girParam of girParams) { if (!girParam._tsData) throw new Error(NO_TSDATA('fixOptionalParameters')) if (typesContainsOptional(girParam._tsData.type) && !this.paramCanBeOptional(girParam, girParams)) { this.setTypesProperty(girParam._tsData.type, 'optional', false) } } return girParams } /** * Merge function types and parameters together * @param baseFunc The function to change or null if you want to create a new property * @param funcs The functions you want to merge * @returns */ public mergeFunctions(baseFunc: TsFunction | null, ...funcs: TsFunction[]) { const returnTypesMap: TsType[] = [] for (const func of funcs) { returnTypesMap.push(...func.returnTypes) } const returnTypes = this.mergeTypes('|', ...returnTypesMap) const inParamsMap = funcs.map((func) => func.inParams) const inParams: GirCallableParamElement[] = [] if (this.paramsHasConflict(...inParamsMap)) { inParams.push(...this.mergeParams(...inParamsMap)) } const outParamsMap = funcs.map((func) => func.outParams) const outParams: GirCallableParamElement[] = [] if (this.paramsHasConflict(...outParamsMap)) { outParams.push(...this.mergeParams(...outParamsMap)) } if (!funcs[0]) { throw new Error('At least one function must exist!') } if (baseFunc) { baseFunc.returnTypes = returnTypes return baseFunc } return this.girFactory.newTsFunction( { name: funcs[0].name, returnTypes: returnTypes, isStatic: funcs[0].isStatic || false, inParams: inParams.map((inParam) => inParam._tsData).filter((inParam) => !!inParam) as TsParameter[], outParams: outParams .map((outParam) => outParam._tsData) .filter((outParam) => !!outParam) as TsParameter[], girTypeName: funcs[0].girTypeName, }, funcs[0].parent, ) } /** * Merge property types together * @param baseProp The property to change or null if you want to create a new property * @param props The properties you want to merge * @returns */ public mergeProperties(typeSeparator: TsTypeSeparator, baseProp: TsProperty | null, ...props: TsProperty[]) { const typesMap: TsType[] = [] for (const prop of props) { typesMap.push(...prop.type) } const types = this.mergeTypes(typeSeparator, ...typesMap) // Merge readonly let readonly = false for (const prop of props) { readonly = readonly || prop.readonly || false } if (!props[0] || !props[0].name) { throw new Error('At least one property to merge must exist!') } if (baseProp) { baseProp.type = types baseProp.readonly = readonly return baseProp } const newProp = this.girFactory.newTsProperty({ readonly: readonly, isStatic: props[0].isStatic || false, name: props[0].name, type: types, girTypeName: props[0].girTypeName, }) return newProp } /** * Check if there is a type conflict between the ts elements a and b * @param a * @param b * @returns */ public hasConflict(a: ConflictChildElement, b: ConflictChildElement) { if (a !== b && a.data.name === b.data.name) { const name = a.data.name // Ignore element with name of: if (name === 'constructor' || name === '_init') { return false } if (this.elementHasConflict(a.data, b.data)) { return true } } return false } public newAnyTsProperty(name: string, girTypeName: TypeGirProperty) { return this.girFactory.newTsProperty({ name, girTypeName, type: [{ type: 'any' }], }) } /** * Returns a new any function: `name(...args: any[]): any` * @param name The name of the function */ public newAnyTsFunction(name: string, girTypeName: TypeGirFunction, isStatic: boolean, parent: TsClass | null) { return this.girFactory.newTsFunction( { name, isStatic, inParams: [ { name: 'args', isRest: true, type: [this.girFactory.newTsType({ type: 'any', isArray: true })], }, ], returnTypes: [{ type: 'any' }], girTypeName, }, parent, ) } public getCompatibleTsProperty(elements: TsProperty[], baseProp: TsProperty) { return elements.find((prop) => !this.propertyHasConflict(baseProp, prop)) } public getCompatibleTsFunction(elements: TsFunction[], baseFunc: TsFunction) { return elements.find((func) => !this.functionHasConflict(baseFunc, func)) } /** * Use this instead of `getCompatibleTsProperty` and `getCompatibleTsProperty` if you can, because it's much faster * @param elements * @param name * @returns */ public getTsElementByName(elements: (TsProperty | TsFunction)[], name: string) { return elements.find((el) => el.name === name) } protected canAddConflictProperty(conflictProperties: TsProperty[], prop: TsProperty) { const canAdd = prop.name && // Only one property can be defined, no overloads !this.getTsElementByName(conflictProperties, prop.name) && // Do not set properties with signal method names !SIGNAL_METHOD_NAMES(this.environment).includes(prop.name) return canAdd } public groupSignalConflicts(signalsMethods: ConflictChildElement<TsFunction>[], baseClass: TsClass) { const groupedConflicts: ConflictGroupedElements = {} for (const base of signalsMethods) { for (const b of signalsMethods) { if (base === b) { continue } if (base.data.name !== 'connect' && base.data.name !== 'connect_after') { continue } const sigNameParam = base.data.inParams[0] // const callbackParam = base.data.inParams[1] const eventName = sigNameParam?._tsData?.type?.[0]?.type // TODO do not render the callback type as a full string, create a TSCallback instead // const callbackType = callbackParam?._tsData?.type?.[0]?.type // console.debug('eventName', eventName, callbackType) if (!eventName || eventName === 'string') { continue } groupedConflicts[eventName] ||= { baseElements: [], inheritedElements: [], baseClass, } const groupedConflict = groupedConflicts[eventName] const isBaseElement = base.depth === 0 if (isBaseElement) { if (!groupedConflict.baseElements.find((c) => isEqual(c.data, base.data))) { groupedConflict.baseElements.push(base) } } else { if (!groupedConflict.inheritedElements.find((c) => isEqual(c.data, base.data))) { groupedConflict.inheritedElements.push(base) } } } } return groupedConflicts } public fixSignalConflicts(groupedElements: ConflictGroupedElements) { for (const eventName of Object.keys(groupedElements)) { const elements = groupedElements[eventName] const bases = elements.baseElements if (!bases.length) { // TODO // return this.fixIndirectSignalConflicts(elements.inheritedElements, elements.baseClass) return } for (const base of bases) { if (base.data.hasUnresolvedConflict) { continue } for (const b of elements.inheritedElements) { if (b === base || b.data.hasUnresolvedConflict) { continue } // TODO } } } } /** * Check conflicts between the implementations / inheritances * To fix type errors like: * ``` * Interface 'PopoverMenu' can\'t simultaneously extend types 'Popover' and 'Native'. * Named property 'parent' of types 'Popover' and 'Native' are not identical. */ public fixIndirectConflicts(name: string, elements: ConflictChildElement[], baseClass: TsClass) { for (const base of elements) { if (base.data.hasUnresolvedConflict) { continue } for (const b of elements) { if (b === base || b.data.hasUnresolvedConflict) { continue } const className = `${baseClass.namespace}-${baseClass.version}.${baseClass.name}` // If a element is a function / method if (this.tsElementIsMethodOrFunction(base.data)) { const baseFunc = base.data as TsFunction // Function vs. Property if (this.tsElementIsPropertyOrVariable(b.data)) { this.log.debug(`${className}.${name} External Function vs. Property`, baseFunc, b.data) b.data.hasUnresolvedConflict = true } // Function vs. Signal else if (this.tsElementIsSignal(b.data)) { this.log.debug(`${className}.${name} External Function vs. Signal`, baseFunc, b.data) baseFunc.hasUnresolvedConflict = true } // Function vs. Function else if (this.tsElementIsMethodOrFunction(b.data)) { const bFunc = b.data as TsFunction this.log.debug( `${className}.${name} External Function vs. Function`, baseFunc.inParams.map((p) => p._tsData?.name).join(', '), bFunc.inParams.map((p) => p._tsData?.name).join(', '), ) // Just add conflicting methods to the class if (!baseClass.conflictMethods.includes(baseFunc)) { baseClass.conflictMethods.push(baseFunc) } if (!baseClass.conflictMethods.includes(bFunc)) { baseClass.conflictMethods.push(bFunc) } } // Function vs. Constructor else if (this.tsElementIsConstructor(base.data)) { const bConstr = b.data as TsFunction this.log.debug(`${className}.${name} External Function vs. Constructor`, baseFunc, bConstr) // Just add conflicting methods to the class if (!baseClass.conflictMethods.includes(baseFunc)) { baseClass.conflictMethods.push(baseFunc) } if (!baseClass.conflictMethods.includes(bConstr)) { baseClass.conflictMethods.push(bConstr) } } // Function vs. Unknown else { this.log.debug(`${className}.${name} External Unknown ${b.data.tsTypeName}`, baseFunc, b.data) baseFunc.hasUnresolvedConflict = true } } // If a element is a constructor else if (this.tsElementIsConstructor(base.data)) { const baseConstr = base.data as TsFunction // Constructor vs. Function if (this.tsElementIsMethodOrFunction(b.data)) { const bFunc = b.data as TsFunction this.log.debug( `${className}.${name} External Constructor vs. Function`, baseConstr.inParams.map((p) => p._tsData?.name).join(', '), bFunc.inParams.map((p) => p._tsData?.name).join(', '), ) if (!baseClass.conflictMethods.includes(baseConstr)) { baseClass.conflictMethods.push(baseConstr) } if (!baseClass.conflictMethods.includes(bFunc)) { baseClass.conflictMethods.push(bFunc) } } // Constructor vs. Constructor else if (this.tsElementIsConstructor(base.data)) { this.log.debug(`${className}.${name} External Constructor vs. Constructor`, baseConstr, b.data) const anyFunc = this.newAnyTsFunction( name, baseConstr.girTypeName, baseConstr.isStatic, baseConstr.parent, ) // Check if any function is not already added if (!this.getTsElementByName(baseClass.conflictMethods, anyFunc.name)) { baseClass.conflictMethods.push(anyFunc) } } } // If a element is a property / variable else if (this.tsElementIsPropertyOrVariable(base.data)) { const baseProp = base.data as TsProperty // Property vs. Function if (this.tsElementIsMethodOrFunction(b.data)) { const bFunc = b.data as TsFunction this.log.debug( `${className}.${name} External Property vs. Function`, baseProp.type[0].type, bFunc, ) baseProp.hasUnresolvedConflict = true } // Property vs. Property else if (this.tsElementIsPropertyOrVariable(b.data)) { const bProp = b.data as TsProperty this.log.debug( `${className}.${name} External Property vs. Property`, baseProp.type[0].type, bProp.type[0].type, ) switch (name) { case 'parent': case 'window': case 'parent_instance': case 'priv': const mergedProp = this.mergeProperties('&', null, baseProp, bProp) if (this.canAddConflictProperty(baseClass.conflictProperties, mergedProp)) { baseClass.conflictProperties.push(mergedProp) } break default: const anyProp = this.newAnyTsProperty(name, baseProp.girTypeName) if (this.canAddConflictProperty(baseClass.conflictProperties, anyProp)) { baseClass.conflictProperties.push(anyProp) } break } } // Property vs. Signal else if (this.tsElementIsSignal(b.data)) { this.log.debug(`${className}.${name} External Property vs. Signal`, baseProp, b.data) base.data.hasUnresolvedConflict = true } else { this.log.error(`${className}.${name} External Unknown ${b.data.tsTypeName}`, baseProp, b.data) } } // Other else { this.log.error(`${className}.${name} External Unknown ${base.data.tsTypeName}`) base.data.hasUnresolvedConflict = true } } } } /** * Check conflicts within the class itself (ignores implementations / inheritances) */ public fixInternalConflicts(name: string, elements: ConflictChildElement[], baseClass: TsClass) { for (const base of elements) { if (base.data.hasUnresolvedConflict) { continue } for (const b of elements) { if (b === base || b.data.hasUnresolvedConflict) { continue } const className = `${baseClass.namespace}-${baseClass.version}.${baseClass.name}` // Conflict between injected and original elements if (base.data.isInjected !== b.data.isInjected) { if (!base.data.isInjected) { base.data.hasUnresolvedConflict = true } else if (!b.data.isInjected) { b.data.hasUnresolvedConflict = true } // Copy doc from original element if not set in the injected element if (!b.data.doc.text && base.data.doc.text) { b.data.doc = base.data.doc } else if (!base.data.doc.text && b.data.doc.text) { base.data.doc = b.data.doc } continue } // If a element is a function / method if (this.tsElementIsMethodOrFunction(base.data)) { const baseFunc = base.data as TsFunction // Function vs. Property if (this.tsElementIsPropertyOrVariable(b.data)) { this.log.debug(`${className}.${name} Internal Function vs. Property`, baseFunc, b.data) b.data.hasUnresolvedConflict = true } // Function vs. Signal else if (this.tsElementIsSignal(b.data)) { this.log.debug(`${className}.${name} Internal Function vs. Signal`, baseFunc, b.data) // Do nothing } // Function vs. Function else if (this.tsElementIsMethodOrFunction(b.data)) { const bFunc = b.data as TsFunction this.log.debug( `${className}.${name} Internal Function vs. Function`, baseFunc.inParams.map((p) => p._tsData?.name).join(', '), bFunc.inParams.map((p) => p._tsData?.name).join(', '), ) // Conflict between virtual and non-virtual methods (this should only occur in node-gtk, because Gjs has a vfunc_ prefix for virtual methods) if (baseFunc.isVirtual !== bFunc.isVirtual) { if (!baseFunc.isVirtual) { baseFunc.hasUnresolvedConflict = true } else { bFunc.hasUnresolvedConflict = true } continue } // Do nothing.. } else { this.log.error(`${className}.${name} Internal Unknown ${b.data.tsTypeName}`, baseFunc, b.data) b.data.hasUnresolvedConflict = true } } // If a element is a property / variable else if (this.tsElementIsPropertyOrVariable(base.data)) { const baseProp = base.data as TsProperty // Property vs. Function if (this.tsElementIsMethodOrFunction(b.data)) { const bFunc = b.data as TsFunction this.log.debug( `${className}.${name} Internal Property vs. Function`, baseProp.type[0].type, bFunc, ) baseProp.hasUnresolvedConflict = true } // Property vs. Property else if (this.tsElementIsPropertyOrVariable(b.data)) { const bProp = b.data as TsProperty this.log.debug( `${className}.${name} Internal Property vs. Property`, baseProp.type[0].type, bProp.type[0].type, ) switch (name) { case 'parent': case 'window': case 'parent_instance': this.mergeProperties('&', baseProp, baseProp, bProp) break default: // Set property type to any baseProp.type = [this.girFactory.newTsType({ ...bProp.type, type: 'any' })] break } } // Property vs. Signal else if (this.tsElementIsSignal(b.data)) { this.log.debug(`${className}.${name} Internal Property vs. Signal`, baseProp, b.data) base.data.hasUnresolvedConflict = true } else { this.log.error(`${className}.${name} Internal Unknown ${b.data.tsTypeName}`, baseProp, b.data) } } // If a element is a signal else if (this.tsElementIsSignal(base.data)) { const baseSig = base.data as TsSignal // Signal vs. Function if (this.tsElementIsMethodOrFunction(b.data)) { this.log.debug(`${className}.${name} Internal Signal vs. Function`, baseSig, b.data) // Do nothing } // Signal vs. Property else if (this.tsElementIsPropertyOrVariable(b.data)) { this.log.debug(`${className}.${name} Internal Signal vs. Property`, baseSig, b.data) b.data.hasUnresolvedConflict = true } } // Other else { this.log.error(`${className}.${name} Internal Unknown ${base.data.tsTypeName}`) } } } } /** * Check conflicts of the class with implementations and inheritances */ public fixDirectConflicts(name: string, elements: ConflictGroupedElement) { const className = `${elements.baseClass.namespace}-${elements.baseClass.version}.${elements.baseClass.name}` for (const base of elements.baseElements) { if (base.data.hasUnresolvedConflict) { continue } // Each conflicting elements for (const b of elements.inheritedElements) { if (b === base || b.data.hasUnresolvedConflict) { continue } // If base element is a function if (this.tsElementIsMethodOrFunction(base.data)) { const baseFunc = base.data as TsFunction // Function vs. Function if (this.tsElementIsMethodOrFunction(b.data)) { const bFunc = b.data as TsFunction this.log.debug( `${className}.${name} Direct Function vs. Function`, baseFunc.inParams.map((p) => p._tsData?.name).join(', '), bFunc.inParams.map((p) => p._tsData?.name).join(', '), ) // Add a function to overload methods if there is not already a compatible version if ( !baseFunc.overloads.includes(bFunc) && !this.getCompatibleTsFunction(baseFunc.overloads, bFunc) ) { baseFunc.overloads.push(bFunc) } } // Function vs. Constructor else if (this.tsElementIsConstructor(b.data)) { const bConstr = b.data as TsFunction this.log.debug( `${className}.${name} Direct Function vs. Constructor`, baseFunc.inParams.map((p) => p._tsData?.name).join(', '), bConstr.inParams.map((p) => p._tsData?.name).join(', '), ) // Add a function to overload methods if there is not already a compatible version if (!this.getCompatibleTsFunction(baseFunc.overloads, bConstr)) { baseFunc.overloads.push(bConstr) } } // Function vs. Property else if (this.tsElementIsPropertyOrVariable(b.data)) { this.log.debug(`${className}.${name} Direct Function vs. Property`) b.data.hasUnresolvedConflict = true } // Function vs. Signal else if (this.tsElementIsSignal(b.data)) { this.log.debug(`${className}.${name} Direct Function vs. Signal`) // Do nothing } } // If base element is a property / variable else if (this.tsElementIsPropertyOrVariable(base.data)) { const baseProp = base.data as TsProperty // Property vs. Property if (this.tsElementIsPropertyOrVariable(b.data)) { const bProp = b.data as TsProperty this.log.debug( `${className}.${name} Direct Property vs. Property`, baseProp.type[0].type, bProp.type[0].type, ) switch (name) { case 'parent': case 'window': case 'parent_instance': this.mergeProperties('&', baseProp, baseProp, bProp) break default: // Set property type to any baseProp.type = [this.girFactory.newTsType({ ...bProp.type, type: 'any' })] break } } // Property vs. Function else if (this.tsElementIsMethodOrFunction(b.data)) { this.log.debug(`${className}.${name} Direct Property vs. Function`) baseProp.hasUnresolvedConflict = true } // Property vs. Signal else if (this.tsElementIsSignal(b.data)) { this.log.debug(`${className}.${name} Direct Property vs. Signal`) baseProp.hasUnresolvedConflict = true } } // If base element is a signal method else if (this.tsElementIsSignal(base.data)) { // Signal vs. Property if (this.tsElementIsPropertyOrVariable(b.data)) { this.log.debug(`${className}.${name} Direct Signal vs. Property`) b.data.hasUnresolvedConflict = true } // Signal vs. Function if (this.tsElementIsMethodOrFunction(b.data)) { this.log.debug(`${className}.${name} Direct Signal vs. Function`) const bFunc = b.data as TsFunction const baseSignal = base.data as TsFunction // Add parent class incompatible method as overload if (!this.getCompatibleTsFunction(baseSignal.overloads, bFunc)) { baseSignal.overloads.push(bFunc) } } } // If a element is a constructor else if (this.tsElementIsConstructor(base.data)) { const baseConstr = base.data as TsFunction // Constructor vs. Function if (this.tsElementIsMethodOrFunction(b.data)) { const bFunc = b.data as TsFunction this.log.debug( `${className}.${name} Direct Constructor vs. Function`, baseConstr.inParams.map((p) => p._tsData?.name).join(', '), bFunc.inParams.map((p) => p._tsData?.name).join(', '), ) // Add a function to overload methods if there is not already a compatible version if (!this.getCompatibleTsFunction(baseConstr.overloads, bFunc)) { baseConstr.overloads.push(bFunc) } } // Constructor vs. Constructor else if (this.tsElementIsConstructor(base.data)) { const bConstr = b.data as TsFunction this.log.debug(`${className}.${name} Direct Constructor vs. Constructor`, baseConstr, bConstr) // Add the constructor to overload methods if there is not already a compatible version if (!this.getCompatibleTsFunction(baseConstr.overloads, bConstr)) { baseConstr.overloads.push(bConstr) } } } else { this.log.warn(`{className}.${name} Unknown ${base.data.tsTypeName}`, base) base.data.hasUnresolvedConflict = true } } } } /** * Fix the conflicts of a class * @param groupedElements */ public fixConflicts(groupedElements: ConflictGroupedElements) { for (const key of Object.keys(groupedElements)) { const elements = groupedElements[key] // Remove the key prefix `_` const name = key.substring(1) if (elements.baseElements.length === 0) { this.fixIndirectConflicts(name, elements.inheritedElements, elements.baseClass) } this.fixInternalConflicts(name, elements.baseElements, elements.baseClass) this.fixDirectConflicts(name, elements) } } /** * Group conflicts by name and sort them by depth for simpler handling of conflicts */ public groupConflicts(elements: ConflictChildElement[], tsClass: TsClass) { const groupedConflicts: ConflictGroupedElements = {} const IGNORE_CONFLICT_NAMES = ['$gtype', '__gtype__'] for (const a of elements) { const name = a.data.name for (const b of elements) { if ( a === b || !name || !b.data.name || IGNORE_CONFLICT_NAMES.includes(name) || IGNORE_CONFLICT_NAMES.includes(b.data.name) ) { continue } if (a && name && b && b.data.name && a !== b && this.hasConflict(a, b)) { const key = `_${name}` // if the key would be `toString` this would be always true so we prefix `_` groupedConflicts[key] ||= { baseElements: [], inheritedElements: [], baseClass: tsClass, } const groupedConflict = groupedConflicts[key] const isBaseElement = a.depth === 0 if (isBaseElement) { if (!groupedConflict.baseElements.find((c) => isEqual(c.data, a.data))) { groupedConflict.baseElements.push(a) } } else { if ( !groupedConflict.baseElements.find((c) => isEqual(c.data, a.data)) && !groupedConflict.inheritedElements.find((c) => isEqual(c.data, a.data)) ) { groupedConflict.inheritedElements.push(a) } } } } } // Sort by depth for (const key of Object.keys(groupedConflicts)) { groupedConflicts[key].inheritedElements = groupedConflicts[key].inheritedElements.sort( (a, b) => a.depth - b.depth, ) } return groupedConflicts } /** * With multiple implementations or a inherit it can happen that the interfaces / parent have the same method and/or property name with incompatible types. * We merge these types here to solve this problem. * @param girClass */ public repairClass(girClass: GirClassElement | GirUnionElement | GirInterfaceElement | GirRecordElement) { if (!girClass._tsData) throw new Error(NO_TSDATA('repairClass')) const classElements = this.getClassElements(girClass._tsData, 0, []) // Do not pass a reference of the array here const elements = [ ...classElements.signalMethods, ...classElements.propertySignalMethods, ...classElements.methods, ...classElements.virtualMethods, ...classElements.staticFunctions, ...classElements.constructors,