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( class ExportedNamespace(
val name: String, val name: String,
val declarations: List<ExportedDeclaration> val declarations: List<ExportedDeclaration>,
) : ExportedDeclaration() ) : ExportedDeclaration()
data class ExportedFunction( data class ExportedFunction(
@@ -59,24 +59,43 @@ class ExportedProperty(
val isField: Boolean, val isField: Boolean,
val irGetter: IrFunction?, val irGetter: IrFunction?,
val irSetter: IrFunction?, val irSetter: IrFunction?,
val exportedObject: ExportedClass? = null,
) : ExportedDeclaration() ) : ExportedDeclaration()
// TODO: Cover all cases with frontend and disable error declarations // TODO: Cover all cases with frontend and disable error declarations
class ErrorDeclaration(val message: String) : ExportedDeclaration() 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 isInterface: Boolean = false,
val isAbstract: Boolean = false, val isAbstract: Boolean = false,
val superClass: ExportedType? = null, override val superClass: ExportedType? = null,
val superInterfaces: List<ExportedType> = emptyList(), override val superInterfaces: List<ExportedType> = emptyList(),
val typeParameters: List<String>, val typeParameters: List<String>,
val members: List<ExportedDeclaration>, override val members: List<ExportedDeclaration>,
val nestedClasses: List<ExportedClass>, override val nestedClasses: List<ExportedClass>,
val ir: IrClass override val ir: IrClass,
) : ExportedDeclaration() ) : 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( class ExportedParameter(
val name: String, val name: String,
@@ -270,9 +270,7 @@ class ExportModelGenerator(
listOf(privateConstructor) + members, listOf(privateConstructor) + members,
nestedClasses nestedClasses
).let { ).let {
(it as ExportedClass).copy( (it as ExportedRegularClass).copy(isAbstract = true)
isAbstract = true,
)
} }
} }
@@ -352,7 +350,18 @@ class ExportModelGenerator(
val name = klass.getExportedIdentifier() val name = klass.getExportedIdentifier()
val exportedClass = ExportedClass( return if (klass.kind == ClassKind.OBJECT) {
return ExportedObject(
ir = klass,
name = name,
members = members,
superClass = superType,
nestedClasses = nestedClasses,
superInterfaces = superInterfaces,
irGetter = context.mapping.objectToGetInstanceFunction[klass]!!
)
} else {
ExportedRegularClass(
name = name, name = name,
isInterface = klass.isInterface, isInterface = klass.isInterface,
isAbstract = klass.modality == Modality.ABSTRACT || klass.modality == Modality.SEALED, isAbstract = klass.modality == Modality.ABSTRACT || klass.modality == Modality.SEALED,
@@ -363,32 +372,7 @@ class ExportModelGenerator(
nestedClasses = nestedClasses, nestedClasses = nestedClasses,
ir = klass 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(
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,
)
}
return exportedClass
} }
private fun exportAsEnumMember( private fun exportAsEnumMember(
@@ -86,17 +86,22 @@ class ExportModelToJsStatements(
is ExportedProperty -> { is ExportedProperty -> {
require(namespace != null) { "Only namespaced properties are allowed" } 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 getter = declaration.irGetter?.let { JsNameRef(namer.getNameForStaticDeclaration(it)) }
val setter = declaration.irSetter?.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 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() if (declaration.isInterface) return emptyList()
val newNameSpace = if (namespace != null) val newNameSpace = if (namespace != null)
jsElementAccess(declaration.name, namespace) jsElementAccess(declaration.name, namespace)
@@ -120,16 +125,13 @@ class ExportModelToJsStatements(
.filter { it is ExportedFunction && it.isStatic } .filter { it is ExportedFunction && it.isStatic }
.takeIf { !declaration.ir.isInner }.orEmpty() .takeIf { !declaration.ir.isInner }.orEmpty()
// Nested objects are exported as static properties val enumEntries = declaration.members.filter { it is ExportedProperty && it.isStatic }
val staticProperties = declaration.members.mapNotNull {
(it as? ExportedProperty)?.takeIf { it.isStatic }
}
val innerClassesAssignments = declaration.nestedClasses val innerClassesAssignments = declaration.nestedClasses
.filter { it.ir.isInner } .filter { it.ir.isInner }
.map { it.generateInnerClassAssignment(declaration) } .map { it.generateInnerClassAssignment(declaration) }
val staticsExport = (staticFunctions + staticProperties + declaration.nestedClasses) val staticsExport = (staticFunctions + enumEntries + declaration.nestedClasses)
.flatMap { generateDeclarationExport(it, newNameSpace, esModules) } .flatMap { generateDeclarationExport(it, newNameSpace, esModules) }
listOfNotNull(klassExport) + staticsExport + innerClassesAssignments listOfNotNull(klassExport) + staticsExport + innerClassesAssignments
@@ -5,64 +5,145 @@
package org.jetbrains.kotlin.ir.backend.js.export 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.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.backend.js.utils.sanitizeName import org.jetbrains.kotlin.ir.backend.js.utils.sanitizeName
import org.jetbrains.kotlin.ir.declarations.IrClass 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.ir.util.parentAsClass
import org.jetbrains.kotlin.js.common.isValidES5Identifier import org.jetbrains.kotlin.js.common.isValidES5Identifier
import org.jetbrains.kotlin.serialization.js.ModuleKind 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 { 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 { fun List<ExportedDeclaration>.toTypeScript(moduleKind: ModuleKind): String {
val declareKeyword = when (moduleKind) { 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 -> "" ModuleKind.PLAIN -> ""
else -> "declare " else -> "$declare "
} }
val types = """ val types = """
type Nullable<T> = T | null | undefined type $Nullable<T> = T | null | undefined
${declareKeyword}const __doNotImplementIt: unique symbol ${declareKeyword}const $doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt type $doNotImplementIt = typeof $doNotImplementIt
""".trimIndent().prependIndent(moduleKind.indent) + "\n" """.trimIndent().prependIndent(module.moduleKind.indent) + "\n"
val declarationsDts = types + dts val declarationsDts = types + module.declarations.toTypeScript(module.moduleKind)
val namespaceName = sanitizeName(name, withHash = false) val namespaceName = sanitizeName(name, withHash = false)
return when (moduleKind) { return when (module.moduleKind) {
ModuleKind.PLAIN -> "declare namespace $namespaceName {\n$declarationsDts\n}\n" ModuleKind.PLAIN -> "declare namespace $namespaceName {\n$declarationsDts\n}\n"
ModuleKind.AMD, ModuleKind.COMMON_JS, ModuleKind.ES -> declarationsDts ModuleKind.AMD, ModuleKind.COMMON_JS, ModuleKind.ES -> declarationsDts
ModuleKind.UMD -> "$declarationsDts\nexport as namespace $namespaceName;" ModuleKind.UMD -> "$declarationsDts\nexport as namespace $namespaceName;"
} }
} }
private val ModuleKind.indent: String fun generateTypeScript(moduleKind: ModuleKind, declarations: List<ExportedDeclaration>): String {
get() = if (this == ModuleKind.PLAIN) " " else "" return declarations.toTypeScript(moduleKind)
}
fun List<ExportedDeclaration>.toTypeScript(moduleKind: ModuleKind): String { private fun List<ExportedDeclaration>.toTypeScript(moduleKind: ModuleKind): String {
return joinToString("\n") { return joinToString("\n") {
it.toTypeScript( it.toTypeScript(
indent = moduleKind.indent, indent = moduleKind.indent,
prefix = if (moduleKind == ModuleKind.PLAIN) "" else "export " prefix = if (moduleKind == ModuleKind.PLAIN) "" else "export "
) )
} + generateObjectsNamespaceIfNeeded(moduleKind.indent)
} }
}
fun List<ExportedDeclaration>.toTypeScript(indent: String): String = 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" } joinToString("") { it.toTypeScript(indent) + "\n" }
fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): String = private fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): String =
indent + when (this) { indent + when (this) {
is ErrorDeclaration -> "/* ErrorDeclaration: $message */" 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)
}
is ExportedNamespace -> private fun ErrorDeclaration.generateTypeScriptString(): String {
"${prefix}namespace $name {\n" + declarations.toTypeScript("$indent ") + "$indent}" return "/* ErrorDeclaration: $message */"
}
is ExportedFunction -> { 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 visibility = if (isProtected) "protected " else ""
val keyword: String = when { val keyword: String = when {
@@ -76,11 +157,11 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) } val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
val renderedTypeParameters = val renderedTypeParameters = if (typeParameters.isNotEmpty()) {
if (typeParameters.isNotEmpty())
"<" + typeParameters.joinToString(", ") { it.toTypeScript(indent) } + ">" "<" + typeParameters.joinToString(", ") { it.toTypeScript(indent) } + ">"
else } else {
"" ""
}
val renderedReturnType = returnType.toTypeScript(indent) val renderedReturnType = returnType.toTypeScript(indent)
val containsUnresolvedChar = !name.isValidES5Identifier() val containsUnresolvedChar = !name.isValidES5Identifier()
@@ -90,52 +171,82 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
else -> name else -> name
} }
if (!isMember && containsUnresolvedChar) "" else "${prefix}$visibility$keyword$escapedName$renderedTypeParameters($renderedParameters): $renderedReturnType;" return if (!isMember && containsUnresolvedChar) {
} ""
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);"
} else { } else {
if (!isMember && containsUnresolvedChar) "" "${prefix}$visibility$keyword$escapedName$renderedTypeParameters($renderedParameters): $renderedReturnType;"
else {
val readonly = if (isMember && !mutable) "readonly " else ""
"$prefix$visibility$possibleStatic$keyword$readonly$memberName: $typeToTypeScript;"
}
} }
} }
is ExportedClass -> { 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 keyword = if (isInterface) "interface" else "class"
val superInterfacesKeyword = if (isInterface) "extends" else "implements" val superInterfacesKeyword = if (isInterface) "extends" else "implements"
val superClassClause = superClass?.let { it.toExtendsClause(indent) } ?: "" val superClassClause = superClass?.let { it.toExtendsClause(indent) } ?: ""
val superInterfacesClause = superInterfaces.toImplementsClause(superInterfacesKeyword, indent) val superInterfacesClause = superInterfaces.toImplementsClause(superInterfacesKeyword, indent)
val (memberObjects, nestedDeclarations) = nestedClasses.partition { it.couldBeProperty() }
val members = members val members = members
.let { if (shouldNotBeImplemented()) it.withMagicProperty() else it } .let { if (shouldNotBeImplemented()) it.withMagicProperty() else it }
.map { .map {
@@ -145,24 +256,24 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
// Remove $outer argument from secondary constructors of inner classes // Remove $outer argument from secondary constructors of inner classes
it.copy(parameters = it.parameters.drop(1)) it.copy(parameters = it.parameters.drop(1))
} }
} } + memberObjects
val (innerClasses, nonInnerClasses) = nestedClasses.partition { it.ir.isInner } val (innerClasses, nonInnerClasses) = nestedDeclarations.partition { it.ir.isInner }
val innerClassesProperties = innerClasses.map { it.toReadonlyProperty() } val innerClassesProperties = innerClasses.map { it.toReadonlyProperty() }
val membersString = (members + innerClassesProperties).joinToString("") { it.toTypeScript("$indent ") + "\n" } val membersString = (members + innerClassesProperties).joinToString("") { it.toTypeScript("$indent ") + "\n" }
// If there are no exported constructors, add a private constructor to disable default one // If there are no exported constructors, add a private constructor to disable default one
val privateCtorString = val privateCtorString = if (!isInterface && !isAbstract && members.none { it is ExportedConstructor }) {
if (!isInterface && !isAbstract && members.none { it is ExportedConstructor })
"$indent private constructor();\n" "$indent private constructor();\n"
else } else {
"" ""
}
val renderedTypeParameters = val renderedTypeParameters = if (typeParameters.isNotEmpty()) {
if (typeParameters.isNotEmpty())
"<" + typeParameters.joinToString(", ") + ">" "<" + typeParameters.joinToString(", ") + ">"
else } else {
"" ""
}
val modifiers = if (isAbstract && !isInterface) "abstract " else "" val modifiers = if (isAbstract && !isInterface) "abstract " else ""
@@ -172,22 +283,24 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
val klassExport = val klassExport =
"$prefix$modifiers$keyword $name$renderedTypeParameters$superClassClause$superInterfacesClause {\n$bodyString}" "$prefix$modifiers$keyword $name$renderedTypeParameters$superClassClause$superInterfacesClause {\n$bodyString}"
val staticsExport = val staticsExport =
if (nestedClasses.isNotEmpty()) "\n" + ExportedNamespace(name, nestedClasses).toTypeScript(indent, prefix) else "" if (nestedClasses.isNotEmpty()) "\n" + ExportedNamespace(name, nestedClasses).toTypeScript(
indent,
prefix
) else ""
if (name.isValidES5Identifier()) klassExport + staticsExport else "" return if (name.isValidES5Identifier()) klassExport + staticsExport else ""
}
} }
fun ExportedType.toExtendsClause(indent: String): String { private fun ExportedType.toExtendsClause(indent: String): String {
val isImplicitlyExportedType = this is ExportedType.ImplicitlyExportedType val isImplicitlyExportedType = this is ExportedType.ImplicitlyExportedType
val extendsClause = " extends ${toTypeScript(indent, isImplicitlyExportedType)}" val extendsClause = " extends ${toTypeScript(indent, isImplicitlyExportedType)}"
return when { return when {
isImplicitlyExportedType -> " /*$extendsClause */" isImplicitlyExportedType -> " /*$extendsClause */"
else -> extendsClause else -> extendsClause
} }
} }
fun List<ExportedType>.toImplementsClause(superInterfacesKeyword: String, indent: String): String { private fun List<ExportedType>.toImplementsClause(superInterfacesKeyword: String, indent: String): String {
val (exportedInterfaces, nonExportedInterfaces) = partition { it !is ExportedType.ImplicitlyExportedType } val (exportedInterfaces, nonExportedInterfaces) = partition { it !is ExportedType.ImplicitlyExportedType }
val listOfNonExportedInterfaces = nonExportedInterfaces.joinToString(", ") { val listOfNonExportedInterfaces = nonExportedInterfaces.joinToString(", ") {
(it as ExportedType.ImplicitlyExportedType).type.toTypeScript(indent, true) (it as ExportedType.ImplicitlyExportedType).type.toTypeScript(indent, true)
@@ -201,17 +314,17 @@ fun List<ExportedType>.toImplementsClause(superInterfacesKeyword: String, indent
} }
else -> "" else -> ""
} }
} }
fun ExportedClass.shouldNotBeImplemented(): Boolean { private fun ExportedRegularClass.shouldNotBeImplemented(): Boolean {
return (isInterface && !ir.isExternal) || superInterfaces.any { it is ExportedType.ClassType && !it.ir.isExternal } return (isInterface && !ir.isExternal) || superInterfaces.any { it is ExportedType.ClassType && !it.ir.isExternal }
} }
fun List<ExportedDeclaration>.withMagicProperty(): List<ExportedDeclaration> { private fun List<ExportedDeclaration>.withMagicProperty(): List<ExportedDeclaration> {
return plus( return plus(
ExportedProperty( ExportedProperty(
"__doNotUseIt", "__doNotUseIt",
ExportedType.TypeParameter("__doNotImplementIt"), ExportedType.TypeParameter(doNotImplementIt),
mutable = false, mutable = false,
isMember = true, isMember = true,
isStatic = false, isStatic = false,
@@ -222,25 +335,25 @@ fun List<ExportedDeclaration>.withMagicProperty(): List<ExportedDeclaration> {
irSetter = null, irSetter = null,
) )
) )
} }
fun IrClass.asNestedClassAccess(): String { private fun IrClass.asNestedClassAccess(): String {
val name = getJsNameOrKotlinName().identifier val name = getJsNameOrKotlinName().identifier
if (parent !is IrClass) return name if (parent !is IrClass) return name
return "${parentAsClass.asNestedClassAccess()}.$name" return "${parentAsClass.asNestedClassAccess()}.$name"
} }
fun ExportedClass.withProtectedConstructors(): ExportedClass { private fun ExportedClass.withProtectedConstructors(): ExportedRegularClass {
return copy(members = members.map { return (this as ExportedRegularClass).copy(members = members.map {
if (it !is ExportedConstructor || it.isProtected) { if (it !is ExportedConstructor || it.isProtected) {
it it
} else { } else {
it.copy(visibility = ExportedVisibility.PROTECTED) it.copy(visibility = ExportedVisibility.PROTECTED)
} }
}) })
} }
fun ExportedClass.toReadonlyProperty(): ExportedProperty { private fun ExportedClass.toReadonlyProperty(): ExportedProperty {
val innerClassReference = ir.asNestedClassAccess() val innerClassReference = ir.asNestedClassAccess()
val allPublicConstructors = members.asSequence() val allPublicConstructors = members.asSequence()
.filterIsInstance<ExportedConstructor>() .filterIsInstance<ExportedConstructor>()
@@ -270,16 +383,16 @@ fun ExportedClass.toReadonlyProperty(): ExportedProperty {
irGetter = null, irGetter = null,
irSetter = null irSetter = null
) )
} }
fun ExportedParameter.toTypeScript(indent: String): String { private fun ExportedParameter.toTypeScript(indent: String): String {
val name = sanitizeName(name, withHash = false) val name = sanitizeName(name, withHash = false)
val type = type.toTypeScript(indent) val type = type.toTypeScript(indent)
val questionMark = if (hasDefaultValue) "?" else "" val questionMark = if (hasDefaultValue) "?" else ""
return "$name$questionMark: $type" return "$name$questionMark: $type"
} }
fun ExportedType.toTypeScript(indent: String, isInCommentContext: Boolean = false): String = when (this) { private fun ExportedType.toTypeScript(indent: String, isInCommentContext: Boolean = false): String = when (this) {
is ExportedType.Primitive -> typescript is ExportedType.Primitive -> typescript
is ExportedType.Array -> "Array<${elementType.toTypeScript(indent, isInCommentContext)}>" is ExportedType.Array -> "Array<${elementType.toTypeScript(indent, isInCommentContext)}>"
is ExportedType.Function -> "(" + parameterTypes is ExportedType.Function -> "(" + parameterTypes
@@ -294,7 +407,7 @@ fun ExportedType.toTypeScript(indent: String, isInCommentContext: Boolean = fals
"typeof $name" "typeof $name"
is ExportedType.ErrorType -> if (isInCommentContext) comment else "any /*$comment*/" is ExportedType.ErrorType -> if (isInCommentContext) comment else "any /*$comment*/"
is ExportedType.Nullable -> "Nullable<" + baseType.toTypeScript(indent, isInCommentContext) + ">" is ExportedType.Nullable -> "$Nullable<" + baseType.toTypeScript(indent, isInCommentContext) + ">"
is ExportedType.InlineInterfaceType -> { is ExportedType.InlineInterfaceType -> {
members.joinToString(prefix = "{\n", postfix = "$indent}", separator = "") { it.toTypeScript("$indent ") + "\n" } members.joinToString(prefix = "{\n", postfix = "$indent}", separator = "") { it.toTypeScript("$indent ") + "\n" }
} }
@@ -315,4 +428,11 @@ fun ExportedType.toTypeScript(indent: String, isInCommentContext: Boolean = fals
} else { } else {
"$name extends ${constraint.toTypeScript(indent, isInCommentContext)}" "$name extends ${constraint.toTypeScript(indent, isInCommentContext)}"
} }
}
private fun ExportedClass.couldBeProperty(): Boolean {
return this is ExportedObject && nestedClasses.all {
it.couldBeProperty() && it.ir.visibility != DescriptorVisibilities.PROTECTED
}
}
} }
@@ -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 -> modules.forEach { module ->
module.files.forEach { StaticMembersLowering(backendContext).lower(it) } module.files.forEach { StaticMembersLowering(backendContext).lower(it) }
@@ -198,5 +198,39 @@ declare namespace JS_TESTS {
hashCode(): number; hashCode(): number;
equals(other: Nullable<any>): boolean; 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;
};
} }
} }
@@ -257,3 +257,35 @@ data class KT39423(
val a: String, val a: String,
val b: Int? = null 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 OuterClass = JS_TESTS.foo.OuterClass;
var KT38262 = JS_TESTS.foo.KT38262; var KT38262 = JS_TESTS.foo.KT38262;
var JsNameTest = JS_TESTS.foo.JsNameTest; 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) { function assert(condition) {
if (!condition) { if (!condition) {
throw "Assertion failed"; throw "Assertion failed";
@@ -135,5 +140,15 @@ function box() {
assert(jsNameTest.runTest() === "JsNameTest"); assert(jsNameTest.runTest() === "JsNameTest");
var jsNameNestedTest = JsNameTest.Companion.createChild(42); var jsNameNestedTest = JsNameTest.Companion.createChild(42);
assert(jsNameNestedTest.value === 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"; return "OK";
} }
@@ -32,6 +32,11 @@ import processInterface = JS_TESTS.foo.processInterface;
import OuterClass = JS_TESTS.foo.OuterClass; import OuterClass = JS_TESTS.foo.OuterClass;
import KT38262 = JS_TESTS.foo.KT38262; import KT38262 = JS_TESTS.foo.KT38262;
import JsNameTest = JS_TESTS.foo.JsNameTest; 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) { function assert(condition: boolean) {
if (!condition) { if (!condition) {
@@ -166,5 +171,16 @@ function box(): string {
assert(jsNameNestedTest.value === 42) 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"; return "OK";
} }
@@ -13,13 +13,13 @@ declare namespace JS_TESTS {
constructor(); constructor();
protected get protectedVal(): number; protected get protectedVal(): number;
protected protectedFun(): number; protected protectedFun(): number;
get publicVal(): number;
publicFun(): number;
protected static get protectedNestedObject(): { protected static get protectedNestedObject(): {
}; };
protected static get Companion(): { protected static get Companion(): {
get companionObjectProp(): number; get companionObjectProp(): number;
}; };
get publicVal(): number;
publicFun(): number;
} }
namespace Class { namespace Class {
class protectedClass { class protectedClass {