feat(Inner Classes): generate typescript type definitions and JS for Inner Classes.

This commit is contained in:
Artem Kobzar
2021-10-28 17:33:41 +00:00
committed by Space
parent 744191099c
commit 7f7aa9a921
13 changed files with 315 additions and 12 deletions
@@ -23,7 +23,7 @@ class ExportedNamespace(
val declarations: List<ExportedDeclaration>
) : ExportedDeclaration()
class ExportedFunction(
data class ExportedFunction(
val name: String,
val returnType: ExportedType,
val parameters: List<ExportedParameter>,
@@ -35,11 +35,16 @@ class ExportedFunction(
val ir: IrSimpleFunction
) : ExportedDeclaration()
class ExportedConstructor(
data class ExportedConstructor(
val parameters: List<ExportedParameter>,
val isProtected: Boolean
) : ExportedDeclaration()
data class ExportedConstructSignature(
val parameters: List<ExportedParameter>,
val returnType: ExportedType,
) : ExportedDeclaration()
class ExportedProperty(
val name: String,
val type: ExportedType,
@@ -56,7 +61,7 @@ class ExportedProperty(
// TODO: Cover all cases with frontend and disable error declarations
class ErrorDeclaration(val message: String) : ExportedDeclaration()
class ExportedClass(
data class ExportedClass(
val name: String,
val isInterface: Boolean = false,
val isAbstract: Boolean = false,
@@ -187,6 +187,7 @@ class ExportModelGenerator(
is IrField -> {
assert(
candidate.origin == IrDeclarationOrigin.FIELD_FOR_OBJECT_INSTANCE
|| candidate.origin == IrDeclarationOrigin.FIELD_FOR_OUTER_THIS
|| candidate.correspondingPropertySymbol != null
) {
"Unexpected field without property ${candidate.fqNameWhenAvailable}"
@@ -8,8 +8,13 @@ package org.jetbrains.kotlin.ir.backend.js.export
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsAstUtils
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.defineProperty
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.jsAssignment
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.prototypeOf
import org.jetbrains.kotlin.ir.backend.js.utils.IrNamer
import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.util.companionObject
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.util.collectionUtils.filterIsInstanceAnd
class ExportModelToJsStatements(
private val namer: IrNamer,
@@ -69,6 +74,7 @@ class ExportModelToJsStatements(
}
is ExportedConstructor -> emptyList()
is ExportedConstructSignature -> emptyList()
is ExportedProperty -> {
require(namespace != null) { "Only namespaced properties are allowed" }
@@ -87,25 +93,81 @@ class ExportModelToJsStatements(
if (namespace == null) {
JsExport(name, alias = JsName(declaration.name, false))
} else {
jsAssignment(
newNameSpace,
JsNameRef(name)
).makeStmt()
jsAssignment(newNameSpace, JsNameRef(name)).makeStmt()
}
// These are only used when exporting secondary constructors annotated with @JsName
val staticFunctions = declaration.members.filter { it is ExportedFunction && it.isStatic }
val staticFunctions = declaration.members
.filter { it is ExportedFunction && it.isStatic }
.takeIf { !declaration.ir.isInner }.orEmpty()
// Nested objects are exported as static properties
val staticProperties = declaration.members.mapNotNull {
(it as? ExportedProperty)?.takeIf { it.isStatic }
}
val innerClassesAssignments = declaration.nestedClasses
.filter { it.ir.isInner }
.map { it.generateInnerClassAssignment(declaration) }
val staticsExport = (staticFunctions + staticProperties + declaration.nestedClasses)
.flatMap { generateDeclarationExport(it, newNameSpace) }
listOf(klassExport) + staticsExport
listOf(klassExport) + staticsExport + innerClassesAssignments
}
}
}
private fun ExportedClass.generateInnerClassAssignment(outerClass: ExportedClass): JsStatement {
val innerClassRef = namer.getNameForStaticDeclaration(ir).makeRef()
val outerClassRef = namer.getNameForStaticDeclaration(outerClass.ir).makeRef()
val companionObject = ir.companionObject()
val secondaryConstructors = members.filterIsInstanceAnd<ExportedFunction> { it.isStatic }
val bindConstructor = JsName("__bind_constructor_", false)
val blockStatements = mutableListOf<JsStatement>(
JsVars(JsVars.JsVar(bindConstructor, innerClassRef.bindToThis()))
)
if (companionObject != null) {
val companionName = companionObject.getJsNameOrKotlinName().identifier
blockStatements.add(
jsAssignment(
JsNameRef(companionName, bindConstructor.makeRef()),
JsNameRef(companionName, innerClassRef),
).makeStmt()
)
}
secondaryConstructors.forEach {
val currentFunRef = namer.getNameForStaticDeclaration(it.ir).makeRef()
val assignment = jsAssignment(
JsNameRef(it.name, bindConstructor.makeRef()),
currentFunRef.bindToThis()
).makeStmt()
blockStatements.add(assignment)
}
blockStatements.add(JsReturn(bindConstructor.makeRef()))
return defineProperty(
prototypeOf(outerClassRef),
name,
JsFunction(
emptyScope,
JsBlock(*blockStatements.toTypedArray()),
"inner class '$name' getter"
),
null
).makeStmt()
}
private fun JsNameRef.bindToThis(): JsInvocation {
return JsInvocation(
JsNameRef("bind", this),
JsNullLiteral(),
JsThisRef()
)
}
}
@@ -5,7 +5,10 @@
package org.jetbrains.kotlin.ir.backend.js.export
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.backend.js.utils.sanitizeName
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.util.parentAsClass
import org.jetbrains.kotlin.serialization.js.ModuleKind
// TODO: Support module kinds other than plain
@@ -73,12 +76,18 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
"${prefix}$visibility$keyword$name$renderedTypeParameters($renderedParameters): $renderedReturnType;"
}
is ExportedConstructor -> {
val visibility = if (isProtected) "protected " else ""
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
"${visibility}constructor($renderedParameters);"
}
is ExportedConstructSignature -> {
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
"new($renderedParameters): ${returnType.toTypeScript(indent)};"
}
is ExportedProperty -> {
val visibility = if (isProtected) "protected " else ""
val keyword = when {
@@ -98,7 +107,18 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
" $superInterfacesKeyword " + superInterfaces.joinToString(", ") { it.toTypeScript(indent) }
} else ""
val membersString = members.joinToString("") { it.toTypeScript("$indent ") + "\n" }
val members = members.map {
if (!ir.isInner || it !is ExportedFunction || !it.isStatic) {
it
} else {
// Remove $outer argument from secondary constructors of inner classes
it.copy(parameters = it.parameters.drop(1))
}
}
val (innerClasses, nonInnerClasses) = nestedClasses.partition { it.ir.isInner }
val innerClassesProperties = innerClasses.map { it.toReadonlyProperty() }
val membersString = (members + innerClassesProperties).joinToString("") { it.toTypeScript("$indent ") + "\n" }
// If there are no exported constructors, add a private constructor to disable default one
val privateCtorString =
@@ -117,12 +137,60 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
val bodyString = privateCtorString + membersString + indent
val nestedClasses = nonInnerClasses + innerClasses.map { it.withProtectedConstructors() }
val klassExport = "$prefix$modifiers$keyword $name$renderedTypeParameters$superClassClause$superInterfacesClause {\n$bodyString}"
val staticsExport = if (nestedClasses.isNotEmpty()) "\n" + ExportedNamespace(name, nestedClasses).toTypeScript(indent, prefix) else ""
klassExport + staticsExport
}
}
fun IrClass.asNestedClassAccess(): String {
val name = getJsNameOrKotlinName().identifier
if (parent !is IrClass) return name
return "${parentAsClass.asNestedClassAccess()}.$name"
}
fun ExportedClass.withProtectedConstructors(): ExportedClass {
return copy(members = members.map {
if (it !is ExportedConstructor || it.isProtected) {
it
} else {
it.copy(isProtected = true)
}
})
}
fun ExportedClass.toReadonlyProperty(): ExportedProperty {
val innerClassReference = ir.asNestedClassAccess()
val allPublicConstructors = members.asSequence()
.filterIsInstance<ExportedConstructor>()
.filterNot { it.isProtected }
.map {
ExportedConstructSignature(
parameters = it.parameters.drop(1),
returnType = ExportedType.TypeParameter(innerClassReference),
)
}
.toList()
val type = ExportedType.IntersectionType(
ExportedType.InlineInterfaceType(allPublicConstructors),
ExportedType.TypeOf(innerClassReference)
)
return ExportedProperty(
name = name,
type = type,
mutable = false,
isMember = true,
isStatic = false,
isAbstract = false,
isProtected = false,
irGetter = null,
irSetter = null
)
}
fun ExportedParameter.toTypeScript(indent: String): String =
"$name: ${type.toTypeScript(indent)}"
@@ -75,6 +75,7 @@ class JsInnerClassesSupport(mapping: JsMapping, private val irFactory: IrFactory
returnType = oldConstructor.returnType
}.also {
it.parent = oldConstructor.parent
it.annotations = oldConstructor.annotations
}
newConstructor.copyTypeParametersFrom(oldConstructor)