feat: add ability to create nested entities inside object.

This commit is contained in:
Artem Kobzar
2022-06-22 11:59:13 +00:00
committed by Space
parent 3766698081
commit ba738cbbff
10 changed files with 559 additions and 337 deletions
@@ -20,7 +20,7 @@ data class ExportedModule(
class ExportedNamespace(
val name: String,
val declarations: List<ExportedDeclaration>
val declarations: List<ExportedDeclaration>,
) : ExportedDeclaration()
data class ExportedFunction(
@@ -59,24 +59,43 @@ class ExportedProperty(
val isField: Boolean,
val irGetter: IrFunction?,
val irSetter: IrFunction?,
val exportedObject: ExportedClass? = null,
) : ExportedDeclaration()
// TODO: Cover all cases with frontend and disable error declarations
class ErrorDeclaration(val message: String) : ExportedDeclaration()
data class ExportedClass(
val name: String,
sealed class ExportedClass : ExportedDeclaration() {
abstract val name: String
abstract val ir: IrClass
abstract val members: List<ExportedDeclaration>
abstract val superClass: ExportedType?
abstract val superInterfaces: List<ExportedType>
abstract val nestedClasses: List<ExportedClass>
}
data class ExportedRegularClass(
override val name: String,
val isInterface: Boolean = false,
val isAbstract: Boolean = false,
val superClass: ExportedType? = null,
val superInterfaces: List<ExportedType> = emptyList(),
override val superClass: ExportedType? = null,
override val superInterfaces: List<ExportedType> = emptyList(),
val typeParameters: List<String>,
val members: List<ExportedDeclaration>,
val nestedClasses: List<ExportedClass>,
val ir: IrClass
) : ExportedDeclaration()
override val members: List<ExportedDeclaration>,
override val nestedClasses: List<ExportedClass>,
override val ir: IrClass,
) : ExportedClass()
data class ExportedObject(
override val name: String,
override val superClass: ExportedType? = null,
override val superInterfaces: List<ExportedType> = emptyList(),
override val members: List<ExportedDeclaration>,
override val nestedClasses: List<ExportedClass>,
override val ir: IrClass,
val irGetter: IrFunction
) : ExportedClass()
class ExportedParameter(
val name: String,
@@ -270,9 +270,7 @@ class ExportModelGenerator(
listOf(privateConstructor) + members,
nestedClasses
).let {
(it as ExportedClass).copy(
isAbstract = true,
)
(it as ExportedRegularClass).copy(isAbstract = true)
}
}
@@ -352,43 +350,29 @@ class ExportModelGenerator(
val name = klass.getExportedIdentifier()
val exportedClass = ExportedClass(
name = name,
isInterface = klass.isInterface,
isAbstract = klass.modality == Modality.ABSTRACT || klass.modality == Modality.SEALED,
superClass = superType,
superInterfaces = superInterfaces,
typeParameters = typeParameters,
members = members,
nestedClasses = nestedClasses,
ir = klass
)
if (klass.kind == ClassKind.OBJECT) {
var t: ExportedType = ExportedType.InlineInterfaceType(members + nestedClasses)
if (superType != null)
t = ExportedType.IntersectionType(t, superType)
for (superInterface in superInterfaces) {
t = ExportedType.IntersectionType(t, superInterface)
}
return ExportedProperty(
return if (klass.kind == ClassKind.OBJECT) {
return ExportedObject(
ir = klass,
name = name,
type = t,
mutable = false,
isMember = klass.parent is IrClass,
isStatic = !klass.isInner,
isAbstract = false,
isProtected = klass.visibility == DescriptorVisibilities.PROTECTED,
irGetter = context.mapping.objectToGetInstanceFunction[klass]!!,
irSetter = null,
exportedObject = exportedClass,
isField = false,
members = members,
superClass = superType,
nestedClasses = nestedClasses,
superInterfaces = superInterfaces,
irGetter = context.mapping.objectToGetInstanceFunction[klass]!!
)
} else {
ExportedRegularClass(
name = name,
isInterface = klass.isInterface,
isAbstract = klass.modality == Modality.ABSTRACT || klass.modality == Modality.SEALED,
superClass = superType,
superInterfaces = superInterfaces,
typeParameters = typeParameters,
members = members,
nestedClasses = nestedClasses,
ir = klass
)
}
return exportedClass
}
private fun exportAsEnumMember(
@@ -86,17 +86,22 @@ class ExportModelToJsStatements(
is ExportedProperty -> {
require(namespace != null) { "Only namespaced properties are allowed" }
val underlying: List<JsStatement> = declaration.exportedObject?.let {
generateDeclarationExport(it, null, esModules)
} ?: emptyList()
val getter = declaration.irGetter?.let { JsNameRef(namer.getNameForStaticDeclaration(it)) }
val setter = declaration.irSetter?.let { JsNameRef(namer.getNameForStaticDeclaration(it)) }
listOf(defineProperty(namespace, declaration.name, getter, setter).makeStmt()) + underlying
listOf(defineProperty(namespace, declaration.name, getter, setter).makeStmt())
}
is ErrorDeclaration -> emptyList()
is ExportedClass -> {
is ExportedObject -> {
require(namespace != null) { "Only namespaced properties are allowed" }
val newNameSpace = jsElementAccess(declaration.name, namespace)
val getter = JsNameRef(namer.getNameForStaticDeclaration(declaration.irGetter))
val staticsExport = declaration.nestedClasses.flatMap { generateDeclarationExport(it, newNameSpace, esModules) }
listOf(defineProperty(namespace, declaration.name, getter, null).makeStmt()) + staticsExport
}
is ExportedRegularClass -> {
if (declaration.isInterface) return emptyList()
val newNameSpace = if (namespace != null)
jsElementAccess(declaration.name, namespace)
@@ -120,16 +125,13 @@ class ExportModelToJsStatements(
.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 enumEntries = declaration.members.filter { it is ExportedProperty && it.isStatic }
val innerClassesAssignments = declaration.nestedClasses
.filter { it.ir.isInner }
.map { it.generateInnerClassAssignment(declaration) }
val staticsExport = (staticFunctions + staticProperties + declaration.nestedClasses)
val staticsExport = (staticFunctions + enumEntries + declaration.nestedClasses)
.flatMap { generateDeclarationExport(it, newNameSpace, esModules) }
listOfNotNull(klassExport) + staticsExport + innerClassesAssignments
@@ -5,314 +5,434 @@
package org.jetbrains.kotlin.ir.backend.js.export
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.backend.js.utils.getFqNameWithJsNameWhenAvailable
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.isObject
import org.jetbrains.kotlin.ir.util.parentAsClass
import org.jetbrains.kotlin.js.common.isValidES5Identifier
import org.jetbrains.kotlin.serialization.js.ModuleKind
// TODO: Support module kinds other than plain
private const val declare = "declare"
private const val Nullable = "Nullable"
private const val doNotImplementIt = "__doNotImplementIt"
private const val objects = "_objects_"
private const val syntheticObjectNameSeparator = '$'
fun ExportedModule.toTypeScript(): String {
return wrapTypeScript(name, moduleKind, declarations.toTypeScript(moduleKind))
return ExportModelToTsDeclarations().generateTypeScript(name, this)
}
fun wrapTypeScript(name: String, moduleKind: ModuleKind, dts: String): String {
val declareKeyword = when (moduleKind) {
ModuleKind.PLAIN -> ""
else -> "declare "
}
val types = """
type Nullable<T> = T | null | undefined
${declareKeyword}const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
""".trimIndent().prependIndent(moduleKind.indent) + "\n"
val declarationsDts = types + dts
val namespaceName = sanitizeName(name, withHash = false)
return when (moduleKind) {
ModuleKind.PLAIN -> "declare namespace $namespaceName {\n$declarationsDts\n}\n"
ModuleKind.AMD, ModuleKind.COMMON_JS, ModuleKind.ES -> declarationsDts
ModuleKind.UMD -> "$declarationsDts\nexport as namespace $namespaceName;"
}
}
private val ModuleKind.indent: String
get() = if (this == ModuleKind.PLAIN) " " else ""
fun List<ExportedDeclaration>.toTypeScript(moduleKind: ModuleKind): String {
return joinToString("\n") {
it.toTypeScript(
indent = moduleKind.indent,
prefix = if (moduleKind == ModuleKind.PLAIN) "" else "export "
return ExportModelToTsDeclarations().generateTypeScript(moduleKind, this)
}
// TODO: Support module kinds other than plain
class ExportModelToTsDeclarations {
private val objectsSyntheticProperties = mutableListOf<ExportedProperty>()
private val ModuleKind.indent: String
get() = if (this == ModuleKind.PLAIN) " " else ""
fun generateTypeScript(name: String, module: ExportedModule): String {
val declareKeyword = when (module.moduleKind) {
ModuleKind.PLAIN -> ""
else -> "$declare "
}
val types = """
type $Nullable<T> = T | null | undefined
${declareKeyword}const $doNotImplementIt: unique symbol
type $doNotImplementIt = typeof $doNotImplementIt
""".trimIndent().prependIndent(module.moduleKind.indent) + "\n"
val declarationsDts = types + module.declarations.toTypeScript(module.moduleKind)
val namespaceName = sanitizeName(name, withHash = false)
return when (module.moduleKind) {
ModuleKind.PLAIN -> "declare namespace $namespaceName {\n$declarationsDts\n}\n"
ModuleKind.AMD, ModuleKind.COMMON_JS, ModuleKind.ES -> declarationsDts
ModuleKind.UMD -> "$declarationsDts\nexport as namespace $namespaceName;"
}
}
fun generateTypeScript(moduleKind: ModuleKind, declarations: List<ExportedDeclaration>): String {
return declarations.toTypeScript(moduleKind)
}
private fun List<ExportedDeclaration>.toTypeScript(moduleKind: ModuleKind): String {
return joinToString("\n") {
it.toTypeScript(
indent = moduleKind.indent,
prefix = if (moduleKind == ModuleKind.PLAIN) "" else "export "
)
} + generateObjectsNamespaceIfNeeded(moduleKind.indent)
}
private fun generateObjectsNamespaceIfNeeded(indent: String): String {
return if (objectsSyntheticProperties.isEmpty()) {
""
} else {
"\n" + ExportedNamespace(objects, objectsSyntheticProperties).toTypeScript(indent, "")
}
}
private fun List<ExportedDeclaration>.toTypeScript(indent: String): String =
joinToString("") { it.toTypeScript(indent) + "\n" }
private fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): String =
indent + when (this) {
is ErrorDeclaration -> generateTypeScriptString()
is ExportedNamespace -> generateTypeScriptString(indent, prefix)
is ExportedFunction -> generateTypeScriptString(indent, prefix)
is ExportedConstructor -> generateTypeScriptString(indent)
is ExportedConstructSignature -> generateTypeScriptString(indent)
is ExportedProperty -> generateTypeScriptString(indent, prefix)
is ExportedObject -> generateTypeScriptString(indent, prefix)
is ExportedRegularClass -> generateTypeScriptString(indent, prefix)
}
private fun ErrorDeclaration.generateTypeScriptString(): String {
return "/* ErrorDeclaration: $message */"
}
private fun ExportedNamespace.generateTypeScriptString(indent: String, prefix: String): String {
return "${prefix}namespace $name {\n" + declarations.toTypeScript("$indent ") + "$indent}"
}
private fun ExportedConstructor.generateTypeScriptString(indent: String): String {
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
return "${visibility.keyword}constructor($renderedParameters);"
}
private fun ExportedConstructSignature.generateTypeScriptString(indent: String): String {
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
return "new($renderedParameters): ${returnType.toTypeScript(indent)};"
}
private fun ExportedProperty.generateTypeScriptString(indent: String, prefix: String): String {
val visibility = if (isProtected) "protected " else ""
val keyword = when {
isMember -> (if (isAbstract) "abstract " else "")
else -> if (mutable) "let " else "const "
}
val possibleStatic = if (isMember && isStatic) "static " else ""
val containsUnresolvedChar = !name.isValidES5Identifier()
val memberName = when {
isMember && containsUnresolvedChar -> "\"$name\""
else -> name
}
val typeToTypeScript = type.toTypeScript(indent)
return if (isMember && !isField) {
val getter = "$prefix$visibility$possibleStatic${keyword}get $memberName(): $typeToTypeScript;"
if (!mutable) {
getter
} else {
getter + "\n" + "$indent$prefix$visibility$possibleStatic${keyword}set $memberName(value: $typeToTypeScript);"
}
} else {
if (!isMember && containsUnresolvedChar) {
""
} else {
val readonly = if (isMember && !mutable) "readonly " else ""
"$prefix$visibility$possibleStatic$keyword$readonly$memberName: $typeToTypeScript;"
}
}
}
private fun ExportedFunction.generateTypeScriptString(indent: String, prefix: String): String {
val visibility = if (isProtected) "protected " else ""
val keyword: String = when {
isMember -> when {
isStatic -> "static "
isAbstract -> "abstract "
else -> ""
}
else -> "function "
}
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
val renderedTypeParameters = if (typeParameters.isNotEmpty()) {
"<" + typeParameters.joinToString(", ") { it.toTypeScript(indent) } + ">"
} else {
""
}
val renderedReturnType = returnType.toTypeScript(indent)
val containsUnresolvedChar = !name.isValidES5Identifier()
val escapedName = when {
isMember && containsUnresolvedChar -> "\"$name\""
else -> name
}
return if (!isMember && containsUnresolvedChar) {
""
} else {
"${prefix}$visibility$keyword$escapedName$renderedTypeParameters($renderedParameters): $renderedReturnType;"
}
}
private fun ExportedObject.generateTypeScriptString(indent: String, prefix: String): String {
val shouldRenderSeparatedAbstractClass = !couldBeProperty()
var t: ExportedType = ExportedType.InlineInterfaceType(members)
if (superClass != null)
t = ExportedType.IntersectionType(t, superClass)
for (superInterface in superInterfaces) {
t = ExportedType.IntersectionType(t, superInterface)
}
if (shouldRenderSeparatedAbstractClass) {
val constructor = ExportedConstructSignature(emptyList(), ExportedType.Primitive.Any)
t = ExportedType.IntersectionType(t, ExportedType.InlineInterfaceType(listOf(constructor)))
}
val maybeParentClass = ir.parent as? IrClass
val propertyName = ir
.takeIf { shouldRenderSeparatedAbstractClass }
?.getFqNameWithJsNameWhenAvailable(true)
?.asString()
?.replace('.', syntheticObjectNameSeparator) ?: name
val property = ExportedProperty(
name = propertyName,
type = t,
mutable = false,
isMember = maybeParentClass != null && !shouldRenderSeparatedAbstractClass,
isStatic = !ir.isInner && maybeParentClass?.isObject == false,
isAbstract = false,
isProtected = ir.visibility == DescriptorVisibilities.PROTECTED,
irGetter = irGetter,
irSetter = null,
isField = false,
)
return if (!shouldRenderSeparatedAbstractClass) {
property.generateTypeScriptString(indent, prefix)
} else {
val propertyRef = "$objects.$propertyName"
val shouldCreateExtraProperty = members.isNotEmpty() || superInterfaces.isNotEmpty() || superClass != null
val newSuperClass = ExportedType.ClassType(propertyRef, emptyList(), ir).takeIf { shouldCreateExtraProperty }
ExportedRegularClass(
name = name,
isInterface = false,
isAbstract = true,
superClass = newSuperClass,
superInterfaces = superInterfaces,
typeParameters = emptyList(),
members = listOf(ExportedConstructor(emptyList(), ExportedVisibility.PRIVATE)),
nestedClasses = nestedClasses,
ir = ir
)
.generateTypeScriptString(indent, prefix)
.also { if (shouldCreateExtraProperty) objectsSyntheticProperties.add(property) }
}
}
private fun ExportedRegularClass.generateTypeScriptString(indent: String, prefix: String): String {
val keyword = if (isInterface) "interface" else "class"
val superInterfacesKeyword = if (isInterface) "extends" else "implements"
val superClassClause = superClass?.let { it.toExtendsClause(indent) } ?: ""
val superInterfacesClause = superInterfaces.toImplementsClause(superInterfacesKeyword, indent)
val (memberObjects, nestedDeclarations) = nestedClasses.partition { it.couldBeProperty() }
val members = members
.let { if (shouldNotBeImplemented()) it.withMagicProperty() else it }
.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))
}
} + memberObjects
val (innerClasses, nonInnerClasses) = nestedDeclarations.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 = if (!isInterface && !isAbstract && members.none { it is ExportedConstructor }) {
"$indent private constructor();\n"
} else {
""
}
val renderedTypeParameters = if (typeParameters.isNotEmpty()) {
"<" + typeParameters.joinToString(", ") + ">"
} else {
""
}
val modifiers = if (isAbstract && !isInterface) "abstract " else ""
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 ""
return if (name.isValidES5Identifier()) klassExport + staticsExport else ""
}
private fun ExportedType.toExtendsClause(indent: String): String {
val isImplicitlyExportedType = this is ExportedType.ImplicitlyExportedType
val extendsClause = " extends ${toTypeScript(indent, isImplicitlyExportedType)}"
return when {
isImplicitlyExportedType -> " /*$extendsClause */"
else -> extendsClause
}
}
private fun List<ExportedType>.toImplementsClause(superInterfacesKeyword: String, indent: String): String {
val (exportedInterfaces, nonExportedInterfaces) = partition { it !is ExportedType.ImplicitlyExportedType }
val listOfNonExportedInterfaces = nonExportedInterfaces.joinToString(", ") {
(it as ExportedType.ImplicitlyExportedType).type.toTypeScript(indent, true)
}
return when {
exportedInterfaces.isEmpty() && nonExportedInterfaces.isNotEmpty() ->
" /* $superInterfacesKeyword $listOfNonExportedInterfaces */"
exportedInterfaces.isNotEmpty() -> {
val nonExportedInterfacesTsString = if (nonExportedInterfaces.isNotEmpty()) "/*, $listOfNonExportedInterfaces */" else ""
" $superInterfacesKeyword " + exportedInterfaces.joinToString(", ") { it.toTypeScript(indent) } + nonExportedInterfacesTsString
}
else -> ""
}
}
private fun ExportedRegularClass.shouldNotBeImplemented(): Boolean {
return (isInterface && !ir.isExternal) || superInterfaces.any { it is ExportedType.ClassType && !it.ir.isExternal }
}
private fun List<ExportedDeclaration>.withMagicProperty(): List<ExportedDeclaration> {
return plus(
ExportedProperty(
"__doNotUseIt",
ExportedType.TypeParameter(doNotImplementIt),
mutable = false,
isMember = true,
isStatic = false,
isAbstract = false,
isProtected = false,
isField = true,
irGetter = null,
irSetter = null,
)
)
}
}
fun List<ExportedDeclaration>.toTypeScript(indent: String): String =
joinToString("") { it.toTypeScript(indent) + "\n" }
private fun IrClass.asNestedClassAccess(): String {
val name = getJsNameOrKotlinName().identifier
if (parent !is IrClass) return name
return "${parentAsClass.asNestedClassAccess()}.$name"
}
fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): String =
indent + when (this) {
is ErrorDeclaration -> "/* ErrorDeclaration: $message */"
is ExportedNamespace ->
"${prefix}namespace $name {\n" + declarations.toTypeScript("$indent ") + "$indent}"
is ExportedFunction -> {
val visibility = if (isProtected) "protected " else ""
val keyword: String = when {
isMember -> when {
isStatic -> "static "
isAbstract -> "abstract "
else -> ""
}
else -> "function "
}
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
val renderedTypeParameters =
if (typeParameters.isNotEmpty())
"<" + typeParameters.joinToString(", ") { it.toTypeScript(indent) } + ">"
else
""
val renderedReturnType = returnType.toTypeScript(indent)
val containsUnresolvedChar = !name.isValidES5Identifier()
val escapedName = when {
isMember && containsUnresolvedChar -> "\"$name\""
else -> name
}
if (!isMember && containsUnresolvedChar) "" else "${prefix}$visibility$keyword$escapedName$renderedTypeParameters($renderedParameters): $renderedReturnType;"
}
is ExportedConstructor -> {
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
"${visibility.keyword}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 {
isMember -> (if (isAbstract) "abstract " else "")
else -> if (mutable) "let " else "const "
}
val possibleStatic = if (isMember && isStatic) "static " else ""
val containsUnresolvedChar = !name.isValidES5Identifier()
val memberName = when {
isMember && containsUnresolvedChar -> "\"$name\""
else -> name
}
val typeToTypeScript = type.toTypeScript(indent)
if (isMember && !isField) {
val getter = "$prefix$visibility$possibleStatic${keyword}get $memberName(): $typeToTypeScript;"
if (!mutable) getter
else getter + "\n" + "$indent$prefix$visibility$possibleStatic${keyword}set $memberName(value: $typeToTypeScript);"
private fun ExportedClass.withProtectedConstructors(): ExportedRegularClass {
return (this as ExportedRegularClass).copy(members = members.map {
if (it !is ExportedConstructor || it.isProtected) {
it
} else {
if (!isMember && containsUnresolvedChar) ""
else {
val readonly = if (isMember && !mutable) "readonly " else ""
"$prefix$visibility$possibleStatic$keyword$readonly$memberName: $typeToTypeScript;"
}
it.copy(visibility = ExportedVisibility.PROTECTED)
}
}
is ExportedClass -> {
val keyword = if (isInterface) "interface" else "class"
val superInterfacesKeyword = if (isInterface) "extends" else "implements"
val superClassClause = superClass?.let { it.toExtendsClause(indent) } ?: ""
val superInterfacesClause = superInterfaces.toImplementsClause(superInterfacesKeyword, indent)
val members = members
.let { if (shouldNotBeImplemented()) it.withMagicProperty() else it }
.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 =
if (!isInterface && !isAbstract && members.none { it is ExportedConstructor })
"$indent private constructor();\n"
else
""
val renderedTypeParameters =
if (typeParameters.isNotEmpty())
"<" + typeParameters.joinToString(", ") + ">"
else
""
val modifiers = if (isAbstract && !isInterface) "abstract " else ""
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 ""
if (name.isValidES5Identifier()) klassExport + staticsExport else ""
}
})
}
fun ExportedType.toExtendsClause(indent: String): String {
val isImplicitlyExportedType = this is ExportedType.ImplicitlyExportedType
val extendsClause = " extends ${toTypeScript(indent, isImplicitlyExportedType)}"
return when {
isImplicitlyExportedType -> " /*$extendsClause */"
else -> extendsClause
}
}
private 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()
fun List<ExportedType>.toImplementsClause(superInterfacesKeyword: String, indent: String): String {
val (exportedInterfaces, nonExportedInterfaces) = partition { it !is ExportedType.ImplicitlyExportedType }
val listOfNonExportedInterfaces = nonExportedInterfaces.joinToString(", ") {
(it as ExportedType.ImplicitlyExportedType).type.toTypeScript(indent, true)
}
return when {
exportedInterfaces.isEmpty() && nonExportedInterfaces.isNotEmpty() ->
" /* $superInterfacesKeyword $listOfNonExportedInterfaces */"
exportedInterfaces.isNotEmpty() -> {
val nonExportedInterfacesTsString = if (nonExportedInterfaces.isNotEmpty()) "/*, $listOfNonExportedInterfaces */" else ""
" $superInterfacesKeyword " + exportedInterfaces.joinToString(", ") { it.toTypeScript(indent) } + nonExportedInterfacesTsString
}
else -> ""
}
}
val type = ExportedType.IntersectionType(
ExportedType.InlineInterfaceType(allPublicConstructors),
ExportedType.TypeOf(innerClassReference)
)
fun ExportedClass.shouldNotBeImplemented(): Boolean {
return (isInterface && !ir.isExternal) || superInterfaces.any { it is ExportedType.ClassType && !it.ir.isExternal }
}
fun List<ExportedDeclaration>.withMagicProperty(): List<ExportedDeclaration> {
return plus(
ExportedProperty(
"__doNotUseIt",
ExportedType.TypeParameter("__doNotImplementIt"),
return ExportedProperty(
name = name,
type = type,
mutable = false,
isMember = true,
isStatic = false,
isAbstract = false,
isProtected = false,
isField = true,
isField = false,
irGetter = null,
irSetter = null,
irSetter = null
)
)
}
}
fun IrClass.asNestedClassAccess(): String {
val name = getJsNameOrKotlinName().identifier
if (parent !is IrClass) return name
return "${parentAsClass.asNestedClassAccess()}.$name"
}
private fun ExportedParameter.toTypeScript(indent: String): String {
val name = sanitizeName(name, withHash = false)
val type = type.toTypeScript(indent)
val questionMark = if (hasDefaultValue) "?" else ""
return "$name$questionMark: $type"
}
fun ExportedClass.withProtectedConstructors(): ExportedClass {
return copy(members = members.map {
if (it !is ExportedConstructor || it.isProtected) {
it
private fun ExportedType.toTypeScript(indent: String, isInCommentContext: Boolean = false): String = when (this) {
is ExportedType.Primitive -> typescript
is ExportedType.Array -> "Array<${elementType.toTypeScript(indent, isInCommentContext)}>"
is ExportedType.Function -> "(" + parameterTypes
.withIndex()
.joinToString(", ") { (index, type) ->
"p$index: ${type.toTypeScript(indent, isInCommentContext)}"
} + ") => " + returnType.toTypeScript(indent, isInCommentContext)
is ExportedType.ClassType ->
name + if (arguments.isNotEmpty()) "<${arguments.joinToString(", ") { it.toTypeScript(indent, isInCommentContext) }}>" else ""
is ExportedType.TypeOf ->
"typeof $name"
is ExportedType.ErrorType -> if (isInCommentContext) comment else "any /*$comment*/"
is ExportedType.Nullable -> "$Nullable<" + baseType.toTypeScript(indent, isInCommentContext) + ">"
is ExportedType.InlineInterfaceType -> {
members.joinToString(prefix = "{\n", postfix = "$indent}", separator = "") { it.toTypeScript("$indent ") + "\n" }
}
is ExportedType.IntersectionType -> {
lhs.toTypeScript(indent) + " & " + rhs.toTypeScript(indent, isInCommentContext)
}
is ExportedType.UnionType -> {
lhs.toTypeScript(indent) + " | " + rhs.toTypeScript(indent, isInCommentContext)
}
is ExportedType.LiteralType.StringLiteralType -> "\"$value\""
is ExportedType.LiteralType.NumberLiteralType -> value.toString()
is ExportedType.ImplicitlyExportedType -> {
val typeString = type.toTypeScript("", true)
if (isInCommentContext) typeString else ExportedType.Primitive.Any.toTypeScript(indent) + "/* $typeString */"
}
is ExportedType.TypeParameter -> if (constraint == null) {
name
} else {
it.copy(visibility = ExportedVisibility.PROTECTED)
"$name extends ${constraint.toTypeScript(indent, isInCommentContext)}"
}
})
}
}
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),
)
private fun ExportedClass.couldBeProperty(): Boolean {
return this is ExportedObject && nestedClasses.all {
it.couldBeProperty() && it.ir.visibility != DescriptorVisibilities.PROTECTED
}
.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,
isField = false,
irGetter = null,
irSetter = null
)
}
}
fun ExportedParameter.toTypeScript(indent: String): String {
val name = sanitizeName(name, withHash = false)
val type = type.toTypeScript(indent)
val questionMark = if (hasDefaultValue) "?" else ""
return "$name$questionMark: $type"
}
fun ExportedType.toTypeScript(indent: String, isInCommentContext: Boolean = false): String = when (this) {
is ExportedType.Primitive -> typescript
is ExportedType.Array -> "Array<${elementType.toTypeScript(indent, isInCommentContext)}>"
is ExportedType.Function -> "(" + parameterTypes
.withIndex()
.joinToString(", ") { (index, type) ->
"p$index: ${type.toTypeScript(indent, isInCommentContext)}"
} + ") => " + returnType.toTypeScript(indent, isInCommentContext)
is ExportedType.ClassType ->
name + if (arguments.isNotEmpty()) "<${arguments.joinToString(", ") { it.toTypeScript(indent, isInCommentContext) }}>" else ""
is ExportedType.TypeOf ->
"typeof $name"
is ExportedType.ErrorType -> if (isInCommentContext) comment else "any /*$comment*/"
is ExportedType.Nullable -> "Nullable<" + baseType.toTypeScript(indent, isInCommentContext) + ">"
is ExportedType.InlineInterfaceType -> {
members.joinToString(prefix = "{\n", postfix = "$indent}", separator = "") { it.toTypeScript("$indent ") + "\n" }
}
is ExportedType.IntersectionType -> {
lhs.toTypeScript(indent) + " & " + rhs.toTypeScript(indent, isInCommentContext)
}
is ExportedType.UnionType -> {
lhs.toTypeScript(indent) + " | " + rhs.toTypeScript(indent, isInCommentContext)
}
is ExportedType.LiteralType.StringLiteralType -> "\"$value\""
is ExportedType.LiteralType.NumberLiteralType -> value.toString()
is ExportedType.ImplicitlyExportedType -> {
val typeString = type.toTypeScript("", true)
if (isInCommentContext) typeString else ExportedType.Primitive.Any.toTypeScript(indent) + "/* $typeString */"
}
is ExportedType.TypeParameter -> if (constraint == null) {
name
} else {
"$name extends ${constraint.toTypeScript(indent, isInCommentContext)}"
}
}
@@ -86,7 +86,7 @@ class IrModuleToJsTransformerTmp(
}
}
val dts = wrapTypeScript(mainModuleName, moduleKind, exportData.values.flatMap { it.values.flatMap { it } }.toTypeScript(moduleKind))
val dts = ExportedModule(mainModuleName, moduleKind, exportData.values.flatMap { it.values.flatten() }).toTypeScript()
modules.forEach { module ->
module.files.forEach { StaticMembersLowering(backendContext).lower(it) }
@@ -198,5 +198,39 @@ declare namespace JS_TESTS {
hashCode(): number;
equals(other: Nullable<any>): boolean;
}
abstract class Parent {
private constructor();
}
namespace Parent {
abstract class Nested1 extends _objects_.foo$Parent$Nested1 {
private constructor();
}
namespace Nested1 {
class Nested2 {
constructor();
}
namespace Nested2 {
abstract class Companion {
private constructor();
}
namespace Companion {
class Nested3 {
constructor();
}
}
}
}
}
function getParent(): typeof foo.Parent;
function createNested1(): typeof foo.Parent.Nested1;
function createNested2(): foo.Parent.Nested1.Nested2;
function createNested3(): foo.Parent.Nested1.Nested2.Companion.Nested3;
}
}
namespace _objects_ {
const foo$Parent$Nested1: {
get value(): string;
} & {
new(): any;
};
}
}
@@ -256,4 +256,36 @@ class __JsNameTest private constructor() {
data class KT39423(
val a: String,
val b: Int? = null
)
)
@JsExport
object Parent {
object Nested1 {
val value: String = "Nested1"
class Nested2 {
companion object {
class Nested3
}
}
}
}
@JsExport
fun getParent(): Parent {
return Parent
}
@JsExport
fun createNested1(): Parent.Nested1 {
return Parent.Nested1
}
@JsExport
fun createNested2(): Parent.Nested1.Nested2 {
return Parent.Nested1.Nested2()
}
@JsExport
fun createNested3(): Parent.Nested1.Nested2.Companion.Nested3 {
return Parent.Nested1.Nested2.Companion.Nested3()
}
@@ -32,6 +32,11 @@ var processInterface = JS_TESTS.foo.processInterface;
var OuterClass = JS_TESTS.foo.OuterClass;
var KT38262 = JS_TESTS.foo.KT38262;
var JsNameTest = JS_TESTS.foo.JsNameTest;
var Parent = JS_TESTS.foo.Parent;
var getParent = JS_TESTS.foo.getParent;
var createNested1 = JS_TESTS.foo.createNested1;
var createNested2 = JS_TESTS.foo.createNested2;
var createNested3 = JS_TESTS.foo.createNested3;
function assert(condition) {
if (!condition) {
throw "Assertion failed";
@@ -135,5 +140,15 @@ function box() {
assert(jsNameTest.runTest() === "JsNameTest");
var jsNameNestedTest = JsNameTest.Companion.createChild(42);
assert(jsNameNestedTest.value === 42);
// Do not strip types from those test cases (it is a check of nested objects types usability)
var parent = Parent;
var nested1 = Parent.Nested1;
var nested2 = new Parent.Nested1.Nested2();
var nested3 = new Parent.Nested1.Nested2.Companion.Nested3();
assert(nested1.value === "Nested1");
assert(getParent() === parent);
assert(createNested1() === nested1);
assert(createNested2() !== nested2);
assert(createNested3() !== nested3);
return "OK";
}
@@ -32,6 +32,11 @@ import processInterface = JS_TESTS.foo.processInterface;
import OuterClass = JS_TESTS.foo.OuterClass;
import KT38262 = JS_TESTS.foo.KT38262;
import JsNameTest = JS_TESTS.foo.JsNameTest;
import Parent = JS_TESTS.foo.Parent;
import getParent = JS_TESTS.foo.getParent;
import createNested1 = JS_TESTS.foo.createNested1;
import createNested2 = JS_TESTS.foo.createNested2;
import createNested3 = JS_TESTS.foo.createNested3;
function assert(condition: boolean) {
if (!condition) {
@@ -166,5 +171,16 @@ function box(): string {
assert(jsNameNestedTest.value === 42)
// Do not strip types from those test cases (it is a check of nested objects types usability)
const parent: typeof Parent = Parent
const nested1: typeof Parent.Nested1 = Parent.Nested1
const nested2: Parent.Nested1.Nested2 = new Parent.Nested1.Nested2()
const nested3: Parent.Nested1.Nested2.Companion.Nested3 = new Parent.Nested1.Nested2.Companion.Nested3()
assert(nested1.value === "Nested1")
assert(getParent() === parent)
assert(createNested1() === nested1)
assert(createNested2() !== nested2)
assert(createNested3() !== nested3)
return "OK";
}
@@ -13,13 +13,13 @@ declare namespace JS_TESTS {
constructor();
protected get protectedVal(): number;
protected protectedFun(): number;
get publicVal(): number;
publicFun(): number;
protected static get protectedNestedObject(): {
};
protected static get Companion(): {
get companionObjectProp(): number;
};
get publicVal(): number;
publicFun(): number;
}
namespace Class {
class protectedClass {