From af924fd3d113d3f599dbca4374c3e450603039f7 Mon Sep 17 00:00:00 2001 From: Artem Kobzar Date: Tue, 16 Nov 2021 16:59:18 +0000 Subject: [PATCH] feat: add implicit export support for Kotlin/JS --- .gitignore | 1 + .../ir/backend/js/export/ExportModel.kt | 10 +++++- .../backend/js/export/ExportModelGenerator.kt | 3 +- .../js/export/ExportModelToTsDeclarations.kt | 35 ++++++++++++++++--- .../ir/IrJsTypeScriptExportTestGenerated.java | 16 +++++++++ .../implicitExport/declarations.d.ts | 18 ++++++++++ .../implicitExport/declarations.kt | 35 +++++++++++++++++++ .../implicitExport/declarations__main.js | 21 +++++++++++ .../implicitExport/declarations__main.ts | 25 +++++++++++++ .../implicitExport/tsconfig.json | 4 +++ 10 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 js/js.translator/testData/typescript-export/implicitExport/declarations.d.ts create mode 100644 js/js.translator/testData/typescript-export/implicitExport/declarations.kt create mode 100644 js/js.translator/testData/typescript-export/implicitExport/declarations__main.js create mode 100644 js/js.translator/testData/typescript-export/implicitExport/declarations__main.ts create mode 100644 js/js.translator/testData/typescript-export/implicitExport/tsconfig.json diff --git a/.gitignore b/.gitignore index 8af53b3b181..bdb97101144 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ build/ !**/test/**/build *.iml !**/testData/**/*.iml +.idea/remote-targets.xml .idea/libraries/Gradle*.xml .idea/libraries/Maven*.xml .idea/artifacts/PILL_*.xml diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt index de6a20ed346..0b3f2c824eb 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt @@ -120,6 +120,14 @@ sealed class 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 + + fun withImplicitlyExported(implicitlyExportedType: Boolean) = + if (implicitlyExportedType) ImplicitlyExportedType(this) else this } diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt index 6a660fcb4c1..5c44d5d5eac 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt @@ -468,6 +468,7 @@ class ExportModelGenerator( classifier is IrClassSymbol -> { val klass = classifier.owner + val isImplicitlyExported = !klass.isExported(context) val name = if (generateNamespacesForPackages) klass.fqNameWhenAvailable!!.asString() else klass.name.asString() when (klass.kind) { @@ -484,7 +485,7 @@ class ExportModelGenerator( name, type.arguments.map { exportTypeArgument(it) } ) - } + }.withImplicitlyExported(isImplicitlyExported) } else -> error("Unexpected classifier $classifier") diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt index 1227c3d4932..f09442ecaef 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt @@ -114,10 +114,8 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin val keyword = if (isInterface) "interface" else "class" val superInterfacesKeyword = if (isInterface) "extends" else "implements" - val superClassClause = superClass?.let { " extends ${it.toTypeScript(indent)}" } ?: "" - val superInterfacesClause = if (superInterfaces.isNotEmpty()) { - " $superInterfacesKeyword " + superInterfaces.joinToString(", ") { it.toTypeScript(indent) } - } else "" + 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) { @@ -151,12 +149,36 @@ fun ExportedDeclaration.toTypeScript(indent: String, prefix: String = ""): Strin val nestedClasses = nonInnerClasses + innerClasses.map { it.withProtectedConstructors() } 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 "" } } +fun ExportedType.toExtendsClause(indent: String): String { + return when (this) { + is ExportedType.ImplicitlyExportedType -> " /*${type.toExtendsClause(indent)} */" + else -> " extends ${toTypeScript(indent)}" + } +} + +fun List.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 { val name = getJsNameOrKotlinName().identifier 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.NumberLiteralType -> value.toString() + is ExportedType.ImplicitlyExportedType -> { + ExportedType.Primitive.Any.toTypeScript(indent) + "/* ${type.toTypeScript("")} */" + } } \ No newline at end of file diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrJsTypeScriptExportTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrJsTypeScriptExportTestGenerated.java index 5b26082397b..8788be3cfb2 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrJsTypeScriptExportTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrJsTypeScriptExportTestGenerated.java @@ -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 @TestMetadata("js/js.translator/testData/typescript-export/inheritance") @TestDataPath("$PROJECT_ROOT") diff --git a/js/js.translator/testData/typescript-export/implicitExport/declarations.d.ts b/js/js.translator/testData/typescript-export/implicitExport/declarations.d.ts new file mode 100644 index 00000000000..462a886db11 --- /dev/null +++ b/js/js.translator/testData/typescript-export/implicitExport/declarations.d.ts @@ -0,0 +1,18 @@ +declare namespace JS_TESTS { + type Nullable = 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): any/* foo.NonExportedType */; + } + class B /* extends foo.NonExportedType */ { + constructor(v: number); + } + class C /* implements foo.NonExportedInterface */ { + constructor(); + } + } +} \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/implicitExport/declarations.kt b/js/js.translator/testData/typescript-export/implicitExport/declarations.kt new file mode 100644 index 00000000000..0850c4f91dd --- /dev/null +++ b/js/js.translator/testData/typescript-export/implicitExport/declarations.kt @@ -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 increment(t: T): NonExportedType { + return NonExportedType(value = t.value + 1) + } +} + +@JsExport +class B(v: Int) : NonExportedType(v) + +@JsExport +class C : NonExportedInterface \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/implicitExport/declarations__main.js b/js/js.translator/testData/typescript-export/implicitExport/declarations__main.js new file mode 100644 index 00000000000..53318082d8c --- /dev/null +++ b/js/js.translator/testData/typescript-export/implicitExport/declarations__main.js @@ -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"; +} diff --git a/js/js.translator/testData/typescript-export/implicitExport/declarations__main.ts b/js/js.translator/testData/typescript-export/implicitExport/declarations__main.ts new file mode 100644 index 00000000000..2fa8c2e9615 --- /dev/null +++ b/js/js.translator/testData/typescript-export/implicitExport/declarations__main.ts @@ -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"; +} \ No newline at end of file diff --git a/js/js.translator/testData/typescript-export/implicitExport/tsconfig.json b/js/js.translator/testData/typescript-export/implicitExport/tsconfig.json new file mode 100644 index 00000000000..17612c56c9d --- /dev/null +++ b/js/js.translator/testData/typescript-export/implicitExport/tsconfig.json @@ -0,0 +1,4 @@ +{ + "include": [ "./*" ], + "extends": "../common.tsconfig.json" +} \ No newline at end of file