* Copyright (c) 2024 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as idl from '../../idl'
import { isOptionalType } from '../../idl'
import { Language } from '../../Language'
import { IndentedPrinter } from "../../IndentedPrinter";
import {
ACCESS_MODIFIERS_SET,
AssignStatement,
BlockStatement,
DelegationCall,
DelegationType,
ExpressionStatement,
FieldModifier,
IfStatement,
LambdaExpression,
LanguageExpression,
LanguageStatement,
LanguageWriter,
MakeAssignOptions,
MakeCastOptions,
Method,
MethodModifier,
MethodSignature,
NamedMethodSignature,
NamespaceOptions,
NaryOpExpression,
ObjectArgs,
ReturnStatement,
StringExpression
} from "../LanguageWriter"
import { ArgConvertor } from "../ArgConvertors"
import { IdlNameConvertor } from "../nameConvertor"
import { RuntimeType } from "../common";
import { rightmostIndexOf } from "../../util"
import { ReferenceResolver } from "../../peer-generation/ReferenceResolver";
import { TSKeywords } from '../../languageSpecificKeywords';
export class TSLambdaExpression extends LambdaExpression {
constructor(
writer: LanguageWriter,
private convertor: IdlNameConvertor,
signature: MethodSignature,
resolver: ReferenceResolver,
body?: LanguageStatement[]) {
super(writer, signature, resolver, body)
}
protected get statementHasSemicolon(): boolean {
return false
}
asString(): string {
const params = this.signature.args.map((it, i) => {
const maybeOptional = idl.isOptionalType(it) ? "?" : ""
return `${this.signature.argName(i)}${maybeOptional}: ${this.convertor.convert(it)}`
})
return `(${params.join(", ")}): ${this.convertor.convert(this.signature.returnType)} =>${this.bodyAsString(true)}`
}
}
export class TSCastExpression implements LanguageExpression {
constructor(public value: LanguageExpression, public type: string, private unsafe = false) {}
asString(): string {
return this.unsafe
? `unsafeCast<${this.type}>(${this.value.asString()})`
: `(${this.value.asString()} as ${this.type})`
}
}
class TSUnwrapOptionalExpression implements LanguageExpression {
constructor(public value: LanguageExpression) {}
asString(): string {
return `(${this.value.asString()})!`
}
}
class TSThrowErrorStatement implements LanguageStatement {
constructor(public message: string) { }
write(writer: LanguageWriter): void {
writer.print(`throw new Error('${this.message}')`)
}
}
export class TSReturnStatement extends ReturnStatement {
constructor(public expression: LanguageExpression) { super(expression) }
}
class TSLoopStatement implements LanguageStatement {
constructor(private counter: string, private limit: string, private statement: LanguageStatement | undefined) {}
write(writer: LanguageWriter): void {
writer.print(`for (let ${this.counter} = 0; ${this.counter} < ${this.limit}; ${this.counter}++) {`)
if (this.statement) {
writer.pushIndent()
this.statement.write(writer)
writer.popIndent()
writer.print("}")
}
}
}
class TSSetForEachStatement implements LanguageStatement {
constructor(
private setAccessor: string,
private elementName: string,
private body: LanguageStatement[],
) {}
write(writer: LanguageWriter): void {
writer.print(`for (let ${this.elementName} of ${this.setAccessor}) {`)
writer.pushIndent()
this.body.forEach(statement => {
statement.write(writer)
})
writer.popIndent()
writer.print('}')
}
}
class TSMapForEachStatement implements LanguageStatement {
constructor(private map: string, private key: string, private value: string, private body: LanguageStatement[]) {}
write(writer: LanguageWriter): void {
writer.print(`for (const [${this.key}, ${this.value}] of ${this.map}) {`)
writer.pushIndent()
writer.writeStatement(new BlockStatement(this.body, false))
writer.popIndent()
writer.print(`}`)
}
}
export class TsTupleAllocStatement implements LanguageStatement {
constructor(private tuple: string) {}
write(writer: LanguageWriter): void {
writer.writeStatement(writer.makeAssign(this.tuple, undefined, writer.makeString("[]"), false, false))
}
}
export class TSLanguageWriter extends LanguageWriter {
protected typeConvertor: IdlNameConvertor
constructor(printer: IndentedPrinter,
resolver: ReferenceResolver,
typeConvertor: IdlNameConvertor,
language: Language = Language.TS) {
super(printer, resolver, language)
this.typeConvertor = typeConvertor
}
maybeSemicolon() { return "" }
pushNamespace(namespace: string, options: NamespaceOptions): void {
this.namespaceStack.push(namespace)
const declaredPrefix = options.isDeclared ? "declare " : ""
if (options.isDefault) {
this.print(`export default ${namespace}`)
}
this.print(`export ${declaredPrefix}namespace ${namespace} {`)
if (options.ident) this.pushIndent()
}
fork(options?: { resolver?: ReferenceResolver }): LanguageWriter {
return new TSLanguageWriter(new IndentedPrinter([], this.indentDepth()), options?.resolver ?? this.resolver, this.typeConvertor, this.language)
}
getNodeName(type: idl.IDLNode): string {
if (idl.isType(type) && idl.isReferenceType(type)) {
if (type.name.startsWith('%TEXT%:')) {
return type.name.substring(7)
}
}
return this.typeConvertor.convert(type)
}
override get interopModule(): string {
return "@koalaui/interop"
}
writeClass(
name: string,
op: (writer: this) => void,
superClass?: string,
interfaces?: string[],
generics?: string[],
isDeclared?: boolean,
isAbstract?: boolean
): void {
let extendsClause = superClass ? ` extends ${superClass}` : ''
let implementsClause = interfaces ? ` implements ${interfaces.join(",")}` : ''
let genericsClause = generics?.length ? `<${generics.join(", ")}>` : ''
let declaredClause = isDeclared ? ` declare` : ''
let abstractClause = isAbstract ? ` abstract` : ''
this.printer.print(`export${declaredClause}${abstractClause} class ${name}${genericsClause}${extendsClause}${implementsClause} {`)
this.pushIndent()
this.classOp(() => op(this))
this.popIndent()
this.printer.print(`}`)
}
override writeInterface(name: string, op: (writer: this) => void, superInterfaces?: string[], generics?: string[], isDeclared?: boolean): void {
const genericsClause = generics?.length ? `<${generics.join(", ")}>` : ''
let extendsClause = superInterfaces?.length ? ` extends ${superInterfaces.join(",")}` : ''
this.printer.print(`export ${isDeclared ? "declare " : ""}interface ${name}${genericsClause}${extendsClause} {`)
this.pushIndent()
op(this)
this.popIndent()
this.printer.print(`}`)
}
writeFunctionDeclaration(name: string, signature: MethodSignature, generics?:string[]): void {
this.printer.print(this.generateFunctionDeclaration(name, signature, generics))
}
writeFunctionImplementation(name: string, signature: MethodSignature, op: (writer: this) => void, generics?:string[]): void {
this.printer.print(`${this.generateFunctionDeclaration(name, signature, generics)} {`)
this.printer.pushIndent()
op(this)
this.printer.popIndent()
this.printer.print('}')
}
private generateFunctionDeclaration(name: string, signature: MethodSignature, generics?:string[]): string {
const rightmostRegularParameterIndex = rightmostIndexOf(signature.args, it => !isOptionalType(it))
const args = signature.args.map((it, index) => {
const optionalToken = idl.isOptionalType(it) && index > rightmostRegularParameterIndex ? '?' : ''
return `${signature.argName(index)}${optionalToken}: ${this.getNodeName(it)}`
})
const returnType = this.getNodeName(signature.returnType)
const typeParams = generics && generics.length ? '<' + generics?.join(', ') + '>' : ''
return `export function ${name}${typeParams}(${args.join(", ")}): ${returnType}`
}
writeEnum(name: string, members: { name: string, alias?: string | undefined, stringId: string | undefined, numberId: number }[], options: { isDeclare?: boolean, isExport: boolean }): void {
this.printer.print(`${options.isExport ? "export " : ""}${options.isDeclare ? "declare " : ""}enum ${name} {`)
this.printer.pushIndent()
for (const [index, member] of members.entries()) {
let value
if (member.alias !== undefined) {
value = member.alias
} else {
value = `${member.stringId != undefined ? `'${member.stringId}'` : `${member.numberId}`}`
}
const maybeComma = index < members.length - 1 ? "," : ""
this.printer.print(`${member.name} = ${value}${maybeComma}`)
}
this.printer.popIndent()
this.printer.print("}")
}
writeFieldDeclaration(name: string, type: idl.IDLType, modifiers: FieldModifier[]|undefined, optional: boolean, initExpr?: LanguageExpression): void {
if (this.writingClassBody && !modifiers?.some(m => ACCESS_MODIFIERS_SET.has(m))) {
modifiers = modifiers?.slice() ?? []
modifiers.unshift(FieldModifier.PUBLIC)
}
let prefix = this.makeFieldModifiersList(modifiers)
if (prefix) prefix += " "
const typeName = this.getNodeName(type)
const isGetter = modifiers?.includes(FieldModifier.GET)
const isSetter = modifiers?.includes(FieldModifier.SET)
if (isGetter) {
this.printer.print(`${prefix}get ${name}(): ${typeName}`)
}
if (isSetter) {
this.printer.print(`${prefix}set ${name}(value: ${typeName})`)
}
if (isGetter || isSetter) return
const init = initExpr != undefined ? ` = ${initExpr.asString()}` : ``
this.printer.print(`${prefix}${name}${optional ? "?" : ""}: ${typeName}${init}`)
}
writeNativeMethodDeclaration(method: Method): void {
let name = method.name
let signature = method.signature
this.writeMethodImplementation(new Method(name, signature, [MethodModifier.STATIC]), writer => {
const selfCallExpression = writer.makeFunctionCall(
`this.${name}`,
signature.args.map((_, i) => writer.makeString(this.escapeKeyword(signature.argName(i))))
)
writer.writeStatement(new IfStatement(
new NaryOpExpression("==", [writer.makeFunctionCall("this._LoadOnce", []), writer.makeString("true")]),
new BlockStatement([
writer.makeReturn(selfCallExpression)
]),
undefined, undefined, undefined
))
writer.writeStatement(writer.makeThrowError("Not implemented"))
})
}
writeMethodDeclaration(name: string, signature: MethodSignature, modifiers?: MethodModifier[]): void {
this.writeDeclaration(name, signature, true, false, modifiers)
}
writeConstructorImplementation(className: string, signature: MethodSignature, op: (writer: this) => void, delegationCall?: DelegationCall, modifiers?: MethodModifier[]) {
this.writeDeclaration(`${modifiers ? modifiers.map((it) => MethodModifier[it].toLowerCase()).join(' ') + ' ' : ''}constructor`, signature, false, true)
this.pushIndent()
if (delegationCall) {
const delegationType = (delegationCall?.delegationType == DelegationType.THIS) ? "this" : "super"
this.print(`${delegationType}(${delegationCall.delegationArgs?.map(it => it.asString()).join(", ")})`)
}
op(this)
this.popIndent()
this.printer.print(`}`)
}
writeMethodImplementation(method: Method, op: (writer: this) => void) {
this.writeDeclaration(method.name, method.signature, true, true, method.modifiers, method.generics)
this.pushIndent()
op(this)
this.popIndent()
this.printer.print(`}`)
}
writeProperty(propName: string, propType: idl.IDLType, modifiers: FieldModifier[], getter?: { method: Method, op: () => void }, setter?: { method: Method, op: () => void }, initExpr?: LanguageExpression): void {
let isStatic = modifiers.includes(FieldModifier.STATIC)
let containerName = propName.concat("_container")
if (getter) {
if(!getter!.op) {
this.print(`private var ${this.getNodeName(propType)} ${containerName}`)
}
this.writeGetterImplementation(
new Method(propName, new MethodSignature(propType, []), isStatic ? [MethodModifier.STATIC] : []),
getter ? getter!.op :
(writer) => {
writer.print(`return ${containerName}`)
}
)
}
if (setter) {
const setSignature = new NamedMethodSignature(idl.IDLVoidType, [propType], [propName])
this.writeSetterImplementation(
new Method(propName, setSignature, isStatic ? [MethodModifier.STATIC] : []),
setter ? setter!.op :
(writer) => {
writer.print(`${containerName} = ${propName}`)
}
)
}
if (getter || setter) return
this.writeFieldDeclaration(propName, propType, modifiers, idl.isOptionalType(propType), initExpr)
}
override writeTypeDeclaration(decl: idl.IDLTypedef): void {
const type = this.getNodeName(decl.type)
const typeParams = decl.typeParameters?.length ? `<${decl.typeParameters.join(",").replace("[]", "")}>` : ""
this.print(`export type ${decl.name}${typeParams} = ${type};`)
}
writeConstant(constName: string, constType: idl.IDLType, constVal?: string): void {
this.print(`export const ${constName}: ${this.getNodeName(constType)}${constVal ? ' = ' + constVal : ''}`)
}
override writeImports(moduleName: string, importedFeatures: string[], aliases: string[]): void {
if (importedFeatures.length !== aliases.length) {
throw new Error(`Inconsistent imports from ${moduleName}`)
}
const importNodes: string[] = []
for (let i = 0; i < importedFeatures.length; i++) {
importNodes.push(importedFeatures[i] + (aliases[i] ? ` as ${aliases[i]}` : ``))
}
this.writeExpressionStatement(this.makeString(`import { ${importNodes.join(', ')} } from '${moduleName}'`))
}
private writeDeclaration(name: string, signature: MethodSignature, needReturn: boolean, needBracket: boolean, modifiers?: MethodModifier[], generics?: string[]) {
let prefix = !modifiers ? undefined : this.supportedModifiers
.filter(it => modifiers.includes(it))
.map(it => this.mapMethodModifier(it)).join(" ")
if (modifiers?.includes(MethodModifier.GETTER)) {
prefix = `${prefix} get`
} else if (modifiers?.includes(MethodModifier.SETTER)) {
prefix = `${prefix} set`
needReturn = false
} else if (modifiers?.includes(MethodModifier.FREE)) {
prefix = `${needBracket ? "" : "declare "}function ${prefix}`
}
prefix = prefix ? prefix.trim() + " " : ""
const typeParams = generics?.length ? `<${generics.join(", ")}>` : ""
const normalizedArgs = signature.args.map((it, i) =>
idl.isOptionalType(it) && signature.isArgOptional(i) ? idl.maybeUnwrapOptionalType(it) : it
)
this.printer.print(`${prefix}${name}${typeParams}(${normalizedArgs.map((it, index) => `${this.escapeKeyword(signature.argName(index))}${signature.isArgOptional(index) ? "?" : ``}: ${this.getNodeName(it)}${signature.argDefault(index) ? ' = ' + signature.argDefault(index) : ""}`).join(", ")})${needReturn ? ": " + this.getNodeName(signature.returnType) : ""}${needBracket ? " {" : ""}`)
}
makeNull(type?: idl.IDLOptionalType): LanguageExpression {
if (type && idl.hasExtAttribute(type, idl.IDLExtendedAttributes.UnionOnlyNull))
return new StringExpression("null")
return new StringExpression("undefined")
}
makeAssign(variableName: string, type: idl.IDLType | undefined, expr: LanguageExpression | undefined, isDeclared: boolean = true, isConst: boolean = true, options?:MakeAssignOptions): LanguageStatement {
return new AssignStatement(variableName, type, expr, isDeclared, isConst, options)
}
makeLambda(signature: MethodSignature, body?: LanguageStatement[]): LanguageExpression {
return new TSLambdaExpression(this, this.typeConvertor, signature, this.resolver, body)
}
makeThrowError(message: string): LanguageStatement {
return new TSThrowErrorStatement(message)
}
makeReturn(expr: LanguageExpression): LanguageStatement {
return new TSReturnStatement(expr)
}
makeStatement(expr: LanguageExpression): LanguageStatement {
return new ExpressionStatement(expr)
}
makeLoop(counter: string, limit: string, statement?: LanguageStatement): LanguageStatement {
return new TSLoopStatement(counter, limit, statement)
}
makeMapForEach(map: string, key: string, value: string, body: LanguageStatement[]): LanguageStatement {
return new TSMapForEachStatement(map, key, value, body)
}
writePrintLog(message: string): void {
this.print(`console.log("${message}")`)
}
makeCast(value: LanguageExpression, node: idl.IDLNode, options?: MakeCastOptions): LanguageExpression {
return new TSCastExpression(value, this.getNodeName(node), options?.unsafe ?? false)
}
override instanceOf(value: string, type: idl.IDLType): LanguageExpression {
return idl.IDLContainerUtils.isSequence(type)
? this.makeString(`Array.isArray(${value})`)
: super.instanceOf(value, type)
}
override typeInstanceOf(type: idl.IDLEntry, value: string, members?: string[]): LanguageExpression {
if (idl.isInterface(type)) {
if (idl.isInterfaceSubkind(type)) {
if (!members) {
throw new Error("Members must be defined for interface type recognition!")
}
return this.makeString(
members!.map(it => `${value}.hasOwnProperty("${it}")`).join("&&")
)
}
if (idl.isClassSubkind(type)) {
return super.typeInstanceOf(type, value, members)
}
}
throw new Error(`typeInstanceOf fails: not class or interface: ${this.getNodeName(type)}`)
}
getObjectAccessor(convertor: ArgConvertor, value: string, args?: ObjectArgs): string {
if (convertor.useArray && args?.index != undefined) {
return `${value}[${args.index}]`
}
return `${value}`
}
makeUndefined(): LanguageExpression {
return this.makeString("undefined")
}
makeRuntimeType(rt: RuntimeType): LanguageExpression {
return this.makeString(`RuntimeType.${RuntimeType[rt]}`)
}
makeDefinedCheck(value: string, type?: idl.IDLOptionalType): LanguageExpression {
if (type) {
if (idl.hasExtAttribute(type, idl.IDLExtendedAttributes.UnionWithNull)) {
return this.makeString(`${value} !== undefined && ${value} !== null`)
} else if (idl.hasExtAttribute(type, idl.IDLExtendedAttributes.UnionOnlyNull)) {
return this.makeString(`${value} !== null`)
}
}
return this.makeString(`${value} !== undefined`)
}
makeTupleAlloc(option: string): LanguageStatement {
return new TsTupleAllocStatement(option)
}
makeArrayInit(type: idl.IDLContainerType, size?:number): LanguageExpression {
return this.makeString(`new Array<${this.getNodeName(type.elementType[0])}>(${size?.toString() ?? ''})`)
}
makeClassInit(type: idl.IDLType, paramenters: LanguageExpression[]): LanguageExpression {
return this.makeString(`new ${this.getNodeName(type)}(${paramenters.map(it => it.asString()).join(", ")})`)
}
makeSetInit(type: idl.IDLType): LanguageExpression {
return this.makeString(`new Set<${this.getNodeName(type)}>()`)
}
makeSetSize(setAccessor: string): LanguageExpression {
return this.makeFieldAccess(setAccessor, 'size')
}
makeSetAdd(setAccessor: string, element: LanguageExpression): LanguageStatement {
return this.makeStatement(this.makeMethodCall(setAccessor, 'add', [element]))
}
makeSetForEach(set: string, element: string, body: LanguageStatement[]): LanguageStatement {
return new TSSetForEachStatement(set, element, body)
}
makeMapInit(type: idl.IDLType): LanguageExpression {
return this.makeString(`new ${this.getNodeName(type)}()`)
}
makeMapInsert(keyAccessor: string, key: string, valueAccessor: string, value: string): LanguageStatement {
return this.makeStatement(this.makeMethodCall(keyAccessor, "set", [this.makeString(key), this.makeString(value)]))
}
makeUnwrapOptional(expression: LanguageExpression): LanguageExpression {
return new TSUnwrapOptionalExpression(expression)
}
getTagType(): idl.IDLType {
return idl.createReferenceType("Tags")
}
getRuntimeType(): idl.IDLType {
return idl.IDLI32Type
}
makeTupleAssign(receiver: string, fields: string[]): LanguageStatement {
return this.makeAssign(receiver, undefined,
this.makeString(`[${fields.map(it=> `${it}!`).join(",")}]`), false)
}
get supportedModifiers(): MethodModifier[] {
return [MethodModifier.PUBLIC, MethodModifier.PRIVATE, MethodModifier.PROTECTED, MethodModifier.STATIC]
}
get supportedFieldModifiers(): FieldModifier[] {
return [FieldModifier.PUBLIC, FieldModifier.PRIVATE, FieldModifier.PROTECTED, FieldModifier.READONLY, FieldModifier.STATIC]
}
enumFromI32(value: LanguageExpression, enumEntry: idl.IDLEnum): LanguageExpression {
const enumName = enumEntry.name
const ordinal = value.asString()
return idl.isStringEnum(enumEntry)
? this.makeString(`Object.values(${enumName})[${ordinal}]`)
: this.makeString(ordinal)
}
override i32FromEnum(value: LanguageExpression, enumEntry: idl.IDLEnum): LanguageExpression {
const enumName = this.getNodeName(enumEntry)
if (idl.isEnum(enumEntry) && idl.isStringEnum(enumEntry)) {
return this.makeString(`Object.values(${enumName}).indexOf(${value.asString()})`)
}
return this.makeString(`${value.asString()}.valueOf()`)
}
override castToBoolean(value: string): string { return `!!${value}` }
override makeCallIsObject(value: string): LanguageExpression {
return this.makeString(`${value} instanceof Object`)
}
override escapeKeyword(keyword: string): string {
return TSKeywords.has(keyword) ? keyword + "_" : keyword
}
override discriminate(value: string, index: number, type: idl.IDLType, runtimeTypes: RuntimeType[]): string {
const runtimeTypeList = runtimeTypes.map(ty => "RuntimeType." + RuntimeType[ty]).join(", ")
return `[${runtimeTypeList}].includes(runtimeType(${value}))`
}
private writingClassBody = false
classOp(op:() => void): void {
const old = this.writingClassBody
this.writingClassBody = true
op()
this.writingClassBody = old
}
}