diff --git a/plugins/base/src/main/kotlin/translators/CollectionExtensions.kt b/plugins/base/src/main/kotlin/translators/CollectionExtensions.kt new file mode 100644 index 0000000000..0de4b5b112 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/CollectionExtensions.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.base.translators + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal inline fun Iterable.firstNotNullOfOrNull(transform: (T) -> R?): R? { + for (element in this) { + val result = transform(element) + if (result != null) { + return result + } + } + return null +} diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt index 2e346c3c3b..6a2f89836a 100644 --- a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -43,10 +43,6 @@ import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor import org.jetbrains.kotlin.idea.kdoc.findKDoc import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi -import org.jetbrains.kotlin.load.java.JvmAbi -import org.jetbrains.kotlin.load.java.descriptors.JavaMethodDescriptor -import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName -import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName import org.jetbrains.kotlin.load.kotlin.toSourceElement import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.* @@ -384,11 +380,10 @@ private class DokkaDescriptorVisitor( return coroutineScope { val descriptorsWithKind = scope.getDescriptorsWithKind() - val propertiesByName = descriptorsWithKind.properties.associateBy { it.name.asString() } - val (accessors, regularFunctions) = descriptorsWithKind.functions.partition { - val property = propertiesByName[it.toPossiblePropertyName()] - property != null && (it.isGetterFor(property) || it.isSetterFor(property)) - } + val (regularFunctions, accessors) = splitFunctionsAndAccessors( + properties = descriptorsWithKind.properties, + functions = descriptorsWithKind.functions + ) val functions = async { regularFunctions.visitFunctions(driWithPlatform) } val properties = async { descriptorsWithKind.properties.visitProperties(driWithPlatform, accessors) } @@ -431,45 +426,6 @@ private class DokkaDescriptorVisitor( } } - private fun FunctionDescriptor.toPossiblePropertyName(): String? { - val stringName = this.name.asString() - return when { - JvmAbi.isSetterName(stringName) -> propertyNamesBySetMethodName(this.name).firstOrNull()?.asString() - JvmAbi.isGetterName(stringName) -> { - // In java, the convention for boolean property accessors is as follows: - // - `private boolean active;` - // - `private boolean isActive();` - // - // Whereas in Kotlin, because there are no explicit accessors, the convention is - // - `val isActive: Boolean` - // - // This makes it difficult to guess the name of the accessor property in case of Java - if (this is JavaMethodDescriptor && JvmAbi.startsWithIsPrefix(stringName)) { - stringName.removePrefix("is").let { newName -> - newName.replaceFirst(newName[0], newName[0].toLowerCase()) - } - } else { - propertyNameByGetMethodName(this.name)?.asString() - } - } - else -> null - } - } - - private fun FunctionDescriptor.isGetterFor(property: PropertyDescriptor): Boolean { - return this.returnType == property.returnType - && this.valueParameters.isEmpty() - && !property.visibility.isPublicAPI - && this.visibility.isPublicAPI - } - - private fun FunctionDescriptor.isSetterFor(property: PropertyDescriptor): Boolean { - return this.valueParameters.size == 1 - && this.valueParameters[0].type == property.returnType - && !property.visibility.isPublicAPI - && this.visibility.isPublicAPI - } - /** * @param implicitAccessors getters/setters that are not part of the property descriptor, for instance * average methods inherited from java sources @@ -865,12 +821,11 @@ private class DokkaDescriptorVisitor( private suspend fun List.visitProperties( parent: DRIWithPlatformInfo, - implicitAccessors: List = emptyList(), + implicitAccessors: Map> = emptyMap(), ): List { - val accessorsByPropertyName = implicitAccessors.groupBy { it.toPossiblePropertyName() } return coroutineScope { parallelMap { - val propertyAccessors = accessorsByPropertyName[it.name.asString()] ?: emptyList() + val propertyAccessors = implicitAccessors[it] ?: emptyList() visitPropertyDescriptor(it, propertyAccessors, parent) } } diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DescriptorAccessorConventionUtil.kt b/plugins/base/src/main/kotlin/translators/descriptors/DescriptorAccessorConventionUtil.kt new file mode 100644 index 0000000000..5bfec7763d --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/descriptors/DescriptorAccessorConventionUtil.kt @@ -0,0 +1,80 @@ +package org.jetbrains.dokka.base.translators.descriptors + +import org.jetbrains.dokka.base.translators.firstNotNullOfOrNull +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.load.java.JvmAbi +import org.jetbrains.kotlin.load.java.descriptors.JavaMethodDescriptor +import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName +import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName + + +internal data class DescriptorFunctionsHolder( + val regularFunctions: List, + val accessors: Map> +) + +internal fun splitFunctionsAndAccessors( + properties: List, + functions: List +): DescriptorFunctionsHolder { + val fieldsByName = properties.associateBy { it.name.asString() } + val regularFunctions = mutableListOf() + val accessors = mutableMapOf>() + functions.forEach { function -> + val possiblePropertyNamesForFunction = function.toPossiblePropertyNames() + val field = possiblePropertyNamesForFunction.firstNotNullOfOrNull { fieldsByName[it] } + if (field != null) { + accessors.getOrPut(field, ::mutableListOf).add(function) + } else { + regularFunctions.add(function) + } + } + return DescriptorFunctionsHolder(regularFunctions, accessors) +} + +internal fun FunctionDescriptor.toPossiblePropertyNames(): List { + val stringName = this.name.asString() + return when { + JvmAbi.isSetterName(stringName) -> propertyNamesBySetMethodName(this.name).map { it.asString() } + JvmAbi.isGetterName(stringName) -> propertyNamesByGetMethod(this) + else -> listOf() + } +} + +internal fun propertyNamesByGetMethod(functionDescriptor: FunctionDescriptor): List { + val stringName = functionDescriptor.name.asString() + // In java, the convention for boolean property accessors is as follows: + // - `private boolean active;` + // - `private boolean isActive();` + // + // Whereas in Kotlin, because there are no explicit accessors, the convention is + // - `val isActive: Boolean` + // + // This makes it difficult to guess the name of the accessor property in case of Java + val javaPropName = if (functionDescriptor is JavaMethodDescriptor && JvmAbi.startsWithIsPrefix(stringName)) { + val javaPropName = stringName.removePrefix("is").let { newName -> + newName.replaceFirst(newName[0], newName[0].toLowerCase()) + } + javaPropName + } else { + null + } + val kotlinPropName = propertyNameByGetMethodName(functionDescriptor.name)?.asString() + return listOfNotNull(javaPropName, kotlinPropName) +} + +internal fun FunctionDescriptor.isGetterFor(property: PropertyDescriptor): Boolean { + return this.returnType == property.returnType + && this.valueParameters.isEmpty() + && !property.visibility.isPublicAPI + && this.visibility.isPublicAPI +} + +internal fun FunctionDescriptor.isSetterFor(property: PropertyDescriptor): Boolean { + return this.valueParameters.size == 1 + && this.valueParameters[0].type == property.returnType + && !property.visibility.isPublicAPI + && this.visibility.isPublicAPI +} + diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index f22bf8d9d2..2a8062a6bd 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -25,7 +25,6 @@ import org.jetbrains.dokka.links.* import org.jetbrains.dokka.model.* import org.jetbrains.dokka.model.AnnotationTarget import org.jetbrains.dokka.model.Nullable -import org.jetbrains.dokka.model.TypeConstructor import org.jetbrains.dokka.model.doc.DocumentationNode import org.jetbrains.dokka.model.doc.Param import org.jetbrains.dokka.model.properties.PropertyContainer @@ -87,14 +86,14 @@ class DefaultPsiToDocumentableTranslator( ) DModule( - context.configuration.moduleName, - psiFiles.parallelMapNotNull { it.safeAs() }.groupBy { it.packageName }.toList() + name = context.configuration.moduleName, + packages = psiFiles.parallelMapNotNull { it.safeAs() }.groupBy { it.packageName }.toList() .parallelMap { (packageName: String, psiFiles: List) -> docParser.parsePackage(packageName, psiFiles) }, - emptyMap(), - null, - setOf(sourceSet) + documentation = emptyMap(), + expectPresentInSet = null, + sourceSets = setOf(sourceSet) ) } } @@ -151,19 +150,19 @@ class DefaultPsiToDocumentableTranslator( val annotations = packageInfo?.packageStatement?.annotationList?.annotations DPackage( - dri, - emptyList(), - emptyList(), - psiFiles.parallelMap { psiFile -> + dri = dri, + functions = emptyList(), + properties = emptyList(), + classlikes = psiFiles.parallelMap { psiFile -> coroutineScope { psiFile.classes.asIterable().parallelMap { parseClasslike(it, dri) } } }.flatten(), - emptyList(), - documentation, - null, - setOf(sourceSetData), - PropertyContainer.withAll( + typealiases = emptyList(), + documentation = documentation, + expectPresentInSet = null, + sourceSets = setOf(sourceSetData), + extra = PropertyContainer.withAll( annotations?.toList().orEmpty().toListOfAnnotations().toSourceSetDependent().toAnnotations() ) ) @@ -239,16 +238,19 @@ class DefaultPsiToDocumentableTranslator( } val ancestry: AncestryNode = traversePsiClassForAncestorsAndInheritedMembers(this) + val (regularFunctions, accessors) = splitFunctionsAndAccessors(psi.fields, psi.methods) - val (regularSuperFunctions, superAccessors) = splitFunctionsAndAccessors(superFields.map { it.first }.toTypedArray(), superMethods.map { it.first }.toTypedArray()) + val (regularSuperFunctions, superAccessors) = splitFunctionsAndAccessors( + fields = superFields.map { it.first }.toTypedArray(), + methods = 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() }) { + val superAccessorsWithDRI = superAccessors.mapValues { (field, methods) -> + val containsJvmField = field.annotations.mapNotNull { it.toAnnotation() }.any { it.isJvmField() } + if (containsJvmField) { emptyList() } else { methods.mapNotNull { method -> superMethods.find { it.first.hash == method.hash } } @@ -258,28 +260,38 @@ class DefaultPsiToDocumentableTranslator( val overridden = regularFunctions.flatMap { it.findSuperMethods().toList() } val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent() val allFunctions = async { - regularFunctions.parallelMapNotNull { + val mappedRegularFunctions = regularFunctions.parallelMapNotNull { if (!it.isConstructor) parseFunction( it, parentDRI = dri ) else null - } + regularSuperFunctionsWithDRI.filter { it.first !in overridden }.parallelMap { parseFunction(it.first, inheritedFrom = it.second) } + } + val mappedSuperFunctions = regularSuperFunctionsWithDRI + .filter { it.first !in overridden } + .parallelMap { parseFunction(it.first, inheritedFrom = it.second) } + + mappedRegularFunctions + mappedSuperFunctions } val allFields = async { - fields.toList().parallelMapNotNull { parseField(it, accessors[it].orEmpty()) } + - superFields.parallelMapNotNull { parseFieldWithInheritingAccessors( - it.first, - superAccessorsWithDRI[it.first].orEmpty(), - inheritedFrom = it.second) + val mappedFields = fields.toList().parallelMapNotNull { + parseField(it, accessors[it].orEmpty()) + } + val mappedSuperFields = superFields.parallelMapNotNull { (field, dri) -> + parseFieldWithInheritingAccessors( + field, + superAccessorsWithDRI[field].orEmpty(), + inheritedFrom = dri + ) } + mappedFields + mappedSuperFields } val source = PsiDocumentableSource(this).toSourceSetDependent() val classlikes = async { innerClasses.asIterable().parallelMap { parseClasslike(it, dri) } } val visibility = getVisibility().toSourceSetDependent() val ancestors = (listOfNotNull(ancestry.superclass?.let { - it.typeConstructor.let { + it.typeConstructor.let { typeConstructor -> TypeConstructorWithKind( - it, + typeConstructor, JavaClassKindTypes.CLASS ) } @@ -290,103 +302,103 @@ class DefaultPsiToDocumentableTranslator( when { isAnnotationType -> DAnnotation( - name.orEmpty(), - dri, - documentation, - null, - source, - allFunctions.await(), - allFields.await(), - classlikes.await(), - visibility, - null, - constructors.map { parseFunction(it, true) }, - mapTypeParameters(dri), - setOf(sourceSetData), - false, - PropertyContainer.withAll( + name = name.orEmpty(), + dri = dri, + documentation = documentation, + expectPresentInSet = null, + sources = source, + functions = allFunctions.await(), + properties = allFields.await(), + classlikes = classlikes.await(), + visibility = visibility, + companion = null, + constructors = constructors.map { parseFunction(it, true) }, + generics = mapTypeParameters(dri), + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = PropertyContainer.withAll( implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations() ) ) isEnum -> DEnum( - dri, - name.orEmpty(), - fields.filterIsInstance().map { entry -> + dri = dri, + name = name.orEmpty(), + entries = fields.filterIsInstance().map { entry -> DEnumEntry( - dri.withClass(entry.name).withEnumEntryExtra(), - entry.name, - javadocParser.parseDocumentation(entry).toSourceSetDependent(), - null, - emptyList(), - emptyList(), - emptyList(), - setOf(sourceSetData), - PropertyContainer.withAll( + dri = dri.withClass(entry.name).withEnumEntryExtra(), + name = entry.name, + documentation = javadocParser.parseDocumentation(entry).toSourceSetDependent(), + expectPresentInSet = null, + functions = emptyList(), + properties = emptyList(), + classlikes = emptyList(), + sourceSets = setOf(sourceSetData), + extra = PropertyContainer.withAll( implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations() ) ) }, - documentation, - null, - source, - allFunctions.await(), - fields.filter { it !is PsiEnumConstant }.map { parseField(it, accessors[it].orEmpty()) }, - classlikes.await(), - visibility, - null, - constructors.map { parseFunction(it, true) }, - ancestors, - setOf(sourceSetData), - false, - PropertyContainer.withAll( + documentation = documentation, + expectPresentInSet = null, + sources = source, + functions = allFunctions.await(), + properties = fields.filter { it !is PsiEnumConstant }.map { parseField(it, accessors[it].orEmpty()) }, + classlikes = classlikes.await(), + visibility = visibility, + companion = null, + constructors = constructors.map { parseFunction(it, true) }, + supertypes = ancestors, + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = PropertyContainer.withAll( implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations() ) ) isInterface -> DInterface( - dri, - name.orEmpty(), - documentation, - null, - source, - allFunctions.await(), - allFields.await(), - classlikes.await(), - visibility, - null, - mapTypeParameters(dri), - ancestors, - setOf(sourceSetData), - false, - PropertyContainer.withAll( + dri = dri, + name = name.orEmpty(), + documentation = documentation, + expectPresentInSet = null, + sources = source, + functions = allFunctions.await(), + properties = allFields.await(), + classlikes = classlikes.await(), + visibility = visibility, + companion = null, + generics = mapTypeParameters(dri), + supertypes = ancestors, + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = PropertyContainer.withAll( implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations() ) ) else -> DClass( - dri, - name.orEmpty(), - constructors.map { parseFunction(it, true) }, - allFunctions.await(), - allFields.await(), - classlikes.await(), - source, - visibility, - null, - mapTypeParameters(dri), - ancestors, - documentation, - null, - modifiers, - setOf(sourceSetData), - false, - PropertyContainer.withAll( + dri = dri, + name = name.orEmpty(), + constructors = constructors.map { parseFunction(it, true) }, + functions = allFunctions.await(), + properties = allFields.await(), + classlikes = classlikes.await(), + sources = source, + visibility = visibility, + companion = null, + generics = mapTypeParameters(dri), + supertypes = ancestors, + documentation = documentation, + expectPresentInSet = null, + modifier = modifiers, + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = PropertyContainer.withAll( implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations(), @@ -411,38 +423,38 @@ class DefaultPsiToDocumentableTranslator( } ?: DRI.from(psi) val docs = javadocParser.parseDocumentation(psi) return DFunction( - dri, - psi.name, - isConstructor, - psi.parameterList.parameters.map { psiParameter -> + dri = dri, + name = psi.name, + isConstructor = isConstructor, + parameters = psi.parameterList.parameters.map { psiParameter -> DParameter( - dri.copy(target = dri.target.nextTarget()), - psiParameter.name, - DocumentationNode( + dri = dri.copy(target = dri.target.nextTarget()), + name = psiParameter.name, + documentation = DocumentationNode( listOfNotNull(docs.firstChildOfTypeOrNull { it.name == psiParameter.name }) ).toSourceSetDependent(), - null, - getBound(psiParameter.type), - setOf(sourceSetData), - PropertyContainer.withAll( + expectPresentInSet = null, + type = getBound(psiParameter.type), + sourceSets = setOf(sourceSetData), + extra = PropertyContainer.withAll( psiParameter.annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations() ) ) }, - docs.toSourceSetDependent(), - null, - PsiDocumentableSource(psi).toSourceSetDependent(), - psi.getVisibility().toSourceSetDependent(), - psi.returnType?.let { getBound(type = it) } ?: Void, - psi.mapTypeParameters(dri), - null, - psi.getModifier().toSourceSetDependent(), - setOf(sourceSetData), - false, - psi.additionalExtras().let { + documentation = docs.toSourceSetDependent(), + expectPresentInSet = null, + sources = PsiDocumentableSource(psi).toSourceSetDependent(), + visibility = psi.getVisibility().toSourceSetDependent(), + type = psi.returnType?.let { getBound(type = it) } ?: Void, + generics = psi.mapTypeParameters(dri), + receiver = null, + modifier = psi.getModifier().toSourceSetDependent(), + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = psi.additionalExtras().let { PropertyContainer.withAll( inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, it.toSourceSetDependent().toAdditionalModifiers(), @@ -479,11 +491,16 @@ class DefaultPsiToDocumentableTranslator( /** * 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 Collection.findJvmFieldAnnotation(): Annotations.Annotation? { + val anyJvmFieldAnnotation = this.any { + it.qualifiedName == "$JVM_FIELD_PACKAGE_NAME.$JVM_FIELD_CLASS_NAMES" + } + return if (anyJvmFieldAnnotation) { + Annotations.Annotation(DRI(JVM_FIELD_PACKAGE_NAME, JVM_FIELD_CLASS_NAMES), emptyMap()) + } else { + null + } + } private fun PsiTypeParameter.annotations(): PropertyContainer = this.annotations.toList().toListOfAnnotations().annotations() private fun PsiType.annotations(): PropertyContainer = this.annotations.toList().toListOfAnnotations().annotations() @@ -569,14 +586,14 @@ class DefaultPsiToDocumentableTranslator( } return typeParameters.map { type -> DTypeParameter( - dri.copy(target = dri.target.nextTarget()), - type.name.orEmpty(), - null, - javadocParser.parseDocumentation(type).toSourceSetDependent(), - null, - mapBounds(type.bounds), - setOf(sourceSetData), - PropertyContainer.withAll( + dri = dri.copy(target = dri.target.nextTarget()), + name = type.name.orEmpty(), + presentableName = null, + documentation = javadocParser.parseDocumentation(type).toSourceSetDependent(), + expectPresentInSet = null, + bounds = mapBounds(type.bounds), + sourceSets = setOf(sourceSetData), + extra = PropertyContainer.withAll( type.annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations() ) @@ -584,69 +601,65 @@ class DefaultPsiToDocumentableTranslator( } } - private fun PsiMethod.getPropertyNameForFunction() = - getAnnotation(DescriptorUtils.JVM_NAME.asString())?.findAttributeValue("name")?.text - ?: when { - JvmAbi.isGetterName(name) -> propertyNameByGetMethodName(Name.identifier(name))?.asString() - JvmAbi.isSetterName(name) -> propertyNamesBySetMethodName(Name.identifier(name)).firstOrNull() - ?.asString() - else -> null - } - - private fun splitFunctionsAndAccessors(fields: Array, methods: Array): Pair, MutableMap>> { - val fieldNames = fields.associateBy { it.name } - val accessors = mutableMapOf>() - val regularMethods = mutableListOf() - methods.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 to accessors + private fun parseFieldWithInheritingAccessors( + psi: PsiField, + accessors: List>, + inheritedFrom: DRI + ): DProperty { + val getter = accessors + .firstOrNull { (method, _) -> method.isGetterFor(psi) } + ?.let { (method, dri) -> parseFunction(method, inheritedFrom = dri) } + + val setter = accessors + .firstOrNull { (method, _) -> method.isSetterFor(psi) } + ?.let { (method, dri) -> parseFunction(method, inheritedFrom = dri) } + + return parseField( + psi = psi, + getter = getter, + setter = setter, + inheritedFrom = inheritedFrom + ) } - 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, accessors: List, inheritedFrom: DRI? = null): DProperty { + val getter = accessors.firstOrNull { it.isGetterFor(psi) }?.let { parseFunction(it) } + val setter = accessors.firstOrNull { it.isSetterFor(psi) }?.let { parseFunction(it) } + return parseField( + psi = psi, + getter = getter, + setter = setter, + inheritedFrom = inheritedFrom + ) + } private fun parseField(psi: PsiField, getter: DFunction?, setter: DFunction?, inheritedFrom: DRI? = null): DProperty { val dri = DRI.from(psi) return DProperty( - dri, - psi.name, - javadocParser.parseDocumentation(psi).toSourceSetDependent(), - null, - PsiDocumentableSource(psi).toSourceSetDependent(), - psi.getVisibility().toSourceSetDependent(), - getBound(psi.type), - null, - getter, - setter, - psi.getModifier().toSourceSetDependent(), - setOf(sourceSetData), - emptyList(), - false, - psi.additionalExtras().let { + dri = dri, + name = psi.name, + documentation = javadocParser.parseDocumentation(psi).toSourceSetDependent(), + expectPresentInSet = null, + sources = PsiDocumentableSource(psi).toSourceSetDependent(), + visibility = psi.getVisibility().toSourceSetDependent(), + type = getBound(psi.type), + receiver = null, + setter = setter, + getter = getter, + modifier = psi.getModifier().toSourceSetDependent(), + sourceSets = setOf(sourceSetData), + generics = emptyList(), + isExpectActual = false, + extra = psi.additionalExtras().let { + val psiAnnotations = psi.annotations.toList().toListOfAnnotations() + val extraModifierAnnotations = it.toListOfAnnotations() + val jvmFieldAnnotation = psi.annotations.toList().findJvmFieldAnnotation() + val annotations = psiAnnotations + extraModifierAnnotations + listOfNotNull(jvmFieldAnnotation) + PropertyContainer.withAll( - inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + inheritedFrom?.let { inheritedFrom -> InheritedMember(inheritedFrom.toSourceSetDependent()) }, it.toSourceSetDependent().toAdditionalModifiers(), - (psi.annotations.toList().toListOfAnnotations() + - it.toListOfAnnotations() + - psi.annotations.toList().getJvmFieldAnnotation() - ).toSourceSetDependent().toAnnotations() + annotations.toSourceSetDependent().toAnnotations() ) } ) diff --git a/plugins/base/src/main/kotlin/translators/psi/PsiAccessorConventionUtil.kt b/plugins/base/src/main/kotlin/translators/psi/PsiAccessorConventionUtil.kt new file mode 100644 index 0000000000..17faead3e8 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/PsiAccessorConventionUtil.kt @@ -0,0 +1,54 @@ +package org.jetbrains.dokka.base.translators.psi + +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import org.jetbrains.dokka.base.translators.firstNotNullOfOrNull +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 +import org.jetbrains.kotlin.resolve.DescriptorUtils + + +internal data class PsiFunctionsHolder( + val regularFunctions: List, + val accessors: Map> +) + +internal fun splitFunctionsAndAccessors(fields: Array, methods: Array): PsiFunctionsHolder { + val fieldsByName = fields.associateBy { it.name } + val regularFunctions = mutableListOf() + val accessors = mutableMapOf>() + methods.forEach { method -> + val possiblePropertyNamesForFunction = method.getPossiblePropertyNamesForFunction() + val field = possiblePropertyNamesForFunction.firstNotNullOfOrNull { fieldsByName[it] } + if (field != null) { + accessors.getOrPut(field, ::mutableListOf).add(method) + } else { + regularFunctions.add(method) + } + } + return PsiFunctionsHolder(regularFunctions, accessors) +} + +internal fun PsiMethod.getPossiblePropertyNamesForFunction(): List { + val jvmName = getAnnotation(DescriptorUtils.JVM_NAME.asString())?.findAttributeValue("name")?.text + return jvmName?.let { listOf(jvmName) } + ?: when { + JvmAbi.isGetterName(name) -> listOfNotNull( + propertyNameByGetMethodName(Name.identifier(name))?.asString() + ) + JvmAbi.isSetterName(name) -> { + propertyNamesBySetMethodName(Name.identifier(name)).map { it.asString() } + } + else -> listOf() + } +} + +internal fun PsiMethod.isGetterFor(field: PsiField): Boolean { + return this.returnType == field.type && !this.hasParameters() +} + +internal fun PsiMethod.isSetterFor(field: PsiField): Boolean { + return parameterList.getParameter(0)?.type == field.type +} diff --git a/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt b/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt index 39d2cf0775..a1582c1421 100644 --- a/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt +++ b/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt @@ -50,7 +50,7 @@ class PsiSuperFieldsTest : BaseAbstractTest() { } @Test - fun `java inheriting kotlin`() { + fun `java inheriting kotlin common case`() { testInline( """ |/src/test/A.kt @@ -78,6 +78,38 @@ class PsiSuperFieldsTest : BaseAbstractTest() { } } + @Test + fun `java inheriting kotlin with boolean property`() { + testInline( + """ + |/src/test/A.kt + |package test + |open class A { + | var isActive: Boolean = true + |} + | + |/src/test/B.java + |package test; + |public class B extends A {} + """.trimIndent(), + commonTestConfiguration + ) { + documentablesMergingStage = { module -> + val inheritorProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + val property = inheritorProperties.single { it.name == "isActive" } + + assertNotNull(property.getter) + assertEquals("isActive", property.getter!!.name) + + assertNotNull(property.setter) + assertEquals("setActive", property.setter!!.name) + + val inheritedFrom = property.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + } + } + } + @Test fun `java inheriting kotlin with @JvmField should not inherit accessors`() { testInline(