diff --git a/core/src/main/kotlin/model/JvmField.kt b/core/src/main/kotlin/model/JvmField.kt index 841dd16c9a..a2f140cd8c 100644 --- a/core/src/main/kotlin/model/JvmField.kt +++ b/core/src/main/kotlin/model/JvmField.kt @@ -2,6 +2,9 @@ package org.jetbrains.dokka.model import org.jetbrains.dokka.links.DRI -fun DRI.isJvmField(): Boolean = packageName == "kotlin.jvm" && classNames == "JvmField" +const val JVM_FIELD_PACKAGE_NAME = "kotlin.jvm" +const val JVM_FIELD_CLASS_NAMES = "JvmField" + +fun DRI.isJvmField(): Boolean = packageName == JVM_FIELD_PACKAGE_NAME && classNames == JVM_FIELD_CLASS_NAMES fun Annotations.Annotation.isJvmField(): Boolean = dri.isJvmName() diff --git a/plugins/base/src/main/kotlin/transformers/documentables/PropertiesMergerTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/PropertiesMergerTransformer.kt index e225748ca1..9b7e1d6aa4 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/PropertiesMergerTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/PropertiesMergerTransformer.kt @@ -7,42 +7,46 @@ import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName import org.jetbrains.kotlin.name.Name +/** + * This transformer is used to merge the backing fields and accessors (getters and setters) + * obtained from Java sources. This way, we could generate more coherent documentation, + * since the model is now aware of the relationship between accessors and the fields. + * This way if we generate Kotlin output we get rid of spare getters and setters, + * and from Kotlin-as-Java perspective we can collect accessors of each property. + */ 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() } + it.mergeAccessorsAndField().copy( + classlikes = it.classlikes.map { it.mergeAccessorsAndField() } ) }) } - 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 T.mergeAccessorsAndField(): T { + val (functions, properties) = mergePotentialAccessorsAndField(this.functions, this.properties) + return when (this) { + is DClass -> { + this.copy(functions = functions, properties = properties) + } + is DEnum -> { + this.copy(functions = functions, properties = properties) + } + is DInterface -> { + this.copy(functions = functions, properties = properties) + } + is DObject -> { + this.copy(functions = functions, properties = properties) + } + is DAnnotation -> { + this.copy(functions = functions, properties = properties) + } + is DPackage -> { + this.copy(functions = functions, properties = properties) + } + else -> this + } as T + } private fun DFunction.getPropertyNameForFunction() = when { @@ -52,13 +56,16 @@ class PropertiesMergerTransformer : PreMergeDocumentableTransformer { else -> null } - private fun mergePotentialBeansAndField( + private fun mergePotentialAccessorsAndField( functions: List, fields: List ): Pair, List> { val fieldNames = fields.associateBy { it.name } - val accessors = mutableMapOf>() + + // Regular methods are methods that are not getters or setters val regularMethods = mutableListOf() + // Accessors are methods that are getters or setters + val accessors = mutableMapOf>() functions.forEach { method -> val field = method.getPropertyNameForFunction()?.let { name -> fieldNames[name] } if (field != null) { @@ -67,7 +74,11 @@ class PropertiesMergerTransformer : PreMergeDocumentableTransformer { regularMethods.add(method) } } - return regularMethods.toList() to accessors.map { (dProperty, dFunctions) -> + + // Properties are triples of field and its getters and/or setters. + // They are wrapped up in DProperty class, + // so we copy accessors into its dedicated DProperty data class properties + val propertiesWithAccessors = 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) @@ -81,6 +92,15 @@ class PropertiesMergerTransformer : PreMergeDocumentableTransformer { } else { dProperty } - } + fields.toSet().minus(accessors.keys.toSet()) + } + + // The above logic is driven by accessors list + // Therefore, if there was no getter or setter, we missed processing the field itself. + // To include them, we collect all fields that have no accessors + val remainingFields = fields.toSet().minus(accessors.keys.toSet()) + + val allProperties = propertiesWithAccessors + remainingFields + + return regularMethods.toList() to allProperties } } \ 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 cf85d729a4..35c2f4ef0c 100644 --- a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -431,6 +431,14 @@ private class DokkaDescriptorVisitor( parent: DRIWithPlatformInfo ): DProperty { val (dri, _) = originalDescriptor.createDRI() + /** + * `createDRI` returns the DRI of the exact element and potential DRI of an element that is overriding it + * (It can be also FAKE_OVERRIDE which is in fact just inheritance of the symbol) + * + * Looking at what PSIs do, they give the DRI of the element within the classnames where it is actually + * declared and inheritedFrom as the same DRI but truncated callable part. + * Therefore, we set callable to null and take the DRI only if it is indeed coming from different class. + */ val inheritedFrom = dri.copy(callable = null).takeIf { parent.dri.classNames != dri.classNames } val descriptor = originalDescriptor.getConcreteDescriptor() val isExpect = descriptor.isExpect @@ -487,6 +495,10 @@ private class DokkaDescriptorVisitor( parent: DRIWithPlatformInfo ): DFunction { val (dri, _) = originalDescriptor.createDRI() + /** + * To avoid redundant docs, please visit [visitPropertyDescriptor] inheritedFrom + * local val documentation. + */ val inheritedFrom = dri.copy(callable = null).takeIf { parent.dri.classNames != dri.classNames } val descriptor = originalDescriptor.getConcreteDescriptor() val isExpect = descriptor.isExpect @@ -650,7 +662,17 @@ private class DokkaDescriptorVisitor( return coroutineScope { val generics = async { descriptor.typeParameters.parallelMap { it.toVariantTypeParameter() } } - fun SourceSetDependent.translateParamToDescription(): SourceSetDependent { + /** + * Workaround for problem with inheriting TagWrappers. + * There is an issue if one declare documentation in the class header for + * property using this syntax: `@property` + * The compiler will propagate it withing this tag to property and to its getters and setters. + * + * Actually, the problem impacts more of these tags, yet this particular tag was blocker for + * some opens-source plugin creators. + * TODO: Should rethink if we could fix it globally in dokka or in compiler itself. + */ + fun SourceSetDependent.translatePropertyToDescription(): SourceSetDependent { return this.mapValues { (_, value) -> value.copy(children = value.children.map { when (it) { @@ -667,7 +689,7 @@ private class DokkaDescriptorVisitor( isConstructor = false, parameters = parameters, visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), - documentation = descriptor.resolveDescriptorData().translateParamToDescription(), + documentation = descriptor.resolveDescriptorData().translatePropertyToDescription(), type = descriptor.returnType!!.toBound(), generics = generics.await(), modifier = descriptor.modifier().toSourceSetDependent(), diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index 3a76353fef..3a18ea7e72 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -474,9 +474,9 @@ class DefaultPsiToDocumentableTranslator( * Workaround for getting JvmField Kotlin annotation in PSIs */ private fun Collection.getJvmFieldAnnotation() = filter { - it.qualifiedName == "kotlin.jvm.JvmField" + it.qualifiedName == "$JVM_FIELD_PACKAGE_NAME.$JVM_FIELD_CLASS_NAMES" }.map { - Annotations.Annotation(DRI("kotlin.jvm", "JvmField"), emptyMap()) + Annotations.Annotation(DRI(JVM_FIELD_PACKAGE_NAME, JVM_FIELD_CLASS_NAMES), emptyMap()) }.distinct() private fun PsiTypeParameter.annotations(): PropertyContainer = this.annotations.toList().toListOfAnnotations().annotations()