[JS IR] Support object declaration export

Fixes KT-39117 and KT-39367
This commit is contained in:
Svyatoslav Kuzmich
2020-06-19 10:40:37 +03:00
parent 4027dae594
commit 4a803e9d2f
17 changed files with 155 additions and 43 deletions
@@ -214,6 +214,11 @@ fun usefulDeclarations(roots: Iterable<IrDeclaration>, context: JsIrBackendConte
(it.classifierOrNull as? IrClassSymbol)?.owner?.enqueue("superTypes")
}
if (declaration.isObject && declaration.isExported(context)) {
context.mapping.objectToGetInstanceFunction[declaration]!!
.enqueue(declaration, "Exported object getInstance function")
}
declaration.annotations.forEach {
val annotationClass = it.symbol.owner.constructedClass
if (annotationClass.isAssociatedObjectAnnotatedAnnotation) {
@@ -14,4 +14,5 @@ object JsLoweredDeclarationOrigin : IrDeclarationOrigin {
object JS_CLOSURE_BOX_CLASS : IrStatementOriginImpl("JS_CLOSURE_BOX_CLASS")
object JS_CLOSURE_BOX_CLASS_DECLARATION : IrDeclarationOriginImpl("JS_CLOSURE_BOX_CLASS_DECLARATION")
object BRIDGE_TO_EXTERNAL_FUNCTION : IrDeclarationOriginImpl("BRIDGE_TO_EXTERNAL_FUNCTION")
object OBJECT_GET_INSTANCE_FUNCTION : IrDeclarationOriginImpl("BRIDGE_TO_EXTERNAL_FUNCTION")
}
@@ -6,7 +6,7 @@
package org.jetbrains.kotlin.ir.backend.js.export
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.serialization.js.ModuleKind
@@ -45,7 +45,8 @@ class ExportedProperty(
val isMember: Boolean = false,
val isStatic: Boolean = false,
val isAbstract: Boolean,
val ir: IrProperty
val irGetter: IrFunction?,
val irSetter: IrFunction?,
) : ExportedDeclaration()
@@ -95,6 +96,13 @@ sealed class ExportedType {
class TypeParameter(val name: String) : ExportedType()
class Nullable(val baseType: ExportedType) : ExportedType()
class ErrorType(val comment: String) : ExportedType()
class TypeOf(val name: String) : ExportedType()
class InlineInterfaceType(
val members: List<ExportedDeclaration>
) : ExportedType()
class IntersectionType(val lhs: ExportedType, val rhs: ExportedType) : ExportedType()
fun withNullability(nullable: Boolean) =
if (nullable) Nullable(this) else this
@@ -112,7 +112,8 @@ class ExportModelGenerator(val context: JsIrBackendContext) {
isMember = parentClass != null,
isStatic = false,
isAbstract = parentClass?.isInterface == false && property.modality == Modality.ABSTRACT,
ir = property
irGetter = property.getter,
irSetter = property.setter
)
}
@@ -120,10 +121,10 @@ class ExportModelGenerator(val context: JsIrBackendContext) {
when (klass.kind) {
ClassKind.ANNOTATION_CLASS,
ClassKind.ENUM_CLASS,
ClassKind.ENUM_ENTRY,
ClassKind.OBJECT ->
ClassKind.ENUM_ENTRY ->
return Exportability.Prohibited("Class ${klass.fqNameWhenAvailable} with kind: ${klass.kind}")
ClassKind.OBJECT,
ClassKind.CLASS,
ClassKind.INTERFACE -> {
}
@@ -137,7 +138,7 @@ class ExportModelGenerator(val context: JsIrBackendContext) {
private fun exportClass(
klass: IrClass
): ExportedClass? {
): ExportedDeclaration? {
when (val exportability = classExportability(klass)) {
is Exportability.Prohibited -> return error(exportability.reason)
is Exportability.NotNeeded -> return null
@@ -160,8 +161,14 @@ class ExportModelGenerator(val context: JsIrBackendContext) {
is IrProperty ->
members.addIfNotNull(exportProperty(candidate))
is IrClass ->
nestedClasses.addIfNotNull(exportClass(candidate))
is IrClass -> {
val ec = exportClass(candidate)
if (ec is ExportedClass) {
nestedClasses.add(ec)
} else {
members.addIfNotNull(ec)
}
}
is IrField -> {
assert(candidate.correspondingPropertySymbol != null) {
@@ -188,6 +195,27 @@ class ExportModelGenerator(val context: JsIrBackendContext) {
val name = klass.getExportedIdentifier()
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 = true,
isAbstract = false,
irGetter = context.mapping.objectToGetInstanceFunction[klass]!!,
irSetter = null
)
}
return ExportedClass(
name = name,
isInterface = klass.isInterface,
@@ -254,11 +282,20 @@ class ExportModelGenerator(val context: JsIrBackendContext) {
classifier is IrClassSymbol -> {
val klass = classifier.owner
when (val exportability = classExportability(klass)) {
is Exportability.Prohibited -> ExportedType.ErrorType(exportability.reason)
is Exportability.NotNeeded -> error("Not needed classes types cannot be used")
else -> ExportedType.ClassType(
klass.fqNameWhenAvailable!!.asString(),
val name = klass.fqNameWhenAvailable!!.asString()
when (klass.kind) {
ClassKind.ANNOTATION_CLASS,
ClassKind.ENUM_CLASS,
ClassKind.ENUM_ENTRY ->
ExportedType.ErrorType("Class $name with kind: ${klass.kind}")
ClassKind.OBJECT ->
ExportedType.TypeOf(name)
ClassKind.CLASS,
ClassKind.INTERFACE -> ExportedType.ClassType(
name,
type.arguments.map { exportTypeArgument(it) }
)
}
@@ -286,7 +323,8 @@ class ExportModelGenerator(val context: JsIrBackendContext) {
return Exportability.NotNeeded
if (function.origin == IrDeclarationOrigin.BRIDGE ||
function.origin == JsLoweredDeclarationOrigin.BRIDGE_TO_EXTERNAL_FUNCTION ||
function.origin == IrDeclarationOrigin.FUNCTION_FOR_DEFAULT_PARAMETER
function.origin == IrDeclarationOrigin.FUNCTION_FOR_DEFAULT_PARAMETER ||
function.origin == JsLoweredDeclarationOrigin.OBJECT_GET_INSTANCE_FUNCTION
) {
return Exportability.NotNeeded
}
@@ -68,8 +68,8 @@ class ExportModelToJsStatements(
is ExportedConstructor -> emptyList()
is ExportedProperty -> {
val getter = declaration.ir.getter?.let { JsNameRef(nameTables.getNameForStaticDeclaration(it)) }
val setter = declaration.ir.setter?.let { JsNameRef(nameTables.getNameForStaticDeclaration(it)) }
val getter = declaration.irGetter?.let { JsNameRef(nameTables.getNameForStaticDeclaration(it)) }
val setter = declaration.irSetter?.let { JsNameRef(nameTables.getNameForStaticDeclaration(it)) }
listOf(defineProperty(namespace, declaration.name, getter, setter).makeStmt())
}
@@ -50,7 +50,7 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
else -> "function "
}
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript() }
val renderedParameters = parameters.joinToString(", ") { it.toTypeScript(indent) }
val renderedTypeParameters =
if (typeParameters.isNotEmpty())
@@ -58,28 +58,28 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
else
""
val renderedReturnType = returnType.toTypeScript()
val renderedReturnType = returnType.toTypeScript(indent)
"${prefix}$keyword$name$renderedTypeParameters($renderedParameters): $renderedReturnType;"
}
is ExportedConstructor ->
"constructor(${parameters.joinToString(", ") { it.toTypeScript() }});"
"constructor(${parameters.joinToString(", ") { it.toTypeScript(indent) }});"
is ExportedProperty -> {
val keyword = when {
isMember -> (if (isAbstract) "abstract " else "") + (if (!mutable) "readonly " else "")
else -> if (mutable) "let " else "const "
}
prefix + keyword + name + ": " + type.toTypeScript() + ";"
prefix + keyword + name + ": " + type.toTypeScript(indent) + ";"
}
is ExportedClass -> {
val keyword = if (isInterface) "interface" else "class"
val superInterfacesKeyword = if (isInterface) "extends" else "implements"
val superClassClause = superClass?.let { " extends ${it.toTypeScript()}" } ?: ""
val superClassClause = superClass?.let { " extends ${it.toTypeScript(indent)}" } ?: ""
val superInterfacesClause = if (superInterfaces.isNotEmpty()) {
" $superInterfacesKeyword " + superInterfaces.joinToString(", ") { it.toTypeScript() }
" $superInterfacesKeyword " + superInterfaces.joinToString(", ") { it.toTypeScript(indent) }
} else ""
val membersString = members.joinToString("") { it.toTypeScript("$indent ") + "\n" }
@@ -107,22 +107,30 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
}
}
fun ExportedParameter.toTypeScript(): String =
"$name: ${type.toTypeScript()}"
fun ExportedParameter.toTypeScript(indent: String): String =
"$name: ${type.toTypeScript(indent)}"
fun ExportedType.toTypeScript(): String = when (this) {
fun ExportedType.toTypeScript(indent: String): String = when (this) {
is ExportedType.Primitive -> typescript
is ExportedType.Array -> "Array<${elementType.toTypeScript()}>"
is ExportedType.Array -> "Array<${elementType.toTypeScript(indent)}>"
is ExportedType.Function -> "(" + parameterTypes
.withIndex()
.joinToString(", ") { (index, type) ->
"p$index: ${type.toTypeScript()}"
} + ") => " + returnType.toTypeScript()
"p$index: ${type.toTypeScript(indent)}"
} + ") => " + returnType.toTypeScript(indent)
is ExportedType.ClassType ->
name + if (arguments.isNotEmpty()) "<${arguments.joinToString(", ") { it.toTypeScript() }}>" else ""
name + if (arguments.isNotEmpty()) "<${arguments.joinToString(", ") { it.toTypeScript(indent) }}>" else ""
is ExportedType.TypeOf ->
"typeof $name"
is ExportedType.ErrorType -> "any /*$comment*/"
is ExportedType.TypeParameter -> name
is ExportedType.Nullable -> "Nullable<" + baseType.toTypeScript() + ">"
is ExportedType.Nullable -> "Nullable<" + baseType.toTypeScript(indent) + ">"
is ExportedType.InlineInterfaceType -> {
members.joinToString(prefix = "{\n", postfix = "$indent}", separator = "") { it.toTypeScript("$indent ") + "\n" }
}
is ExportedType.IntersectionType -> {
lhs.toTypeScript(indent) + " & " + rhs.toTypeScript(indent)
}
}
@@ -12,8 +12,10 @@ import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
import org.jetbrains.kotlin.backend.common.lower.irBlockBody
import org.jetbrains.kotlin.backend.common.lower.irIfThen
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.JsCommonBackendContext
import org.jetbrains.kotlin.ir.backend.js.JsLoweredDeclarationOrigin
import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.builders.declarations.buildField
@@ -111,6 +113,7 @@ private fun JsCommonBackendContext.getOrCreateGetInstanceFunction(obj: IrClass)
jsIrDeclarationBuilder.buildFunction(
obj.name.asString() + "_getInstance",
returnType = obj.defaultType,
parent = obj.parent
parent = obj.parent,
origin = JsLoweredDeclarationOrigin.OBJECT_GET_INSTANCE_FUNCTION
)
}
@@ -1,9 +1,5 @@
// EXPECTED_REACHABLE_NODES: 1290
// TODO: Support JsExport on object declarations: KT-39117
// IGNORE_BACKEND: JS_IR
// IGNORE_BACKEND: JS_IR_ES6
@JsExport
object A {
@JsName("js_method") fun f() = "method"
-4
View File
@@ -1,9 +1,5 @@
// EXPECTED_REACHABLE_NODES: 1291
// TODO: Support JsExport on object declarations: KT-39117
// IGNORE_BACKEND: JS_IR
// IGNORE_BACKEND: JS_IR_ES6
@JsExport
object A {
@JsName("js_f") fun f(x: Int) = "f($x)"
@@ -42,5 +42,12 @@ declare namespace JS_TESTS {
_varCustom: number;
_varCustomWithField: number;
}
const O0: {
};
const O: {
readonly x: number;
foo(): number;
};
function takesO(o: typeof foo.O): number;
}
}
@@ -82,4 +82,15 @@ class A4 {
var _varCustomWithField: Int = 1
get() = field * 10
set(value) { field = value * 10 }
}
}
object O0
object O {
val x = 10
fun foo() = 20
}
fun takesO(o: O): Int =
O.x + O.foo()
@@ -20,6 +20,8 @@ var A3 = JS_TESTS.foo.A3;
var _valCustom = JS_TESTS.foo._valCustom;
var _valCustomWithField = JS_TESTS.foo._valCustomWithField;
var A4 = JS_TESTS.foo.A4;
var O = JS_TESTS.foo.O;
var takesO = JS_TESTS.foo.takesO;
function assert(condition) {
if (!condition) {
throw "Assertion failed";
@@ -75,5 +77,8 @@ function box() {
assert(a4._varCustomWithField === 10);
a4._varCustomWithField = 10;
assert(a4._varCustomWithField === 1000);
assert(O.x === 10);
assert(O.foo() === 20);
assert(takesO(O) === 30);
return "OK";
}
@@ -20,6 +20,8 @@ import A3 = JS_TESTS.foo.A3;
import _valCustom = JS_TESTS.foo._valCustom;
import _valCustomWithField = JS_TESTS.foo._valCustomWithField;
import A4 = JS_TESTS.foo.A4;
import O = JS_TESTS.foo.O;
import takesO = JS_TESTS.foo.takesO;
function assert(condition: boolean) {
if (!condition) {
@@ -86,5 +88,9 @@ function box(): string {
a4._varCustomWithField = 10;
assert(a4._varCustomWithField === 1000);
assert(O.x === 10);
assert(O.foo() === 20);
assert(takesO(O) === 30);
return "OK";
}
@@ -30,5 +30,10 @@ declare namespace JS_TESTS {
class FC extends foo.OC {
constructor();
}
const O1: {
} & foo.OC;
const O2: {
foo(): number;
} & foo.OC;
}
}
@@ -40,4 +40,10 @@ open class OC(
private fun privateFun(): String = "privateFun"
}
final class FC : OC(true, "FC")
final class FC : OC(true, "FC")
object O1 : OC(true, "O1")
object O2 : OC(true, "O2") {
fun foo(): Int = 10
}
@@ -15,6 +15,8 @@ var __extends = (this && this.__extends) || (function () {
var OC = JS_TESTS.foo.OC;
var AC = JS_TESTS.foo.AC;
var FC = JS_TESTS.foo.FC;
var O1 = JS_TESTS.foo.O1;
var O2 = JS_TESTS.foo.O2;
var Impl = /** @class */ (function (_super) {
__extends(Impl, _super);
function Impl() {
@@ -24,12 +26,12 @@ var Impl = /** @class */ (function (_super) {
};
Object.defineProperty(Impl.prototype, "acAbstractProp", {
get: function () { return "Impl"; },
enumerable: true,
enumerable: false,
configurable: true
});
Object.defineProperty(Impl.prototype, "y", {
get: function () { return true; },
enumerable: true,
enumerable: false,
configurable: true
});
return Impl;
@@ -56,5 +58,13 @@ function box() {
if (fc.acAbstractProp !== "FC")
return "Fail 6";
fc.z(10);
if (O1.y !== true || O2.y !== true)
return "Fail 7";
if (O1.acAbstractProp != "O1")
return "Fail 8";
if (O2.acAbstractProp != "O2")
return "Fail 9";
if (O2.foo() != 10)
return "Fail 10";
return "OK";
}
@@ -1,6 +1,8 @@
import OC = JS_TESTS.foo.OC;
import AC = JS_TESTS.foo.AC;
import FC = JS_TESTS.foo.FC;
import O1 = JS_TESTS.foo.O1;
import O2 = JS_TESTS.foo.O2;
class Impl extends AC {
z(z: number): void {
@@ -29,5 +31,10 @@ function box(): string {
if (fc.acAbstractProp !== "FC") return "Fail 6";
fc.z(10);
if (O1.y !== true || O2.y !== true) return "Fail 7";
if (O1.acAbstractProp != "O1") return "Fail 8";
if (O2.acAbstractProp != "O2") return "Fail 9";
if (O2.foo() != 10) return "Fail 10";
return "OK";
}