UNPKG

@ts-for-gir/lib

Version:

Typescript .d.ts generator from GIR for gjs

599 lines (519 loc) 21 kB
import { GirDirection } from "@gi.ts/parser"; import { LazyReporter } from "@ts-for-gir/reporter"; import { IntrospectedConstructor } from "../gir/constructor.ts"; import { FilterBehavior } from "../gir/data.ts"; import type { IntrospectedFunction } from "../gir/function.ts"; import type { IntrospectedClassMember } from "../gir/introspected-class-member.ts"; import type { IntrospectedBaseClass } from "../gir/introspected-classes.ts"; import { IntrospectedClass, IntrospectedClassFunction, IntrospectedInterface, IntrospectedStaticClassFunction, IntrospectedVirtualClassFunction, } from "../gir/introspected-classes.ts"; import type { IntrospectedNamespace } from "../gir/namespace.ts"; import { IntrospectedFunctionParameter } from "../gir/parameter.ts"; import { IntrospectedField, IntrospectedProperty } from "../gir/property.ts"; import type { TypeIdentifier } from "../gir.ts"; import { AnyType, ArrayType, ConflictType, NeverType, TypeConflict } from "../gir.ts"; import { findMap } from "../util.ts"; import { isSubtypeOf } from "./type-resolution.ts"; export const conflictsReporter = new LazyReporter("conflicts"); // Use a function to always get the current reporter instance. // A module-level `const log = conflictsReporter.get()` would capture a stale // reference from before configure() is called, causing problems to be logged // to console but not stored in the report. function getLog() { return conflictsReporter.get(); } // Constants for GObject methods that always conflict const GOBJECT_RESERVED_METHODS = ["connect", "connect_after", "emit"] as const; export function isConflictingFunction( namespace: IntrospectedNamespace, childThis: TypeIdentifier, child: IntrospectedFunction | IntrospectedClassFunction | IntrospectedConstructor, parentThis: TypeIdentifier, parent: IntrospectedClassFunction | IntrospectedFunction | IntrospectedConstructor, ): boolean { if (!parent.isIntrospectable || !child.isIntrospectable) { return false; } // Handle constructor conflicts if (isConstructorConflict(namespace, childThis, child, parentThis, parent)) { return true; } // Handle mixed constructor/function conflicts if (isMixedConstructorFunctionConflict(child, parent)) { return true; } // Handle different function types (no conflict if different prototypes) if (hasDifferentPrototypes(child, parent)) { return false; } // Check parameter and return type conflicts return hasParameterOrReturnTypeConflicts(namespace, childThis, child, parentThis, parent); } export function filterFunctionConflict< T extends | IntrospectedStaticClassFunction | IntrospectedVirtualClassFunction | IntrospectedClassFunction | IntrospectedConstructor, >( ns: IntrospectedNamespace, base: IntrospectedBaseClass, elements: T[], conflict_ids: string[], isInheritedMethods: boolean = false, ): T[] { const nextType = base.getType(); return elements .filter((m) => m.name) .reduce((prev, next) => { const conflictResult = checkFunctionConflicts(ns, base, next, conflict_ids, nextType); if (conflictResult.shouldOmit) { // Always omit methods that conflict with properties/fields getLog().reportTypeConflict( "field_property", next.name, next.parent?.namespace.namespace || "unknown", "Field/property name conflict", ); } else if (conflictResult.hasConflict) { if (isInheritedMethods) { getLog().reportTypeConflict( "method", next.name, next.parent?.namespace.namespace || "unknown", "Parent method conflict", ); } else { const neverFunction = createNeverFunction(next, base, conflictResult.message); prev.push(next, neverFunction as T); } } else { prev.push(next); } return prev; }, [] as T[]); } export function filterConflicts<T extends IntrospectedClassMember | IntrospectedClassFunction | IntrospectedProperty>( ns: IntrospectedNamespace, c: IntrospectedBaseClass, elements: T[], behavior = FilterBehavior.PRESERVE, ): T[] { const filtered = elements.filter((p) => p?.name); const thisType = c.getType(); const result: T[] = []; for (const element of filtered) { const conflictType = detectConflictType(ns, c, element, thisType); if (conflictType) { if (behavior === FilterBehavior.PRESERVE) { const conflictElement = createConflictElement(element, conflictType); if (conflictElement) { result.push(conflictElement); } else { result.push(element); } } } else { result.push(element); } } return result; } // Helper Functions function isConstructorConflict( namespace: IntrospectedNamespace, childThis: TypeIdentifier, child: IntrospectedFunction | IntrospectedClassFunction | IntrospectedConstructor, parentThis: TypeIdentifier, parent: IntrospectedClassFunction | IntrospectedFunction | IntrospectedConstructor, ): boolean { if (!(child instanceof IntrospectedConstructor && parent instanceof IntrospectedConstructor)) { return false; } return ( child.parameters.length > parent.parameters.length || !isSubtypeOf(namespace, childThis, parentThis, child.return(), parent.return()) || child.parameters.some((p, i) => !isSubtypeOf(namespace, childThis, parentThis, p.type, parent.parameters[i].type)) ); } function isMixedConstructorFunctionConflict( child: IntrospectedFunction | IntrospectedClassFunction | IntrospectedConstructor, parent: IntrospectedClassFunction | IntrospectedFunction | IntrospectedConstructor, ): boolean { return child instanceof IntrospectedConstructor !== parent instanceof IntrospectedConstructor; } function hasDifferentPrototypes( child: IntrospectedFunction | IntrospectedClassFunction | IntrospectedConstructor, parent: IntrospectedClassFunction | IntrospectedFunction | IntrospectedConstructor, ): boolean { // This occurs if two functions of the same name are passed but they // are different types (e.g. GirStaticClassFunction vs GirClassFunction) return Object.getPrototypeOf(child) !== Object.getPrototypeOf(parent); } function hasParameterOrReturnTypeConflicts( namespace: IntrospectedNamespace, childThis: TypeIdentifier, child: IntrospectedFunction | IntrospectedClassFunction | IntrospectedConstructor, parentThis: TypeIdentifier, parent: IntrospectedClassFunction | IntrospectedFunction | IntrospectedConstructor, ): boolean { // Check basic parameter conflicts if (child.parameters.length > parent.parameters.length) { return true; } // Check return type conflicts if (!isSubtypeOf(namespace, childThis, parentThis, child.return(), parent.return())) { return true; } // Check parameter type conflicts if ( child.parameters.some((np, i) => !isSubtypeOf(namespace, childThis, parentThis, np.type, parent.parameters[i].type)) ) { return true; } // Only check output parameters if both functions have them (constructors don't have output_parameters) const childHasOutputParams = "output_parameters" in child; const parentHasOutputParams = "output_parameters" in parent; if (childHasOutputParams && parentHasOutputParams) { // Length mismatch if (child.output_parameters.length !== parent.output_parameters.length) { return true; } // Type mismatch if ( child.output_parameters.some( (np, i) => !isSubtypeOf(namespace, childThis, parentThis, np.type, parent.output_parameters[i].type), ) ) { return true; } } return false; } interface ConflictResult { hasConflict: boolean; shouldOmit: boolean; message: string | null; } function checkFunctionConflicts<T extends IntrospectedClassFunction | IntrospectedConstructor>( ns: IntrospectedNamespace, base: IntrospectedBaseClass, functionElement: T, conflict_ids: string[], nextType: TypeIdentifier, ): ConflictResult { let message: string | null = null; // Check explicit conflict IDs if (conflict_ids.includes(functionElement.name)) { return { hasConflict: true, shouldOmit: false, message }; } // Check parent function conflicts const hasParentConflict = base.someParent((resolved_parent) => { const parentType = resolved_parent.getType(); return [...resolved_parent.constructors, ...resolved_parent.members].some((p) => { if (p.name && p.name === functionElement.name) { const conflicting = isConflictingFunction(ns, nextType, functionElement, parentType, p); if (conflicting) { message = `// Conflicted with ${resolved_parent.namespace.namespace}.${resolved_parent.name}.${p.name}`; return true; } return false; } return false; }); }); // Static methods can coexist with instance fields/properties of the same name // in TypeScript (e.g. `static map(...)` alongside `map: T[]`), so skip the check // for them. The conflict only applies to instance methods. const hasFieldConflicts = functionElement instanceof IntrospectedStaticClassFunction ? false : checkFieldPropertyConflicts(base, functionElement.name); // Check GObject reserved methods const hasGObjectConflicts = checkGObjectConflicts(base, functionElement.name); const hasConflict = hasParentConflict || hasGObjectConflicts; return { hasConflict, shouldOmit: hasFieldConflicts && !hasConflict, message, }; } function checkFieldPropertyConflicts(base: IntrospectedBaseClass, name: string): boolean { // Check if the method name conflicts with any props or fields either on // the class or in the parent... return ( [...base.props, ...base.fields].some((p) => p.name && p.name === name) || base.someParent((resolved_parent) => [...resolved_parent.props, ...resolved_parent.fields].some((p) => p.name && p.name === name), ) ); } function checkGObjectConflicts(base: IntrospectedBaseClass, name: string): boolean { const isGObject = base.someParent((p) => p.namespace.namespace === "GObject" && p.name === "Object"); return isGObject && (GOBJECT_RESERVED_METHODS as readonly string[]).includes(name); } function createNeverFunction<T extends IntrospectedClassFunction | IntrospectedConstructor>( original: T, base: IntrospectedBaseClass, message: string | null, ): | IntrospectedClassFunction | IntrospectedConstructor | IntrospectedStaticClassFunction | IntrospectedVirtualClassFunction { const neverParam = new IntrospectedFunctionParameter({ name: "args", direction: GirDirection.In, isVarArgs: true, type: new ArrayType(NeverType), }); const neverOptions = { name: original.name, parent: base, parameters: [neverParam], return_type: AnyType, }; let neverFunction: | IntrospectedClassFunction | IntrospectedConstructor | IntrospectedStaticClassFunction | IntrospectedVirtualClassFunction; if (original instanceof IntrospectedConstructor) { neverFunction = new IntrospectedConstructor(neverOptions); } else if (original instanceof IntrospectedStaticClassFunction) { neverFunction = new IntrospectedStaticClassFunction({ ...neverOptions, parent: original.parent }); } else if (original instanceof IntrospectedVirtualClassFunction && original.parent instanceof IntrospectedClass) { neverFunction = new IntrospectedVirtualClassFunction({ ...neverOptions, parent: original.parent }); } else if (original instanceof IntrospectedClassFunction) { neverFunction = new IntrospectedClassFunction({ ...neverOptions, parent: original.parent }); } else { const parent = Object.getPrototypeOf(original as (...args: unknown[]) => unknown) as | ((...args: unknown[]) => unknown) | null | undefined; throw new Error(`Unknown function type ${parent?.name} encountered.`); } if (message) { neverFunction.setWarning(message); } return neverFunction; } function detectConflictType<T extends IntrospectedClassMember | IntrospectedClassFunction | IntrospectedProperty>( ns: IntrospectedNamespace, c: IntrospectedBaseClass, element: T, thisType: TypeIdentifier, ): ConflictType | undefined { // Check field conflicts first const fieldConflict = checkFieldConflicts(c, element); if (fieldConflict) return fieldConflict; // Check property conflicts const propertyConflict = checkPropertyConflicts(ns, c, element, thisType); if (propertyConflict) return propertyConflict; // Check virtual function signature conflicts (for interfaces) if (element instanceof IntrospectedVirtualClassFunction) { const vfuncConflict = checkVfuncSignatureConflicts(ns, c, element, thisType); if (vfuncConflict) return vfuncConflict; } // Check function conflicts return checkFunctionNameConflicts(ns, c, element, thisType); } function checkFieldConflicts<T extends IntrospectedClassMember | IntrospectedClassFunction | IntrospectedProperty>( c: IntrospectedBaseClass, element: T, ): ConflictType | undefined { return c.findParentMap((resolved_parent) => { return findMap([...resolved_parent.fields], (p) => { if (p.name && p.name === element.name) { if (element instanceof IntrospectedProperty) { return ConflictType.ACCESSOR_PROPERTY_CONFLICT; } if ( element instanceof IntrospectedField && !isSubtypeOf(c.namespace, c.getType(), resolved_parent.getType(), element.type, p.type) ) { return ConflictType.FIELD_NAME_CONFLICT; } } return undefined; }); }); } function checkPropertyConflicts<T extends IntrospectedClassMember | IntrospectedClassFunction | IntrospectedProperty>( ns: IntrospectedNamespace, c: IntrospectedBaseClass, element: T, thisType: TypeIdentifier, ): ConflictType | undefined { return c.findParentMap((resolved_parent) => { return findMap([...resolved_parent.props], (p) => { if (p.name && p.name === element.name) { // Classes can override parent interface accessors with properties _but_ // classes cannot override parent class accessors with properties without an error occurring. if (p.parent instanceof IntrospectedClass && element instanceof IntrospectedField) { return ConflictType.PROPERTY_ACCESSOR_CONFLICT; } if ( element instanceof IntrospectedProperty && !isSubtypeOf(ns, thisType, resolved_parent.getType(), element.type, p.type) ) { getLog().reportTypeConflict( "general", element.name, element.parent?.namespace.namespace || "unknown", `Conflict with ${p.parent?.name}.${p.name}`, ); return ConflictType.PROPERTY_NAME_CONFLICT; } } return undefined; }); }); } function checkFunctionNameConflicts< T extends IntrospectedClassMember | IntrospectedClassFunction | IntrospectedProperty, >(ns: IntrospectedNamespace, c: IntrospectedBaseClass, element: T, thisType: TypeIdentifier): ConflictType | undefined { return c.findParentMap((resolved_parent) => findMap([...resolved_parent.constructors, ...resolved_parent.members], (p) => { if (p.name && p.name === element.name) { if (element instanceof IntrospectedProperty) { // Properties that conflict with parent functions should be treated as FUNCTION_NAME_CONFLICT return ConflictType.FUNCTION_NAME_CONFLICT; } if ( !(element instanceof IntrospectedClassFunction) || isConflictingFunction(ns, thisType, element, resolved_parent.getType(), p) ) { return ConflictType.FUNCTION_NAME_CONFLICT; } } return undefined; }), ); } function checkVfuncSignatureConflicts( ns: IntrospectedNamespace, c: IntrospectedBaseClass, element: IntrospectedVirtualClassFunction, thisType: TypeIdentifier, ): ConflictType | undefined { // Only check for vfunc conflicts on interfaces if (!(c instanceof IntrospectedInterface)) { return undefined; } // Check if this virtual method conflicts with parent class or interface methods return c.findParentMap((resolved_parent) => { // Look for virtual methods with the same name in parent classes/interfaces const parentVirtualMethods = resolved_parent.members.filter( (m) => m instanceof IntrospectedVirtualClassFunction && m.name === element.name, ); for (const parentMethod of parentVirtualMethods) { // Check if signatures conflict if (isConflictingFunction(ns, thisType, element, resolved_parent.getType(), parentMethod)) { return ConflictType.VFUNC_SIGNATURE_CONFLICT; } } // Also check if the parent is an interface that might have virtual methods from its Interface namespace // This is important for cases like List extends Collection where both have vfunc_get_read_only_view if (resolved_parent instanceof IntrospectedInterface) { // Get the virtual methods from the parent interface const parentInterfaceVirtualMethods = resolved_parent.members.filter( (m) => m instanceof IntrospectedVirtualClassFunction && m.name === element.name, ); for (const parentMethod of parentInterfaceVirtualMethods) { // Check if signatures conflict between the interfaces if (isConflictingFunction(ns, thisType, element, resolved_parent.getType(), parentMethod)) { return ConflictType.VFUNC_SIGNATURE_CONFLICT; } } } return undefined; }); } function createConflictElement<T extends IntrospectedClassMember | IntrospectedClassFunction | IntrospectedProperty>( element: T, conflictType: ConflictType, ): T | null { if (element instanceof IntrospectedField || element instanceof IntrospectedProperty) { return element.copy({ type: new TypeConflict(element.type, conflictType), }) as T; } // For VFUNC_SIGNATURE_CONFLICT, we'll handle it differently in the generator // Just mark the element so the generator knows to create overloads if (conflictType === ConflictType.VFUNC_SIGNATURE_CONFLICT && element instanceof IntrospectedVirtualClassFunction) { // Return the element with a marker that will be handled in the generator // We don't use TypeConflict here as that causes resolution errors return element; } return null; } /** * Check if an interface has virtual methods that conflict with parent class or interface methods. * This is used to determine whether to inherit from Interface namespace or generate method overloads. */ export function hasVfuncSignatureConflicts(ns: IntrospectedNamespace, interfaceClass: IntrospectedInterface): boolean { const thisType = interfaceClass.getType(); const virtualMethods = interfaceClass.members.filter( (m) => m instanceof IntrospectedVirtualClassFunction, ) as IntrospectedVirtualClassFunction[]; // If we don't have any virtual methods, no conflicts possible if (virtualMethods.length === 0) { return false; } // Check each virtual method for conflicts with parent classes/interfaces for (const vmethod of virtualMethods) { const conflictType = checkVfuncSignatureConflicts(ns, interfaceClass, vmethod, thisType); if (conflictType === ConflictType.VFUNC_SIGNATURE_CONFLICT) { return true; } } // Check if any parent interface already inherits from its own .Interface namespace // If it does, and we have virtual methods with the same name but different signatures, // we have a conflict (e.g., List extends Collection, List.Interface and BidirList extends List, BidirList.Interface) const hasParentWithVirtualMethods = interfaceClass.someParent((parent) => { // Only check parent interfaces (not classes) if (!(parent instanceof IntrospectedInterface)) { return false; } // Check if the parent interface has virtual methods (which means it probably inherits from its .Interface namespace) const parentHasVirtualMethods = parent.members.some((m) => m instanceof IntrospectedVirtualClassFunction); if (!parentHasVirtualMethods) { return false; // Parent has no virtual methods, no conflict } // Check if any of our virtual methods have the same name as parent's virtual methods // but with different signatures (especially return types) for (const vmethod of virtualMethods) { // Find virtual methods with the same name in the parent const parentVirtualMethods = parent.members.filter( (m) => m instanceof IntrospectedVirtualClassFunction && m.name === vmethod.name, ) as IntrospectedVirtualClassFunction[]; for (const parentMethod of parentVirtualMethods) { // For interfaces, even if the return type is a subtype, TypeScript won't allow // multiple inheritance from interfaces with the same method but different return types // So we need to check if there's any difference in signatures // Note: We can't just use isConflictingFunction because it allows subtype relationships // but TypeScript doesn't allow that for interface multiple inheritance // Check if return types are different (even if one is a subtype of the other) const ourReturn = vmethod.return(); const parentReturn = parentMethod.return(); // Check if return types are not exactly the same // For interface inheritance, even subtype relationships cause conflicts if (!ourReturn.equals(parentReturn)) { return true; // Different return types = conflict } // Also check parameters using the existing conflict detection if (isConflictingFunction(ns, thisType, vmethod, parent.getType(), parentMethod)) { return true; } } } return false; }); return hasParentWithVirtualMethods; }