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 c63e09e89ca..28e34756bd7 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 @@ -55,138 +55,142 @@ fun List.toTypeScript(moduleKind: ModuleKind): String { fun List.toTypeScript(indent: String): String = joinToString("") { it.toTypeScript(indent) + "\n" } -fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): String = indent + when (this) { - is ErrorDeclaration -> "/* ErrorDeclaration: $message */" +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 ExportedNamespace -> + "${prefix}namespace $name {\n" + declarations.toTypeScript("$indent ") + "$indent}" - is ExportedFunction -> { - val visibility = if (isProtected) "protected " else "" + is ExportedFunction -> { + val visibility = if (isProtected) "protected " else "" - val keyword: String = when { - isMember -> when { - isStatic -> "static " - isAbstract -> "abstract " - else -> "" + val keyword: String = when { + isMember -> when { + isStatic -> "static " + isAbstract -> "abstract " + else -> "" + } + else -> "function " } - else -> "function " - } - val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) } + val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) } - val renderedTypeParameters = - if (typeParameters.isNotEmpty()) - "<" + typeParameters.joinToString(", ") { it.toTypeScript(indent) } + ">" - else - "" + val renderedTypeParameters = + if (typeParameters.isNotEmpty()) + "<" + typeParameters.joinToString(", ") { it.toTypeScript(indent) } + ">" + else + "" - val renderedReturnType = returnType.toTypeScript(indent) - val containsUnresolvedChar = !name.isValidES5Identifier() + 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);" - } else { - if (!isMember && containsUnresolvedChar) "" - else { - val readonly = if (isMember && !mutable) "readonly " else "" - "$prefix$visibility$possibleStatic$keyword$readonly$memberName: $typeToTypeScript;" + val escapedName = when { + isMember && containsUnresolvedChar -> "\"$name\"" + else -> name } + + if (!isMember && containsUnresolvedChar) "" else "${prefix}$visibility$keyword$escapedName$renderedTypeParameters($renderedParameters): $renderedReturnType;" } - } - is ExportedClass -> { - val keyword = if (isInterface) "interface" else "class" - val superInterfacesKeyword = if (isInterface) "extends" else "implements" + is ExportedConstructor -> { + val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) } + "${visibility.keyword}constructor($renderedParameters);" + } - val superClassClause = superClass?.let { it.toExtendsClause(indent) } ?: "" - val superInterfacesClause = superInterfaces.toImplementsClause(superInterfacesKeyword, indent) + is ExportedConstructSignature -> { + val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) } + "new($renderedParameters): ${returnType.toTypeScript(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)) + 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 { + if (!isMember && containsUnresolvedChar) "" + else { + val readonly = if (isMember && !mutable) "readonly " else "" + "$prefix$visibility$possibleStatic$keyword$readonly$memberName: $typeToTypeScript;" } } + } - val (innerClasses, nonInnerClasses) = nestedClasses.partition { it.ir.isInner } - val innerClassesProperties = innerClasses.map { it.toReadonlyProperty() } - val membersString = (members + innerClassesProperties).joinToString("") { it.toTypeScript("$indent ") + "\n" } + is ExportedClass -> { + val keyword = if (isInterface) "interface" else "class" + val superInterfacesKeyword = if (isInterface) "extends" else "implements" - // 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 superClassClause = superClass?.let { it.toExtendsClause(indent) } ?: "" + val superInterfacesClause = superInterfaces.toImplementsClause(superInterfacesKeyword, indent) - val renderedTypeParameters = - if (typeParameters.isNotEmpty()) - "<" + typeParameters.joinToString(", ") + ">" - else - "" + 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 modifiers = if (isAbstract && !isInterface) "abstract " else "" + val (innerClasses, nonInnerClasses) = nestedClasses.partition { it.ir.isInner } + val innerClassesProperties = innerClasses.map { it.toReadonlyProperty() } + val membersString = (members + innerClassesProperties).joinToString("") { it.toTypeScript("$indent ") + "\n" } - val bodyString = privateCtorString + membersString + indent + // 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 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 "" + val renderedTypeParameters = + if (typeParameters.isNotEmpty()) + "<" + typeParameters.joinToString(", ") + ">" + else + "" - if (name.isValidES5Identifier()) klassExport + staticsExport 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 { - return when (this) { - is ExportedType.ImplicitlyExportedType -> " /*${type.toExtendsClause(indent)} */" - else -> " extends ${toTypeScript(indent)}" + val isImplicitlyExportedType = this is ExportedType.ImplicitlyExportedType + val extendsClause = " extends ${toTypeScript(indent, isImplicitlyExportedType)}" + return when { + isImplicitlyExportedType -> " /*$extendsClause */" + else -> extendsClause } } 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) + (it as ExportedType.ImplicitlyExportedType).type.toTypeScript(indent, true) } return when { exportedInterfaces.isEmpty() && nonExportedInterfaces.isNotEmpty() -> @@ -275,39 +279,40 @@ fun ExportedParameter.toTypeScript(indent: String): String { return "$name$questionMark: $type" } -fun ExportedType.toTypeScript(indent: String): String = when (this) { +fun ExportedType.toTypeScript(indent: String, isInCommentContext: Boolean = false): String = when (this) { is ExportedType.Primitive -> typescript - is ExportedType.Array -> "Array<${elementType.toTypeScript(indent)}>" + is ExportedType.Array -> "Array<${elementType.toTypeScript(indent, isInCommentContext)}>" is ExportedType.Function -> "(" + parameterTypes .withIndex() .joinToString(", ") { (index, type) -> - "p$index: ${type.toTypeScript(indent)}" - } + ") => " + returnType.toTypeScript(indent) + "p$index: ${type.toTypeScript(indent, isInCommentContext)}" + } + ") => " + returnType.toTypeScript(indent, isInCommentContext) is ExportedType.ClassType -> - name + if (arguments.isNotEmpty()) "<${arguments.joinToString(", ") { it.toTypeScript(indent) }}>" else "" + name + if (arguments.isNotEmpty()) "<${arguments.joinToString(", ") { it.toTypeScript(indent, isInCommentContext) }}>" else "" is ExportedType.TypeOf -> "typeof $name" - is ExportedType.ErrorType -> "any /*$comment*/" - is ExportedType.Nullable -> "Nullable<" + baseType.toTypeScript(indent) + ">" + 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) + lhs.toTypeScript(indent) + " & " + rhs.toTypeScript(indent, isInCommentContext) } is ExportedType.UnionType -> { - lhs.toTypeScript(indent) + " | " + rhs.toTypeScript(indent) + lhs.toTypeScript(indent) + " | " + rhs.toTypeScript(indent, isInCommentContext) } is ExportedType.LiteralType.StringLiteralType -> "\"$value\"" is ExportedType.LiteralType.NumberLiteralType -> value.toString() is ExportedType.ImplicitlyExportedType -> { - ExportedType.Primitive.Any.toTypeScript(indent) + "/* ${type.toTypeScript("")} */" + 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)}" + "$name extends ${constraint.toTypeScript(indent, isInCommentContext)}" } } \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/implicitExport/declarations.d.ts b/js/js.translator/testData/typescript-export/implicitExport/declarations.d.ts index 597b97cf691..f27b2a7d3e5 100644 --- a/js/js.translator/testData/typescript-export/implicitExport/declarations.d.ts +++ b/js/js.translator/testData/typescript-export/implicitExport/declarations.d.ts @@ -31,5 +31,11 @@ declare namespace JS_TESTS { class F extends foo.A /* implements foo.NonExportedInterface */ { constructor(); } + class G /* implements foo.NonExportedGenericInterface */ { + constructor(); + } + class H /* extends foo.NonExportedGenericType */ { + constructor(); + } } } diff --git a/js/js.translator/testData/typescript-export/implicitExport/declarations.kt b/js/js.translator/testData/typescript-export/implicitExport/declarations.kt index cade038d655..233610e6818 100644 --- a/js/js.translator/testData/typescript-export/implicitExport/declarations.kt +++ b/js/js.translator/testData/typescript-export/implicitExport/declarations.kt @@ -9,7 +9,9 @@ package foo interface NonExportedInterface +interface NonExportedGenericInterface open class NonExportedType(val value: Int) +open class NonExportedGenericType(val value: T) @JsExport interface ExportedInterface @@ -44,4 +46,10 @@ class D : NonExportedInterface, ExportedInterface class E : NonExportedType(42), ExportedInterface @JsExport -class F : A(NonExportedType(42)), NonExportedInterface \ No newline at end of file +class F : A(NonExportedType(42)), NonExportedInterface + +@JsExport +class G : NonExportedGenericInterface + +@JsExport +class H : NonExportedGenericType(NonExportedType(42)) \ No newline at end of file