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
@@ -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 {
@@ -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
}
+79
View File
@@ -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}'"
}
@@ -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";
}
@@ -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";
}
@@ -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();
}
}
}
@@ -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;
@@ -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;
@@ -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;