diff --git a/core/api/core.api b/core/api/core.api index 063f56c5fe..c11acda133 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -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 diff --git a/core/src/main/kotlin/model/JvmField.kt b/core/src/main/kotlin/model/JvmField.kt new file mode 100644 index 0000000000..623115e0bd --- /dev/null +++ b/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() diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index a38c2cfd8f..ac7439cd25 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -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; @@ -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 ()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 (Lorg/jetbrains/dokka/plugability/DokkaContext;)V public final fun getDokkaContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; 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..77c88285ef --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/PropertiesMergerTransformer.kt @@ -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, + * 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.mergeAccessorsAndField().copy( + classlikes = it.classlikes.map { it.mergeAccessorsAndField() } + ) + }) } + + 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 + } + + /** + * 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, + fields: List + ): Pair, List> { + val fieldNames = fields.associateBy { it.name } + + // 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) { + 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) + }?.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 + } +} \ 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 259f374af1..59811bf8df 100644 --- a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -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 @@ -448,10 +457,10 @@ private class DokkaDescriptorVisitor( }, sources = actual, getter = descriptor.accessors.filterIsInstance().singleOrNull()?.let { - visitPropertyAccessorDescriptor(it, descriptor, dri) + visitPropertyAccessorDescriptor(it, descriptor, dri, inheritedFrom) }, setter = descriptor.accessors.filterIsInstance().singleOrNull()?.let { - visitPropertyAccessorDescriptor(it, descriptor, dri) + visitPropertyAccessorDescriptor(it, descriptor, dri, inheritedFrom) }, visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), documentation = descriptor.resolveDescriptorData(), @@ -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()) }, ) ) ) @@ -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 @@ -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(), @@ -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 @@ -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. + */ + fun SourceSetDependent.translatePropertyTagToDescription(): SourceSetDependent { + 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(), @@ -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()) } ) ) } @@ -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) } diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index 7acf735aee..3a18ea7e72 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -122,6 +122,9 @@ class DefaultPsiToDocumentableTranslator( private val PsiMethod.hash: Int get() = "$returnType $name$parameterList".hashCode() + private val PsiField.hash: Int + get() = "$type $name".hashCode() + private val PsiClassType.shouldBeIgnored: Boolean get() = isClass("java.lang.Enum") || isClass("java.lang.Object") @@ -171,10 +174,16 @@ class DefaultPsiToDocumentableTranslator( val dri = parent.withClass(name.toString()) val superMethodsKeys = hashSetOf() val superMethods = mutableListOf>() + val superFieldsKeys = hashSetOf() + val superFields = mutableListOf>() methods.asIterable().parallelForEach { superMethodsKeys.add(it.hash) } /** - * Caution! This method mutates superMethodsKeys and superMethods + * Caution! This method mutates + * - superMethodsKeys + * - superMethods + * - superFieldsKeys + * - superKeys */ fun Array.getSuperTypesPsiClasses(): List> { forEach { type -> @@ -189,6 +198,13 @@ class DefaultPsiToDocumentableTranslator( superMethods.add(Pair(method, definedAt)) } } + it.fields.forEach { field -> + val hash = field.hash + if (!superFieldsKeys.contains(hash)) { + superFieldsKeys.add(hash) + superFields.add(Pair(field, definedAt)) + } + } } } return filter { !it.shouldBeIgnored }.mapNotNull { supertypePsi -> @@ -217,7 +233,22 @@ class DefaultPsiToDocumentableTranslator( } val ancestry: AncestryNode = traversePsiClassForAncestorsAndInheritedMembers(this) - val (regularFunctions, accessors) = splitFunctionsAndAccessors() + val (regularFunctions, accessors) = splitFunctionsAndAccessors(psi.fields, psi.methods) + val (regularSuperFunctions, superAccessors) = splitFunctionsAndAccessors(superFields.map { it.first }.toTypedArray(), superMethods.map { it.first }.toTypedArray()) + + val regularSuperFunctionsKeys = regularSuperFunctions.map { it.hash }.toSet() + + val regularSuperFunctionsWithDRI = superMethods.filter { it.first.hash in regularSuperFunctionsKeys } + + val superAccessorsWithDRI = superAccessors + .mapValues { (field, methods) -> + if (field.annotations.mapNotNull { it.toAnnotation() }.any { it.isJvmField() }) { + emptyList() + } else { + methods.mapNotNull { method -> superMethods.find { it.first.hash == method.hash } } + } + } + val overridden = regularFunctions.flatMap { it.findSuperMethods().toList() } val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent() val allFunctions = async { @@ -226,7 +257,15 @@ class DefaultPsiToDocumentableTranslator( it, parentDRI = dri ) else null - } + superMethods.filter { it.first !in overridden }.parallelMap { parseFunction(it.first, inheritedFrom = it.second) } + } + regularSuperFunctionsWithDRI.filter { it.first !in overridden }.parallelMap { parseFunction(it.first, inheritedFrom = it.second) } + } + val allFields = async { + fields.toList().parallelMapNotNull { parseField(it, accessors[it].orEmpty()) } + + superFields.parallelMapNotNull { parseFieldWithInheritingAccessors( + it.first, + superAccessorsWithDRI[it.first].orEmpty(), + inheritedFrom = it.second) + } } val source = PsiDocumentableSource(this).toSourceSetDependent() val classlikes = async { innerClasses.asIterable().parallelMap { parseClasslike(it, dri) } } @@ -251,7 +290,7 @@ class DefaultPsiToDocumentableTranslator( null, source, allFunctions.await(), - fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + allFields.await(), classlikes.await(), visibility, null, @@ -310,7 +349,7 @@ class DefaultPsiToDocumentableTranslator( null, source, allFunctions.await(), - fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + allFields.await(), classlikes.await(), visibility, null, @@ -329,7 +368,7 @@ class DefaultPsiToDocumentableTranslator( name.orEmpty(), constructors.map { parseFunction(it, true) }, allFunctions.await(), - fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + allFields.await(), classlikes.await(), source, visibility, @@ -399,7 +438,7 @@ class DefaultPsiToDocumentableTranslator( false, psi.additionalExtras().let { PropertyContainer.withAll( - InheritedMember(inheritedFrom.toSourceSetDependent()), + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, it.toSourceSetDependent().toAdditionalModifiers(), (psi.annotations.toList() .toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent() @@ -431,6 +470,15 @@ class DefaultPsiToDocumentableTranslator( Annotations.Annotation(DRI("kotlin.jvm", "JvmStatic"), emptyMap()) } + /** + * Workaround for getting JvmField Kotlin annotation in PSIs + */ + private fun Collection.getJvmFieldAnnotation() = filter { + it.qualifiedName == "$JVM_FIELD_PACKAGE_NAME.$JVM_FIELD_CLASS_NAMES" + }.map { + Annotations.Annotation(DRI(JVM_FIELD_PACKAGE_NAME, JVM_FIELD_CLASS_NAMES), emptyMap()) + }.distinct() + private fun PsiTypeParameter.annotations(): PropertyContainer = this.annotations.toList().toListOfAnnotations().annotations() private fun PsiType.annotations(): PropertyContainer = this.annotations.toList().toListOfAnnotations().annotations() @@ -452,7 +500,7 @@ class DefaultPsiToDocumentableTranslator( type.resolve()?.let { resolved -> when { resolved.qualifiedName == "java.lang.Object" -> JavaObject(type.annotations()) - resolved is PsiTypeParameter -> getProjection(resolved) + resolved is PsiTypeParameter -> getProjection(resolved).copy(extra = type.annotations()) Regex("kotlin\\.jvm\\.functions\\.Function.*").matches(resolved.qualifiedName ?: "") || Regex("java\\.util\\.function\\.Function.*").matches( resolved.qualifiedName ?: "" @@ -539,7 +587,7 @@ class DefaultPsiToDocumentableTranslator( else -> null } - private fun PsiClass.splitFunctionsAndAccessors(): Pair, MutableMap>> { + private fun splitFunctionsAndAccessors(fields: Array, methods: Array): Pair, MutableMap>> { val fieldNames = fields.associateBy { it.name } val accessors = mutableMapOf>() val regularMethods = mutableListOf() @@ -554,7 +602,21 @@ class DefaultPsiToDocumentableTranslator( return regularMethods to accessors } - private fun parseField(psi: PsiField, accessors: List): DProperty { + private fun parseFieldWithInheritingAccessors(psi: PsiField, accessors: List>, inheritedFrom: DRI): DProperty = parseField( + psi, + accessors.firstOrNull { it.first.hasParameters() }?.let { parseFunction(it.first, inheritedFrom = it.second) }, + accessors.firstOrNull { it.first.returnType == psi.type }?.let { parseFunction(it.first, inheritedFrom = it.second) }, + inheritedFrom + ) + + private fun parseField(psi: PsiField, accessors: List, inheritedFrom: DRI? = null): DProperty = parseField( + psi, + accessors.firstOrNull { it.hasParameters() }?.let { parseFunction(it) }, + accessors.firstOrNull { it.returnType == psi.type }?.let { parseFunction(it) }, + inheritedFrom + ) + + private fun parseField(psi: PsiField, getter: DFunction?, setter: DFunction?, inheritedFrom: DRI? = null): DProperty { val dri = DRI.from(psi) return DProperty( dri, @@ -565,18 +627,20 @@ class DefaultPsiToDocumentableTranslator( psi.getVisibility().toSourceSetDependent(), getBound(psi.type), null, - accessors.firstOrNull { it.hasParameters() }?.let { parseFunction(it) }, - accessors.firstOrNull { it.returnType == psi.type }?.let { parseFunction(it) }, + getter, + setter, psi.getModifier().toSourceSetDependent(), setOf(sourceSetData), emptyList(), false, psi.additionalExtras().let { PropertyContainer.withAll( + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, it.toSourceSetDependent().toAdditionalModifiers(), - (psi.annotations.toList() - .toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent() - .toAnnotations() + (psi.annotations.toList().toListOfAnnotations() + + it.toListOfAnnotations() + + psi.annotations.toList().getJvmFieldAnnotation() + ).toSourceSetDependent().toAnnotations() ) } ) diff --git a/plugins/base/src/test/kotlin/model/PropertyTest.kt b/plugins/base/src/test/kotlin/model/PropertyTest.kt index 17f526f3d2..dc35d62151 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") { @@ -158,6 +161,10 @@ class PropertyTest : AbstractModelTest("/src/main/kotlin/property/Test.kt", "pro with(getter.assertNotNull("Getter")) { type.name equals "Int" } + extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + classNames equals "Foo" + callable equals null + } } } } 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..7cac6649ea --- /dev/null +++ b/plugins/base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt @@ -0,0 +1,164 @@ +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.links.TypeConstructor +import org.jetbrains.dokka.links.TypeReference +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"), + this + ) + } + this.getter.run { + this!!.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A"), + 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"), + this + ) + } + this.getter.run { + this!!.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A"), + this + ) + } + } + this.setter.run { + this!!.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A"), + 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"), + this + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt b/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt new file mode 100644 index 0000000000..257bd94683 --- /dev/null +++ b/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt @@ -0,0 +1,142 @@ +package superFields + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Annotations +import org.jetbrains.dokka.model.InheritedMember +import org.jetbrains.dokka.model.isJvmField +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + + +class PsiSuperFieldsTest : BaseAbstractTest() { + + @Disabled // TODO: Remove with Kotlin 1.6.20 + @Test + fun `java inheriting java`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | public int a = 1; + |} + | + |/src/test/B.java + |package test; + |public class B extends 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 { + assertNotNull(this) + this.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A"), + this + ) + } + } + } + } + } + + @Disabled // TODO: Remove with Kotlin 1.6.20 + @Test + fun `java inheriting kotlin`() { + testInline( + """ + |/src/test/A.kt + |package test + |open class A { + | var a: Int = 1 + |} + | + |/src/test/B.java + |package test; + |public class B extends 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 { + assertNotNull(this) + assertNotNull(this.getter) + assertNotNull(this.setter) + this.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A"), + this + ) + } + } + } + } + } + + @Disabled // TODO: Remove with Kotlin 1.6.20 + @Test + fun `java inheriting kotlin with @JvmField should not inherit beans`() { + testInline( + """ + |/src/test/A.kt + |package test + |open class A { + | @kotlin.jvm.JvmField + | var a: Int = 1 + |} + | + |/src/test/B.java + |package test; + |public class B extends 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 { + assertNotNull(this) + assertNull(this.getter) + assertNull(this.setter) + assertNotNull(this.extra[Annotations]?.directAnnotations?.values?.single()?.find { + it.isJvmField() + }) + this.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A"), + this + ) + } + } + } + } + } +} diff --git a/plugins/javadoc/src/test/kotlin/org/jetbrains/dokka/javadoc/JavadocIndexTest.kt b/plugins/javadoc/src/test/kotlin/org/jetbrains/dokka/javadoc/JavadocIndexTest.kt index 801213c2be..53c42cd6f1 100644 --- a/plugins/javadoc/src/test/kotlin/org/jetbrains/dokka/javadoc/JavadocIndexTest.kt +++ b/plugins/javadoc/src/test/kotlin/org/jetbrains/dokka/javadoc/JavadocIndexTest.kt @@ -21,7 +21,7 @@ internal class JavadocIndexTest : AbstractJavadocTemplateMapTest() { AnnotationTarget.ANNOTATION_CLASS::class.java.methods.any { it.name == "describeConstable" } testIndexPages { indexPages -> - assertEquals(if (hasAdditionalFunction()) 41 else 40, indexPages.sumBy { it.elements.size }) + assertEquals(if (hasAdditionalFunction()) 32 else 31, indexPages.sumBy { it.elements.size }) } } diff --git a/plugins/kotlin-as-java/api/kotlin-as-java.api b/plugins/kotlin-as-java/api/kotlin-as-java.api index 817a78edd2..acf5c6d928 100644 --- a/plugins/kotlin-as-java/api/kotlin-as-java.api +++ b/plugins/kotlin-as-java/api/kotlin-as-java.api @@ -7,12 +7,12 @@ public final class org/jetbrains/dokka/kotlinAsJava/KotlinAsJavaPlugin : org/jet } public final class org/jetbrains/dokka/kotlinAsJava/TransformToJavaKt { - public static final fun transformToJava (Lorg/jetbrains/dokka/model/DClasslike;Lorg/jetbrains/dokka/plugability/DokkaContext;)Lorg/jetbrains/dokka/model/DClasslike; - public static final fun transformToJava (Lorg/jetbrains/dokka/model/DFunction;Lorg/jetbrains/dokka/plugability/DokkaContext;Ljava/lang/String;Z)Ljava/util/List; - public static final fun transformToJava (Lorg/jetbrains/dokka/model/DPackage;Lorg/jetbrains/dokka/plugability/DokkaContext;)Lorg/jetbrains/dokka/model/DPackage; - public static final fun transformToJava (Lorg/jetbrains/dokka/model/DProperty;Lorg/jetbrains/dokka/plugability/DokkaContext;ZLjava/lang/String;)Lorg/jetbrains/dokka/model/DProperty; - public static synthetic fun transformToJava$default (Lorg/jetbrains/dokka/model/DFunction;Lorg/jetbrains/dokka/plugability/DokkaContext;Ljava/lang/String;ZILjava/lang/Object;)Ljava/util/List; - public static synthetic fun transformToJava$default (Lorg/jetbrains/dokka/model/DProperty;Lorg/jetbrains/dokka/plugability/DokkaContext;ZLjava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/dokka/model/DProperty; + public static final fun transformToJava (Lorg/jetbrains/dokka/model/DClasslike;Lorg/jetbrains/dokka/utilities/DokkaLogger;)Lorg/jetbrains/dokka/model/DClasslike; + public static final fun transformToJava (Lorg/jetbrains/dokka/model/DFunction;Lorg/jetbrains/dokka/utilities/DokkaLogger;Ljava/lang/String;Z)Ljava/util/List; + public static final fun transformToJava (Lorg/jetbrains/dokka/model/DPackage;Lorg/jetbrains/dokka/utilities/DokkaLogger;)Lorg/jetbrains/dokka/model/DPackage; + public static final fun transformToJava (Lorg/jetbrains/dokka/model/DProperty;Lorg/jetbrains/dokka/utilities/DokkaLogger;ZLjava/lang/String;)Lorg/jetbrains/dokka/model/DProperty; + public static synthetic fun transformToJava$default (Lorg/jetbrains/dokka/model/DFunction;Lorg/jetbrains/dokka/utilities/DokkaLogger;Ljava/lang/String;ZILjava/lang/Object;)Ljava/util/List; + public static synthetic fun transformToJava$default (Lorg/jetbrains/dokka/model/DProperty;Lorg/jetbrains/dokka/utilities/DokkaLogger;ZLjava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/dokka/model/DProperty; } public final class org/jetbrains/dokka/kotlinAsJava/converters/KotlinToJavaConverterKt { 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 1e3bd80042..8ff357812b 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() @@ -275,7 +275,7 @@ internal fun DClass.functionsInJava(): List = .flatMap { property -> listOfNotNull(property.getter, property.setter) } .plus(functions) .filterNot { it.hasJvmSynthetic() } - .flatMap { it.asJava(dri.classNames ?: name) } + .flatMap { it.asJava(it.dri.classNames ?: it.name) } private fun DTypeParameter.asJava(): DTypeParameter = copy( variantTypeParameter = variantTypeParameter.withDri(dri.possiblyAsJava()), @@ -317,7 +317,7 @@ internal fun DEnum.asJava(): DEnum = copy( functions = functions .plus( properties - .filterNot { it.hasJvmSynthetic() } + .filter { it.jvmField() == null && !it.hasJvmSynthetic() } .flatMap { listOf(it.getter, it.setter) } ) .filterNotNull() @@ -335,7 +335,7 @@ internal fun DObject.asJava(): DObject = copy( functions = functions .plus( properties - .filterNot { it.hasJvmSynthetic() } + .filter { it.jvmField() == null && !it.hasJvmSynthetic() } .flatMap { listOf(it.getter, it.setter) } ) .filterNotNull() @@ -373,7 +373,7 @@ internal fun DInterface.asJava(): DInterface = copy( functions = functions .plus( properties - .filterNot { it.hasJvmSynthetic() } + .filter { it.jvmField() == null && !it.hasJvmSynthetic() } .flatMap { listOf(it.getter, it.setter) } ) .filterNotNull() diff --git a/plugins/kotlin-as-java/src/main/kotlin/transformToJava.kt b/plugins/kotlin-as-java/src/main/kotlin/transformToJava.kt index 69df21eec9..933234ce04 100644 --- a/plugins/kotlin-as-java/src/main/kotlin/transformToJava.kt +++ b/plugins/kotlin-as-java/src/main/kotlin/transformToJava.kt @@ -7,23 +7,24 @@ import org.jetbrains.dokka.model.DFunction import org.jetbrains.dokka.model.DPackage import org.jetbrains.dokka.model.DProperty import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.utilities.DokkaLogger private val JVM_NAME_DOCUMENTABLE_TRANSFORMER by lazy { JvmNameDocumentableTransformer() } -fun DPackage.transformToJava(context: DokkaContext): DPackage { - return JVM_NAME_DOCUMENTABLE_TRANSFORMER.transform(this.asJava(), context) +fun DPackage.transformToJava(logger: DokkaLogger): DPackage { + return JVM_NAME_DOCUMENTABLE_TRANSFORMER.transform(this.asJava(), logger) } -fun DClasslike.transformToJava(context: DokkaContext): DClasslike { - return JVM_NAME_DOCUMENTABLE_TRANSFORMER.transform(this.asJava(), context) +fun DClasslike.transformToJava(logger: DokkaLogger): DClasslike { + return JVM_NAME_DOCUMENTABLE_TRANSFORMER.transform(this.asJava(), logger) } -fun DFunction.transformToJava(context: DokkaContext, containingClassName: String, isTopLevel: Boolean = false): List { - return this.asJava(containingClassName, isTopLevel).map { JVM_NAME_DOCUMENTABLE_TRANSFORMER.transform(it, context) } +fun DFunction.transformToJava(logger: DokkaLogger, containingClassName: String, isTopLevel: Boolean = false): List { + return this.asJava(containingClassName, isTopLevel).map { JVM_NAME_DOCUMENTABLE_TRANSFORMER.transform(it, logger) } } -fun DProperty.transformToJava(context: DokkaContext, isTopLevel: Boolean = false, relocateToClass: String? = null): DProperty { - return JVM_NAME_DOCUMENTABLE_TRANSFORMER.transform(this.asJava(isTopLevel, relocateToClass), context) +fun DProperty.transformToJava(logger: DokkaLogger, isTopLevel: Boolean = false, relocateToClass: String? = null): DProperty { + return JVM_NAME_DOCUMENTABLE_TRANSFORMER.transform(this.asJava(isTopLevel, relocateToClass), logger) } diff --git a/plugins/kotlin-as-java/src/main/kotlin/transformers/JvmNameDocumentableTransformer.kt b/plugins/kotlin-as-java/src/main/kotlin/transformers/JvmNameDocumentableTransformer.kt index 28cb345388..23d1a3fb29 100644 --- a/plugins/kotlin-as-java/src/main/kotlin/transformers/JvmNameDocumentableTransformer.kt +++ b/plugins/kotlin-as-java/src/main/kotlin/transformers/JvmNameDocumentableTransformer.kt @@ -4,21 +4,22 @@ import org.jetbrains.dokka.model.* import org.jetbrains.dokka.model.properties.PropertyContainer import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer +import org.jetbrains.dokka.utilities.DokkaLogger class JvmNameDocumentableTransformer : DocumentableTransformer { private val jvmNameProvider = JvmNameProvider() override fun invoke(original: DModule, context: DokkaContext): DModule { - return original.copy(packages = original.packages.map { transform(it, context) }) + return original.copy(packages = original.packages.map { transform(it, context.logger) }) } - internal fun transform(documentable: T, context: DokkaContext): T = + internal fun transform(documentable: T, logger: DokkaLogger): T = with(documentable) { when (this) { is DPackage -> copy( - functions = functions.map { transform(it, context) }, - properties = properties.map { transform(it, context) }, - classlikes = classlikes.map { transform(it, context) }, + functions = functions.map { transform(it, logger) }, + properties = properties.map { transform(it, logger) }, + classlikes = classlikes.map { transform(it, logger) }, ) is DFunction -> { val name = jvmNameProvider.nameFor(this) @@ -29,14 +30,14 @@ class JvmNameDocumentableTransformer : DocumentableTransformer { ) } is DProperty -> transformGetterAndSetter(this) - is DClasslike -> transformClassLike(this, context) + is DClasslike -> transformClassLike(this, logger) is DEnumEntry -> copy( - functions = functions.map { transform(it, context) }, - properties = properties.map { transform(it, context) }, - classlikes = classlikes.map { transform(it, context) }, + functions = functions.map { transform(it, logger) }, + properties = properties.map { transform(it, logger) }, + classlikes = classlikes.map { transform(it, logger) }, ) else -> { - context.logger.warn("Failed to translate a JvmName for ${this.javaClass.canonicalName}") + logger.warn("Failed to translate a JvmName for ${this.javaClass.canonicalName}") this } } @@ -53,33 +54,33 @@ class JvmNameDocumentableTransformer : DocumentableTransformer { return extraWithoutAnnotations.addAll(listOfNotNull(annotationsWithoutJvmName)) } - private fun transformClassLike(documentable: DClasslike, context: DokkaContext): DClasslike = + private fun transformClassLike(documentable: DClasslike, logger: DokkaLogger): DClasslike = with(documentable) { when (this) { is DClass -> copy( - functions = functions.map { transform(it, context) }, - properties = properties.map { transform(it, context) }, - classlikes = classlikes.map { transform(it, context) }, + functions = functions.map { transform(it, logger) }, + properties = properties.map { transform(it, logger) }, + classlikes = classlikes.map { transform(it, logger) }, ) is DAnnotation -> copy( - functions = functions.map { transform(it, context) }, - properties = properties.map { transform(it, context) }, - classlikes = classlikes.map { transform(it, context) }, + functions = functions.map { transform(it, logger) }, + properties = properties.map { transform(it, logger) }, + classlikes = classlikes.map { transform(it, logger) }, ) is DObject -> copy( - functions = functions.map { transform(it, context) }, - properties = properties.map { transform(it, context) }, - classlikes = classlikes.map { transform(it, context) }, + functions = functions.map { transform(it, logger) }, + properties = properties.map { transform(it, logger) }, + classlikes = classlikes.map { transform(it, logger) }, ) is DEnum -> copy( - functions = functions.map { transform(it, context) }, - properties = properties.map { transform(it, context) }, - classlikes = classlikes.map { transform(it, context) }, + functions = functions.map { transform(it, logger) }, + properties = properties.map { transform(it, logger) }, + classlikes = classlikes.map { transform(it, logger) }, ) is DInterface -> copy( - functions = functions.map { transform(it, context) }, - properties = properties.map { transform(it, context) }, - classlikes = classlikes.map { transform(it, context) }, + functions = functions.map { transform(it, logger) }, + properties = properties.map { transform(it, logger) }, + classlikes = classlikes.map { transform(it, logger) }, ) } } diff --git a/plugins/kotlin-as-java/src/test/kotlin/JvmFieldTest.kt b/plugins/kotlin-as-java/src/test/kotlin/JvmFieldTest.kt index 2f49042150..65894d15fb 100644 --- a/plugins/kotlin-as-java/src/test/kotlin/JvmFieldTest.kt +++ b/plugins/kotlin-as-java/src/test/kotlin/JvmFieldTest.kt @@ -5,6 +5,7 @@ import org.jetbrains.dokka.model.JavaVisibility import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertNull class JvmFieldTest : BaseAbstractTest() { val configuration = dokkaConfiguration { @@ -78,4 +79,31 @@ class JvmFieldTest : BaseAbstractTest() { } } } + + @Test + fun `object jvmfield property should have no getters`(){ + 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) + assertEquals( + emptyList(), + classLike.functions.map { it.name } + ) + assertNull(property.getter) + assertNull(property.setter) + } + } + } } \ No newline at end of file