Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix gathering inherited properties #2353

Closed
7 changes: 7 additions & 0 deletions core/api/core.api
Expand Up @@ -1837,6 +1837,13 @@ public final class org/jetbrains/dokka/model/JavaVisibility$Public : org/jetbrai
public static final field INSTANCE Lorg/jetbrains/dokka/model/JavaVisibility$Public;
}

public final class org/jetbrains/dokka/model/JvmFieldKt {
public static final field JVM_FIELD_CLASS_NAMES Ljava/lang/String;
public static final field JVM_FIELD_PACKAGE_NAME Ljava/lang/String;
public static final fun isJvmField (Lorg/jetbrains/dokka/links/DRI;)Z
public static final fun isJvmField (Lorg/jetbrains/dokka/model/Annotations$Annotation;)Z
}

public final class org/jetbrains/dokka/model/JvmNameKt {
public static final fun isJvmName (Lorg/jetbrains/dokka/links/DRI;)Z
public static final fun isJvmName (Lorg/jetbrains/dokka/model/Annotations$Annotation;)Z
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/kotlin/model/JvmField.kt
@@ -0,0 +1,10 @@
package org.jetbrains.dokka.model

import org.jetbrains.dokka.links.DRI

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.isJvmField()
6 changes: 6 additions & 0 deletions plugins/base/api/base.api
Expand Up @@ -46,6 +46,7 @@ public final class org/jetbrains/dokka/base/DokkaBase : org/jetbrains/dokka/plug
public final fun getPageMergerStrategy ()Lorg/jetbrains/dokka/plugability/ExtensionPoint;
public final fun getPathToRootConsumer ()Lorg/jetbrains/dokka/plugability/Extension;
public final fun getPreMergeDocumentableTransformer ()Lorg/jetbrains/dokka/plugability/ExtensionPoint;
public final fun getPropertiesMergerTransformer ()Lorg/jetbrains/dokka/plugability/Extension;
public final fun getPsiToDocumentableTranslator ()Lorg/jetbrains/dokka/plugability/Extension;
public final fun getReplaceVersionConsumer ()Lorg/jetbrains/dokka/plugability/Extension;
public final fun getResolveLinkConsumer ()Lorg/jetbrains/dokka/plugability/Extension;
Expand Down Expand Up @@ -1166,6 +1167,11 @@ public final class org/jetbrains/dokka/base/transformers/documentables/ObviousFu
public fun shouldBeSuppressed (Lorg/jetbrains/dokka/model/Documentable;)Z
}

public final class org/jetbrains/dokka/base/transformers/documentables/PropertiesMergerTransformer : org/jetbrains/dokka/transformers/documentation/PreMergeDocumentableTransformer {
public fun <init> ()V
public fun invoke (Ljava/util/List;)Ljava/util/List;
}

