@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
599 lines (519 loc) • 21 kB
text/typescript
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;
}