@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
911 lines (735 loc) • 24.5 kB
text/typescript
import type { IntrospectedNamespace } from "./gir/namespace.ts";
/**
* A list of possible type conflicts.
*
* The format is CHILD_PARENT_CONFLICT so
* ACCESSOR_PROPERTY_CONFLICT means there
* is an accessor on a child class and a
* property on the parent class, which is a
* conflict.
*
* Starts at '1' because the value is often
* used as truthy.
*/
export enum ConflictType {
PROPERTY_NAME_CONFLICT = 1,
FIELD_NAME_CONFLICT,
FUNCTION_NAME_CONFLICT,
ACCESSOR_PROPERTY_CONFLICT,
PROPERTY_ACCESSOR_CONFLICT,
VFUNC_SIGNATURE_CONFLICT,
}
import { type ConsoleReporter, LazyReporter } from "@ts-for-gir/reporter";
import type { IntrospectedField, IntrospectedProperty } from "./gir/property.ts";
import type { OptionsGeneration } from "./types/index.ts";
import { isInvalid, sanitizeIdentifierName, sanitizeNamespace } from "./utils/naming.ts";
export abstract class TypeExpression {
isPointer = false;
abstract equals(type: TypeExpression): boolean;
abstract unwrap(): TypeExpression;
deepUnwrap(): TypeExpression {
return this.unwrap();
}
abstract rewrap(type: TypeExpression): TypeExpression;
abstract resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression;
abstract print(namespace: IntrospectedNamespace, options: OptionsGeneration): string;
rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return this.print(namespace, options);
}
}
export class TypeIdentifier extends TypeExpression {
readonly name: string;
readonly namespace: string;
private static readonly reporter = new LazyReporter("TypeIdentifier");
static configureReporter(enabled: boolean, output?: string) {
TypeIdentifier.reporter.configure(enabled, output);
}
get log(): ConsoleReporter {
return TypeIdentifier.reporter.get();
}
constructor(name: string, namespace: string) {
super();
this.name = name;
this.namespace = namespace;
}
equals(type: TypeExpression): boolean {
return type instanceof TypeIdentifier && type.name === this.name && type.namespace === this.namespace;
}
is(namespace: string, name: string) {
return this.namespace === namespace && this.name === name;
}
unwrap() {
return this;
}
rewrap(type: TypeExpression): TypeExpression {
return type;
}
/**
* TODO: gi.ts didn't deal with sanitizing types but probably should have to avoid
* invalid names such as "3gppProfile"
*/
sanitize() {
return new TypeIdentifier(sanitizeIdentifierName(this.namespace, this.name), sanitizeNamespace(this.namespace));
}
protected _resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeIdentifier | null {
const name: string = sanitizeIdentifierName(null, this.name);
const unresolvedNamespaceName = this.namespace;
const ns = namespace.assertInstalledImport(unresolvedNamespaceName);
if (ns.hasSymbol(name)) {
const c = ns.getClass(name);
// Some records are structs for other class types.
// GirRecord.prototype.getType resolves this relationship.
if (c) return c.getType();
return new TypeIdentifier(name, ns.namespace);
}
// Handle "class callback" types (they're in a definition-merged module)
let [cb, corrected_name] = ns.findClassCallback(name);
let resolved_name: string | null = null;
if (!cb) {
resolved_name = ns.resolveSymbolFromTypeName(name);
}
let c_resolved_name: string | null = null;
if (!c_resolved_name) {
c_resolved_name = ns.resolveSymbolFromTypeName(`${unresolvedNamespaceName}${name}`);
}
if (!c_resolved_name) {
c_resolved_name = ns.resolveSymbolFromTypeName(`${ns.namespace}${name}`);
}
if (!cb && !resolved_name && !c_resolved_name) {
// Don't warn if a missing import is at fault, this will be dealt with later.
if (namespace.namespace === ns.namespace) {
this.log.reportTypeResolutionWarning(
this.name,
this.namespace,
`Attempting to fall back on c:type inference for ${ns.namespace}.${name}`,
`Fallback to c:type inference attempted`,
);
}
[cb, corrected_name] = ns.findClassCallback(`${ns.namespace}${name}`);
if (cb) {
this.log.reportTypeResolutionWarning(
this.name,
this.namespace,
`Falling back on c:type inference for ${ns.namespace}.${name} and found ${ns.namespace}.${corrected_name}`,
`Successfully resolved using c:type fallback`,
);
}
}
if (cb) {
if (options.verbose) {
this.log.debug(`Callback found: ${cb}.${corrected_name}`);
}
return new ModuleTypeIdentifier(corrected_name, cb, ns.namespace);
} else if (resolved_name) {
return new TypeIdentifier(resolved_name, ns.namespace);
} else if (c_resolved_name) {
this.log.reportTypeResolutionWarning(
this.name,
this.namespace,
`Fall back on c:type inference for ${ns.namespace}.${name} and found ${ns.namespace}.${corrected_name}`,
`Using c:type as fallback for type resolution`,
);
return new TypeIdentifier(c_resolved_name, ns.namespace);
} else if (namespace.namespace === ns.namespace) {
this.log.reportTypeResolutionWarning(
this.name,
ns.namespace,
`Unable to resolve type ${this.name} in same namespace ${ns.namespace}!`,
`Type resolution failed within the same namespace`,
);
return null;
}
this.log.reportTypeResolutionWarning(
this.name,
this.namespace,
`Type ${this.name} could not be resolved in ${namespace.namespace} ${namespace.version}`,
`Referenced from ${namespace.namespace} ${namespace.version}`,
`${namespace.namespace} ${namespace.version}`,
);
return null;
}
resolveIdentifier(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeIdentifier | null {
return this._resolve(namespace, options);
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
const resolved = this._resolve(namespace, options);
// Generally if we can't resolve a type it is not introspectable,
// thus we annotate it as "never".
return resolved ?? NeverType;
}
static new({ name, namespace }: { name: string; namespace: string }) {
return new TypeIdentifier(name, namespace);
}
print(namespace: IntrospectedNamespace, _options: OptionsGeneration): string {
if (namespace.hasSymbol(this.namespace) && this.namespace !== namespace.namespace) {
// TODO: Move to TypeScript generator...
// Libraries like zbar have classes named things like "Gtk"
return `${this.namespace}__.${this.name}`;
}
if (namespace.namespace === this.namespace) {
return `${this.name}`;
} else {
return `${this.namespace}.${this.name}`;
}
}
}
export class ModuleTypeIdentifier extends TypeIdentifier {
readonly moduleName: string;
declare readonly namespace: string;
constructor(name: string, moduleName: string, namespace: string) {
super(name, namespace);
this.moduleName = moduleName;
this.namespace = namespace;
}
equals(type: TypeExpression): boolean {
return super.equals(type) && type instanceof ModuleTypeIdentifier && this.moduleName === type.moduleName;
}
is(namespace: string, moduleName: string, name?: string) {
return this.namespace === namespace && this.moduleName === moduleName && this.name === name && name !== undefined;
}
unwrap() {
return this;
}
rewrap(type: TypeExpression): TypeExpression {
return type;
}
sanitize() {
return new ModuleTypeIdentifier(
sanitizeIdentifierName(this.namespace, this.name),
sanitizeIdentifierName(this.namespace, this.moduleName),
sanitizeNamespace(this.namespace),
);
}
protected _resolve(_namespace: IntrospectedNamespace, _options: OptionsGeneration): ModuleTypeIdentifier | null {
return this;
}
print(namespace: IntrospectedNamespace, _options: OptionsGeneration): string {
if (namespace.namespace === this.namespace) {
return `${this.moduleName}.${this.name}`;
} else {
return `${this.namespace}.${this.moduleName}.${this.name}`;
}
}
}
/**
* This class overrides the default printing for types
*/
export class ClassStructTypeIdentifier extends TypeIdentifier {
equals(type: TypeExpression): boolean {
return type instanceof ClassStructTypeIdentifier && super.equals(type);
}
print(namespace: IntrospectedNamespace, _options: OptionsGeneration): string {
if (namespace.namespace === this.namespace) {
// TODO: Mapping to invalid names should happen at the generator level...
return `typeof ${isInvalid(this.name) ? `__${this.name}` : this.name}`;
} else {
return `typeof ${this.namespace}.${isInvalid(this.name) ? `__${this.name}` : this.name}`;
}
}
}
export class GenerifiedTypeIdentifier extends TypeIdentifier {
generics: TypeExpression[];
constructor(name: string, namespace: string, generics: TypeExpression[] = []) {
super(name, namespace);
this.generics = generics;
}
print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
const Generics = this.generics.map((generic) => generic.print(namespace, options)).join(", ");
if (namespace.namespace === this.namespace) {
return `${this.name}${this.generics.length > 0 ? `<${Generics}>` : ""}`;
} else {
return `${this.namespace}.${this.name}${this.generics.length > 0 ? `<${Generics}>` : ""}`;
}
}
_resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeIdentifier | null {
const iden = super._resolve(namespace, options);
if (iden) {
return new GenerifiedTypeIdentifier(iden.name, iden.namespace, [...this.generics]);
}
return iden;
}
}
export class NativeType extends TypeExpression {
readonly expression: (options?: OptionsGeneration) => string;
constructor(expression: ((options?: OptionsGeneration) => string) | string) {
super();
this.expression = typeof expression === "string" ? () => expression : expression;
}
rewrap(type: TypeExpression): TypeExpression {
return type;
}
resolve(): TypeExpression {
return this;
}
print(_namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return this.expression(options);
}
equals(type: TypeExpression, options?: OptionsGeneration): boolean {
return type instanceof NativeType && this.expression(options) === type.expression(options);
}
unwrap(): TypeExpression {
return this;
}
static withGenerator(generator: (options?: OptionsGeneration) => string): TypeExpression {
return new NativeType(generator);
}
static of(nativeType: string): NativeType {
return new NativeType(nativeType);
}
}
export class OrType extends TypeExpression {
readonly types: ReadonlyArray<TypeExpression>;
constructor(...types: TypeExpression[]) {
super();
this.types = [...types];
}
rewrap(type: TypeExpression): TypeExpression {
return type;
}
unwrap(): TypeExpression {
return this;
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
return makeUnion(...this.types.map((t) => t.resolve(namespace, options)));
}
print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return `${this.types.map((t) => t.print(namespace, options)).join(" | ")}`;
}
rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return this.print(namespace, options);
}
equals(type: TypeExpression) {
if (type instanceof OrType) {
return this.types.every((t) => type.types.some((type) => type.equals(t)));
} else {
return false;
}
}
}
export function makeUnion(...inputTypes: TypeExpression[]) {
const types: Set<TypeExpression> = new Set();
for (const type of inputTypes) {
if (type instanceof BinaryType) {
types.add(type.a);
types.add(type.b);
} else if (type instanceof OrType && !(type instanceof TupleType)) {
for (const t of type.types) {
types.add(t);
}
} else {
types.add(type);
}
}
if (types.size === 1) {
return [...types][0];
}
if (types.size === 2) {
const typesArray = [...types];
if (typesArray[0] === NullType) {
return new NullableType(typesArray[1]);
}
if (typesArray[1] === NullType) {
return new NullableType(typesArray[0]);
}
return new BinaryType(...typesArray);
}
return new OrType(...types);
}
export class TupleType extends OrType {
print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return `[${this.types.map((t) => t.print(namespace, options)).join(", ")}]`;
}
rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return this.print(namespace, options);
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
const [type, ...types] = this.types;
return new TupleType(type.resolve(namespace, options), ...types.map((t) => t.resolve(namespace, options)));
}
equals(type: TypeExpression) {
if (type instanceof TupleType) {
return this.types.length === type.types.length && this.types.every((t, i) => type.types[i].equals(t));
} else {
return false;
}
}
}
export class BinaryType extends OrType {
unwrap(): TypeExpression {
return this;
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration) {
return new BinaryType(this.a.resolve(namespace, options), this.b.resolve(namespace, options));
}
is() {
return false;
}
get a() {
return this.types[0];
}
get b() {
return this.types[1];
}
}
export class FunctionType extends TypeExpression {
parameterTypes: { [name: string]: TypeExpression };
returnType: TypeExpression;
constructor(parameters: { [name: string]: TypeExpression }, returnType: TypeExpression) {
super();
this.parameterTypes = parameters;
this.returnType = returnType;
}
equals(type: TypeExpression): boolean {
if (type instanceof FunctionType) {
return (
Object.values(this.parameterTypes).every((t) =>
Object.values(type.parameterTypes).some((tp) => t.equals(tp)),
) &&
Object.values(type.parameterTypes).every((t) =>
Object.values(this.parameterTypes).some((tp) => t.equals(tp)),
) &&
this.returnType.equals(type.returnType)
);
}
return false;
}
rewrap(type: TypeExpression): TypeExpression {
return type;
}
unwrap(): TypeExpression {
return this;
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
return new FunctionType(
Object.fromEntries(
Object.entries(this.parameterTypes).map(([k, p]) => {
return [k, p.resolve(namespace, options)];
}),
),
this.returnType.resolve(namespace, options),
);
}
rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
const Parameters = Object.entries(this.parameterTypes)
.map(([k, v]) => {
return `${k}: ${v.rootPrint(namespace, options)}`;
})
.join(", ");
return `(${Parameters}) => ${this.returnType.print(namespace, options)}`;
}
print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return `(${this.rootPrint(namespace, options)})`;
}
}
export class Generic {
private _deriveFrom: TypeIdentifier | null;
private _genericType: GenericType;
private _defaultType: TypeExpression | null;
private _constraint: TypeExpression | null;
private _propagate: boolean;
constructor(
genericType: GenericType,
defaultType?: TypeExpression,
deriveFrom?: TypeIdentifier,
constraint?: TypeExpression,
propagate = true,
) {
this._deriveFrom = deriveFrom ?? null;
this._genericType = genericType;
this._defaultType = defaultType ?? null;
this._constraint = constraint ?? null;
this._propagate = propagate;
}
unwrap() {
return this._genericType;
}
get propagate() {
return this._propagate;
}
get type() {
return this._genericType;
}
get defaultType() {
return this._defaultType;
}
get constraint() {
return this._constraint;
}
get parent() {
return this._deriveFrom;
}
}
export class GenerifiedType extends TypeExpression {
type: TypeExpression;
generic: GenericType;
constructor(type: TypeExpression, generic: GenericType) {
super();
this.type = type;
this.generic = generic;
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration) {
return new GenerifiedType(this.type.resolve(namespace, options), this.generic);
}
unwrap() {
return this.type;
}
rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration) {
return this.type.rootPrint(namespace, options);
}
print(namespace: IntrospectedNamespace, options: OptionsGeneration) {
return `${this.type.print(namespace, options)}<${this.generic.print()}>`;
}
equals(type: TypeExpression): boolean {
if (type instanceof GenerifiedType) {
return this.type.equals(type.type) && this.generic.equals(type.generic);
}
return this.type.equals(type);
}
rewrap(type: TypeExpression): TypeExpression {
return new GenerifiedType(this.type.rewrap(type), this.generic);
}
}
export class GenericType extends TypeExpression {
identifier: string;
replacedType?: TypeExpression;
constructor(identifier: string, replacedType?: TypeExpression) {
super();
this.identifier = identifier;
this.replacedType = replacedType;
}
equals(type: TypeExpression): boolean {
if (type instanceof GenericType) {
const genericType = type;
return this.identifier === genericType.identifier;
}
return false;
}
unwrap(): TypeExpression {
return this;
}
rewrap(type: TypeExpression): TypeExpression {
return type;
}
resolve(): GenericType {
return this;
}
print(): string {
return this.identifier;
}
}
export class NullableType extends BinaryType {
constructor(type: TypeExpression) {
super(type, NullType);
}
unwrap() {
return this.a;
}
rewrap(type: TypeExpression): TypeExpression {
return makeNullable(this.a.rewrap(type));
}
get type() {
return this.a;
}
}
export function makeNullable(type: TypeExpression) {
if (type instanceof NullableType) return type;
if (type === RawPointerType) return NullType;
if (type === AnyType) return AnyType;
return makeUnion(type, NullType);
}
export class PromiseType extends TypeExpression {
type: TypeExpression;
constructor(type: TypeExpression) {
super();
this.type = type;
}
equals(type: TypeExpression): boolean {
return type instanceof PromiseType && type.type.equals(this.type);
}
unwrap() {
return this;
}
rewrap(type: TypeExpression): TypeExpression {
return new PromiseType(this.type.rewrap(type));
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
return new PromiseType(this.type.resolve(namespace, options));
}
print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
if (this.type.equals(VoidType)) {
return "globalThis.Promise<void>";
}
return `globalThis.Promise<${this.type.print(namespace, options)}>`;
}
rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return this.print(namespace, options);
}
}
/**
* This is one of the more interesting usages of our type
* system. To handle type conflicts we wrap conflicting types
* in this class with a ConflictType to denote why they are a
* conflict.
*
* TypeConflict will throw if it is printed or resolved, so generators
* must unwrap it and "resolve" the conflict. Some generators like JSON
* just disregard this info, other generators like DTS attempt to
* resolve the conflicts so the typing stays sound.
*/
export class TypeConflict extends TypeExpression {
readonly conflictType: ConflictType;
readonly type: TypeExpression;
constructor(type: TypeExpression, conflictType: ConflictType) {
super();
this.type = type;
this.conflictType = conflictType;
}
rewrap(type: TypeExpression): TypeConflict {
return new TypeConflict(this.type.rewrap(type), this.conflictType);
}
unwrap(): TypeExpression {
return this.type;
}
// TODO: This constant "true" is a remnant of the Anyified type.
equals(): boolean {
return true;
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
const resolvedType = this.type.resolve(namespace, options);
const typeString = resolvedType.print(namespace, options);
throw new Error(`Type conflict was not resolved for ${typeString} in ${namespace.namespace}`);
}
print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
const resolvedType = this.type.resolve(namespace, options);
const typeString = resolvedType.print(namespace, options);
throw new Error(`Type conflict was not resolved for ${typeString} in ${namespace.namespace}`);
}
}
export class ClosureType extends TypeExpression {
type: TypeExpression;
user_data: number | null = null;
constructor(type: TypeExpression) {
super();
this.type = type;
}
equals(type: TypeExpression): boolean {
if (type instanceof ClosureType) {
const closureType = type;
return this.type.equals(closureType.type);
}
return false;
}
deepUnwrap(): TypeExpression {
return this.type;
}
rewrap(type: TypeExpression): TypeExpression {
const closure = new ClosureType(this.type.rewrap(type));
closure.user_data = this.user_data;
return closure;
}
unwrap(): TypeExpression {
return this;
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration) {
const { user_data, type } = this;
return ClosureType.new({
user_data,
type: type.resolve(namespace, options),
});
}
print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
return this.type.print(namespace, options);
}
static new({ type, user_data = null }: { type: TypeExpression; user_data?: number | null }) {
const vt = new ClosureType(type);
vt.user_data = user_data;
return vt;
}
}
export class ArrayType extends TypeExpression {
type: TypeExpression;
arrayDepth: number = 1;
length: number | null = null;
constructor(type: TypeExpression) {
super();
this.type = type;
}
deepUnwrap(): TypeExpression {
return this.type;
}
unwrap(): TypeExpression {
return this;
}
rewrap(type: TypeExpression): TypeExpression {
const array = new ArrayType(this.type.rewrap(type));
array.arrayDepth = this.arrayDepth;
array.length = this.length;
return array;
}
equals(type: TypeExpression) {
if (type instanceof ArrayType) {
const arrayType: ArrayType = type;
return arrayType.type.equals(this.type) && type.arrayDepth === this.arrayDepth;
}
return false;
}
resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
const { type, arrayDepth, length } = this;
return ArrayType.new({
type: type.resolve(namespace, options),
arrayDepth,
length,
});
}
print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
const depth = this.arrayDepth;
let typeSuffix: string = "";
if (depth === 0) {
typeSuffix = "";
} else if (depth === 1) {
typeSuffix = "[]";
} else {
typeSuffix = "".padStart(2 * depth, "[]");
}
if (this.type instanceof OrType && !(this.type instanceof TupleType))
return `(${this.type.print(namespace, options)})${typeSuffix}`;
return `${this.type.print(namespace, options)}${typeSuffix}`;
}
static new({
type,
arrayDepth = 1,
length = null,
}: {
type: TypeExpression;
length?: number | null;
arrayDepth?: number;
}) {
const vt = new ArrayType(type);
vt.length = length;
vt.arrayDepth = arrayDepth;
return vt;
}
}
/**
* Common native types as constants.
* These represent TypeScript types in generated .d.ts output for GJS runtime,
* not internal tool types. Uses of `any` here are intentional — they produce
* correct type definitions for GLib/GIO/GObject APIs that accept dynamic values.
*/
export const ThisType = new NativeType("this");
export const ObjectType = new NativeType("object");
export const AnyType = new NativeType("any");
export const NeverType = new NativeType("never");
export const Uint8ArrayType = new NativeType("Uint8Array");
export const BooleanType = new NativeType("boolean");
export const StringType = new NativeType("string");
export const NumberType = new NativeType("number");
export const BigintOrNumberType = new BinaryType(new NativeType("bigint"), NumberType);
export const NullType = new NativeType("null");
export const VoidType = new NativeType("void");
export const UnknownType = new NativeType("unknown");
export const AnyFunctionType = new NativeType("(...args: any[]) => any");
// Distinct from NeverType, so that we can transform it into NullType when
// marshalled from C to JS
export const RawPointerType = new NativeType("never");
export type GirClassField = IntrospectedProperty | IntrospectedField;