public final class org/jetbrains/dokka/base/transformers/documentables/SuppressTagDocumentableFilter : org/jetbrains/dokka/base/transformers/documentables/SuppressedByConditionDocumentableFilterTransformer {
public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V
public final fun getDokkaContext ()Lorg/jetbrains/dokka/plugability/DokkaContext;
Expand Down
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,118 @@
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

/**
* 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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this transformer only applicable to java sources? If so:

  1. what do you think about renaming it to JavaPropertiesMergerTransformer?
  2. Can we somehow filter processed items and avoid processing kotlin classes? Does it make sense? Although I don't know if we have the ability to extract that info from DClass. Or maybe you already have that filter somewhere, e.g via KotlinVisibility?

My main concern is that if we don't limit it, then kotlin classes might satisfy the same conditions as Java and it'll introduce bugs (i.e not display getters/setters of a kotlin class for explicit declarations). If it's only java classes that are processed (somehow), should be safer.

But maybe this can never happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it can happen, I am not sure how I could limit it to only Java classes. Do you have any idea?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* 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 {
BarkingBad marked this conversation as resolved.
Show resolved Hide resolved

override fun invoke(modules: List<DModule>) =
modules.map { it.copy(packages = it.packages.map {
it.mergeAccessorsAndField().copy(
classlikes = it.classlikes.map { it.mergeAccessorsAndField() }
)
}) }

private fun <T : WithScope> 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
}

/**
* This is copied from here
* [org.jetbrains.dokka.base.translators.psi.DefaultPsiToDocumentableTranslator.DokkaPsiParser.getPropertyNameForFunction]
* we should consider if we could unify that.
* TODO: Revisit that
*/
private fun DFunction.getPropertyNameForFunction() =
when {
JvmAbi.isGetterName(name) -> propertyNameByGetMethodName(Name.identifier(name))?.asString()
JvmAbi.isSetterName(name) -> propertyNamesBySetMethodName(Name.identifier(name)).firstOrNull()
?.asString()
else -> null
}

/**
* This is loosely copied from here
* [org.jetbrains.dokka.base.translators.psi.DefaultPsiToDocumentableTranslator.DokkaPsiParser.splitFunctionsAndAccessors]
* we should consider if we could unify that.
* TODO: Revisit that
*/
private fun mergePotentialAccessorsAndField(
functions: List<DFunction>,
fields: List<DProperty>
): Pair<List<DFunction>, List<DProperty>> {
val fieldNames = fields.associateBy { it.name }

// Regular methods are methods that are not getters or setters
val regularMethods = mutableListOf<DFunction>()
// Accessors are methods that are getters or setters
val accessors = mutableMapOf<DProperty, MutableList<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)
}
}

// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be extracted into a variable with a name that explains intention (something like overridableVisibilities if I got the idea right), plus will avoid unnecessary allocation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look into that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not necesarily overridability that we put emphasis on here, but rather we struggle with the proper visibility for property. The problem is, the java field will most likely be private and its accessors will be public/protected. We have to replace the visibility of the DProperty as it would be not visible in Kotlin. On the other hand I am now thinking about situation where we have public getter and private setter. That way we would like to have our DProperty to be public, yet be presented as val. Probably will have to rewrite the logic here...
I am also curious how dokka is handling private setter vars, is it rendering it like vals? I have to double check that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need time to investigate it before we merge this PR? Can what you're talking about break something or introduce bugs?

nice comments btw, thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should hardcode the configurations like these manually... I don't know if I have time to do it this week.

}?.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
}
}

