Skip to content

Commit

Permalink
Add logic to merge inherited properties in kotlin from java sources.
Browse files Browse the repository at this point in the history
  • Loading branch information
BarkingBad committed Feb 23, 2022
1 parent 581e76c commit ac524d9
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 4 deletions.
6 changes: 6 additions & 0 deletions plugins/base/src/main/kotlin/DokkaBase.kt
Expand Up @@ -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()
}
Expand Down
@@ -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<DModule>) =
modules.map { it.copy(packages = it.packages.map {
it.mergeBeansAndField().copy(
classlikes = it.classlikes.map { it.mergeBeansAndField() }
)
}) }

private fun <T : Documentable> 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<DFunction>,
fields: List<DProperty>
): Pair<List<DFunction>, List<DProperty>> {
val fieldNames = fields.associateBy { it.name }
val accessors = mutableMapOf<DProperty, MutableList<DFunction>>()
val regularMethods = mutableListOf<DFunction>()
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())
}
}
Expand Up @@ -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<DRI, DRI?>
else it
}
val descriptor = originalDescriptor.getConcreteDescriptor()
val isExpect = descriptor.isExpect
val isActual = descriptor.isActual
Expand Down Expand Up @@ -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<DRI, DRI?>
else it
}
val descriptor = originalDescriptor.getConcreteDescriptor()
val isExpect = descriptor.isExpect
val isActual = descriptor.isActual
Expand Down
8 changes: 7 additions & 1 deletion 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") {

Expand Down Expand Up @@ -152,12 +155,15 @@ class PropertyTest : AbstractModelTest("/src/main/kotlin/property/Test.kt", "pro
) {
with((this / "property").cast<DPackage>()) {
with((this / "Bar" / "property").cast<DProperty>()) {
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"
}
}
}
}
Expand Down
@@ -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
)
}
}
}
}
}
}
Expand Up @@ -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()
Expand Down

0 comments on commit ac524d9

Please sign in to comment.