feat(@JsExport for interfaces): add ability to export interfaces in TypeScript.
This commit is contained in:
+8
-4
@@ -17,6 +17,7 @@ import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker
|
||||
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.isEffectivelyExternal
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtensionProperty
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.isInsideInterface
|
||||
import org.jetbrains.kotlin.resolve.inline.isInlineWithReified
|
||||
import org.jetbrains.kotlin.resolve.isInlineClass
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
@@ -109,12 +110,15 @@ object JsExportDeclarationChecker : DeclarationChecker {
|
||||
}
|
||||
|
||||
val wrongDeclaration: String? = when (descriptor.kind) {
|
||||
INTERFACE -> if (descriptor.isExternal) null else "interface"
|
||||
ANNOTATION_CLASS -> "annotation class"
|
||||
CLASS -> if (descriptor.isInlineClass()) {
|
||||
"${if (descriptor.isInline) "inline " else ""}${if (descriptor.isValue) "value " else ""}class"
|
||||
CLASS -> when {
|
||||
descriptor.isInsideInterface -> "nested class inside exported interface"
|
||||
descriptor.isInlineClass() -> "${if (descriptor.isInline) "inline " else ""}${if (descriptor.isValue) "value " else ""}class"
|
||||
else -> null
|
||||
}
|
||||
else -> if (descriptor.isInsideInterface) {
|
||||
"${if (descriptor.isCompanionObject) "companion object" else "nested/inner declaration"} inside exported interface"
|
||||
} else null
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (wrongDeclaration != null) {
|
||||
|
||||
@@ -2070,6 +2070,12 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest {
|
||||
runTest("js/js.translator/testData/box/export/exportEnumClass.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("exportInterface.kt")
|
||||
public void testExportInterface() throws Exception {
|
||||
runTest("js/js.translator/testData/box/export/exportInterface.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("exportNestedClass.kt")
|
||||
public void testExportNestedClass() throws Exception {
|
||||
|
||||
+6
@@ -2460,6 +2460,12 @@ public class IrBoxJsTestGenerated extends AbstractIrBoxJsTest {
|
||||
runTest("js/js.translator/testData/box/export/exportEnumClass.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("exportInterface.kt")
|
||||
public void testExportInterface() throws Exception {
|
||||
runTest("js/js.translator/testData/box/export/exportInterface.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("exportNestedClass.kt")
|
||||
public void testExportNestedClass() throws Exception {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// EXPECTED_REACHABLE_NODES: 1288
|
||||
// IGNORE_BACKEND: JS_IR
|
||||
// IGNORE_BACKEND: JS_IR_ES6
|
||||
@JsExport
|
||||
interface I {
|
||||
val a: Char
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// IGNORE_BACKEND: JS
|
||||
// RUN_PLAIN_BOX_FUNCTION
|
||||
// INFER_MAIN_MODULE
|
||||
|
||||
// MODULE: export_interface
|
||||
// FILE: lib.kt
|
||||
|
||||
@JsExport
|
||||
interface I {
|
||||
val value: Int
|
||||
var variable: Int
|
||||
fun foo(): String
|
||||
}
|
||||
|
||||
open class NotExportedClass(override var value: Int) : I {
|
||||
override var variable: Int = value
|
||||
override open fun foo(): String = "Not Exported"
|
||||
}
|
||||
|
||||
@JsExport
|
||||
class ExportedClass(override val value: Int) : I {
|
||||
override var variable: Int = value
|
||||
override fun foo(): String = "Exported"
|
||||
}
|
||||
|
||||
@JsExport
|
||||
class AnotherOne : NotExportedClass(42) {
|
||||
override fun foo(): String = "Another One Exported"
|
||||
}
|
||||
|
||||
@JsExport
|
||||
fun generateNotExported(value: Int): NotExportedClass {
|
||||
return NotExportedClass(value)
|
||||
}
|
||||
|
||||
@JsExport
|
||||
fun consume(i: I): String {
|
||||
return "Value is ${i.value}, variable is ${i.variable} and result is '${i.foo()}'"
|
||||
}
|
||||
|
||||
// FILE: test.js
|
||||
function box() {
|
||||
const { I, ExportedClass, AnotherOne, generateNotExported, consume } = this["export_interface"]
|
||||
|
||||
if (I !== undefined) return "Fail: module should not export interface in runtime"
|
||||
|
||||
const exported = new ExportedClass(1)
|
||||
const another = new AnotherOne()
|
||||
const notExported = generateNotExported (3)
|
||||
|
||||
if (exported.foo() !== "Exported") return "Fail: foo function was not generated for ExportedClass"
|
||||
if (another.foo() !== "Another One Exported") return "Fail: foo function was not generated for AnotherOne"
|
||||
if (notExported.foo() !== "Not Exported") return "Fail: foo function was not generated for NotExportedClass"
|
||||
|
||||
if (exported.value !== 1) return "Fail: value getter was not generated for ExportedClass"
|
||||
if (another.value !== 42) return "Fail: value getter was not generated for AnotherOne"
|
||||
if (notExported.value !== 3) return "Fail: value getter was not generated for NotExportedClass"
|
||||
|
||||
if (exported.variable !== 1) return "Fail: variable getter was not generated for ExportedClass"
|
||||
if (another.variable !== 42) return "Fail: variable getter was not generated for AnotherOne"
|
||||
if (notExported.variable !== 3) return "Fail: variable getter was not generated for NotExportedClass"
|
||||
|
||||
exported.variable = 101
|
||||
another.variable = 102
|
||||
notExported.variable = 103
|
||||
|
||||
if (exported.variable !== 101) return "Fail: variable setter was not generated for ExportedClass"
|
||||
if (another.variable !== 102) return "Fail: variable setter was not generated for AnotherOne"
|
||||
if (notExported.variable !== 103) return "Fail: variable setter was not generated for NotExportedClass"
|
||||
|
||||
notExported.value = 42
|
||||
if (notExported.value !== 3) return "Fail: value setter was generated for NotExportedClass, but it shouldn't"
|
||||
|
||||
if (consume(exported) !== "Value is 1, variable is 101 and result is 'Exported'") return "Fail: methods or fields of ExportedClass was mangled"
|
||||
if (consume(another) !== "Value is 42, variable is 102 and result is 'Another One Exported'") return "Fail: methods or fields of AnotherOne was mangled"
|
||||
if (consume(notExported) !== "Value is 3, variable is 103 and result is 'Not Exported'") return "Fail: methods or fields of NotExported was mangled"
|
||||
|
||||
return "OK"
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo {
|
||||
class TestInner {
|
||||
constructor(a: string);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
class ClassWithDefaultCtor {
|
||||
constructor();
|
||||
readonly x: string;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo {
|
||||
const _val: number;
|
||||
let _var: number;
|
||||
@@ -123,5 +125,17 @@ declare namespace JS_TESTS {
|
||||
readonly prop: string;
|
||||
}
|
||||
}
|
||||
interface TestInterface {
|
||||
readonly value: string;
|
||||
getOwnerName(): string;
|
||||
readonly __doNotUseIt: __doNotImplementIt;
|
||||
}
|
||||
class TestInterfaceImpl implements foo.TestInterface {
|
||||
constructor(value: string);
|
||||
readonly value: string;
|
||||
getOwnerName(): string;
|
||||
readonly __doNotUseIt: __doNotImplementIt;
|
||||
}
|
||||
function processInterface(test: foo.TestInterface): string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,4 +179,21 @@ enum class TestEnumClass(val constructorParameter: String) {
|
||||
class Nested {
|
||||
val prop: String = "hello2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsExport
|
||||
interface TestInterface {
|
||||
val value: String
|
||||
fun getOwnerName(): String
|
||||
}
|
||||
|
||||
@JsExport
|
||||
class TestInterfaceImpl(override val value: String) : TestInterface {
|
||||
override fun getOwnerName() = "TestInterfaceImpl"
|
||||
}
|
||||
|
||||
@JsExport
|
||||
fun processInterface(test: TestInterface): String {
|
||||
return "Owner ${test.getOwnerName()} has value '${test.value}'"
|
||||
}
|
||||
+5
@@ -27,6 +27,8 @@ var TestSealed = JS_TESTS.foo.TestSealed;
|
||||
var TestAbstract = JS_TESTS.foo.TestAbstract;
|
||||
var TestDataClass = JS_TESTS.foo.TestDataClass;
|
||||
var TestEnumClass = JS_TESTS.foo.TestEnumClass;
|
||||
var TestInterfaceImpl = JS_TESTS.foo.TestInterfaceImpl;
|
||||
var processInterface = JS_TESTS.foo.processInterface;
|
||||
function assert(condition) {
|
||||
if (!condition) {
|
||||
throw "Assertion failed";
|
||||
@@ -112,5 +114,8 @@ function box() {
|
||||
assert(TestEnumClass.A.ordinal === 0);
|
||||
assert(TestEnumClass.B.ordinal === 1);
|
||||
assert(new TestEnumClass.Nested().prop == "hello2");
|
||||
assert(processInterface(new TestInterfaceImpl("bar")) == "Owner TestInterfaceImpl has value 'bar'");
|
||||
// @ts-expect-error "Just test that this code will throw compilation error for a user"
|
||||
assert(processInterface({ value: "bar", getOwnerName: function () { return "RandomObject"; } }) == "Owner RandomObject has value 'bar'");
|
||||
return "OK";
|
||||
}
|
||||
|
||||
+7
@@ -27,6 +27,8 @@ import TestSealed = JS_TESTS.foo.TestSealed;
|
||||
import TestAbstract = JS_TESTS.foo.TestAbstract;
|
||||
import TestDataClass = JS_TESTS.foo.TestDataClass;
|
||||
import TestEnumClass = JS_TESTS.foo.TestEnumClass;
|
||||
import TestInterfaceImpl = JS_TESTS.foo.TestInterfaceImpl;
|
||||
import processInterface = JS_TESTS.foo.processInterface;
|
||||
|
||||
function assert(condition: boolean) {
|
||||
if (!condition) {
|
||||
@@ -133,5 +135,10 @@ function box(): string {
|
||||
|
||||
assert(new TestEnumClass.Nested().prop == "hello2")
|
||||
|
||||
assert(processInterface(new TestInterfaceImpl("bar")) == "Owner TestInterfaceImpl has value 'bar'")
|
||||
|
||||
// @ts-expect-error "Just test that this code will throw compilation error for a user"
|
||||
assert(processInterface({ value: "bar", getOwnerName: () => "RandomObject" }) == "Owner RandomObject has value 'bar'")
|
||||
|
||||
return "OK";
|
||||
}
|
||||
+2
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo {
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo {
|
||||
interface ExportedInterface {
|
||||
readonly __doNotUseIt: __doNotImplementIt;
|
||||
}
|
||||
function producer(value: number): any/* foo.NonExportedType */;
|
||||
function consumer(value: any/* foo.NonExportedType */): number;
|
||||
class A {
|
||||
@@ -14,5 +19,16 @@ declare namespace JS_TESTS {
|
||||
class C /* implements foo.NonExportedInterface */ {
|
||||
constructor();
|
||||
}
|
||||
class D implements foo.ExportedInterface/*, foo.NonExportedInterface */ {
|
||||
constructor();
|
||||
readonly __doNotUseIt: __doNotImplementIt;
|
||||
}
|
||||
class E /* extends foo.NonExportedType */ implements foo.ExportedInterface {
|
||||
constructor();
|
||||
readonly __doNotUseIt: __doNotImplementIt;
|
||||
}
|
||||
class F extends foo.A /* implements foo.NonExportedInterface */ {
|
||||
constructor();
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -11,6 +11,9 @@ package foo
|
||||
interface NonExportedInterface
|
||||
open class NonExportedType(val value: Int)
|
||||
|
||||
@JsExport
|
||||
interface ExportedInterface
|
||||
|
||||
@JsExport
|
||||
fun producer(value: Int): NonExportedType {
|
||||
return NonExportedType(value)
|
||||
@@ -22,7 +25,7 @@ fun consumer(value: NonExportedType): Int {
|
||||
}
|
||||
|
||||
@JsExport
|
||||
class A(var value: NonExportedType) {
|
||||
open class A(var value: NonExportedType) {
|
||||
fun <T: NonExportedType> increment(t: T): NonExportedType {
|
||||
return NonExportedType(value = t.value + 1)
|
||||
}
|
||||
@@ -32,4 +35,13 @@ class A(var value: NonExportedType) {
|
||||
class B(v: Int) : NonExportedType(v)
|
||||
|
||||
@JsExport
|
||||
class C : NonExportedInterface
|
||||
class C : NonExportedInterface
|
||||
|
||||
@JsExport
|
||||
class D : NonExportedInterface, ExportedInterface
|
||||
|
||||
@JsExport
|
||||
class E : NonExportedType(42), ExportedInterface
|
||||
|
||||
@JsExport
|
||||
class F : A(NonExportedType(42)), NonExportedInterface
|
||||
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo {
|
||||
interface I<T, S, U> {
|
||||
x: T;
|
||||
|
||||
+2
-2
@@ -26,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;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
type Nullable<T> = T | null | undefined
|
||||
declare const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
export namespace foo {
|
||||
const prop: number;
|
||||
class C {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo {
|
||||
const prop: number;
|
||||
class C {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
type Nullable<T> = T | null | undefined
|
||||
declare const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
export namespace foo {
|
||||
const prop: number;
|
||||
class C {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo.bar.baz {
|
||||
class C1 {
|
||||
constructor(value: string);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo {
|
||||
const _any: any;
|
||||
const _throwable: Error;
|
||||
|
||||
+2
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
namespace foo {
|
||||
interface ExportedInternalInterface {
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
declare namespace JS_TESTS {
|
||||
type Nullable<T> = T | null | undefined
|
||||
const __doNotImplementIt: unique symbol
|
||||
type __doNotImplementIt = typeof __doNotImplementIt
|
||||
interface publicInterface {
|
||||
}
|
||||
const publicVal: number;
|
||||
|
||||
Reference in New Issue
Block a user