From c934bdb8f3123b230b987b723d6fb54378564c12 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Tue, 16 Aug 2022 22:19:50 +0300 Subject: [PATCH 1/8] Display inherited extensions --- .../utilities/parallelCollectionOperations.kt | 6 +- plugins/base/api/base.api | 9 +- .../ExtensionExtractorTransformer.kt | 202 ++++++++++-------- .../documentables/utils/ClassGraphBuilder.kt | 66 ++++++ .../src/test/kotlin/model/ExtensionsTest.kt | 112 ++++++++++ 5 files changed, 298 insertions(+), 97 deletions(-) create mode 100644 plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt create mode 100644 plugins/base/src/test/kotlin/model/ExtensionsTest.kt 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 4e1185d2d7..1cd3e7d83a 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -1207,10 +1207,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 @@ -1267,6 +1263,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/ClassGraphBuilder { + 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..f80a07bccd 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.ClassGraphBuilder import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.links.DriOfAny import org.jetbrains.dokka.model.* @@ -17,97 +11,138 @@ 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.kotlin.utils.addToStdlib.popLast +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 { ClassGraphBuilder()(original) } + 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()) + } + + 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 fun Map>.find(dri: DRI) = get(dri)?.toSet()?.let(::CallableExtensions) + 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) } + } } + } - if (documentable is DObject || documentable is DPackage) { - (documentable.properties.asSequence() + documentable.functions.asSequence()) - .flatMap(Callable::asPairsWithReceiverDRIs) - .forEach { channel.send(it) } + private fun T.findExtensions( + classGraph: SourceSetDependent>>, + extensionMap: Map> + ): CallableExtensions? { + val resultSet = mutableSetOf() + extensionMap[dri]?.let { resultSet.addAll(it) } + + val queue = mutableListOf() + sourceSets.forEach { classGraph[it]?.get(dri)?.let { supertypesDRIs -> queue.addAll(supertypesDRIs) } } + while (queue.isNotEmpty()) { + val element = queue.popLast() + sourceSets.forEach { classGraph[it]?.get(element)?.let { supertypesDRIs -> queue.addAll(supertypesDRIs) } } + extensionMap[element]?.let { resultSet.addAll(it) } } + 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 +151,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/ClassGraphBuilder.kt b/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt new file mode 100644 index 0000000000..90d119e9c4 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt @@ -0,0 +1,66 @@ +package org.jetbrains.dokka.base.transformers.documentables.utils + +import kotlinx.coroutines.* +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.DescriptorDocumentableSource +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 + + +/** + * The class allows lo build a full class hierarchy via descriptors + */ +class ClassGraphBuilder { + suspend operator fun invoke(original: DModule,): SourceSetDependent>> = coroutineScope{ + val map = original.sourceSets.associateWith { ConcurrentHashMap>() } + original.packages.parallelForEach{ visitDocumentable(it, map) } + map + } + + private suspend fun collectSupertypesFromKotlinType( + DRIWithKType: Pair, + sourceSet: DokkaConfiguration.DokkaSourceSet, + supersMap: SourceSetDependent>> + ): Unit = coroutineScope { + val supertypes = DRIWithKType.second.immediateSupertypes().filterNot { it.isAnyOrNullableAny() } + val supertypesDRIWithKType = supertypes.mapNotNull { supertype -> + supertype.constructor.declarationDescriptor?.let { + DRI.from(it) to supertype + } + } + + supersMap[sourceSet]?.let { + if (it[DRIWithKType.first] == null) { + // another thread can rewrite the same value, but it isn't a problem + it[DRIWithKType.first] = supertypesDRIWithKType.map { it.first } + supertypesDRIWithKType.parallelForEach { collectSupertypesFromKotlinType(it, sourceSet, supersMap) } + } + } + } + + private suspend fun visitDocumentable( + documentable: Documentable, + supersMap: SourceSetDependent>> + ): Unit = coroutineScope { + if (documentable is WithScope) { + documentable.classlikes. + parallelForEach{ visitDocumentable(it, supersMap) } + } + if(documentable is DClasslike) { + documentable.sources.forEach { (sourceSet, source) -> + if (source is DescriptorDocumentableSource) { + val descriptor = source.descriptor as ClassDescriptor + val type = descriptor.defaultType + collectSupertypesFromKotlinType( documentable.dri to type, sourceSet, supersMap) + } + } + } + } +} \ 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..56053d0329 --- /dev/null +++ b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt @@ -0,0 +1,112 @@ +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 extensionForSubclasses() { + 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 extensionForInterfaces() { + 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 extensionForExternalClasses() { + 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 extensionForTypeAlias() { + inlineModelTest( + """ + |class A {} + |class B: A {} + |class C: B {} + |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 + } + } + } +} \ No newline at end of file From 76b6bd17d1eae147dd3b536cce49ad069065fcb2 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Wed, 17 Aug 2022 16:40:04 +0300 Subject: [PATCH 2/8] Support java classes --- .../documentables/utils/ClassGraphBuilder.kt | 32 +++++++++-- .../src/test/kotlin/model/ExtensionsTest.kt | 54 ++++++++++++++++--- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt b/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt index 90d119e9c4..bb72f4f53c 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt @@ -1,8 +1,10 @@ package org.jetbrains.dokka.base.transformers.documentables.utils +import com.intellij.psi.PsiClass import kotlinx.coroutines.* import org.jetbrains.dokka.DokkaConfiguration 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.* @@ -36,15 +38,32 @@ class ClassGraphBuilder { } } - supersMap[sourceSet]?.let { - if (it[DRIWithKType.first] == null) { + supersMap[sourceSet]?.let { map -> + if (map[DRIWithKType.first] == null) { // another thread can rewrite the same value, but it isn't a problem - it[DRIWithKType.first] = supertypesDRIWithKType.map { it.first } + map[DRIWithKType.first] = supertypesDRIWithKType.map { it.first } supertypesDRIWithKType.parallelForEach { collectSupertypesFromKotlinType(it, sourceSet, supersMap) } } } } + private suspend fun collectSupertypesFromPsiClass( + DRIWithPsiClass: Pair, + sourceSet: DokkaConfiguration.DokkaSourceSet, + supersMap: SourceSetDependent>> + ): Unit = coroutineScope { + val supertypes = DRIWithPsiClass.second.superTypes.mapNotNull { it.resolve() }.filterNot { it.qualifiedName == "java.lang.Object" } + val supertypesDRIWithPsiClass = supertypes.map { DRI.from(it) to it} + + supersMap[sourceSet]?.let { map -> + if (map[DRIWithPsiClass.first] == null) { + // another thread can rewrite the same value, but it isn't a problem + map[DRIWithPsiClass.first] = supertypesDRIWithPsiClass.map { it.first } + supertypesDRIWithPsiClass.parallelForEach { collectSupertypesFromPsiClass(it, sourceSet, supersMap) } + } + } + } + private suspend fun visitDocumentable( documentable: Documentable, supersMap: SourceSetDependent>> @@ -54,11 +73,16 @@ class ClassGraphBuilder { parallelForEach{ visitDocumentable(it, supersMap) } } 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 - collectSupertypesFromKotlinType( documentable.dri to type, sourceSet, supersMap) + collectSupertypesFromKotlinType(documentable.dri to type, sourceSet, supersMap) + } else if (source is PsiDocumentableSource) { + val psi = source.psi as PsiClass + collectSupertypesFromPsiClass(documentable.dri to psi, sourceSet, supersMap) } } } diff --git a/plugins/base/src/test/kotlin/model/ExtensionsTest.kt b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt index 56053d0329..b62f8f54e6 100644 --- a/plugins/base/src/test/kotlin/model/ExtensionsTest.kt +++ b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt @@ -15,7 +15,7 @@ class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "cl } @Test - fun extensionForSubclasses() { + fun `should be extension for subclasses`() { inlineModelTest( """ |open class A @@ -41,7 +41,7 @@ class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "cl } @Test - fun extensionForInterfaces() { + fun `should be extension for interfaces`() { inlineModelTest( """ |interface I @@ -64,7 +64,7 @@ class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "cl } @Test - fun extensionForExternalClasses() { + fun `should be extension for external classes`() { inlineModelTest( """ |abstract class A: AbstractList() @@ -84,13 +84,13 @@ class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "cl } @Test - fun extensionForTypeAlias() { + fun `should be extension for typealias`() { inlineModelTest( """ - |class A {} - |class B: A {} - |class C: B {} - |class D: C {} + |open class A + |open class B: A() + |open class C: B() + |open class D: C() |typealias B2 = B |fun B2.extension() = "" """ @@ -108,5 +108,43 @@ class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "cl 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 From 7972d556d1e45fe5740a6ae4a1785385a63e573e Mon Sep 17 00:00:00 2001 From: vmishenev Date: Wed, 17 Aug 2022 16:40:47 +0300 Subject: [PATCH 3/8] Simplify algorithm --- .../documentables/ExtensionExtractorTransformer.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt index f80a07bccd..8a76c6eee3 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt @@ -11,7 +11,6 @@ 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.kotlin.utils.addToStdlib.popLast import org.jetbrains.dokka.utilities.parallelForEach import org.jetbrains.dokka.utilities.parallelMap @@ -101,15 +100,13 @@ class ExtensionExtractorTransformer : DocumentableTransformer { extensionMap: Map> ): CallableExtensions? { val resultSet = mutableSetOf() - extensionMap[dri]?.let { resultSet.addAll(it) } - val queue = mutableListOf() - sourceSets.forEach { classGraph[it]?.get(dri)?.let { supertypesDRIs -> queue.addAll(supertypesDRIs) } } - while (queue.isNotEmpty()) { - val element = queue.popLast() - sourceSets.forEach { classGraph[it]?.get(element)?.let { supertypesDRIs -> queue.addAll(supertypesDRIs) } } + 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) } From 864ede185bcd7aa85f2b4252e9e535d53b0af76c Mon Sep 17 00:00:00 2001 From: vmishenev Date: Wed, 17 Aug 2022 17:06:16 +0300 Subject: [PATCH 4/8] Format --- .../documentables/utils/ClassGraphBuilder.kt | 14 +++++++------- .../base/src/test/kotlin/model/ExtensionsTest.kt | 4 +++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt b/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt index bb72f4f53c..5370b8b92e 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt @@ -20,9 +20,9 @@ import java.util.concurrent.ConcurrentHashMap * The class allows lo build a full class hierarchy via descriptors */ class ClassGraphBuilder { - suspend operator fun invoke(original: DModule,): SourceSetDependent>> = coroutineScope{ + suspend operator fun invoke(original: DModule): SourceSetDependent>> = coroutineScope { val map = original.sourceSets.associateWith { ConcurrentHashMap>() } - original.packages.parallelForEach{ visitDocumentable(it, map) } + original.packages.parallelForEach { visitDocumentable(it, map) } map } @@ -52,8 +52,9 @@ class ClassGraphBuilder { sourceSet: DokkaConfiguration.DokkaSourceSet, supersMap: SourceSetDependent>> ): Unit = coroutineScope { - val supertypes = DRIWithPsiClass.second.superTypes.mapNotNull { it.resolve() }.filterNot { it.qualifiedName == "java.lang.Object" } - val supertypesDRIWithPsiClass = supertypes.map { DRI.from(it) to it} + val supertypes = DRIWithPsiClass.second.superTypes.mapNotNull { it.resolve() } + .filterNot { it.qualifiedName == "java.lang.Object" } + val supertypesDRIWithPsiClass = supertypes.map { DRI.from(it) to it } supersMap[sourceSet]?.let { map -> if (map[DRIWithPsiClass.first] == null) { @@ -69,10 +70,9 @@ class ClassGraphBuilder { supersMap: SourceSetDependent>> ): Unit = coroutineScope { if (documentable is WithScope) { - documentable.classlikes. - parallelForEach{ visitDocumentable(it, supersMap) } + documentable.classlikes.parallelForEach { visitDocumentable(it, supersMap) } } - if(documentable is DClasslike) { + 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) -> diff --git a/plugins/base/src/test/kotlin/model/ExtensionsTest.kt b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt index b62f8f54e6..f2657ef8f8 100644 --- a/plugins/base/src/test/kotlin/model/ExtensionsTest.kt +++ b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt @@ -108,7 +108,9 @@ class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "cl extra[CallableExtensions] equals null } } - } @Test + } + + @Test fun `should be extension for java classes`() { val testConfiguration = dokkaConfiguration { suppressObviousFunctions = false From 73d689b003f66ec75f9547ce285d1f706aabecec Mon Sep 17 00:00:00 2001 From: vmishenev Date: Thu, 25 Aug 2022 02:08:09 +0300 Subject: [PATCH 5/8] Refactor --- .../ExtensionExtractorTransformer.kt | 4 +- ...uilder.kt => FullClassHierarchyBuilder.kt} | 39 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) rename plugins/base/src/main/kotlin/transformers/documentables/utils/{ClassGraphBuilder.kt => FullClassHierarchyBuilder.kt} (71%) diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt index 8a76c6eee3..fadba4819a 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt @@ -2,7 +2,7 @@ package org.jetbrains.dokka.base.transformers.documentables import kotlinx.coroutines.* import kotlinx.coroutines.channels.* -import org.jetbrains.dokka.base.transformers.documentables.utils.ClassGraphBuilder +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,7 +17,7 @@ import org.jetbrains.dokka.utilities.parallelMap class ExtensionExtractorTransformer : DocumentableTransformer { override fun invoke(original: DModule, context: DokkaContext): DModule = runBlocking(Dispatchers.Default) { - val classGraph = async { ClassGraphBuilder()(original) } + val classGraph = async { FullClassHierarchyBuilder()(original) } val channel = Channel>(10) launch { diff --git a/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt b/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt similarity index 71% rename from plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt rename to plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt index 5370b8b92e..a5d8d6274e 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/utils/ClassGraphBuilder.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt @@ -15,52 +15,53 @@ 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> -/** - * The class allows lo build a full class hierarchy via descriptors - */ -class ClassGraphBuilder { - suspend operator fun invoke(original: DModule): SourceSetDependent>> = coroutineScope { +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, + driWithKType: Pair, sourceSet: DokkaConfiguration.DokkaSourceSet, - supersMap: SourceSetDependent>> + supersMap: SourceSetDependent> ): Unit = coroutineScope { - val supertypes = DRIWithKType.second.immediateSupertypes().filterNot { it.isAnyOrNullableAny() } - val supertypesDRIWithKType = supertypes.mapNotNull { supertype -> + 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 } } supersMap[sourceSet]?.let { map -> - if (map[DRIWithKType.first] == null) { + if (map[dri] == null) { // another thread can rewrite the same value, but it isn't a problem - map[DRIWithKType.first] = supertypesDRIWithKType.map { it.first } - supertypesDRIWithKType.parallelForEach { collectSupertypesFromKotlinType(it, sourceSet, supersMap) } + map[dri] = supertypesDriWithKType.map { it.first } + supertypesDriWithKType.parallelForEach { collectSupertypesFromKotlinType(it, sourceSet, supersMap) } } } } private suspend fun collectSupertypesFromPsiClass( - DRIWithPsiClass: Pair, + driWithPsiClass: Pair, sourceSet: DokkaConfiguration.DokkaSourceSet, - supersMap: SourceSetDependent>> + supersMap: SourceSetDependent> ): Unit = coroutineScope { - val supertypes = DRIWithPsiClass.second.superTypes.mapNotNull { it.resolve() } + 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 } + val supertypesDriWithPsiClass = supertypes.map { DRI.from(it) to it } supersMap[sourceSet]?.let { map -> - if (map[DRIWithPsiClass.first] == null) { + if (map[dri] == null) { // another thread can rewrite the same value, but it isn't a problem - map[DRIWithPsiClass.first] = supertypesDRIWithPsiClass.map { it.first } - supertypesDRIWithPsiClass.parallelForEach { collectSupertypesFromPsiClass(it, sourceSet, supersMap) } + map[dri] = supertypesDriWithPsiClass.map { it.first } + supertypesDriWithPsiClass.parallelForEach { collectSupertypesFromPsiClass(it, sourceSet, supersMap) } } } } From fecc72d94f5a123eb73ca6c0a193047d9d599333 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Thu, 25 Aug 2022 02:34:48 +0300 Subject: [PATCH 6/8] Apply `suppressInheritedMembers` flag --- .../documentables/ExtensionExtractorTransformer.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt index fadba4819a..19af0564c0 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt @@ -17,7 +17,12 @@ import org.jetbrains.dokka.utilities.parallelMap class ExtensionExtractorTransformer : DocumentableTransformer { override fun invoke(original: DModule, context: DokkaContext): DModule = runBlocking(Dispatchers.Default) { - val classGraph = async { FullClassHierarchyBuilder()(original) } + val classGraph = async { + if (!context.configuration.suppressInheritedMembers) + FullClassHierarchyBuilder()(original) + else + emptyMap() + } val channel = Channel>(10) launch { From f34a73c783976e02b7a84dbc4af74744a31a53e5 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Thu, 25 Aug 2022 02:42:45 +0300 Subject: [PATCH 7/8] Dump API --- plugins/base/api/base.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index 1cd3e7d83a..b1cdead961 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -1263,7 +1263,7 @@ 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/ClassGraphBuilder { +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; } From acde3ce1b911c42f785be7d7cd9d39be459a6844 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Fri, 26 Aug 2022 13:21:05 +0300 Subject: [PATCH 8/8] Simplify --- .../utils/FullClassHierarchyBuilder.kt | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt b/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt index a5d8d6274e..d657fa3288 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/utils/FullClassHierarchyBuilder.kt @@ -2,7 +2,6 @@ package org.jetbrains.dokka.base.transformers.documentables.utils import com.intellij.psi.PsiClass import kotlinx.coroutines.* -import org.jetbrains.dokka.DokkaConfiguration import org.jetbrains.dokka.analysis.DescriptorDocumentableSource import org.jetbrains.dokka.analysis.PsiDocumentableSource import org.jetbrains.dokka.analysis.from @@ -27,8 +26,7 @@ class FullClassHierarchyBuilder { private suspend fun collectSupertypesFromKotlinType( driWithKType: Pair, - sourceSet: DokkaConfiguration.DokkaSourceSet, - supersMap: SourceSetDependent> + supersMap: MutableMap ): Unit = coroutineScope { val (dri, kotlinType) = driWithKType val supertypes = kotlinType.immediateSupertypes().filterNot { it.isAnyOrNullableAny() } @@ -38,40 +36,35 @@ class FullClassHierarchyBuilder { } } - supersMap[sourceSet]?.let { map -> - if (map[dri] == null) { - // another thread can rewrite the same value, but it isn't a problem - map[dri] = supertypesDriWithKType.map { it.first } - supertypesDriWithKType.parallelForEach { collectSupertypesFromKotlinType(it, sourceSet, supersMap) } - } + 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, - sourceSet: DokkaConfiguration.DokkaSourceSet, - supersMap: SourceSetDependent> + 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 } - supersMap[sourceSet]?.let { map -> - if (map[dri] == null) { - // another thread can rewrite the same value, but it isn't a problem - map[dri] = supertypesDriWithPsiClass.map { it.first } - supertypesDriWithPsiClass.parallelForEach { collectSupertypesFromPsiClass(it, sourceSet, supersMap) } - } + 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, - supersMap: SourceSetDependent>> + hierarchy: SourceSetDependent>> ): Unit = coroutineScope { if (documentable is WithScope) { - documentable.classlikes.parallelForEach { visitDocumentable(it, supersMap) } + documentable.classlikes.parallelForEach { visitDocumentable(it, hierarchy) } } if (documentable is DClasslike) { // to build a full class graph, using supertypes from Documentable @@ -80,10 +73,10 @@ class FullClassHierarchyBuilder { if (source is DescriptorDocumentableSource) { val descriptor = source.descriptor as ClassDescriptor val type = descriptor.defaultType - collectSupertypesFromKotlinType(documentable.dri to type, sourceSet, supersMap) + hierarchy[sourceSet]?.let { collectSupertypesFromKotlinType(documentable.dri to type, it) } } else if (source is PsiDocumentableSource) { val psi = source.psi as PsiClass - collectSupertypesFromPsiClass(documentable.dri to psi, sourceSet, supersMap) + hierarchy[sourceSet]?.let { collectSupertypesFromPsiClass(documentable.dri to psi, it) } } } }