From 47682aee8b32f9e01940e962978cc2c6c7f38e86 Mon Sep 17 00:00:00 2001
From: Valentin Rocher <valentin.rocher@webedia-group.com>
Date: Thu, 4 Feb 2021 08:20:18 +0100
Subject: JvmOverloads (#1712)

* add JvmOverloads support and static modifier for top-level functions

* apply requested changed

* revert auto-imports
---
 .../kotlin/converters/KotlinToJavaConverter.kt     | 82 +++++++++++++++++-----
 .../kotlin-as-java/src/main/kotlin/jvmOverloads.kt | 14 ++++
 .../src/test/kotlin/JvmOverloadsTest.kt            | 56 +++++++++++++++
 .../src/test/kotlin/KotlinAsJavaPluginTest.kt      | 33 +++++++++
 4 files changed, 168 insertions(+), 17 deletions(-)
 create mode 100644 plugins/kotlin-as-java/src/main/kotlin/jvmOverloads.kt
 create mode 100644 plugins/kotlin-as-java/src/test/kotlin/JvmOverloadsTest.kt

(limited to 'plugins/kotlin-as-java')

diff --git a/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt
index 6ccc9ecc..d45d39d9 100644
--- a/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt
+++ b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt
@@ -1,5 +1,6 @@
 package org.jetbrains.dokka.kotlinAsJava.converters
 
+import org.jetbrains.dokka.kotlinAsJava.hasJvmOverloads
 import org.jetbrains.dokka.kotlinAsJava.jvmField
 import org.jetbrains.dokka.kotlinAsJava.transformers.JvmNameProvider
 import org.jetbrains.dokka.kotlinAsJava.transformers.withCallableName
@@ -43,7 +44,7 @@ internal fun DPackage.asJava(): DPackage {
                                 .filterNot { it.isConst || it.isJvmField }
                                 .flatMap { it.javaAccessors(relocateToClass = syntheticClassName.name) } +
                                     nodes.filterIsInstance<DFunction>()
-                                        .map { it.asJava(syntheticClassName.name) }), // TODO: methods are static and receiver is a param
+                                        .flatMap { it.asJava(syntheticClassName.name, true) }), // TODO: methods are static and receiver is a param
                     classlikes = emptyList(),
                     sources = emptyMap(),
                     expectPresentInSet = null,
@@ -169,12 +170,12 @@ internal fun DProperty.javaAccessors(isTopLevel: Boolean = false, relocateToClas
         }
     )
 
