feat: add implicit export support for Kotlin/JS
This commit is contained in:
@@ -26,6 +26,7 @@ build/
|
|||||||
!**/test/**/build
|
!**/test/**/build
|
||||||
*.iml
|
*.iml
|
||||||
!**/testData/**/*.iml
|
!**/testData/**/*.iml
|
||||||
|
.idea/remote-targets.xml
|
||||||
.idea/libraries/Gradle*.xml
|
.idea/libraries/Gradle*.xml
|
||||||
.idea/libraries/Maven*.xml
|
.idea/libraries/Maven*.xml
|
||||||
.idea/artifacts/PILL_*.xml
|
.idea/artifacts/PILL_*.xml
|
||||||
|
|||||||
@@ -120,6 +120,14 @@ sealed class ExportedType {
|
|||||||
|
|
||||||
class IntersectionType(val lhs: ExportedType, val rhs: ExportedType) : ExportedType()
|
class IntersectionType(val lhs: ExportedType, val rhs: ExportedType) : ExportedType()
|
||||||
|
|
||||||
fun withNullability(nullable: Boolean) =
|
class ImplicitlyExportedType(val type: ExportedType) : ExportedType() {
|
||||||
|
override fun withNullability(nullable: Boolean) =
|
||||||
|
ImplicitlyExportedType(type.withNullability(nullable))
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun withNullability(nullable: Boolean) =
|
||||||
if (nullable) Nullable(this) else this
|
if (nullable) Nullable(this) else this
|
||||||
|
|
||||||
|
fun withImplicitlyExported(implicitlyExportedType: Boolean) =
|
||||||
|
if (implicitlyExportedType) ImplicitlyExportedType(this) else this
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -468,6 +468,7 @@ class ExportModelGenerator(
|
|||||||
|
|
||||||
classifier is IrClassSymbol -> {
|
classifier is IrClassSymbol -> {
|
||||||
val klass = classifier.owner
|
val klass = classifier.owner
|
||||||
|
val isImplicitlyExported = !klass.isExported(context)
|
||||||
val name = if (generateNamespacesForPackages) klass.fqNameWhenAvailable!!.asString() else klass.name.asString()
|
val name = if (generateNamespacesForPackages) klass.fqNameWhenAvailable!!.asString() else klass.name.asString()
|
||||||
|
|
||||||
when (klass.kind) {
|
when (klass.kind) {
|
||||||
@@ -484,7 +485,7 @@ class ExportModelGenerator(
|
|||||||
name,
|
name,
|
||||||
type.arguments.map { exportTypeArgument(it) }
|
type.arguments.map { exportTypeArgument(it) }
|
||||||
)
|
)
|
||||||
}
|
}.withImplicitlyExported(isImplicitlyExported)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("Unexpected classifier $classifier")
|
else -> error("Unexpected classifier $classifier")
|
||||||
|
|||||||
+30
-5
@@ -114,10 +114,8 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
|
|||||||
val keyword = if (isInterface) "interface" else "class"
|
val keyword = if (isInterface) "interface" else "class"
|
||||||
val superInterfacesKeyword = if (isInterface) "extends" else "implements"
|
val superInterfacesKeyword = if (isInterface) "extends" else "implements"
|
||||||
|
|
||||||
val superClassClause = superClass?.let { " extends ${it.toTypeScript(indent)}" } ?: ""
|
val superClassClause = superClass?.let { it.toExtendsClause(indent) } ?: ""
|
||||||
val superInterfacesClause = if (superInterfaces.isNotEmpty()) {
|
val superInterfacesClause = superInterfaces.toImplementsClause(superInterfacesKeyword, indent)
|
||||||
" $superInterfacesKeyword " + superInterfaces.joinToString(", ") { it.toTypeScript(indent) }
|
|
||||||
} else ""
|
|
||||||
|
|
||||||
val members = members.map {
|
val members = members.map {
|
||||||
if (!ir.isInner || it !is ExportedFunction || !it.isStatic) {
|
if (!ir.isInner || it !is ExportedFunction || !it.isStatic) {
|
||||||
@@ -151,12 +149,36 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin
|
|||||||
|
|
||||||
val nestedClasses = nonInnerClasses + innerClasses.map { it.withProtectedConstructors() }
|
val nestedClasses = nonInnerClasses + innerClasses.map { it.withProtectedConstructors() }
|
||||||
val klassExport = "$prefix$modifiers$keyword $name$renderedTypeParameters$superClassClause$superInterfacesClause {\n$bodyString}"
|
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 staticsExport =
|
||||||
|
if (nestedClasses.isNotEmpty()) "\n" + ExportedNamespace(name, nestedClasses).toTypeScript(indent, prefix) else ""
|
||||||
|
|
||||||
if (name.isValidES5Identifier()) klassExport + staticsExport 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)}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<ExportedType>.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)
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
exportedInterfaces.isEmpty() && nonExportedInterfaces.isNotEmpty() ->
|
||||||
|
" /* $superInterfacesKeyword $listOfNonExportedInterfaces */"
|
||||||
|
exportedInterfaces.isNotEmpty() -> {
|
||||||
|
val nonExportedInterfacesTsString = if (nonExportedInterfaces.isNotEmpty()) "/*, $listOfNonExportedInterfaces */" else ""
|
||||||
|
" $superInterfacesKeyword " + exportedInterfaces.joinToString(", ") { it.toTypeScript(indent) } + nonExportedInterfacesTsString
|
||||||
|
}
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun IrClass.asNestedClassAccess(): String {
|
fun IrClass.asNestedClassAccess(): String {
|
||||||
val name = getJsNameOrKotlinName().identifier
|
val name = getJsNameOrKotlinName().identifier
|
||||||
if (parent !is IrClass) return name
|
if (parent !is IrClass) return name
|
||||||
@@ -235,4 +257,7 @@ fun ExportedType.toTypeScript(indent: String): String = when (this) {
|
|||||||
}
|
}
|
||||||
is ExportedType.LiteralType.StringLiteralType -> "\"$value\""
|
is ExportedType.LiteralType.StringLiteralType -> "\"$value\""
|
||||||
is ExportedType.LiteralType.NumberLiteralType -> value.toString()
|
is ExportedType.LiteralType.NumberLiteralType -> value.toString()
|
||||||
|
is ExportedType.ImplicitlyExportedType -> {
|
||||||
|
ExportedType.Primitive.Any.toTypeScript(indent) + "/* ${type.toTypeScript("")} */"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Generated
+16
@@ -89,6 +89,22 @@ public class IrJsTypeScriptExportTestGenerated extends AbstractIrJsTypeScriptExp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestMetadata("js/js.translator/testData/typescript-export/implicitExport")
|
||||||
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
|
public class ImplicitExport {
|
||||||
|
@Test
|
||||||
|
public void testAllFilesPresentInImplicitExport() throws Exception {
|
||||||
|
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/typescript-export/implicitExport"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS_IR, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestMetadata("declarations.kt")
|
||||||
|
public void testDeclarations() throws Exception {
|
||||||
|
runTest("js/js.translator/testData/typescript-export/implicitExport/declarations.kt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@TestMetadata("js/js.translator/testData/typescript-export/inheritance")
|
@TestMetadata("js/js.translator/testData/typescript-export/inheritance")
|
||||||
@TestDataPath("$PROJECT_ROOT")
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
declare namespace JS_TESTS {
|
||||||
|
type Nullable<T> = T | null | undefined
|
||||||
|
namespace foo {
|
||||||
|
function producer(value: number): any/* foo.NonExportedType */;
|
||||||
|
function consumer(value: any/* foo.NonExportedType */): number;
|
||||||
|
class A {
|
||||||
|
constructor(value: any/* foo.NonExportedType */);
|
||||||
|
value: any/* foo.NonExportedType */;
|
||||||
|
increment<T>(t: T): any/* foo.NonExportedType */;
|
||||||
|
}
|
||||||
|
class B /* extends foo.NonExportedType */ {
|
||||||
|
constructor(v: number);
|
||||||
|
}
|
||||||
|
class C /* implements foo.NonExportedInterface */ {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// CHECK_TYPESCRIPT_DECLARATIONS
|
||||||
|
// RUN_PLAIN_BOX_FUNCTION
|
||||||
|
// SKIP_MINIFICATION
|
||||||
|
// SKIP_NODE_JS
|
||||||
|
// INFER_MAIN_MODULE
|
||||||
|
// MODULE: JS_TESTS
|
||||||
|
// FILE: declarations.kt
|
||||||
|
|
||||||
|
package foo
|
||||||
|
|
||||||
|
interface NonExportedInterface
|
||||||
|
open class NonExportedType(val value: Int)
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun producer(value: Int): NonExportedType {
|
||||||
|
return NonExportedType(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun consumer(value: NonExportedType): Int {
|
||||||
|
return value.value
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
class A(var value: NonExportedType) {
|
||||||
|
fun <T: NonExportedType> increment(t: T): NonExportedType {
|
||||||
|
return NonExportedType(value = t.value + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
class B(v: Int) : NonExportedType(v)
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
class C : NonExportedInterface
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
"use strict";
|
||||||
|
var producer = JS_TESTS.foo.producer;
|
||||||
|
var consumer = JS_TESTS.foo.consumer;
|
||||||
|
var A = JS_TESTS.foo.A;
|
||||||
|
var B = JS_TESTS.foo.B;
|
||||||
|
function assert(condition) {
|
||||||
|
if (!condition) {
|
||||||
|
throw "Assertion failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function box() {
|
||||||
|
var nonExportedType = producer(42);
|
||||||
|
var a = new A(nonExportedType);
|
||||||
|
var b = new B(43);
|
||||||
|
assert(consumer(nonExportedType) == 42);
|
||||||
|
a.value = producer(24);
|
||||||
|
assert(consumer(b) == 43);
|
||||||
|
assert(consumer(a.value) == 24);
|
||||||
|
assert(consumer(a.increment(nonExportedType)) == 43);
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import producer = JS_TESTS.foo.producer;
|
||||||
|
import consumer = JS_TESTS.foo.consumer;
|
||||||
|
import A = JS_TESTS.foo.A;
|
||||||
|
import B = JS_TESTS.foo.B;
|
||||||
|
|
||||||
|
function assert(condition: boolean) {
|
||||||
|
if (!condition) {
|
||||||
|
throw "Assertion failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function box(): string {
|
||||||
|
const nonExportedType = producer(42)
|
||||||
|
const a = new A(nonExportedType)
|
||||||
|
const b = new B(43)
|
||||||
|
|
||||||
|
assert(consumer(nonExportedType) == 42)
|
||||||
|
|
||||||
|
a.value = producer(24)
|
||||||
|
assert(consumer(b) == 43)
|
||||||
|
assert(consumer(a.value) == 24)
|
||||||
|
assert(consumer(a.increment(nonExportedType)) == 43)
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"include": [ "./*" ],
|
||||||
|
"extends": "../common.tsconfig.json"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user