diff --git a/core/src/main/kotlin/utilities/parallelCollectionOperations.kt b/core/src/main/kotlin/utilities/parallelCollectionOperations.kt index b3191e8b09..35ad48fd36 100644 --- a/core/src/main/kotlin/utilities/parallelCollectionOperations.kt +++ b/core/src/main/kotlin/utilities/parallelCollectionOperations.kt @@ -1,8 +1,6 @@ package org.jetbrains.dokka.utilities -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* suspend inline fun Iterable.parallelMap(crossinline f: suspend (A) -> B): List = coroutineScope { map { async { f(it) } }.awaitAll() @@ -13,5 +11,5 @@ suspend inline fun Iterable.parallelMapNotNull(crossinline f: suspend } suspend inline fun Iterable.parallelForEach(crossinline f: suspend (A) -> Unit): Unit = coroutineScope { - map { async { f(it) } }.awaitAll() + forEach { launch { f(it) } } } diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index 8cdfe530b5..ec708ea85e 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -1217,10 +1217,6 @@ public final class org/jetbrains/dokka/base/transformers/documentables/Extension public fun invoke (Lorg/jetbrains/dokka/model/DModule;Lorg/jetbrains/dokka/plugability/DokkaContext;)Lorg/jetbrains/dokka/model/DModule; } -public final class org/jetbrains/dokka/base/transformers/documentables/ExtensionExtractorTransformerKt { - public static final fun consumeAsFlow (Lkotlinx/coroutines/channels/ReceiveChannel;)Lkotlinx/coroutines/flow/Flow; -} - public final class org/jetbrains/dokka/base/transformers/documentables/InheritedEntriesDocumentableFilterTransformer : org/jetbrains/dokka/base/transformers/documentables/SuppressedByConditionDocumentableFilterTransformer { public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V public fun shouldBeSuppressed (Lorg/jetbrains/dokka/model/Documentable;)Z @@ -1278,6 +1274,11 @@ public final class org/jetbrains/dokka/base/transformers/documentables/UtilsKt { public static final fun isException (Lorg/jetbrains/dokka/model/properties/WithExtraProperties;)Z } +public final class org/jetbrains/dokka/base/transformers/documentables/utils/FullClassHierarchyBuilder { + public fun ()V + public final fun invoke (Lorg/jetbrains/dokka/model/DModule;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class org/jetbrains/dokka/base/transformers/pages/annotations/SinceKotlinTransformer : org/jetbrains/dokka/transformers/documentation/DocumentableTransformer { public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt index 73023a8688..19af0564c0 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt @@ -1,14 +1,8 @@ package org.jetbrains.dokka.base.transformers.documentables import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedReceiveChannelException -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.channels.* +import org.jetbrains.dokka.base.transformers.documentables.utils.FullClassHierarchyBuilder import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.links.DriOfAny import org.jetbrains.dokka.model.* @@ -17,97 +11,140 @@ import org.jetbrains.dokka.model.properties.MergeStrategy import org.jetbrains.dokka.model.properties.plus import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer +import org.jetbrains.dokka.utilities.parallelForEach +import org.jetbrains.dokka.utilities.parallelMap class ExtensionExtractorTransformer : DocumentableTransformer { override fun invoke(original: DModule, context: DokkaContext): DModule = runBlocking(Dispatchers.Default) { + val classGraph = async { + if (!context.configuration.suppressInheritedMembers) + FullClassHierarchyBuilder()(original) + else + emptyMap() + } + val channel = Channel>(10) launch { - coroutineScope { - original.packages.forEach { launch { collectExtensions(it, channel) } } - } + original.packages.parallelForEach { collectExtensions(it, channel) } channel.close() } - val extensionMap = channel.consumeAsFlow().toList().toMultiMap() + val extensionMap = channel.toList().toMultiMap() - val newPackages = original.packages.map { async { it.addExtensionInformation(extensionMap) } } - original.copy(packages = newPackages.awaitAll()) + val newPackages = original.packages.parallelMap { it.addExtensionInformation(classGraph.await(), extensionMap) } + original.copy(packages = newPackages) } -} -private suspend fun T.addExtensionInformation( - extensionMap: Map> -): T = coroutineScope { - val newClasslikes = (this@addExtensionInformation as? WithScope) - ?.classlikes - ?.map { async { it.addExtensionInformation(extensionMap) } } - .orEmpty() - - @Suppress("UNCHECKED_CAST") - when (this@addExtensionInformation) { - is DPackage -> { - val newTypealiases = typealiases.map { async { it.addExtensionInformation(extensionMap) } } - copy(classlikes = newClasslikes.awaitAll(), typealiases = newTypealiases.awaitAll()) - } - is DClass -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) - is DEnum -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) - is DInterface -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) - is DObject -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) - is DAnnotation -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) - is DTypeAlias -> copy(extra = extra + extensionMap.find(dri)) - else -> throw IllegalStateException( - "${this@addExtensionInformation::class.simpleName} is not expected to have extensions" - ) - } as T -} + private suspend fun T.addExtensionInformation( + classGraph: SourceSetDependent>>, + extensionMap: Map> + ): T = coroutineScope { + val newClasslikes = (this@addExtensionInformation as? WithScope) + ?.classlikes + ?.map { async { it.addExtensionInformation(classGraph, extensionMap) } } + .orEmpty() + + @Suppress("UNCHECKED_CAST") + when (this@addExtensionInformation) { + is DPackage -> { + val newTypealiases = typealiases.map { async { it.addExtensionInformation(classGraph, extensionMap) } } + copy(classlikes = newClasslikes.awaitAll(), typealiases = newTypealiases.awaitAll()) + } -private fun Map>.find(dri: DRI) = get(dri)?.toSet()?.let(::CallableExtensions) + is DClass -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DEnum -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DInterface -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DObject -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DAnnotation -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DTypeAlias -> copy(extra = extra + findExtensions(classGraph, extensionMap)) + else -> throw IllegalStateException( + "${this@addExtensionInformation::class.simpleName} is not expected to have extensions" + ) + } as T + } + + private suspend fun collectExtensions( + documentable: Documentable, + channel: SendChannel> + ): Unit = coroutineScope { + if (documentable is WithScope) { + documentable.classlikes.forEach { + launch { collectExtensions(it, channel) } + } -private suspend fun collectExtensions( - documentable: Documentable, - channel: SendChannel> -): Unit = coroutineScope { - if (documentable is WithScope) { - documentable.classlikes.forEach { - launch { collectExtensions(it, channel) } + if (documentable is DObject || documentable is DPackage) { + (documentable.properties.asSequence() + documentable.functions.asSequence()) + .flatMap { it.asPairsWithReceiverDRIs() } + .forEach { channel.send(it) } + } } + } + + private fun T.findExtensions( + classGraph: SourceSetDependent>>, + extensionMap: Map> + ): CallableExtensions? { + val resultSet = mutableSetOf() - if (documentable is DObject || documentable is DPackage) { - (documentable.properties.asSequence() + documentable.functions.asSequence()) - .flatMap(Callable::asPairsWithReceiverDRIs) - .forEach { channel.send(it) } + fun collectFrom(element: DRI) { + extensionMap[element]?.let { resultSet.addAll(it) } + sourceSets.forEach { sourceSet -> classGraph[sourceSet]?.get(element)?.forEach { collectFrom(it) } } } + collectFrom(dri) + + return if (resultSet.isEmpty()) null else CallableExtensions(resultSet) } -} + private fun Callable.asPairsWithReceiverDRIs(): Sequence> = + receiver?.type?.let { findReceiverDRIs(it) }.orEmpty().map { it to this } + + // In normal cases we return at max one DRI, but sometimes receiver type can be bound by more than one type constructor + // for example `fun T.example() where T: A, T: B` is extension of both types A and B + // another one `typealias A = B` + // Note: in some cases returning empty sequence doesn't mean that we cannot determine the DRI but only that we don't + // care about it since there is nowhere to put documentation of given extension. + private fun Callable.findReceiverDRIs(bound: Bound): Sequence = when (bound) { + is Nullable -> findReceiverDRIs(bound.inner) + is DefinitelyNonNullable -> findReceiverDRIs(bound.inner) + is TypeParameter -> + if (this is DFunction && bound.dri == this.dri) + generics.find { it.name == bound.name }?.bounds?.asSequence()?.flatMap { findReceiverDRIs(it) }.orEmpty() + else + emptySequence() + + is TypeConstructor -> sequenceOf(bound.dri) + is PrimitiveJavaType -> emptySequence() + is Void -> emptySequence() + is JavaObject -> sequenceOf(DriOfAny) + is Dynamic -> sequenceOf(DriOfAny) + is UnresolvedBound -> emptySequence() + is TypeAliased -> findReceiverDRIs(bound.typeAlias) + findReceiverDRIs(bound.inner) + } -private fun Callable.asPairsWithReceiverDRIs(): Sequence> = - receiver?.type?.let(::findReceiverDRIs).orEmpty().map { it to this } - -// In normal cases we return at max one DRI, but sometimes receiver type can be bound by more than one type constructor -// for example `fun T.example() where T: A, T: B` is extension of both types A and B -// Note: in some cases returning empty sequence doesn't mean that we cannot determine the DRI but only that we don't -// care about it since there is nowhere to put documentation of given extension. -private fun Callable.findReceiverDRIs(bound: Bound): Sequence = when (bound) { - is Nullable -> findReceiverDRIs(bound.inner) - is DefinitelyNonNullable -> findReceiverDRIs(bound.inner) - is TypeParameter -> - if (this is DFunction && bound.dri == this.dri) - generics.find { it.name == bound.name }?.bounds?.asSequence()?.flatMap(::findReceiverDRIs).orEmpty() - else - emptySequence() - is TypeConstructor -> sequenceOf(bound.dri) - is PrimitiveJavaType -> emptySequence() - is Void -> emptySequence() - is JavaObject -> sequenceOf(DriOfAny) - is Dynamic -> sequenceOf(DriOfAny) - is UnresolvedBound -> emptySequence() - is TypeAliased -> findReceiverDRIs(bound.typeAlias) + private fun Iterable>.toMultiMap(): Map> = + groupBy(Pair::first, Pair<*, U>::second) } -private fun Iterable>.toMultiMap(): Map> = - groupBy(Pair::first, Pair<*, U>::second) - data class CallableExtensions(val extensions: Set) : ExtraProperty { companion object Key : ExtraProperty.Key { override fun mergeStrategyFor(left: CallableExtensions, right: CallableExtensions) = @@ -116,14 +153,3 @@ data class CallableExtensions(val extensions: Set) : ExtraProperty ReceiveChannel.consumeAsFlow(): Flow = flow { - try { - while (true) { - emit(receive()) - } - } catch (_: ClosedReceiveChannelException) { - // cool and good - } -}.flowOn(Dispatchers.Default) diff --git a/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt b/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt new file mode 100644 index 0000000000..d657fa3288 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt @@ -0,0 +1,84 @@ +package org.jetbrains.dokka.base.transformers.documentables.utils + +import com.intellij.psi.PsiClass +import kotlinx.coroutines.* +import org.jetbrains.dokka.analysis.DescriptorDocumentableSource +import org.jetbrains.dokka.analysis.PsiDocumentableSource +import org.jetbrains.dokka.analysis.from +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.utilities.parallelForEach +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.typeUtil.immediateSupertypes +import org.jetbrains.kotlin.types.typeUtil.isAnyOrNullableAny +import java.util.concurrent.ConcurrentHashMap + +typealias Supertypes = List +typealias ClassHierarchy = SourceSetDependent> + +class FullClassHierarchyBuilder { + suspend operator fun invoke(original: DModule): ClassHierarchy = coroutineScope { + val map = original.sourceSets.associateWith { ConcurrentHashMap>() } + original.packages.parallelForEach { visitDocumentable(it, map) } + map + } + + private suspend fun collectSupertypesFromKotlinType( + driWithKType: Pair, + supersMap: MutableMap + ): Unit = coroutineScope { + val (dri, kotlinType) = driWithKType + val supertypes = kotlinType.immediateSupertypes().filterNot { it.isAnyOrNullableAny() } + val supertypesDriWithKType = supertypes.mapNotNull { supertype -> + supertype.constructor.declarationDescriptor?.let { + DRI.from(it) to supertype + } + } + + if (supersMap[dri] == null) { + // another thread can rewrite the same value, but it isn't a problem + supersMap[dri] = supertypesDriWithKType.map { it.first } + supertypesDriWithKType.parallelForEach { collectSupertypesFromKotlinType(it, supersMap) } + } + } + + private suspend fun collectSupertypesFromPsiClass( + driWithPsiClass: Pair, + supersMap: MutableMap + ): Unit = coroutineScope { + val (dri, psiClass) = driWithPsiClass + val supertypes = psiClass.superTypes.mapNotNull { it.resolve() } + .filterNot { it.qualifiedName == "java.lang.Object" } + val supertypesDriWithPsiClass = supertypes.map { DRI.from(it) to it } + + if (supersMap[dri] == null) { + // another thread can rewrite the same value, but it isn't a problem + supersMap[dri] = supertypesDriWithPsiClass.map { it.first } + supertypesDriWithPsiClass.parallelForEach { collectSupertypesFromPsiClass(it, supersMap) } + } + } + + private suspend fun visitDocumentable( + documentable: Documentable, + hierarchy: SourceSetDependent>> + ): Unit = coroutineScope { + if (documentable is WithScope) { + documentable.classlikes.parallelForEach { visitDocumentable(it, hierarchy) } + } + if (documentable is DClasslike) { + // to build a full class graph, using supertypes from Documentable + // is not enough since it keeps only one level of hierarchy + documentable.sources.forEach { (sourceSet, source) -> + if (source is DescriptorDocumentableSource) { + val descriptor = source.descriptor as ClassDescriptor + val type = descriptor.defaultType + hierarchy[sourceSet]?.let { collectSupertypesFromKotlinType(documentable.dri to type, it) } + } else if (source is PsiDocumentableSource) { + val psi = source.psi as PsiClass + hierarchy[sourceSet]?.let { collectSupertypesFromPsiClass(documentable.dri to psi, it) } + } + } + } + } +} \ No newline at end of file diff --git a/plugins/base/src/test/kotlin/model/ExtensionsTest.kt b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt new file mode 100644 index 0000000000..f2657ef8f8 --- /dev/null +++ b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt @@ -0,0 +1,152 @@ +package model + +import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions +import org.jetbrains.dokka.model.* +import org.junit.jupiter.api.Test +import utils.AbstractModelTest +import org.jetbrains.dokka.model.properties.WithExtraProperties + +class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "classes") { + private fun , R : Documentable> T.checkExtension(name: String = "extension") = + with(extra[CallableExtensions]?.extensions) { + this notNull "extensions" + this counts 1 + (this?.single() as? DFunction)?.name equals name + } + + @Test + fun `should be extension for subclasses`() { + inlineModelTest( + """ + |open class A + |open class B: A() + |open class C: B() + |open class D: C() + |fun B.extension() = "" + """ + ) { + with((this / "classes" / "B").cast()) { + checkExtension() + } + with((this / "classes" / "C").cast()) { + checkExtension() + } + with((this / "classes" / "D").cast()) { + checkExtension() + } + with((this / "classes" / "A").cast()) { + extra[CallableExtensions] equals null + } + } + } + + @Test + fun `should be extension for interfaces`() { + inlineModelTest( + """ + |interface I + |interface I2 : I + |open class A: I2 + |fun I.extension() = "" + """ + ) { + + with((this / "classes" / "A").cast()) { + checkExtension() + } + with((this / "classes" / "I2").cast()) { + checkExtension() + } + with((this / "classes" / "I").cast()) { + checkExtension() + } + } + } + + @Test + fun `should be extension for external classes`() { + inlineModelTest( + """ + |abstract class A: AbstractList() + |fun AbstractCollection.extension() {} + | + |class B:Exception() + |fun Throwable.extension() = "" + """ + ) { + with((this / "classes" / "A").cast()) { + checkExtension() + } + with((this / "classes" / "B").cast()) { + checkExtension() + } + } + } + + @Test + fun `should be extension for typealias`() { + inlineModelTest( + """ + |open class A + |open class B: A() + |open class C: B() + |open class D: C() + |typealias B2 = B + |fun B2.extension() = "" + """ + ) { + with((this / "classes" / "B").cast()) { + checkExtension() + } + with((this / "classes" / "C").cast()) { + checkExtension() + } + with((this / "classes" / "D").cast()) { + checkExtension() + } + with((this / "classes" / "A").cast()) { + extra[CallableExtensions] equals null + } + } + } + + @Test + fun `should be extension for java classes`() { + val testConfiguration = dokkaConfiguration { + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/") + classpath += jvmStdlibPath!! + } + } + } + testInline( + """ + |/src/main/kotlin/classes/Test.kt + | package classes + | fun A.extension() = "" + | + |/src/main/kotlin/classes/A.java + | package classes; + | public class A {} + | + | /src/main/kotlin/classes/B.java + | package classes; + | public class B extends A {} + """, + configuration = testConfiguration + ) { + documentablesTransformationStage = { + it.run { + with((this / "classes" / "B").cast()) { + checkExtension() + } + with((this / "classes" / "A").cast()) { + checkExtension() + } + } + } + } + } +} \ No newline at end of file