diff options
Diffstat (limited to 'plugins/kotlin-as-java')
8 files changed, 849 insertions, 0 deletions
diff --git a/plugins/kotlin-as-java/build.gradle.kts b/plugins/kotlin-as-java/build.gradle.kts new file mode 100644 index 00000000..eda6114f --- /dev/null +++ b/plugins/kotlin-as-java/build.gradle.kts @@ -0,0 +1,12 @@ +import org.jetbrains.registerDokkaArtifactPublication + +dependencies { + implementation(project(":plugins:base")) + testImplementation(project(":plugins:base")) + testImplementation(project(":plugins:base:test-utils")) + testImplementation(project(":test-tools")) +} + +registerDokkaArtifactPublication("kotlinAsJavaPlugin") { + artifactId = "kotlin-as-java-plugin" +} diff --git a/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPlugin.kt b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPlugin.kt new file mode 100644 index 00000000..5f06852e --- /dev/null +++ b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPlugin.kt @@ -0,0 +1,19 @@ +package org.jetbrains.dokka.kotlinAsJava + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.kotlinAsJava.signatures.JavaSignatureProvider +import org.jetbrains.dokka.kotlinAsJava.transformers.KotlinAsJavaDocumentableTransformer +import org.jetbrains.dokka.plugability.DokkaPlugin + +class KotlinAsJavaPlugin : DokkaPlugin() { + val kotlinAsJavaDocumentableToPageTranslator by extending { + CoreExtensions.documentableTransformer with KotlinAsJavaDocumentableTransformer() + } + val javaSignatureProvider by extending { + val dokkaBasePlugin = plugin<DokkaBase>() + dokkaBasePlugin.signatureProvider providing { ctx -> + JavaSignatureProvider(ctx.single(dokkaBasePlugin.commentsToContentConverter), ctx.logger) + } override dokkaBasePlugin.kotlinSignatureProvider + } +}
\ No newline at end of file diff --git a/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt new file mode 100644 index 00000000..e66266a2 --- /dev/null +++ b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt @@ -0,0 +1,298 @@ +package org.jetbrains.dokka.kotlinAsJava.converters + +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.DAnnotation +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.Nullable +import org.jetbrains.dokka.model.TypeConstructor +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.jvm.JvmPrimitiveType +import java.lang.IllegalStateException + +private fun <T : WithExpectActual> List<T>.groupedByLocation() = + map { it.sources to it } + .groupBy({ (location, _) -> + location.let { + it.entries.first().value.path.split("/").last().split(".").first() + "Kt" + } // TODO: first() does not look reasonable + }) { it.second } + +internal fun DPackage.asJava(): DPackage { + @Suppress("UNCHECKED_CAST") + val syntheticClasses = ((properties + functions) as List<WithExpectActual>) + .groupedByLocation() + .map { (syntheticClassName, nodes) -> + DClass( + dri = dri.withClass(syntheticClassName), + name = syntheticClassName, + properties = nodes.filterIsInstance<DProperty>().map { it.asJava() }, + constructors = emptyList(), + functions = ( + nodes.filterIsInstance<DProperty>() + .flatMap { it.javaAccessors() } + + nodes.filterIsInstance<DFunction>() + .map { it.asJava(syntheticClassName) }), // TODO: methods are static and receiver is a param + classlikes = emptyList(), + sources = emptyMap(), + expectPresentInSet = null, + visibility = sourceSets.map { + it to JavaVisibility.Public + }.toMap(), + companion = null, + generics = emptyList(), + supertypes = emptyMap(), + documentation = emptyMap(), + modifier = sourceSets.map { it to JavaModifier.Final }.toMap(), + sourceSets = sourceSets, + extra = PropertyContainer.empty() + ) + } + + return copy( + functions = emptyList(), + properties = emptyList(), + classlikes = classlikes.map { it.asJava() } + syntheticClasses + ) +} + +internal fun DProperty.asJava(isTopLevel: Boolean = false, relocateToClass: String? = null) = + copy( + dri = if (relocateToClass.isNullOrBlank()) { + dri + } else { + dri.withClass(relocateToClass) + }, + modifier = javaModifierFromSetter(), + visibility = visibility.mapValues { it.value.propertyVisibilityAsJava() }, + type = type.asJava(), // TODO: check + setter = null, + getter = null, // Removing getters and setters as they will be available as functions + extra = if (isTopLevel) extra + + extra.mergeAdditionalModifiers( + sourceSets.map { + it to setOf(ExtraModifiers.JavaOnlyModifiers.Static) + }.toMap() + ) + else extra + ) + +internal fun DProperty.javaModifierFromSetter() = + modifier.mapValues { + when { + it.value is JavaModifier -> it.value + setter == null -> JavaModifier.Final + else -> JavaModifier.Empty + } + } + +internal fun DProperty.javaAccessors(isTopLevel: Boolean = false, relocateToClass: String? = null): List<DFunction> = + listOfNotNull( + getter?.copy( + dri = if (relocateToClass.isNullOrBlank()) { + dri + } else { + dri.withClass(relocateToClass) + }, + name = "get" + name.capitalize(), + modifier = javaModifierFromSetter(), + visibility = visibility.mapValues { JavaVisibility.Public }, + type = type.asJava(), // TODO: check + extra = if (isTopLevel) getter!!.extra + + getter!!.extra.mergeAdditionalModifiers( + sourceSets.map { + it to setOf(ExtraModifiers.JavaOnlyModifiers.Static) + }.toMap() + ) + else getter!!.extra + ), + setter?.copy( + dri = if (relocateToClass.isNullOrBlank()) { + dri + } else { + dri.withClass(relocateToClass) + }, + name = "set" + name.capitalize(), + modifier = javaModifierFromSetter(), + visibility = visibility.mapValues { JavaVisibility.Public }, + type = type.asJava(), // TODO: check + extra = if (isTopLevel) setter!!.extra + setter!!.extra.mergeAdditionalModifiers( + sourceSets.map { + it to setOf(ExtraModifiers.JavaOnlyModifiers.Static) + }.toMap() + ) + else setter!!.extra + ) + ) + + +internal fun DFunction.asJava(containingClassName: String): DFunction { + val newName = when { + isConstructor -> containingClassName + else -> name + } + return copy( +// dri = dri.copy(callable = dri.callable?.asJava()), + name = newName, + type = type.asJava(), + modifier = if (modifier.all { (_, v) -> v is KotlinModifier.Final } && isConstructor) + sourceSets.map { it to JavaModifier.Empty }.toMap() + else sourceSets.map { it to modifier.values.first() }.toMap(), + parameters = listOfNotNull(receiver?.asJava()) + parameters.map { it.asJava() }, + receiver = null + ) // TODO static if toplevel +} + +internal fun DClasslike.asJava(): DClasslike = when (this) { + is DClass -> asJava() + is DEnum -> asJava() + is DAnnotation -> asJava() + is DObject -> asJava() + is DInterface -> asJava() + else -> throw IllegalArgumentException("$this shouldn't be here") +} + +internal fun DClass.asJava(): DClass = copy( + constructors = constructors.map { it.asJava(name) }, + functions = (functions + properties.map { it.getter } + properties.map { it.setter }).filterNotNull().map { + it.asJava(name) + }, + properties = properties.map { it.asJava() }, + classlikes = classlikes.map { it.asJava() }, + generics = generics.map { it.asJava() }, + supertypes = supertypes.mapValues { it.value.map { it.asJava() } }, + modifier = if (modifier.all { (_, v) -> v is KotlinModifier.Empty }) sourceSets.map { it to JavaModifier.Final } + .toMap() + else sourceSets.map { it to modifier.values.first() }.toMap() +) + +private fun DTypeParameter.asJava(): DTypeParameter = copy( + dri = dri.possiblyAsJava(), + bounds = bounds.map { it.asJava() } +) + +private fun Bound.asJava(): Bound = when (this) { + is TypeConstructor -> copy( + dri = dri.possiblyAsJava() + ) + is Nullable -> copy( + inner = inner.asJava() + ) + else -> this +} + +internal fun DEnum.asJava(): DEnum = copy( + constructors = constructors.map { it.asJava(name) }, + functions = (functions + properties.map { it.getter } + properties.map { it.setter }).filterNotNull().map { + it.asJava(name) + }, + properties = properties.map { it.asJava() }, + classlikes = classlikes.map { it.asJava() }, + supertypes = supertypes.mapValues { it.value.map { it.asJava() } } +// , entries = entries.map { it.asJava() } +) + +internal fun DObject.asJava(): DObject = copy( + functions = (functions + properties.map { it.getter } + properties.map { it.setter }) + .filterNotNull() + .map { it.asJava(name.orEmpty()) }, + properties = properties.map { it.asJava() } + + DProperty( + name = "INSTANCE", + modifier = sourceSets.map { it to JavaModifier.Final }.toMap(), + dri = dri.copy(callable = Callable("INSTANCE", null, emptyList())), + documentation = emptyMap(), + sources = emptyMap(), + visibility = sourceSets.map { + it to JavaVisibility.Public + }.toMap(), + type = TypeConstructor(dri, emptyList()), + setter = null, + getter = null, + sourceSets = sourceSets, + receiver = null, + generics = emptyList(), + expectPresentInSet = expectPresentInSet, + extra = PropertyContainer.withAll(sourceSets.map { + mapOf(it to setOf(ExtraModifiers.JavaOnlyModifiers.Static)).toAdditionalModifiers() + }) + ), + classlikes = classlikes.map { it.asJava() }, + supertypes = supertypes.mapValues { it.value.map { it.asJava() } } +) + +internal fun DInterface.asJava(): DInterface = copy( + functions = (functions + properties.map { it.getter } + properties.map { it.setter }) + .filterNotNull() + .map { it.asJava(name) }, + properties = emptyList(), + classlikes = classlikes.map { it.asJava() }, // TODO: public static final class DefaultImpls with impls for methods + generics = generics.map { it.asJava() }, + supertypes = supertypes.mapValues { it.value.map { it.asJava() } } +) + +internal fun DAnnotation.asJava(): DAnnotation = copy( + properties = properties.map { it.asJava() }, + constructors = emptyList(), + classlikes = classlikes.map { it.asJava() } +) // TODO investigate if annotation class can have methods and properties not from constructor + +internal fun DParameter.asJava(): DParameter = copy( + type = type.asJava(), + name = if (name.isNullOrBlank()) "\$self" else name +) + +internal fun Visibility.propertyVisibilityAsJava(): Visibility = + if(this is JavaVisibility) this + else JavaVisibility.Private + +internal fun String.getAsPrimitive(): JvmPrimitiveType? = org.jetbrains.kotlin.builtins.PrimitiveType.values() + .find { it.typeFqName.asString() == this } + ?.let { JvmPrimitiveType.get(it) } + +private fun DRI.partialFqName() = packageName?.let { "$it." } + classNames +private fun DRI.possiblyAsJava() = this.partialFqName().mapToJava()?.toDRI(this) ?: this + +private fun String.mapToJava(): ClassId? = + JavaToKotlinClassMap.mapKotlinToJava(FqName(this).toUnsafe()) + +internal fun ClassId.toDRI(dri: DRI?): DRI = DRI( + packageName = packageFqName.asString(), + classNames = classNames(), + callable = dri?.callable,//?.asJava(), TODO: check this + extra = null, + target = PointingToDeclaration +) + +internal fun DriWithKind.asJava(): DriWithKind = + DriWithKind( + dri = dri.possiblyAsJava(), + kind = kind.asJava() + ) + +internal fun ClassKind.asJava(): ClassKind { + return when(this){ + is JavaClassKindTypes -> this + KotlinClassKindTypes.CLASS -> JavaClassKindTypes.CLASS + KotlinClassKindTypes.INTERFACE -> JavaClassKindTypes.INTERFACE + KotlinClassKindTypes.ENUM_CLASS -> JavaClassKindTypes.ENUM_CLASS + KotlinClassKindTypes.ENUM_ENTRY -> JavaClassKindTypes.ENUM_ENTRY + KotlinClassKindTypes.ANNOTATION_CLASS -> JavaClassKindTypes.ANNOTATION_CLASS + KotlinClassKindTypes.OBJECT -> JavaClassKindTypes.CLASS + else -> throw IllegalStateException("Non exchaustive match while trying to convert $this to Java") + } +} + +private fun PropertyContainer<out Documentable>.mergeAdditionalModifiers(second: SourceSetDependent<Set<ExtraModifiers>>) = + this[AdditionalModifiers]?.squash(AdditionalModifiers(second)) ?: AdditionalModifiers(second) + +private fun AdditionalModifiers.squash(second: AdditionalModifiers) = + AdditionalModifiers(content + second.content) + +internal fun ClassId.classNames(): String = + shortClassName.identifier + (outerClassId?.classNames()?.let { ".$it" } ?: "")
\ No newline at end of file diff --git a/plugins/kotlin-as-java/src/main/kotlin/signatures/JavaSignatureProvider.kt b/plugins/kotlin-as-java/src/main/kotlin/signatures/JavaSignatureProvider.kt new file mode 100644 index 00000000..158dda22 --- /dev/null +++ b/plugins/kotlin-as-java/src/main/kotlin/signatures/JavaSignatureProvider.kt @@ -0,0 +1,175 @@ +package org.jetbrains.dokka.kotlinAsJava.signatures + +import org.jetbrains.dokka.base.signatures.JvmSignatureUtils +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.sureClassNames +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.ContentKind +import org.jetbrains.dokka.pages.ContentNode +import org.jetbrains.dokka.pages.TextStyle +import org.jetbrains.dokka.utilities.DokkaLogger +import kotlin.text.Typography.nbsp + +class JavaSignatureProvider(ctcc: CommentsToContentConverter, logger: DokkaLogger) : SignatureProvider, + JvmSignatureUtils by JavaSignatureUtils { + private val contentBuilder = PageContentBuilder(ctcc, this, logger) + + private val ignoredVisibilities = setOf(JavaVisibility.Default) + + private val ignoredModifiers = + setOf(KotlinModifier.Open, JavaModifier.Empty, KotlinModifier.Empty, KotlinModifier.Sealed) + + override fun signature(documentable: Documentable): List<ContentNode> = when (documentable) { + is DFunction -> signature(documentable) + is DProperty -> signature(documentable) + is DClasslike -> signature(documentable) + is DEnumEntry -> signature(documentable) + is DTypeParameter -> signature(documentable) + else -> throw NotImplementedError( + "Cannot generate signature for ${documentable::class.qualifiedName} ${documentable.name}" + ) + } + + private fun signature(e: DEnumEntry) = + e.sourceSets.map { + contentBuilder.contentFor( + e, + ContentKind.Symbol, + setOf(TextStyle.Monospace) + e.stylesIfDeprecated(it), + sourceSets = setOf(it) + ) { + link(e.name, e.dri) + } + } + + private fun signature(c: DClasslike) = + c.sourceSets.map { + contentBuilder.contentFor( + c, + ContentKind.Symbol, + setOf(TextStyle.Monospace) + ((c as? WithExtraProperties<out Documentable>)?.stylesIfDeprecated(it) + ?: emptySet()), + sourceSets = setOf(it) + ) { + text(c.visibility[it]?.takeIf { it !in ignoredVisibilities }?.name?.plus(" ") ?: "") + + if (c is DClass) { + text(c.modifier[it]?.takeIf { it !in ignoredModifiers }?.name?.plus(" ") ?: "") + text(c.modifiers()[it]?.toSignatureString() ?: "") + } + + when (c) { + is DClass -> text("class ") + is DInterface -> text("interface ") + is DEnum -> text("enum ") + is DObject -> text("class ") + is DAnnotation -> text("@interface ") + } + link(c.name!!, c.dri) + if (c is WithGenerics) { + list(c.generics, prefix = "<", suffix = ">") { + +buildSignature(it) + } + } + if (c is WithSupertypes) { + c.supertypes.map { (p, dris) -> + val (classes, interfaces) = dris.partition { it.kind == JavaClassKindTypes.CLASS } + list(classes, prefix = " extends ", sourceSets = setOf(p)) { + link(it.dri.sureClassNames, it.dri, sourceSets = setOf(p)) + } + list(interfaces, prefix = " implements ", sourceSets = setOf(p)){ + link(it.dri.sureClassNames, it.dri, sourceSets = setOf(p)) + } + } + } + } + } + + private fun signature(p: DProperty) = + p.sourceSets.map { + contentBuilder.contentFor( + p, + ContentKind.Symbol, + setOf(TextStyle.Monospace, TextStyle.Block) + p.stylesIfDeprecated(it), + sourceSets = setOf(it) + ) { + annotationsBlock(p) + text(p.visibility[it]?.takeIf { it !in ignoredVisibilities }?.name?.plus(" ") ?: "") + text(p.modifier[it]?.name + " ") + text(p.modifiers()[it]?.toSignatureString() ?: "") + signatureForProjection(p.type) + text(nbsp.toString()) + link(p.name, p.dri) + } + } + + private fun signature(f: DFunction) = + f.sourceSets.map { + contentBuilder.contentFor( + f, + ContentKind.Symbol, + setOf(TextStyle.Monospace, TextStyle.Block) + f.stylesIfDeprecated(it), + sourceSets = setOf(it) + ) { + annotationsBlock(f) + text(f.modifier[it]?.takeIf { it !in ignoredModifiers }?.name?.plus(" ") ?: "") + text(f.modifiers()[it]?.toSignatureString() ?: "") + val returnType = f.type + signatureForProjection(returnType) + text(nbsp.toString()) + link(f.name, f.dri) + list(f.generics, prefix = "<", suffix = ">") { + +buildSignature(it) + } + text("(") + list(f.parameters) { + annotationsInline(it) + text(it.modifiers()[it]?.toSignatureString() ?: "") + signatureForProjection(it.type) + text(nbsp.toString()) + link(it.name!!, it.dri) + } + text(")") + } + } + + private fun signature(t: DTypeParameter) = + t.sourceSets.map { + contentBuilder.contentFor(t, styles = t.stylesIfDeprecated(it), sourceSets = setOf(it)) { + text(t.name.substringAfterLast(".")) + list(t.bounds, prefix = " extends ") { + signatureForProjection(it) + } + } + + } + + private fun PageContentBuilder.DocumentableContentBuilder.signatureForProjection(p: Projection): Unit = when (p) { + is OtherParameter -> link(p.name, p.declarationDRI) + + is TypeConstructor -> group(styles = emptySet()) { + link(p.dri.classNames.orEmpty(), p.dri) + list(p.projections, prefix = "<", suffix = ">") { + signatureForProjection(it) + } + } + + is Variance -> group(styles = emptySet()) { + text(p.kind.toString() + " ") // TODO: "super" && "extends" + signatureForProjection(p.inner) + } + + is Star -> text("?") + + is Nullable -> signatureForProjection(p.inner) + + is JavaObject, is Dynamic -> link("Object", DRI("java.lang", "Object")) + is Void -> text("void") + is PrimitiveJavaType -> text(p.name) + is UnresolvedBound -> text(p.name) + } +}
\ No newline at end of file diff --git a/plugins/kotlin-as-java/src/main/kotlin/signatures/JavaSignatureUtils.kt b/plugins/kotlin-as-java/src/main/kotlin/signatures/JavaSignatureUtils.kt new file mode 100644 index 00000000..ecb97617 --- /dev/null +++ b/plugins/kotlin-as-java/src/main/kotlin/signatures/JavaSignatureUtils.kt @@ -0,0 +1,35 @@ +package org.jetbrains.dokka.kotlinAsJava.signatures + +import org.jetbrains.dokka.base.signatures.All +import org.jetbrains.dokka.base.signatures.JvmSignatureUtils +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.WithExtraProperties + +object JavaSignatureUtils : JvmSignatureUtils { + + private val ignoredAnnotations = setOf( + Annotations.Annotation(DRI("kotlin.jvm", "Transient"), emptyMap()), + Annotations.Annotation(DRI("kotlin.jvm", "Volatile"), emptyMap()), + Annotations.Annotation(DRI("kotlin.jvm", "Transitive"), emptyMap()), + Annotations.Annotation(DRI("kotlin.jvm", "Strictfp"), emptyMap()), + Annotations.Annotation(DRI("kotlin.jvm", "JvmStatic"), emptyMap()) + ) + + private val strategy = All + private val listBrackets = Pair('{', '}') + private val classExtension = ".class" + + override fun PageContentBuilder.DocumentableContentBuilder.annotationsBlock(d: Documentable) = + annotationsBlockWithIgnored(d, ignoredAnnotations, strategy, listBrackets, classExtension) + + override fun PageContentBuilder.DocumentableContentBuilder.annotationsInline(d: Documentable) = + annotationsInlineWithIgnored(d, ignoredAnnotations, strategy, listBrackets, classExtension) + + override fun <T : Documentable> WithExtraProperties<T>.modifiers() = + extra[AdditionalModifiers]?.content?.entries?.map { + it.key to it.value.filterIsInstance<ExtraModifiers.JavaOnlyModifiers>().toSet() + }?.toMap() ?: emptyMap() + +}
\ No newline at end of file diff --git a/plugins/kotlin-as-java/src/main/kotlin/transformers/KotlinAsJavaDocumentableTransformer.kt b/plugins/kotlin-as-java/src/main/kotlin/transformers/KotlinAsJavaDocumentableTransformer.kt new file mode 100644 index 00000000..8b07670f --- /dev/null +++ b/plugins/kotlin-as-java/src/main/kotlin/transformers/KotlinAsJavaDocumentableTransformer.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.kotlinAsJava.transformers + +import org.jetbrains.dokka.kotlinAsJava.converters.asJava +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer + +class KotlinAsJavaDocumentableTransformer : DocumentableTransformer { + override fun invoke(original: DModule, context: DokkaContext): DModule = + original.copy(packages = original.packages.map { it.asJava() }) +} diff --git a/plugins/kotlin-as-java/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/plugins/kotlin-as-java/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..8ff3df82 --- /dev/null +++ b/plugins/kotlin-as-java/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.kotlinAsJava.KotlinAsJavaPlugin diff --git a/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt b/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt new file mode 100644 index 00000000..af66a48e --- /dev/null +++ b/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt @@ -0,0 +1,298 @@ +package kotlinAsJavaPlugin + +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest +import org.jetbrains.kotlin.utils.addToStdlib.cast +import org.junit.jupiter.api.Test +import matchers.content.* + +class KotlinAsJavaPluginTest : AbstractCoreTest() { + + @Test + fun topLevelTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt + |package kotlinAsJavaPlugin + | + |object TestObj {} + | + |fun testFL(l: List<String>) = l + |fun testF() {} + |fun testF2(i: Int) = i + |fun testF3(to: TestObj) = to + |fun <T : Char> testF4(t: T) = listOf(t) + |val testV = 1 + """, + configuration, + cleanupOutput = true + ) { + pagesGenerationStage = { root -> + val content = (root.children.single().children.first { it.name == "TestKt" } as ContentPage).content + + val children = content.mainContents.first().cast<ContentGroup>() + .children.filterIsInstance<ContentTable>() + .filter { it.children.isNotEmpty() } + + children.assertCount(2) + } + } + } + + @Test + fun topLevelWithClassTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt + |package kotlinAsJavaPlugin + | + |class Test { + | fun testFC() {} + | val testVC = 1 + |} + | + |fun testF(i: Int) = i + |val testV = 1 + """, + configuration, + cleanupOutput = true + ) { + pagesGenerationStage = { root -> + val contentList = root.children + .flatMap { it.children<ContentPage>() } + .map { it.content } + + val children = contentList.flatMap { content -> + content.mainContents.first().cast<ContentGroup>().children + .filterIsInstance<ContentTable>() + .filter { it.children.isNotEmpty() } + }.filterNot { it.toString().contains("<init>") } + + children.assertCount(4) + } + } + } + + @Test + fun kotlinAndJavaTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt + |package kotlinAsJavaPlugin + | + |fun testF(i: Int) = i + |/src/main/kotlin/kotlinAsJavaPlugin/TestJ.java + |package kotlinAsJavaPlugin + | + |class TestJ { + | int testF(int i) { return i; } + |} + """, + configuration, + cleanupOutput = true + ) { + pagesGenerationStage = { root -> + val classes = root.children.first().children.associateBy { it.name } + classes.values.assertCount(2, "Class count: ") + + classes["TestKt"].let { + it?.children.orEmpty().assertCount(1, "(Kotlin) TestKt members: ") + it!!.children.first() + .let { assert(it.name == "testF") { "(Kotlin) Expected method name: testF, got: ${it.name}" } } + } + + classes["TestJ"].let { + it?.children.orEmpty().assertCount(1, "(Java) TestJ members: ") + it!!.children.first() + .let { assert(it.name == "testF") { "(Java) Expected method name: testF, got: ${it.name}" } } + } + } + } + } + + @Test + fun `public kotlin properties should have a getter with same visibilities`(){ + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt + |package kotlinAsJavaPlugin + | + |class Test { + | public val publicProperty: String = "" + |} + """, + configuration, + cleanupOutput = true + ) { + pagesTransformationStage = { rootPageNode -> + val propertyGetter = rootPageNode.dfs { it is MemberPageNode && it.name == "getPublicProperty" } as? MemberPageNode + assert(propertyGetter != null) + propertyGetter!!.content.assertNode { + group { + header(1) { + +"getPublicProperty" + } + } + divergentGroup { + divergentInstance { + divergent { + group { + +"final" + group { + link { + +" String" + } + } + link { + +"getPublicProperty" + } + +"()" + } + } + } + } + } + } + } + } + + @Test + fun `java properties should keep its modifiers`(){ + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/TestJ.java + |package kotlinAsJavaPlugin + | + |class TestJ { + | public Int publicProperty = 1; + |} + """, + configuration, + cleanupOutput = true + ) { + pagesGenerationStage = { root -> + val testClass = root.dfs { it.name == "TestJ" } as? ClasslikePageNode + assert(testClass != null) + (testClass!!.content as ContentGroup).children.last().assertNode { + group { + header(2){ + +"Properties" + } + table { + group { + link { + +"publicProperty" + } + platformHinted { + group { + +"public Int" + link { + +"publicProperty" + } + } + } + } + } + } + } + } + } + } + + @Test + fun `koltin interfaces and classes should be split to extends and implements`(){ + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt + |package kotlinAsJavaPlugin + | + | open class A { } + | interface B + | class C : A(), B + """, + configuration, + cleanupOutput = true + ) { + pagesGenerationStage = { root -> + val testClass = root.dfs { it.name == "C" } as? ClasslikePageNode + assert(testClass != null) + testClass!!.content.assertNode { + group { + header(expectedLevel = 1) { + +"C" + } + platformHinted { + group { + +"public final class" + link { + +"C" + } + +" extends " + link { + +"A" + } + +" implements " + link { + +"B" + } + } + } + } + skipAllNotMatching() + } + } + } + } + + private fun <T> Collection<T>.assertCount(n: Int, prefix: String = "") = + assert(count() == n) { "${prefix}Expected $n, got ${count()}" } + +} + +private val ContentNode.mainContents: List<ContentNode> + get() = (this as ContentGroup).children + .filterIsInstance<ContentGroup>() + .single { it.dci.kind == ContentKind.Main }.children |