// 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
}
}
Expand Up @@ -430,7 +430,16 @@ private class DokkaDescriptorVisitor(
originalDescriptor: PropertyDescriptor,
parent: DRIWithPlatformInfo
): DProperty {
val (dri, inheritedFrom) = originalDescriptor.createDRI()
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 || parent.dri.packageName != dri.packageName }
val descriptor = originalDescriptor.getConcreteDescriptor()
val isExpect = descriptor.isExpect
val isActual = descriptor.isActual
Expand All @@ -448,10 +457,10 @@ private class DokkaDescriptorVisitor(
},
sources = actual,
getter = descriptor.accessors.filterIsInstance<PropertyGetterDescriptor>().singleOrNull()?.let {
visitPropertyAccessorDescriptor(it, descriptor, dri)
visitPropertyAccessorDescriptor(it, descriptor, dri, inheritedFrom)
},
setter = descriptor.accessors.filterIsInstance<PropertySetterDescriptor>().singleOrNull()?.let {
visitPropertyAccessorDescriptor(it, descriptor, dri)
visitPropertyAccessorDescriptor(it, descriptor, dri, inheritedFrom)
},
visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(),
documentation = descriptor.resolveDescriptorData(),
Expand All @@ -468,7 +477,7 @@ private class DokkaDescriptorVisitor(
(descriptor.getAnnotationsWithBackingField() + descriptor.fileLevelAnnotations()).toSourceSetDependent()
.toAnnotations(),
descriptor.getDefaultValue()?.let { DefaultValue(it) },
InheritedMember(inheritedFrom.toSourceSetDependent()),
inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) },
)
)
)
Expand All @@ -485,7 +494,12 @@ private class DokkaDescriptorVisitor(
originalDescriptor: FunctionDescriptor,
parent: DRIWithPlatformInfo
): DFunction {
val (dri, inheritedFrom) = originalDescriptor.createDRI()
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 || parent.dri.packageName != dri.packageName }
val descriptor = originalDescriptor.getConcreteDescriptor()
val isExpect = descriptor.isExpect
val isActual = descriptor.isActual
Expand Down Expand Up @@ -515,7 +529,7 @@ private class DokkaDescriptorVisitor(
sourceSets = setOf(sourceSet),
isExpectActual = (isExpect || isActual),
extra = PropertyContainer.withAll(
InheritedMember(inheritedFrom.toSourceSetDependent()),
inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) },
descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(),
(descriptor.getAnnotations() + descriptor.fileLevelAnnotations()).toSourceSetDependent()
.toAnnotations(),
Expand Down Expand Up @@ -594,7 +608,8 @@ private class DokkaDescriptorVisitor(
private suspend fun visitPropertyAccessorDescriptor(
descriptor: PropertyAccessorDescriptor,
propertyDescriptor: PropertyDescriptor,
parent: DRI
parent: DRI,
inheritedFrom: DRI? = null
): DFunction {
val dri = parent.copy(callable = Callable.from(descriptor))
val isGetter = descriptor is PropertyGetterDescriptor
Expand Down Expand Up @@ -647,13 +662,34 @@ private class DokkaDescriptorVisitor(
return coroutineScope {
val generics = async { descriptor.typeParameters.parallelMap { it.toVariantTypeParameter() } }

/**
* 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 the text wrapped in 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What solution do you suggest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be fixed in the compiler. On the other hand, the compiler returns corresponding KDoc for some property etc. that is based on the positions of the original KDoc string, and therefore it is wrapped in the Property section, which is somehow correct. There could be some service in dokka to translate all of these, though one should rethink all the possibilities and cover them manually. Probably it won't be so hard afterall

*/
fun SourceSetDependent<DocumentationNode>.translatePropertyTagToDescription(): SourceSetDependent<DocumentationNode> {
return this.mapValues { (_, value) ->
value.copy(children = value.children.map {
when (it) {
is Property -> Description(it.root)
else -> it
}
})
}
}

DFunction(
dri,
name,
isConstructor = false,
parameters = parameters,
visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(),
documentation = descriptor.resolveDescriptorData(),
documentation = descriptor.resolveDescriptorData().translatePropertyTagToDescription(),
type = descriptor.returnType!!.toBound(),
generics = generics.await(),
modifier = descriptor.modifier().toSourceSetDependent(),
Expand All @@ -669,7 +705,8 @@ private class DokkaDescriptorVisitor(
isExpectActual = (isExpect || isActual),
extra = PropertyContainer.withAll(
descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(),
descriptor.getAnnotations().toSourceSetDependent().toAnnotations()
descriptor.getAnnotations().toSourceSetDependent().toAnnotations(),
inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }
)
)
}
Expand Down Expand Up @@ -904,9 +941,9 @@ private class DokkaDescriptorVisitor(
)
} ?: getJavaDocs())?.takeIf { it.children.isNotEmpty() }

private fun DeclarationDescriptor.getJavaDocs() = (this as? CallableDescriptor)
?.overriddenDescriptors
?.mapNotNull { it.findPsi() as? PsiNamedElement }
private fun DeclarationDescriptor.getJavaDocs() = (
((this as? CallableDescriptor)?.overriddenDescriptors ?: emptyList()) + listOf(this)
)?.mapNotNull { it.findPsi() as? PsiNamedElement }
?.firstOrNull()
?.let { javadocParser.parseDocumentation(it) }

Expand Down