-
-internal fun DFunction.asJava(containingClassName: String): DFunction {
-    val newName = when {
-        isConstructor -> containingClassName
-        else -> name
-    }
+private fun DFunction.asJava(
+    containingClassName: String,
+    newName: String,
+    parameters: List<DParameter>,
+    isTopLevel: Boolean = false
+): DFunction {
     return copy(
         dri = dri.copy(classNames = containingClassName, callable = dri.callable?.copy(name = newName)),
         name = newName,
@@ -184,8 +185,54 @@ internal fun DFunction.asJava(containingClassName: String): DFunction {
         else sourceSets.map { it to modifier.values.first() }.toMap(),
         parameters = listOfNotNull(receiver?.asJava()) + parameters.map { it.asJava() },
         visibility = visibility.map { (sourceSet, visibility) -> Pair(sourceSet, visibility.asJava()) }.toMap(),
-        receiver = null
-    ) // TODO static if toplevel
+        receiver = null,
+        extra = if (isTopLevel) {
+            extra + extra.mergeAdditionalModifiers(
+                sourceSets.map {
+                    it to setOf(ExtraModifiers.JavaOnlyModifiers.Static)
+                }.toMap()
+            )
+        } else {
+            extra
+        }
+    )
+}
+
+private fun DFunction.withJvmOverloads(
+    containingClassName: String,
+    newName: String,
+    isTopLevel: Boolean = false
+): List<DFunction>? {
+    val (paramsWithDefaults, paramsWithoutDefaults) = parameters
+        .withIndex()
+        .partition { (_, p) -> p.extra[DefaultValue] != null }
+    return paramsWithDefaults
+        .runningFold(paramsWithoutDefaults) { acc, param -> (acc + param) }
+        .map { params ->
+            asJava(
+                containingClassName,
+                newName,
+                params
+                    .sortedBy(IndexedValue<DParameter>::index)
+                    .map { it.value },
+                isTopLevel
+            )
+        }
+        .reversed()
+        .takeIf { it.isNotEmpty() }
+}
+
+internal fun DFunction.asJava(containingClassName: String, isTopLevel: Boolean = false): List<DFunction> {
+    val newName = when {
+        isConstructor -> containingClassName
+        else -> name
+    }
+    val baseFunction = asJava(containingClassName, newName, parameters, isTopLevel)
+    return if (hasJvmOverloads()) {
+        withJvmOverloads(containingClassName, newName, isTopLevel) ?: listOf(baseFunction)
+    } else {
+        listOf(baseFunction)
+    }
 }
 
 internal fun DClasslike.asJava(): DClasslike = when (this) {
@@ -198,7 +245,7 @@ internal fun DClasslike.asJava(): DClasslike = when (this) {
 }
 
 internal fun DClass.asJava(): DClass = copy(
-    constructors = constructors.map { it.asJava(dri.classNames ?: name) }, // name may not always be valid here, however classNames should always be not null
+    constructors = constructors.flatMap { it.asJava(dri.classNames ?: name) }, // name may not always be valid here, however classNames should always be not null
     functions = functionsInJava(),
     properties = properties.map { it.asJava() },
     classlikes = classlikes.map { it.asJava() },
@@ -211,9 +258,10 @@ internal fun DClass.asJava(): DClass = copy(
 
 internal fun DClass.functionsInJava(): List<DFunction> =
     (properties.filter { it.jvmField() == null }
-        .flatMap { property -> listOfNotNull(property.getter, property.setter) } + functions).map {
-        it.asJava(dri.classNames ?: name)
-    }
+        .flatMap { property -> listOfNotNull(property.getter, property.setter) } + functions)
+        .flatMap {
+            it.asJava(dri.classNames ?: name)
+        }
 
 private fun DTypeParameter.asJava(): DTypeParameter = copy(
     variantTypeParameter = variantTypeParameter.withDri(dri.possiblyAsJava()),
@@ -251,8 +299,8 @@ private fun Bound.asJava(): Bound = when (this) {
 }
 
 internal fun DEnum.asJava(): DEnum = copy(
-    constructors = constructors.map { it.asJava(dri.classNames ?: name) },
-    functions = (functions + properties.map { it.getter } + properties.map { it.setter }).filterNotNull().map {
+    constructors = constructors.flatMap { it.asJava(dri.classNames ?: name) },
+    functions = (functions + properties.map { it.getter } + properties.map { it.setter }).filterNotNull().flatMap {
         it.asJava(dri.classNames ?: name)
     },
     properties = properties.map { it.asJava() },
@@ -264,7 +312,7 @@ internal fun DEnum.asJava(): DEnum = copy(
 internal fun DObject.asJava(): DObject = copy(
     functions = (functions + properties.map { it.getter } + properties.map { it.setter })
         .filterNotNull()
-        .map { it.asJava(dri.classNames ?: name.orEmpty()) },
+        .flatMap { it.asJava(dri.classNames ?: name.orEmpty()) },
     properties = properties.map { it.asJava() } +
             DProperty(
                 name = "INSTANCE",
@@ -294,7 +342,7 @@ internal fun DObject.asJava(): DObject = copy(
 internal fun DInterface.asJava(): DInterface = copy(
     functions = (functions + properties.map { it.getter } + properties.map { it.setter })
         .filterNotNull()
-        .map { it.asJava(dri.classNames ?: name) },
+        .flatMap { it.asJava(dri.classNames ?: name) },
     properties = emptyList(),
     classlikes = classlikes.map { it.asJava() }, // TODO: public static final class DefaultImpls with impls for methods
     generics = generics.map { it.asJava() },
diff --git a/plugins/kotlin-as-java/src/main/kotlin/jvmOverloads.kt b/plugins/kotlin-as-java/src/main/kotlin/jvmOverloads.kt
new file mode 100644
index 00000000..d8e4f67c
--- /dev/null
+++ b/plugins/kotlin-as-java/src/main/kotlin/jvmOverloads.kt
@@ -0,0 +1,14 @@
+package org.jetbrains.dokka.kotlinAsJava
+
+import org.jetbrains.dokka.model.Annotations
+import org.jetbrains.dokka.model.Documentable
+import org.jetbrains.dokka.model.properties.WithExtraProperties
+
+internal fun WithExtraProperties<out Documentable>.hasJvmOverloads(): Boolean {
+    return extra[Annotations]
+        ?.directAnnotations
+        ?.entries
+        ?.any { (_, annotations) ->
+            annotations.any { it.dri.packageName == "kotlin.jvm" && it.dri.classNames == "JvmOverloads" }
+        } == true
+}
\ No newline at end of file
diff --git a/plugins/kotlin-as-java/src/test/kotlin/JvmOverloadsTest.kt b/plugins/kotlin-as-java/src/test/kotlin/JvmOverloadsTest.kt
new file mode 100644
index 00000000..79619215
--- /dev/null
+++ b/plugins/kotlin-as-java/src/test/kotlin/JvmOverloadsTest.kt
@@ -0,0 +1,56 @@
+package kotlinAsJavaPlugin
+
+import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+
+class JvmOverloadsTest : BaseAbstractTest() {
+    private val configuration = dokkaConfiguration {
+        sourceSets {
+            sourceSet {
+                sourceRoots = listOf("src/")
+                classpath += jvmStdlibPath!!
+            }
+        }
+    }
+
+    @Test
+    fun `should generate multiple functions`() {
+        testInline(
+            """
+            |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt
+            |package kotlinAsJavaPlugin
+            |@JvmOverloads
+            |fun sample(a: Int = 0, b: String, c: Int = 0): String = ""
+        """.trimMargin(),
+            configuration,
+        ) {
+            documentablesTransformationStage = { module ->
+                val functions = module.packages.flatMap { it.classlikes }.flatMap { it.functions }
+                assertEquals(3, functions.size)
+                assertEquals(3, functions[0].parameters.size)
+                assertEquals(2, functions[1].parameters.size)
+                assertEquals(1, functions[2].parameters.size)
+            }
+        }
+    }
+
+    @Test
+    fun `should do nothing if there is no default values`() {
+        testInline(
+            """
+            |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt
+            |package kotlinAsJavaPlugin
+            |@JvmOverloads
+            |fun sample(a: Int, b: String, c: Int): String = ""
+        """.trimMargin(),
+            configuration,
+        ) {
+            documentablesTransformationStage = { module ->
+                val functions = module.packages.flatMap { it.classlikes }.flatMap { it.functions }
+                assertEquals(1, functions.size)
+                assertEquals(3, functions[0].parameters.size)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt b/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt
index 8e7b798a..760bfcff 100644
--- a/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt
+++ b/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt
@@ -404,6 +404,39 @@ class KotlinAsJavaPluginTest : BaseAbstractTest() {
             }
         }
     }
+
+    @Test
+    fun `function in top level`() {
+        val writerPlugin = TestOutputWriterPlugin()
+        val configuration = dokkaConfiguration {
+            sourceSets {
+                sourceSet {
+                    sourceRoots = listOf("src/")
+                    externalDocumentationLinks = listOf(
+                        DokkaConfiguration.ExternalDocumentationLink.jdk(8),
+                        stdlibExternalDocumentationLink
+                    )
+                }
+            }
+        }
+        testInline(
+            """
+            |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt
+            |package kotlinAsJavaPlugin
+            |
+            |fun sample(a: Int) = ""
+        """.trimMargin(),
+            configuration,
+            pluginOverrides = listOf(writerPlugin),
+            cleanupOutput = true
+        ) {
+            renderingStage = { _, _ ->
+                writerPlugin.writer.renderedContent("root/kotlinAsJavaPlugin/-test-kt/sample.html").signature().first().match(
+                    "final static ", A("String"), A("sample"), "(", A("Integer"), "a)", Span()
+                )
+            }
+        }
+    }
 }
 
 private val ContentNode.mainContents: List<ContentNode>
-- 
cgit