diff --git a/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinCompanion.kt b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinCompanion.kt new file mode 100644 index 0000000000..dec1591a18 --- /dev/null +++ b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinCompanion.kt @@ -0,0 +1,70 @@ +package org.jetbrains.dokka.kotlinAsJava.converters + +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.PropertyContainer + +private const val DEFAULT_COMPANION_NAME = "Companion" + +internal fun DObject?.staticFunctionsForJava(): List { + if (this == null) return emptyList() + return functions.filter { it.isJvmStatic } +} + +/** + * @return properties that will be visible as static for java. + * See [Static fields](https://kotlinlang.org/docs/java-to-kotlin-interop.html#static-fields) + */ +internal fun DObject?.staticPropertiesForJava(): List { + if (this == null) return emptyList() + return properties.filter { it.isJvmField || it.isConst || it.isLateInit } +} + +internal fun DObject.companionInstancePropertyForJava(): DProperty? { + if (hasNothingToRender()) return null // do not show if companion not rendered + + return DProperty( + name = name ?: DEFAULT_COMPANION_NAME, + modifier = sourceSets.associateWith { JavaModifier.Final }, + dri = dri.copy(callable = Callable(name ?: DEFAULT_COMPANION_NAME, null, emptyList())), + documentation = emptyMap(), + sources = emptyMap(), + visibility = sourceSets.associateWith { + JavaVisibility.Public + }, + type = GenericTypeConstructor(dri, emptyList()), + setter = null, + getter = null, + sourceSets = sourceSets, + receiver = null, + generics = emptyList(), + expectPresentInSet = expectPresentInSet, + isExpectActual = false, + extra = PropertyContainer.withAll(sourceSets.map { + mapOf(it to setOf(ExtraModifiers.JavaOnlyModifiers.Static)).toAdditionalModifiers() + }) + ) +} + +internal fun DObject.companionAsJava(): DObject? { + if (hasNothingToRender()) return null + + return asJava( + excludedProps = staticPropertiesForJava(), + excludedFunctions = staticFunctionsForJava() + ) +} + +/** + * Hide companion object if there isn't members of parents. + * Properties and functions that are moved to outer class are not counted as members. + */ +private fun DObject.hasNothingToRender(): Boolean { + val nonStaticPropsCount = properties.size - staticPropertiesForJava().size + val nonStaticFunctionsCount = functions.size - staticFunctionsForJava().size + val classLikesCount = classlikes.size + val superTypesCount = supertypes.values.firstOrNull()?.size ?: 0 + + return nonStaticFunctionsCount + nonStaticPropsCount + + classLikesCount + superTypesCount == 0 +} 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 4a21aeae97..5f98494e55 100644 --- a/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt +++ b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt @@ -1,8 +1,6 @@ package org.jetbrains.dokka.kotlinAsJava.converters -import org.jetbrains.dokka.kotlinAsJava.hasJvmOverloads -import org.jetbrains.dokka.kotlinAsJava.hasJvmSynthetic -import org.jetbrains.dokka.kotlinAsJava.jvmField +import org.jetbrains.dokka.kotlinAsJava.* import org.jetbrains.dokka.kotlinAsJava.transformers.JvmNameProvider import org.jetbrains.dokka.kotlinAsJava.transformers.withCallableName import org.jetbrains.dokka.links.Callable @@ -17,17 +15,20 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.jvm.JvmPrimitiveType val jvmNameProvider = JvmNameProvider() +internal const val OBJECT_INSTANCE_NAME = "INSTANCE" -private val DProperty.isConst: Boolean - get() = extra[AdditionalModifiers] - ?.content - ?.any { (_, modifiers) -> - ExtraModifiers.KotlinOnlyModifiers.Const in modifiers - } == true +internal val DProperty.isConst: Boolean + get() = hasModifier(ExtraModifiers.KotlinOnlyModifiers.Const) + +internal val DProperty.isLateInit: Boolean + get() = hasModifier(ExtraModifiers.KotlinOnlyModifiers.LateInit) -private val DProperty.isJvmField: Boolean +internal val DProperty.isJvmField: Boolean get() = jvmField() != null +internal val DFunction.isJvmStatic: Boolean + get() = jvmStatic() != null + internal fun DPackage.asJava(): DPackage { val syntheticClasses = (properties.map { jvmNameProvider.nameForSyntheticClass(it) to it } @@ -76,7 +77,11 @@ internal fun DPackage.asJava(): DPackage { ) } -internal fun DProperty.asJava(isTopLevel: Boolean = false, relocateToClass: String? = null) = +internal fun DProperty.asJava( + isTopLevel: Boolean = false, + relocateToClass: String? = null, + isFromObjectOrCompanion: Boolean = false +) = copy( dri = if (relocateToClass.isNullOrBlank()) { dri @@ -85,9 +90,7 @@ internal fun DProperty.asJava(isTopLevel: Boolean = false, relocateToClass: Stri }, modifier = javaModifierFromSetter(), visibility = visibility.mapValues { - if (isTopLevel && isConst) { - JavaVisibility.Public - } else if (jvmField() != null || (getter == null && setter == null)) { + if (isConst || isJvmField || (getter == null && setter == null) || (isFromObjectOrCompanion && isLateInit)) { it.value.asJava() } else { it.value.propertyVisibilityAsJava() @@ -96,12 +99,12 @@ internal fun DProperty.asJava(isTopLevel: Boolean = false, relocateToClass: Stri 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.associateWith { - setOf(ExtraModifiers.JavaOnlyModifiers.Static) - } - ) + extra = if (isTopLevel || isConst || (isFromObjectOrCompanion && isJvmField) || (isFromObjectOrCompanion && isLateInit)) + extra + extra.mergeAdditionalModifiers( + sourceSets.associateWith { + setOf(ExtraModifiers.JavaOnlyModifiers.Static) + } + ) else extra ) @@ -192,7 +195,7 @@ private fun DFunction.asJava( parameters = listOfNotNull(receiver?.asJava()) + parameters.map { it.asJava() }, visibility = visibility.map { (sourceSet, visibility) -> Pair(sourceSet, visibility.asJava()) }.toMap(), receiver = null, - extra = if (isTopLevel) { + extra = if (isTopLevel || isJvmStatic) { extra + extra.mergeAdditionalModifiers( sourceSets.associateWith { setOf(ExtraModifiers.JavaOnlyModifiers.Static) @@ -259,24 +262,51 @@ internal fun DClass.asJava(): DClass = copy( ) }, // name may not always be valid here, however classNames should always be not null functions = functionsInJava(), - properties = properties - .filterNot { it.hasJvmSynthetic() } - .map { it.asJava() }, - classlikes = classlikes.map { it.asJava() }, + properties = propertiesInJava(), + classlikes = classlikesInJava(), generics = generics.map { it.asJava() }, + companion = companion?.companionAsJava(), supertypes = supertypes.mapValues { it.value.map { it.asJava() } }, modifier = if (modifier.all { (_, v) -> v is KotlinModifier.Empty }) sourceSets.associateWith { JavaModifier.Final } else sourceSets.associateWith { modifier.values.first() } ) +/** + * Companion objects requires some custom logic for rendering as Java. + * They are excluded from usual classlikes rendering and added after. + */ +internal fun DClass.classlikesInJava(): List { + val classlikes = classlikes + .filter { it.name != companion?.name } + .map { it.asJava() } + + val companionAsJava = companion?.companionAsJava() + return if (companionAsJava != null) classlikes.plus(companionAsJava) else classlikes +} + + internal fun DClass.functionsInJava(): List = properties - .filter { it.jvmField() == null && !it.hasJvmSynthetic() } + .filter { !it.isJvmField && !it.hasJvmSynthetic() } .flatMap { property -> listOfNotNull(property.getter, property.setter) } .plus(functions) + .plus(companion.staticFunctionsForJava()) .filterNot { it.hasJvmSynthetic() } .flatMap { it.asJava(it.dri.classNames ?: it.name) } +internal fun DClass.propertiesInJava(): List { + val propertiesFromCompanion = companion + .staticPropertiesForJava() + .filterNot { it.hasJvmSynthetic() } + .map { it.asJava(isFromObjectOrCompanion = true) } + val companionInstanceProperty = companion?.companionInstancePropertyForJava() + val ownProperties = properties + .filterNot { it.hasJvmSynthetic() } + .map { it.asJava() } + + return propertiesFromCompanion + ownProperties + listOfNotNull(companionInstanceProperty) +} + private fun DTypeParameter.asJava(): DTypeParameter = copy( variantTypeParameter = variantTypeParameter.withDri(dri.possiblyAsJava()), bounds = bounds.map { it.asJava() } @@ -318,7 +348,7 @@ internal fun DEnum.asJava(): DEnum = copy( functions = functions .plus( properties - .filter { it.jvmField() == null && !it.hasJvmSynthetic() } + .filter { !it.isJvmField && !it.hasJvmSynthetic() } .flatMap { listOf(it.getter, it.setter) } ) .filterNotNull() @@ -332,23 +362,33 @@ internal fun DEnum.asJava(): DEnum = copy( // , entries = entries.map { it.asJava() } ) -internal fun DObject.asJava(): DObject = copy( +/** + * Parameters [excludedProps] and [excludedFunctions] used for rendering companion objects + * where some members (that lifted to outer class) are not rendered + */ +internal fun DObject.asJava( + excludedProps: List = emptyList(), + excludedFunctions: List = emptyList() +): DObject = copy( functions = functions .plus( properties - .filter { it.jvmField() == null && !it.hasJvmSynthetic() } + .filterNot { it in excludedProps } + .filter { !it.isJvmField && !it.isConst && !it.isLateInit && !it.hasJvmSynthetic() } .flatMap { listOf(it.getter, it.setter) } ) .filterNotNull() + .filterNot { it in excludedFunctions } .filterNot { it.hasJvmSynthetic() } .flatMap { it.asJava(dri.classNames ?: name.orEmpty()) }, properties = properties .filterNot { it.hasJvmSynthetic() } - .map { it.asJava() } + + .filterNot { it in excludedProps } + .map { it.asJava(isFromObjectOrCompanion = true) } + DProperty( - name = "INSTANCE", + name = OBJECT_INSTANCE_NAME, modifier = sourceSets.associateWith { JavaModifier.Final }, - dri = dri.copy(callable = Callable("INSTANCE", null, emptyList())), + dri = dri.copy(callable = Callable(OBJECT_INSTANCE_NAME, null, emptyList())), documentation = emptyMap(), sources = emptyMap(), visibility = sourceSets.associateWith { @@ -450,3 +490,8 @@ private fun AdditionalModifiers.squash(second: AdditionalModifiers) = internal fun ClassId.classNames(): String = shortClassName.identifier + (outerClassId?.classNames()?.let { ".$it" } ?: "") + +private fun DProperty.hasModifier(modifier: ExtraModifiers.KotlinOnlyModifiers): Boolean = + extra[AdditionalModifiers] + ?.content + ?.any { (_, modifiers) -> modifier in modifiers } == true \ No newline at end of file diff --git a/plugins/kotlin-as-java/src/main/kotlin/jvmStatic.kt b/plugins/kotlin-as-java/src/main/kotlin/jvmStatic.kt new file mode 100644 index 0000000000..10372ac9df --- /dev/null +++ b/plugins/kotlin-as-java/src/main/kotlin/jvmStatic.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.kotlinAsJava + +import org.jetbrains.dokka.model.Annotations +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.kotlin.util.firstNotNullResult + +internal fun WithExtraProperties.jvmStatic(): Annotations.Annotation? = + extra[Annotations]?.directAnnotations?.entries?.firstNotNullResult { (_, annotations) -> annotations.jvmStaticAnnotation() } + +internal fun List.jvmStaticAnnotation(): Annotations.Annotation? = + firstOrNull { it.dri.packageName == "kotlin.jvm" && it.dri.classNames == "JvmStatic" } \ No newline at end of file diff --git a/plugins/kotlin-as-java/src/test/kotlin/CompanionAsJavaTest.kt b/plugins/kotlin-as-java/src/test/kotlin/CompanionAsJavaTest.kt new file mode 100644 index 0000000000..3b2a8e8943 --- /dev/null +++ b/plugins/kotlin-as-java/src/test/kotlin/CompanionAsJavaTest.kt @@ -0,0 +1,548 @@ +package kotlinAsJavaPlugin + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.* +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +private const val COMPANION_NAME = "C" + +class CompanionAsJavaTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath += jvmStdlibPath!! + } + } + } + + @Test + fun `empty companion object should not be rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME {} + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionNotRendered(parentClass) + } + } + } + + @Test + fun `companion object with only jvmField should not be rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | @JvmField val jvmFieldProp: String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionNotRendered(parentClass) + } + } + } + + @Test + fun `companion property with jvmField should be static`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | @JvmField val jvmFieldProp: String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + val parentClassProperty = parentClass.properties.firstOrNull { it.name == "jvmFieldProp" } + assertNotNull(parentClassProperty, "Parent class should contain the companion jvmField property") + assertIsStatic(parentClassProperty) + } + } + } + + @Test + fun `companion object with only const should not be rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | const val constProp: Int = 0 + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionNotRendered(parentClass) + } + } + } + + @Test + fun `companion property with const should be static`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | const val constProp: Int = 0 + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + val parentClassProperty = parentClass.properties.firstOrNull { it.name == "constProp" } + assertNotNull(parentClassProperty, "Parent class should contain the companion const property") + assertIsStatic(parentClassProperty) + } + } + } + + @Test + fun `companion object with only lateinit not rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | lateinit var lateInitProp: String + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionNotRendered(parentClass) + } + } + } + + @Test + fun `companion property with lateinit should be static`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | lateinit var lateInitProp: String + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + val parentClassProperty = parentClass.properties.firstOrNull { it.name == "lateInitProp" } + assertNotNull(parentClassProperty, "Parent class should contain the companion lateinit property") + assertIsStatic(parentClassProperty) + } + } + } + + @Test + fun `companion object with only jvmStatic fun not rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | @JvmStatic fun staticFun(): String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionNotRendered(parentClass) + } + } + } + + @Test + fun `companion function with JvmStatic should be static`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | @JvmStatic fun staticFun(): String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + val parentClassFunction = parentClass.functions.firstOrNull { it.name == "staticFun" } + assertNotNull(parentClassFunction, "Parent class should contains the companion jvmStatic function") + assertIsStatic(parentClassFunction) + } + } + } + + @Test + fun `companion object with nested classes is rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | @JvmStatic + | fun staticFun1(): String = "" + | + | const val CONST_VAL: Int = 100 + | + | class NestedClass + | object NestedObject + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionRendered(parentClass) + + val classLikes = parentClass.companion?.classlikes + assertNotNull(classLikes) + assertEquals(2, classLikes.size, + "Classlike list should contains nested class and object") + assertTrue(classLikes.any { it.name == "NestedClass" }) + assertTrue(classLikes.any { it.name == "NestedObject" }) + + } + } + } + + @Test + fun `companion object with supertype is rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + | + |class Parent + |interface IParent + |class MyClass { + | companion object $COMPANION_NAME : Parent(), IParent { + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionRendered(parentClass) + } + } + } + + @Test + fun `companion object rendered for own properties`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | @JvmField + | val jvmField: String = "" + | const val contVal: Int = 0 + | lateinit var lateInit: String + | + | val rendered: Int = TODO() + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionRendered(parentClass) + + val properties = parentClass.companion?.properties + + assertNotNull(properties) + assertEquals(2, properties.size) // including INSTANCE + assertTrue(properties.any { it.name == "rendered" }) + assertTrue(properties.none { it.name == "jvmField1" }) + assertTrue(properties.none { it.name == "contVal" }) + assertTrue(properties.none { it.name == "lateInit" }) + } + } + } + + @Test + fun `companion object rendered for own functions`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | @JvmStatic + | fun staticFun(): String = "" + | + | fun renderedFun(): String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertCompanionRendered(parentClass) + + val functions = parentClass.companion?.functions + + assertNotNull(functions) + assertEquals(1, functions.size) + assertTrue(functions.any { it.name == "renderedFun" }) + assertTrue(functions.none { it.name == "staticFun" }) + } + } + } + + @Test + fun `companion const value should be rendered as public by default`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | const val constProp: String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertEquals( + JavaVisibility.Public, + parentClass.properties.firstOrNull()?.visibility?.values?.first() + ) + assertNull(parentClass.findFunction("constProp"), "There is no getter for the cont field") + } + } + } + + @Test + fun `companion const value should preserve Java modifier`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | protected const val constProp: String = "" + | } + |} + """.trimMargin(), + dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath += jvmStdlibPath!! + documentedVisibilities = setOf( + org.jetbrains.dokka.DokkaConfiguration.Visibility.PUBLIC, + org.jetbrains.dokka.DokkaConfiguration.Visibility.PROTECTED + ) + } + } + }, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertEquals( + JavaVisibility.Protected, + parentClass.properties.firstOrNull()?.visibility?.values?.first() + ) + assertNull(parentClass.findFunction("constProp"), "There is no getter for the cont field") + } + } + } + + @Test + fun `companion lateinit value should be rendered as public by default`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | lateinit var lateInitProp: String + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertEquals( + JavaVisibility.Public, + parentClass.properties.firstOrNull()?.visibility?.values?.first() + ) + assertNull(parentClass.findFunction("lateInitProp"), "There is no getter for the cont field") + } + } + } + + @Test + fun `named companion instance property should be rendered if companion rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | var property: String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertNotNull(parentClass.properties.any { it.name == COMPANION_NAME }) + } + } + } + + @Test + fun `default named companion instance property should be rendered if companion rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object { + | var property: String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertTrue(parentClass.properties.any { it.name == "Companion" }) + } + } + } + + @Test + fun `companion instance property should be hidden if companion not rendered`() { + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |class MyClass { + | companion object $COMPANION_NAME { + | const val property: String = "" + | } + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val parentClass = module.findClass("MyClass") + + assertTrue(parentClass.properties.none { it.name == COMPANION_NAME }) + } + } + } +} + +private fun DModule.findClass(name: String) = packages.flatMap { it.classlikes } + .firstOrNull { it.name == name } as DClass + +private fun DClass.findFunction(name: String) = functions.firstOrNull { it.name.contains(name, ignoreCase = true) } + +private fun assertCompanionRendered(parentClass: DClass) { + assertNotNull(parentClass.companion, "Companion should not be null") + assertTrue( + parentClass.classlikes.any { it.name == COMPANION_NAME }, + "Companion should be in classlikes list" + ) +} + +private fun assertCompanionNotRendered(parentClass: DClass) { + assertNull(parentClass.companion, "Companion should be null") + assertTrue( + parentClass.classlikes.none { it.name == COMPANION_NAME }, + "Companion should not be in classlikes list" + ) +} + +private fun assertIsStatic(property: DProperty) { + val extra = property.extra[AdditionalModifiers] + assertNotNull(extra, "extra for property is present") + assertTrue( + extra.content.values.contains(setOf(ExtraModifiers.JavaOnlyModifiers.Static)), + "Property contains extra modifier static" + ) +} + +private fun assertIsStatic(function: DFunction) { + val extra = function.extra[AdditionalModifiers] + assertNotNull(extra, "extra for property is present") + assertTrue( + extra.content.values.contains(setOf(ExtraModifiers.JavaOnlyModifiers.Static)), + "Function contains extra modifier static" + ) +} \ No newline at end of file diff --git a/plugins/kotlin-as-java/src/test/kotlin/JvmFieldTest.kt b/plugins/kotlin-as-java/src/test/kotlin/JvmFieldTest.kt index 99ea017b4d..f0c4453033 100644 --- a/plugins/kotlin-as-java/src/test/kotlin/JvmFieldTest.kt +++ b/plugins/kotlin-as-java/src/test/kotlin/JvmFieldTest.kt @@ -1,11 +1,14 @@ package kotlinAsJavaPlugin import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.AdditionalModifiers +import org.jetbrains.dokka.model.ExtraModifiers import org.jetbrains.dokka.model.JavaVisibility import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class JvmFieldTest : BaseAbstractTest() { val configuration = dokkaConfiguration { @@ -106,4 +109,62 @@ class JvmFieldTest : BaseAbstractTest() { } } } + + @Test + fun `enum jvmfield property should have no getters`(){ + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |enum class MyEnum { + | ITEM; + | + | @JvmField + | val property: String = TODO() + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val classLike = module.packages.flatMap { it.classlikes }.first() + val property = classLike.properties.singleOrNull { it.name == "property" } + assertNotNull(property) + assertEquals( + emptyList(), + classLike.functions + .filter{ it.name.contains("property", ignoreCase = true) } + .map { it.name } + ) + assertNull(property.getter) + assertNull(property.setter) + } + } + } + + + @Test + fun `object jvmfield property should be static`(){ + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/sample.kt + |package kotlinAsJavaPlugin + |object MyObject { + | @JvmField + | val property: String = TODO() + |} + """.trimMargin(), + configuration, + ) { + documentablesTransformationStage = { module -> + val classLike = module.packages.flatMap { it.classlikes }.first() + val property = classLike.properties.singleOrNull { it.name == "property" } + assertNotNull(property) + + val extra = property.extra[AdditionalModifiers] + assertNotNull(extra, "Additional modifiers for property are exist") + assertTrue(extra.content.values.contains(setOf(ExtraModifiers.JavaOnlyModifiers.Static)), + "Extra modifiers contains static") + } + } + } }