feat(@JsExport for interfaces): add ability to export interfaces in TypeScript.

This commit is contained in:
Artem Kobzar
2021-11-18 10:07:38 +00:00
committed by Space
parent 4dcfd38236
commit d5dd35cb20
35 changed files with 366 additions and 55 deletions
@@ -106,7 +106,7 @@ sealed class ExportedType {
val returnType: ExportedType
) : ExportedType()
class ClassType(val name: String, val arguments: List<ExportedType>) : ExportedType()
class ClassType(val name: String, val arguments: List<ExportedType>, val ir: IrClass) : ExportedType()
class TypeParameter(val name: String) : ExportedType()
class Nullable(val baseType: ExportedType) : ExportedType()
class ErrorType(val comment: String) : ExportedType()
@@ -483,7 +483,8 @@ class ExportModelGenerator(
ClassKind.ENUM_CLASS,
ClassKind.INTERFACE -> ExportedType.ClassType(
name,
type.arguments.map { exportTypeArgument(it) }
type.arguments.map { exportTypeArgument(it) },
klass
)
}.withImplicitlyExported(isImplicitlyExported)
}
@@ -19,7 +19,15 @@ fun ExportedModule.toTypeScript(): String {
}
fun wrapTypeScript(name: String, moduleKind: ModuleKind, dts: String): String {
val types = "${moduleKind.indent}type Nullable<T> = T | null | undefined\n"
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
@@ -117,14 +125,16 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
val superClassClause = superClass?.let { it.toExtendsClause(indent) } ?: ""
val superInterfacesClause = superInterfaces.toImplementsClause(superInterfacesKeyword, indent)
val members = members.map {
if (!ir.isInner || it !is ExportedFunction || !it.isStatic) {
it
} else {
// Remove $outer argument from secondary constructors of inner classes
it.copy(parameters = it.parameters.drop(1))
val 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() }
@@ -179,6 +189,26 @@ fun List<ExportedType>.toImplementsClause(superInterfacesKeyword: String, indent
}
}
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"),
mutable = false,
isMember = true,
isStatic = false,
isAbstract = false,
isProtected = false,
irGetter = null,
irSetter = null
)
)
}
fun IrClass.asNestedClassAccess(): String {
val name = getJsNameOrKotlinName().identifier
if (parent !is IrClass) return name
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.DescriptorVisibility
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.ir.backend.js.export.isEnumFakeOverriddenDeclaration
import org.jetbrains.kotlin.ir.backend.js.export.isExported
@@ -125,24 +126,21 @@ class JsClassGenerator(private val irClass: IrClass, val context: JsGenerationCo
)
}
fun IrSimpleFunction.accessorRef(): JsNameRef? =
when (visibility) {
DescriptorVisibilities.PRIVATE -> null
else -> JsNameRef(
context.getNameForMemberFunction(this),
classPrototypeRef
)
}
val overriddenSymbols = property.getter?.overriddenSymbols.orEmpty()
// Don't generate `defineProperty` if the property overrides a property from an exported class,
// because we've already generated `defineProperty` for the base class property.
// In other words, we only want to generate `defineProperty` once for each property.
// The exception is case when we override val with var,
// so we need regenerate `defineProperty` with setter.
val noOverriddenGetter = property.getter?.overriddenSymbols?.isEmpty() == true
// P.S. If the overridden property is owned by an interface - we should generate defineProperty
// for overridden property in the first class which override those properties
val hasOverriddenExportedInterfaceProperties = overriddenSymbols.any { it.owner.parentClassOrNull.isExportedInterface() }
&& !overriddenSymbols.any { it.owner.parentClassOrNull.isExportedClass() }
val overriddenExportedGetter = (property.getter?.overriddenSymbols?.isNotEmpty() == true &&
property.getter?.isOverriddenExported(context.staticContext.backendContext) == true)
val getterOverridesExternal = property.getter?.overridesExternal() == true
val overriddenExportedGetter = !property.getter?.overriddenSymbols.isNullOrEmpty() &&
property.getter?.isOverriddenExported(context.staticContext.backendContext) == true
val noOverriddenExportedSetter = property.setter?.isOverriddenExported(context.staticContext.backendContext) == false
@@ -150,8 +148,9 @@ class JsClassGenerator(private val irClass: IrClass, val context: JsGenerationCo
property.isEnumFakeOverriddenDeclaration(context.staticContext.backendContext)
if (irClass.isExported(context.staticContext.backendContext) &&
(noOverriddenGetter || needsOverride) ||
property.getter?.overridesExternal() == true ||
(overriddenSymbols.isEmpty() || needsOverride) ||
hasOverriddenExportedInterfaceProperties ||
getterOverridesExternal ||
property.getJsName() != null
) {
@@ -168,31 +167,24 @@ class JsClassGenerator(private val irClass: IrClass, val context: JsGenerationCo
// set: Foo.prototype._set_prop__a4enbm_k$
// });
val getterForwarder = if (property.getter?.modality == Modality.FINAL) property.getter?.accessorRef()
else {
property.getter?.propertyAccessorForwarder("getter forwarder") { getterRef ->
JsReturn(
JsInvocation(
getterRef
)
)
val getterForwarder = property.getter
.takeIf { it.shouldExportAccessor() }
.getOrGenerateIfFinal {
propertyAccessorForwarder("getter forwarder") {
JsReturn(JsInvocation(it))
}
}
}
val setterForwarder = if (property.setter?.modality == Modality.FINAL) property.setter?.accessorRef()
else {
property.setter?.let {
val setterForwarder = property.setter
.takeIf { it.shouldExportAccessor() }
.getOrGenerateIfFinal {
val setterArgName = JsName("value", false)
it.propertyAccessorForwarder("setter forwarder") { setterRef ->
JsInvocation(
setterRef,
JsNameRef(setterArgName)
).makeStmt()
propertyAccessorForwarder("setter forwarder") {
JsInvocation(it, JsNameRef(setterArgName)).makeStmt()
}?.apply {
parameters.add(JsParameter(setterArgName))
}
}
}
classBlock.statements += JsExpressionStatement(
defineProperty(
@@ -209,6 +201,34 @@ class JsClassGenerator(private val irClass: IrClass, val context: JsGenerationCo
return classBlock
}
private inline fun IrSimpleFunction?.getOrGenerateIfFinal(generateFunc: IrSimpleFunction.() -> JsFunction?): JsExpression? {
if (this == null) return null
return if (modality == Modality.FINAL) accessorRef() else generateFunc()
}
private fun IrSimpleFunction?.shouldExportAccessor(): Boolean {
if (this == null) return false
if (parentAsClass.isExported(context.staticContext.backendContext)) return true
val property = correspondingPropertySymbol!!.owner
if (property.isOverriddenExported(context.staticContext.backendContext)) {
return isOverriddenExported(context.staticContext.backendContext)
}
return overridesExternal() || property.getJsName() != null
}
private fun IrSimpleFunction.accessorRef(): JsNameRef? =
when (visibility) {
DescriptorVisibilities.PRIVATE -> null
else -> JsNameRef(
context.getNameForMemberFunction(this),
classPrototypeRef
)
}
private fun IrSimpleFunction.generateAssignmentIfMangled(memberRef: JsExpression) {
if (
irClass.isExported(context.staticContext.backendContext) &&
@@ -143,11 +143,14 @@ fun translateCall(
val jsExtensionReceiver = expression.extensionReceiver?.accept(transformer, context)
val arguments = translateCallArguments(expression, context, transformer)
// Transform external property accessor call
// @JsName-annotated external property accessors are translated as function calls
// Transform external and interface's property accessor call
// @JsName-annotated external and interface's property accessors are translated as function calls
if (function.getJsName() == null) {
val property = function.correspondingPropertySymbol?.owner
if (property != null && property.isEffectivelyExternal()) {
if (
property != null &&
(property.isEffectivelyExternal() || property.isExportedInterfaceMember())
) {
val propertyName = context.getNameForProperty(property)
val nameRef = when (jsDispatchReceiver) {
null -> jsGlobalVarRef(JsNameRef(propertyName))
@@ -0,0 +1,21 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.ir.backend.js.utils
import org.jetbrains.kotlin.descriptors.isInterface
import org.jetbrains.kotlin.descriptors.isClass
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.util.parentClassOrNull
fun IrDeclaration?.isExportedClass() =
this is IrClass && kind.isClass && isJsExport()
fun IrDeclaration?.isExportedInterface() =
this is IrClass && kind.isInterface && isJsExport()
fun IrDeclaration.isExportedInterfaceMember() =
parentClassOrNull.isExportedInterface()
@@ -20,7 +20,7 @@ open class ExportedGenericClass<T>
class <!NON_EXPORTABLE_TYPE("super; ExportedGenericClass<NonExportedClass>")!>ExportedClass3<!> : ExportedGenericClass<NonExportedClass>()
@JsExport
interface <!WRONG_EXPORTED_DECLARATION!>ExportedGenericInterface<!><T>
interface ExportedGenericInterface<T>
@JsExport
class <!NON_EXPORTABLE_TYPE("super; ExportedGenericInterface<NonExportedClass>")!>ExportedClass4<!> : ExportedGenericInterface<NonExportedClass>
@@ -14,4 +14,4 @@ fun <<!NON_EXPORTABLE_TYPE("upper bound; C")!>T : C<!>>foo() { }
class A<<!NON_EXPORTABLE_TYPE("upper bound; C")!>T : C<!>, <!NON_EXPORTABLE_TYPE("upper bound; I")!>S: I<!>>
@JsExport
interface <!WRONG_EXPORTED_DECLARATION!>I2<!><<!NON_EXPORTABLE_TYPE("upper bound; C"), NON_EXPORTABLE_TYPE("upper bound; I")!>T<!>> where T : C, T : I
interface I2<<!NON_EXPORTABLE_TYPE("upper bound; C"), NON_EXPORTABLE_TYPE("upper bound; I")!>T<!>> where T : C, T : I
@@ -18,11 +18,23 @@ val String.extensionProperty<!>
annotation class <!WRONG_EXPORTED_DECLARATION("annotation class")!>AnnotationClass<!>
@JsExport
interface <!WRONG_EXPORTED_DECLARATION("interface")!>SomeInterface<!>
interface SomeInterface
@JsExport
external interface GoodInterface
@JsExport
interface InterfaceWithCompanion {
companion <!WRONG_EXPORTED_DECLARATION("companion object inside exported interface")!>object<!> {
fun foo() = 42
}
}
@JsExport
interface OuterInterface {
class <!WRONG_EXPORTED_DECLARATION("nested class inside exported interface")!>Nested<!>
}
@JsExport
value class <!WRONG_EXPORTED_DECLARATION("value class")!>A(val a: Int)<!>
@@ -50,6 +50,33 @@ package foo {
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.js.JsExport public interface InterfaceWithCompanion {
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
public companion object Companion {
private constructor Companion()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final fun foo(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
@kotlin.js.JsExport public interface OuterInterface {
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
public final class Nested {
public constructor Nested()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
@kotlin.js.JsExport public interface SomeInterface {
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int