diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt index 5b72f668bcb..661738babe4 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt @@ -20,7 +20,7 @@ data class ExportedModule( class ExportedNamespace( val name: String, - val declarations: List + val declarations: List, ) : 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 + abstract val superClass: ExportedType? + abstract val superInterfaces: List + abstract val nestedClasses: List +} + +data class ExportedRegularClass( + override val name: String, val isInterface: Boolean = false, val isAbstract: Boolean = false, - val superClass: ExportedType? = null, - val superInterfaces: List = emptyList(), + override val superClass: ExportedType? = null, + override val superInterfaces: List = emptyList(), val typeParameters: List, - val members: List, - val nestedClasses: List, - val ir: IrClass -) : ExportedDeclaration() + override val members: List, + override val nestedClasses: List, + override val ir: IrClass, +) : ExportedClass() + +data class ExportedObject( + override val name: String, + override val superClass: ExportedType? = null, + override val superInterfaces: List = emptyList(), + override val members: List, + override val nestedClasses: List, + override val ir: IrClass, + val irGetter: IrFunction +) : ExportedClass() class ExportedParameter( val name: String, diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt index 17bfc2f575f..5fc3820d15f 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt @@ -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( diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt index 04cd437fc0b..a0e1bc227bf 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt @@ -86,17 +86,22 @@ class ExportModelToJsStatements( is ExportedProperty -> { require(namespace != null) { "Only namespaced properties are allowed" } - val underlying: List = 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 diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt index 28e34756bd7..a01e2837765 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt @@ -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 | 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.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() + + 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 | 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): String { + return declarations.toTypeScript(moduleKind) + } + + private fun List.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.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.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.withMagicProperty(): List { + 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.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() + .filterNot { it.isProtected } + .map { + ExportedConstructSignature( + parameters = it.parameters.drop(1), + returnType = ExportedType.TypeParameter(innerClassReference), + ) + } + .toList() -fun List.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.withMagicProperty(): List { - 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() - .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)}" - } -} \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformerTmp.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformerTmp.kt index 0670580a63f..335e2def6b3 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformerTmp.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformerTmp.kt @@ -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) } diff --git a/js/js.translator/testData/typescript-export/declarations/declarations.d.ts b/js/js.translator/testData/typescript-export/declarations/declarations.d.ts index 0036bcc66ec..a1d4070f068 100644 --- a/js/js.translator/testData/typescript-export/declarations/declarations.d.ts +++ b/js/js.translator/testData/typescript-export/declarations/declarations.d.ts @@ -198,5 +198,39 @@ declare namespace JS_TESTS { hashCode(): number; equals(other: Nullable): 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; + }; + } +} \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/declarations/declarations.kt b/js/js.translator/testData/typescript-export/declarations/declarations.kt index c106b43358b..5e1a695ca67 100644 --- a/js/js.translator/testData/typescript-export/declarations/declarations.kt +++ b/js/js.translator/testData/typescript-export/declarations/declarations.kt @@ -256,4 +256,36 @@ class __JsNameTest private constructor() { data class KT39423( val a: String, val b: Int? = null -) \ No newline at end of file +) + +@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() +} diff --git a/js/js.translator/testData/typescript-export/declarations/declarations__main.js b/js/js.translator/testData/typescript-export/declarations/declarations__main.js index ee3a4f205be..314051ae496 100644 --- a/js/js.translator/testData/typescript-export/declarations/declarations__main.js +++ b/js/js.translator/testData/typescript-export/declarations/declarations__main.js @@ -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"; } diff --git a/js/js.translator/testData/typescript-export/declarations/declarations__main.ts b/js/js.translator/testData/typescript-export/declarations/declarations__main.ts index ee6a3282464..f14bbdc9c82 100644 --- a/js/js.translator/testData/typescript-export/declarations/declarations__main.ts +++ b/js/js.translator/testData/typescript-export/declarations/declarations__main.ts @@ -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"; } \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/visibility/visibility.d.ts b/js/js.translator/testData/typescript-export/visibility/visibility.d.ts index a70cce14a66..d22784eba68 100644 --- a/js/js.translator/testData/typescript-export/visibility/visibility.d.ts +++ b/js/js.translator/testData/typescript-export/visibility/visibility.d.ts @@ -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 {