@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
683 lines (668 loc) • 29.9 kB
JavaScript
import { FormatGenerator } from "./generator.js";
import { IntrospectedInterface, filterConflicts, filterFunctionConflict, FilterBehavior } from "../gir/class.js";
import { promisifyFunctions } from "../gir/promisify.js";
import { IntrospectedSignalType } from "../gir/signal.js";
import { IntrospectedConstructor, IntrospectedFunctionParameter } from "../gir/function.js";
import { IntrospectedClassFunction, IntrospectedStaticClassFunction } from "../gir/function.js";
import { sanitizeIdentifierName, isInvalid, resolveDirectedType } from "../gir/util.js";
import { NativeType, AnyType, VoidType, StringType, NumberType, ArrayType, AnyFunctionType, ConflictType, TypeConflict, BinaryType } from "../gir.js";
import { GirDirection } from "@gi.ts/parser";
export function versionImportFormat(versionFormat, namespace, version) {
const versionSlug = version.toLowerCase().split(".")[0];
const namespaceLowercase = namespace.toLowerCase();
return `${versionFormat
.replace("{version}", version)
.replace("{version-slug}", versionSlug)
.replace("{namespace}", namespace)
.replace("{namespace-lower}", namespaceLowercase)}`;
}
export class DtsGenerator extends FormatGenerator {
constructor(namespace, options) {
super(namespace, options);
}
generateParameters(parameters) {
return parameters
.map(p => {
return p.asString(this);
})
.join(", ");
}
generateGenerics(nodes, withDefaults = true) {
const { namespace, options } = this;
const list = nodes.map(generic => {
const Type = generic.type.rootPrint(namespace, options);
if (generic.defaultType && withDefaults) {
const defaultType = generic.defaultType.rootPrint(namespace, options);
if (generic.constraint) {
const constraint = generic.constraint.rootPrint(namespace, options);
return `${Type} extends ${constraint} = ${defaultType}`;
}
return `${Type} = ${defaultType}`;
}
else if (generic.constraint && withDefaults) {
const constraint = generic.constraint.rootPrint(namespace, options);
return `${Type} extends ${constraint}`;
}
else {
return `${Type}`;
}
});
if (list.length > 0) {
return `<${list.join(", ")}>`;
}
return "";
}
generateCallbackType(node) {
const { namespace, options } = this;
const Parameters = this.generateParameters(node.parameters);
if (node.generics.length > 0) {
const GenericDefinitions = this.generateGenerics(node.generics);
return [
`${GenericDefinitions}`,
`(${Parameters}) => ${node.return().resolve(namespace, options).print(namespace, options)}`
];
}
return ["", `(${Parameters}) => ${node.return().resolve(namespace, options).print(namespace, options)}`];
}
generateCallback(node) {
return `${this.docString(node)}export type ${node.name}${this.generateCallbackType(node).join(" = ")};`;
}
generateClassCallback(node) {
return this.generateCallback(node);
}
generateReturn(return_type, output_parameters) {
const { namespace, options } = this;
const resolved_return_type = resolveDirectedType(return_type, GirDirection.Out)?.resolve(namespace, options) ??
return_type.resolve(namespace, options);
const type = resolved_return_type.rootPrint(namespace, options);
if (output_parameters.length > 0) {
const exclude_first = type === "void" || type === "";
const returns = [
...(exclude_first ? [] : [`${type}`]),
...output_parameters
.map(op => {
return (resolveDirectedType(op.type, GirDirection.Out)?.resolve(namespace, options) ??
op.type.resolve(namespace, options));
})
.map(p => p.rootPrint(namespace, options))
];
if (returns.length > 1) {
return `[${returns.join(", ")}]`;
}
else {
return `${returns[0]}`;
}
}
return type;
}
generateEnum(node) {
const { namespace } = this;
const isInvalidEnum = Array.from(node.members.keys()).some(name => name.match(/^[0-9]+$/) || name === "NaN" || name === "Infinity");
if (isInvalidEnum) {
return node.asClass().asString(this);
}
// So we can use GObject.GType
this.namespace.assertInstalledImport("GObject");
return `
export namespace ${node.name} {
export const $gtype: ${namespace.namespace !== "GObject" ? "GObject." : ""}GType<${node.name}>;
}
${this.docString(node)}export enum ${node.name} {
${Array.from(node.members.values())
.map(member => `${member.asString(this)}`)
.join("\n ")}
}`;
}
generateError(node) {
const { namespace } = this;
const clazz = node.asClass();
clazz.members = [];
clazz.members.push(...Array.from(node.functions.values()));
const GLib = namespace.assertInstalledImport("GLib");
const GLibError = GLib.assertClass("Error");
clazz.superType = GLibError.getType();
// Manually construct a GLib.Error constructor.
clazz.mainConstructor = new IntrospectedConstructor({
name: "new",
parent: clazz,
parameters: [
new IntrospectedFunctionParameter({
name: "options",
type: NativeType.of("{ message: string, code: number}"),
direction: GirDirection.In
})
],
return_type: clazz.getType()
});
return clazz.asString(this);
}
generateConst(node) {
const { namespace, options } = this;
return `${this.docString(node)}export const ${node.name}: ${node.type
.resolve(namespace, options)
.print(namespace, options)};`;
}
implements(node) {
const { namespace, options } = this;
const interfaces = node.interfaces.map(i => {
const identifier = i.resolveIdentifier(namespace, options);
if (!identifier) {
throw new Error(`Unable to resolve type: ${i.name} from ${i.namespace} in ${node.namespace.namespace} ${node.namespace.version}`);
}
return identifier;
});
if (interfaces.length > 0) {
return ` implements ${interfaces
.map(i => {
const Type = i.print(namespace, options);
return `${Type}`;
})
.join(", ")}`;
}
return "";
}
extends(node) {
const { namespace: ns, options } = this;
if (node.superType) {
const ResolvedType = node.superType.resolveIdentifier(ns, options);
const Type = ResolvedType?.print(ns, options);
if (Type) {
return ` extends ${Type}`;
}
throw new Error(`Unable to resolve type: ${node.superType.name} from ${node.superType.namespace} in ${node.namespace.namespace} ${node.namespace.version}`);
}
return "";
}
generateInterface(node) {
const { namespace, options } = this;
const isGObject = node.someParent(p => p.namespace.namespace === "GObject" && p.name === "Object");
const name = node.name;
const generics = node.generics;
let Generics = "";
let GenericTypes = "";
if (generics.length > 0) {
Generics = `${this.generateGenerics(generics)}`;
GenericTypes = `${this.generateGenerics(generics, false)}`;
}
const Extends = this.extends(node);
const filteredFunctions = filterFunctionConflict(node.namespace, node, node.members, []);
const functions = options.promisify ? promisifyFunctions(filteredFunctions) : filteredFunctions;
const staticFunctions = functions.filter(f => f instanceof IntrospectedStaticClassFunction);
const staticFields = node.fields
.filter(f => f.isStatic)
.map(f => f.copy({
isStatic: false
}));
const nonStaticFunctions = functions.filter(f => !(f instanceof IntrospectedStaticClassFunction));
const nonStaticFields = node.fields.filter(f => !f.isStatic);
const hasNamespace = isGObject || staticFunctions.length > 0 || node.callbacks.length > 0;
if (isGObject) {
// So we can use GObject.GType
this.namespace.assertInstalledImport("GObject");
}
return `
${node.callbacks.length > 0
? `export module ${name} {
${node.callbacks.map(c => c.asString(this)).join("\n")}
}`
: ""}
${hasNamespace
? `${this.docString(node)}export interface ${name}Namespace {
${isGObject ? `$gtype: ${namespace.namespace !== "GObject" ? "GObject." : ""}GType<${name}>;` : ""}
prototype: ${name}Prototype;
${staticFields.length > 0 ? staticFields.map(sf => sf.asString(this)).join("\n") : ""}
${staticFunctions.length > 0
? staticFunctions.map(sf => IntrospectedClassFunction.prototype.asString.call(sf, this)).join("\n")
: ""}
}`
: ""}
export type ${name}${Generics} = ${name}Prototype${GenericTypes};
export interface ${name}Prototype${Generics}${Extends} {${node.__ts__indexSignature ? `\n${node.__ts__indexSignature}\n` : ""}
${node.props.length > 0 ? "// Properties" : ""}
${filterConflicts(node.namespace, node, node.props)
.map(p => p.asString(this))
.join("\n")}
${nonStaticFields.length > 0 ? "// Fields" : ""}
${filterConflicts(node.namespace, node, nonStaticFields)
.map(p => p.asString(this))
.join("\n")}
${nonStaticFunctions.length > 0 ? "// Members\n" : ""}
${nonStaticFunctions.map(m => m.asString(this)).join("\n")}
}${hasNamespace ? `\n\nexport const ${name}: ${name}Namespace;\n` : ""}`;
}
generateRecord(node) {
const { options, namespace } = this;
const { name } = node;
const Extends = this.extends(node);
let Generics = "";
if (node.generics.length > 0) {
Generics = `${this.generateGenerics(node.generics)}`;
}
let MainConstructor = "";
if (node.isForeign()) {
MainConstructor = "";
}
else if (node.mainConstructor) {
MainConstructor = node.mainConstructor.asString(this);
}
const hasCallbacks = node.callbacks.length > 0;
const Properties = filterConflicts(node.namespace, node, node.props)
.map(v => v.asString(this))
.join("\n");
const Fields = filterConflicts(node.namespace, node, node.fields)
.map(v => v.asString(this))
.join("\n");
const Constructors = filterConflicts(node.namespace, node, node.constructors)
.map(v => this.generateConstructorFunction(v))
.join("\n");
const FilteredMembers = filterFunctionConflict(node.namespace, node, node.members, []);
const Members = (options.promisify ? promisifyFunctions(FilteredMembers) : FilteredMembers)
.map(v => v.asString(this))
.join("\n");
// So we can use GObject.GType
this.namespace.assertInstalledImport("GObject");
return `${hasCallbacks
? `export module ${name} {
${node.callbacks.map(c => c.asString(this)).join("\n")}
}`
: ""}
${this.docString(node)}export class ${name}${Generics}${Extends} {${node.__ts__indexSignature ? `\n${node.__ts__indexSignature}\n` : ""}
static $gtype: ${namespace.namespace !== "GObject" ? "GObject." : ""}GType<${name}>;
${MainConstructor}
constructor(copy: ${node.name});
${node.props.length > 0 ? "// Properties" : ""}
${Properties}
${node.fields.length > 0 ? "// Fields" : ""}
${Fields}
${node.constructors.length > 0 ? "// Constructors" : ""}
${Constructors}
${node.members.length > 0 ? "// Members" : ""}
${Members}
}`;
}
generateClass(node) {
const { options, namespace } = this;
const name = node.name;
const injectConstructorBucket = !node.mainConstructor;
let Generics = "";
let GenericTypes = "";
if (node.generics.length > 0) {
Generics = `${this.generateGenerics(node.generics)}`;
GenericTypes = `${this.generateGenerics(node.generics, false)}`;
}
const Extends = this.extends(node);
const Implements = this.implements(node);
const implementedProperties = node.implementedProperties();
const implementedMethods = node.implementedMethods(implementedProperties);
let MainConstructor = "";
if (node.mainConstructor) {
MainConstructor = `\n${node.mainConstructor.asString(this)}`;
}
else {
MainConstructor = `\nconstructor(properties?: Partial<${name}.ConstructorProperties${GenericTypes}>, ...args: any[]);\n`;
// TODO: options migration
//if (!options.noInitTypes) {
// MainConstructor += `_init(properties?: Partial<${name}.ConstructorProperties${GenericTypes}>, ...args: any[]): void;\n`;
//} else {
MainConstructor += "_init(...args: any[]): void;\n";
//}
}
const ConstructorProps = filterConflicts(node.namespace, node,
// TODO: Include properties from interface parents too.
node.props)
.map(v => v.asString(this, true))
.join("\n ");
const Properties = filterConflicts(node.namespace, node, node.props)
.map(v => v.asString(this))
.join("\n ");
const Fields = filterConflicts(node.namespace, node, node.fields)
.map(v => v.asString(this))
.join("\n ");
const Constructors = filterFunctionConflict(node.namespace, node, node.constructors, [])
.map(v => this.generateConstructorFunction(v))
.join("\n ");
const FilteredMembers = filterFunctionConflict(node.namespace, node, node.members, []);
const Members = (options.promisify ? promisifyFunctions(FilteredMembers) : FilteredMembers)
.map(v => v.asString(this))
.join("\n ");
const ImplementedProperties = filterConflicts(node.namespace, node, implementedProperties)
.map(m => m.asString(this))
.join("\n ");
const FilteredImplMethods = filterFunctionConflict(node.namespace, node, implementedMethods, []);
const ImplementedMethods = (options.promisify ? promisifyFunctions(FilteredImplMethods) : FilteredImplMethods)
.map(m => m.asString(this))
.join("\n ");
// TODO Move these to a cleaner place.
const Connect = new IntrospectedClassFunction({
name: "connect",
parent: node,
parameters: [
new IntrospectedFunctionParameter({
name: "id",
type: StringType,
direction: GirDirection.In
}),
new IntrospectedFunctionParameter({
name: "callback",
type: AnyFunctionType,
direction: GirDirection.In
})
],
return_type: NumberType
});
const ConnectAfter = new IntrospectedClassFunction({
name: "connect_after",
parent: node,
parameters: [
new IntrospectedFunctionParameter({
name: "id",
type: StringType,
direction: GirDirection.In
}),
new IntrospectedFunctionParameter({
name: "callback",
type: AnyFunctionType,
direction: GirDirection.In
})
],
return_type: NumberType
});
const Emit = new IntrospectedClassFunction({
name: "emit",
parent: node,
parameters: [
new IntrospectedFunctionParameter({
name: "id",
type: StringType,
direction: GirDirection.In
}),
new IntrospectedFunctionParameter({
name: "args",
isVarArgs: true,
type: new ArrayType(AnyType),
direction: GirDirection.In
})
],
return_type: VoidType
});
let default_signals = [];
let hasConnect, hasConnectAfter, hasEmit;
if (node.signals.length > 0) {
hasConnect = node.members.some(m => m.name === "connect");
hasConnectAfter = node.members.some(m => m.name === "connect_after");
hasEmit = node.members.some(m => m.name === "emit");
if (!hasConnect) {
default_signals.push(Connect);
}
if (!hasConnectAfter) {
default_signals.push(ConnectAfter);
}
if (!hasEmit) {
default_signals.push(Emit);
}
default_signals = filterConflicts(namespace, node, default_signals, FilterBehavior.DELETE);
hasConnect = !default_signals.some(s => s.name === "connect");
hasConnectAfter = !default_signals.some(s => s.name === "connect_after");
hasEmit = !default_signals.some(s => s.name === "emit");
}
const SignalsList = [
// TODO Relocate these.
...default_signals.map(s => s.asString(this)),
...node.signals
.map(s => {
const methods = [];
if (!hasConnect)
methods.push(s.asString(this, IntrospectedSignalType.CONNECT));
if (!hasConnectAfter)
methods.push(s.asString(this, IntrospectedSignalType.CONNECT_AFTER));
if (!hasEmit)
methods.push(s.asString(this, IntrospectedSignalType.EMIT));
return methods;
})
.flat()
];
const hasSignals = SignalsList.length > 0;
const Signals = SignalsList.join("\n");
const hasCallbacks = node.callbacks.length > 0;
const hasModule = injectConstructorBucket || hasCallbacks;
// So we can use GObject.GType
this.namespace.assertInstalledImport("GObject");
const parts = Extends.split("<");
const ExtendsInterface = parts[0];
let ExtendsGenerics = parts[1] ?? "";
if (ExtendsGenerics.length > 0) {
ExtendsGenerics = `<${ExtendsGenerics}`;
}
return `${hasModule
? `export module ${name} {
${hasCallbacks ? node.callbacks.map(c => c.asString(this)).join("\n") : ""}
${injectConstructorBucket
? `export interface ConstructorProperties${Generics}${Extends ? `${ExtendsInterface}.ConstructorProperties${ExtendsGenerics}` : ""} {
[key: string]: any;
${ConstructorProps}
}`
: ""}
}`
: ""}
export ${node.isAbstract ? "abstract " : ""}class ${name}${Generics}${Extends}${Implements} {${node.__ts__indexSignature ? `\n${node.__ts__indexSignature}\n` : ""}
static $gtype: ${namespace.namespace !== "GObject" ? "GObject." : ""}GType<${name}>;
${MainConstructor}
${node.props.length > 0 ? "// Properties" : ""}
${Properties}
${node.fields.length > 0 ? "// Fields" : ""}
${Fields}
${hasSignals ? "// Signals\n" : ""}
${Signals}
${implementedProperties.length > 0 ? "// Implemented Properties\n" : ""}
${ImplementedProperties}
${node.constructors.length > 0 ? "// Constructors\n" : ""}
${Constructors}
${node.members.length > 0 ? "// Members\n" : ""}
${Members}
${implementedMethods.length > 0 ? "// Implemented Members\n" : ""}
${ImplementedMethods}
}`;
}
generateField(node) {
const { namespace, options } = this;
const { name, computed } = node;
const invalid = isInvalid(name);
const Static = node.isStatic ? "static" : "";
const ReadOnly = node.writable ? "" : "readonly";
const Modifier = [Static, ReadOnly].filter(a => a !== "").join(" ");
const Name = computed ? `[${name}]` : invalid ? `"${name}"` : name;
let { type } = node;
let fieldAnnotation = "";
if (type instanceof TypeConflict) {
if (type.conflictType === ConflictType.PROPERTY_ACCESSOR_CONFLICT) {
fieldAnnotation = `// This accessor conflicts with a property, field, or function name in a parent class or interface.
// @ts-expect-error\n`;
}
type = new BinaryType(type.unwrap(), AnyType);
}
return `${this.docString(node)}${fieldAnnotation}${Modifier} ${Name}${node.optional ? "?" : ""}: ${type
.resolve(namespace, options)
.rootPrint(namespace, options)};`;
}
generateProperty(node, construct = false) {
const { namespace, options } = this;
const invalid = isInvalid(node.name);
const Name = invalid ? `"${node.name}"` : node.name;
let type = node.type;
let getterAnnotation = "";
let setterAnnotation = "";
let getterSetterAnnotation = "";
if (type instanceof TypeConflict) {
switch (type.conflictType) {
case ConflictType.FUNCTION_NAME_CONFLICT:
case ConflictType.FIELD_NAME_CONFLICT:
getterSetterAnnotation = setterAnnotation =
"// This accessor conflicts with a property, field, or function name in a parent class or interface.\n";
case ConflictType.ACCESSOR_PROPERTY_CONFLICT:
getterSetterAnnotation = getterAnnotation =
"// This accessor conflicts with a property, field, or function name in a parent class or interface.\n";
type = new BinaryType(type.unwrap(), AnyType);
break;
case ConflictType.PROPERTY_ACCESSOR_CONFLICT:
type = new BinaryType(type.unwrap(), AnyType);
break;
case ConflictType.PROPERTY_NAME_CONFLICT:
getterSetterAnnotation =
setterAnnotation =
getterAnnotation =
"// This accessor conflicts with another accessor's type in a parent class or interface.\n";
type = new BinaryType(type.unwrap(), AnyType);
break;
}
if (construct && !(type instanceof BinaryType)) {
// For constructor properties we just convert to any.
type = new BinaryType(type, AnyType);
}
}
const Type = type.resolve(namespace, options).rootPrint(namespace, options) || "any";
if (construct) {
return `${Name}: ${Type};`;
}
const { readable, writable, constructOnly } = node;
const hasGetter = readable;
const hasSetter = writable && !constructOnly;
if (node.parent instanceof IntrospectedInterface) {
if (!hasSetter && hasGetter) {
return `readonly ${Name}: ${Type};`;
}
else {
return `${Name}: ${Type};`;
}
}
if (hasGetter && hasSetter) {
return `${getterAnnotation} get ${Name}(): ${Type};
${setterAnnotation} set ${Name}(val: ${Type});`;
}
else if (hasGetter) {
return `${getterSetterAnnotation} get ${Name}(): ${Type};`;
}
else {
return `${getterSetterAnnotation} set ${Name}(val: ${Type});`;
}
}
generateSignal(node, type = IntrospectedSignalType.CONNECT) {
switch (type) {
case IntrospectedSignalType.CONNECT:
return node.asConnect(false).asString(this);
case IntrospectedSignalType.CONNECT_AFTER:
return node.asConnect(true).asString(this);
case IntrospectedSignalType.EMIT:
return node.asEmit().asString(this);
}
}
generateEnumMember(node) {
const invalid = isInvalid(node.name);
if (node.value != null && !Number.isNaN(Number.parseInt(node.value, 10))) {
return invalid
? `${this.docString(node)}"${node.name}" = ${node.value},`
: `${this.docString(node)}${node.name} = ${node.value},`;
}
else {
return invalid ? `${this.docString(node)}"${node.name}",` : `${this.docString(node)}${node.name},`;
}
}
generateParameter(node) {
const { namespace, options } = this;
const type = resolveDirectedType(node.type, node.direction)?.resolve(namespace, options).rootPrint(namespace, options) ??
node.type.resolve(namespace, options).rootPrint(namespace, options);
if (node.isVarArgs) {
return `...args: ${type}`;
}
if (node.isOptional) {
return `${node.name}?: ${type}`;
}
else {
return `${node.name}: ${type}`;
}
}
docString(node) {
// TODO: Support node.doc not being a string?
return typeof node.doc === "string" && !this.options.noComments
? `/**
${node.doc
.split("\n")
.map(line => ` * ${line
.trim()
.replace("*/", "*\\/")
.replace(/@([a-z_]+?)([. ])/g, "`$1$2`")}`.replace(/@([a-z])/g, "$1"))
.join("\n")}
*/\n`
: "";
}
generateFunction(node) {
const { namespace } = this;
// Register our identifier with the sanitized identifiers.
// We avoid doing this in fromXML because other class-level function classes
// depends on that code.
sanitizeIdentifierName(namespace.namespace, node.raw_name);
const Parameters = this.generateParameters(node.parameters);
const ReturnType = this.generateReturn(node.return(), node.output_parameters);
const Generics = this.generateGenerics(node.generics);
return `${this.docString(node)}export function ${node.name}${Generics}(${Parameters}): ${ReturnType};`;
}
generateConstructorFunction(node) {
const { namespace, options } = this;
const Parameters = this.generateParameters(node.parameters);
const invalid = isInvalid(node.name);
const name = invalid ? `["${node.name}"]` : node.name;
const warning = node.getWarning();
return `${warning ? `${warning}\n` : ""}${this.docString(node)}static ${name}(${Parameters}): ${node
.return()
.resolve(namespace, options)
.rootPrint(namespace, options)};`;
}
generateConstructor(node) {
const Parameters = this.generateParameters(node.parameters);
return `constructor(${Parameters});`;
}
generateDirectAllocationConstructor(node) {
const ConstructorFields = node.parameters.map(param => param.asField().asString(this)).join("\n");
return `
constructor(properties?: Partial<{
${ConstructorFields}
}>);`;
}
generateClassFunction(node) {
const invalid = isInvalid(node.name);
const parameters = node.parameters;
const output_parameters = node.output_parameters;
const return_type = node.return();
const Parameters = this.generateParameters(parameters);
const ReturnType = this.generateReturn(return_type, output_parameters);
const Generics = this.generateGenerics(node.generics);
if (node.shouldAnyify()) {
return `${invalid ? `["${node.name}"]` : node.name}: ${Generics}((${Parameters}) => ${ReturnType}) | any;`;
}
const warning = node.getWarning();
return `${warning ? `${warning}\n` : ""}${this.docString(node)}${invalid ? `["${node.name}"]` : node.name}${Generics}(${Parameters}): ${ReturnType};`;
}
generateStaticClassFunction(node) {
const Generics = this.generateGenerics(node.generics);
const ReturnType = this.generateReturn(node.return(), node.output_parameters);
const warning = node.getWarning();
return `${warning ? `${warning}\n` : ""}${this.docString(node)}static ${node.name}${Generics}(${this.generateParameters(node.parameters)}): ${ReturnType};`;
}
generateAlias(node) {
const { namespace, options } = this;
const Type = node.type.resolve(namespace, options).print(namespace, options);
const GenericBase = node.generics
.map(g => {
if (g.type) {
return `${g.name} = ${g.type.resolve(namespace, options).rootPrint(namespace, options)}`;
}
return `${g.name}`;
})
.join(", ");
const Generic = GenericBase ? `<${GenericBase}>` : "";
return `${this.docString(node)}export type ${node.name}${Generic} = ${Type};`;
}
generateVirtualClassFunction(node) {
return this.generateClassFunction(node);
}
}
//# sourceMappingURL=dts.js.map