diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index 0443b136af..22295d5639 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -122,6 +122,12 @@ class DokkaBase : DokkaPlugin() { preMergeDocumentableTransformer providing ::ModuleAndPackageDocumentationTransformer } + val propertiesMergerTransformer by extending { + preMergeDocumentableTransformer with PropertiesMergerTransformer() order { + before(documentableVisibilityFilter) + } + } + val actualTypealiasAdder by extending { CoreExtensions.documentableTransformer with ActualTypealiasAdder() } diff --git a/plugins/base/src/main/kotlin/transformers/documentables/PropertiesMergerTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/PropertiesMergerTransformer.kt new file mode 100644 index 0000000000..e225748ca1 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/PropertiesMergerTransformer.kt @@ -0,0 +1,86 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer +import org.jetbrains.kotlin.load.java.JvmAbi +import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName +import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName +import org.jetbrains.kotlin.name.Name + +class PropertiesMergerTransformer : PreMergeDocumentableTransformer { + + override fun invoke(modules: List) = + modules.map { it.copy(packages = it.packages.map { + it.mergeBeansAndField().copy( + classlikes = it.classlikes.map { it.mergeBeansAndField() } + ) + }) } + + private fun T.mergeBeansAndField(): T = when (this) { + is DClass -> { + val (functions, properties) = mergePotentialBeansAndField(this.functions, this.properties) + this.copy(functions = functions, properties = properties) + } + is DEnum -> { + val (functions, properties) = mergePotentialBeansAndField(this.functions, this.properties) + this.copy(functions = functions, properties = properties) + } + is DInterface -> { + val (functions, properties) = mergePotentialBeansAndField(this.functions, this.properties) + this.copy(functions = functions, properties = properties) + } + is DObject -> { + val (functions, properties) = mergePotentialBeansAndField(this.functions, this.properties) + this.copy(functions = functions, properties = properties) + } + is DAnnotation -> { + val (functions, properties) = mergePotentialBeansAndField(this.functions, this.properties) + this.copy(functions = functions, properties = properties) + } + is DPackage -> { + val (functions, properties) = mergePotentialBeansAndField(this.functions, this.properties) + this.copy(functions = functions, properties = properties) + } + else -> this + } as T + + private fun DFunction.getPropertyNameForFunction() = + when { + JvmAbi.isGetterName(name) -> propertyNameByGetMethodName(Name.identifier(name))?.asString() + JvmAbi.isSetterName(name) -> propertyNamesBySetMethodName(Name.identifier(name)).firstOrNull() + ?.asString() + else -> null + } + + private fun mergePotentialBeansAndField( + functions: List, + fields: List + ): Pair, List> { + val fieldNames = fields.associateBy { it.name } + val accessors = mutableMapOf>() + val regularMethods = mutableListOf() + functions.forEach { method -> + val field = method.getPropertyNameForFunction()?.let { name -> fieldNames[name] } + if (field != null) { + accessors.getOrPut(field, ::mutableListOf).add(method) + } else { + regularMethods.add(method) + } + } + return regularMethods.toList() to accessors.map { (dProperty, dFunctions) -> + if (dProperty.visibility.values.all { it is KotlinVisibility.Private }) { + dFunctions.flatMap { it.visibility.values }.toSet().singleOrNull()?.takeIf { + it in listOf(KotlinVisibility.Public, KotlinVisibility.Protected) + }?.let { visibility -> + dProperty.copy( + getter = dFunctions.firstOrNull { it.type == dProperty.type }, + setter = dFunctions.firstOrNull { it.parameters.isNotEmpty() }, + visibility = dProperty.visibility.mapValues { visibility } + ) + } ?: dProperty + } else { + dProperty + } + } + fields.toSet().minus(accessors.keys.toSet()) + } +} \ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt index 6c9b375b6c..a4273cb4df 100644 --- a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -430,7 +430,10 @@ private class DokkaDescriptorVisitor( originalDescriptor: PropertyDescriptor, parent: DRIWithPlatformInfo ): DProperty { - val (dri, inheritedFrom) = originalDescriptor.createDRI() + val (dri, inheritedFrom) = originalDescriptor.createDRI().let { + if (it.second != null) (it.second to it.first) as Pair + else it + } val descriptor = originalDescriptor.getConcreteDescriptor() val isExpect = descriptor.isExpect val isActual = descriptor.isActual @@ -485,7 +488,10 @@ private class DokkaDescriptorVisitor( originalDescriptor: FunctionDescriptor, parent: DRIWithPlatformInfo ): DFunction { - val (dri, inheritedFrom) = originalDescriptor.createDRI() + val (dri, inheritedFrom) = originalDescriptor.createDRI().let { + if (it.second != null) (it.second to it.first) as Pair + else it + } val descriptor = originalDescriptor.getConcreteDescriptor() val isExpect = descriptor.isExpect val isActual = descriptor.isActual diff --git a/plugins/base/src/test/kotlin/model/PropertyTest.kt b/plugins/base/src/test/kotlin/model/PropertyTest.kt index 17f526f3d2..33b0acba03 100644 --- a/plugins/base/src/test/kotlin/model/PropertyTest.kt +++ b/plugins/base/src/test/kotlin/model/PropertyTest.kt @@ -1,10 +1,13 @@ package model +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.* import org.junit.jupiter.api.Test import utils.AbstractModelTest import utils.assertNotNull import utils.name +import kotlin.test.assertEquals class PropertyTest : AbstractModelTest("/src/main/kotlin/property/Test.kt", "property") { @@ -152,12 +155,15 @@ class PropertyTest : AbstractModelTest("/src/main/kotlin/property/Test.kt", "pro ) { with((this / "property").cast()) { with((this / "Bar" / "property").cast()) { - dri.classNames equals "Foo" + dri.classNames equals "Bar" name equals "property" children counts 0 with(getter.assertNotNull("Getter")) { type.name equals "Int" } + extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + classNames equals "Foo" + } } } } diff --git a/plugins/base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt b/plugins/base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt new file mode 100644 index 0000000000..92b523db54 --- /dev/null +++ b/plugins/base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt @@ -0,0 +1,138 @@ +package superFields + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.InheritedMember +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class DescriptorSuperPropertiesTest : BaseAbstractTest() { + + @Test + fun `kotlin inheriting java should append getter`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | public int getA() { return a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + } + } + } + ) { + this.documentablesTransformationStage = { + it.packages.single().classlikes.single { it.name == "B" }.properties.single { it.name == "a" }.run { + Assertions.assertNotNull(this) + Assertions.assertNotNull(this.getter) + Assertions.assertNull(this.setter) + this.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A", callable = Callable("a", params = emptyList())), + this + ) + } + } + } + } + } + + @Test + fun `kotlin inheriting java should append getter and setter`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | public int getA() { return a; } + | public void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + } + } + } + ) { + documentablesMergingStage = { + it.packages.single().classlikes.single { it.name == "B" }.properties.single { it.name == "a" }.run { + Assertions.assertNotNull(this) + Assertions.assertNotNull(this.getter) + Assertions.assertNotNull(this.setter) + this.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A", callable = Callable("a", params = emptyList())), + this + ) + } + } + } + } + } + + @Test + fun `kotlin inheriting java should not append anything since field is public`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | public int a = 1; + | public int getA() { return a; } + | public void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + classpath += jvmStdlibPath!! + } + } + } + ) { + documentablesMergingStage = { + it.packages.single().classlikes.single { it.name == "B" }.properties.single { it.name == "a" }.run { + Assertions.assertNotNull(this) + Assertions.assertNull(this.getter) + Assertions.assertNull(this.setter) + this.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A", callable = Callable("a", params = emptyList())), + this + ) + } + } + } + } + } +} \ 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 index 71d6393ac0..106229eba2 100644 --- a/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt +++ b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt @@ -87,7 +87,7 @@ internal fun DProperty.asJava(isTopLevel: Boolean = false, relocateToClass: Stri visibility = visibility.mapValues { if (isTopLevel && isConst) { JavaVisibility.Public - } else if (jvmField() != null) { + } else if (jvmField() != null || (getter == null && setter == null)) { it.value.asJava() } else { it.value.propertyVisibilityAsJava()