diff options
Diffstat (limited to 'plugins/base')
4 files changed, 356 insertions, 93 deletions
| diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index 8cdfe530..ec708ea8 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 <init> (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 <init> ()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 <init> (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 73023a86..19af0564 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<Pair<DRI, Callable>>(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 : Documentable> T.addExtensionInformation( -    extensionMap: Map<DRI, List<Callable>> -): 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 : Documentable> T.addExtensionInformation( +        classGraph: SourceSetDependent<Map<DRI, List<DRI>>>, +        extensionMap: Map<DRI, List<Callable>> +    ): 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<DRI, List<Callable>>.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<Pair<DRI, Callable>> +    ): Unit = coroutineScope { +        if (documentable is WithScope) { +            documentable.classlikes.forEach { +                launch { collectExtensions(it, channel) } +            } -private suspend fun collectExtensions( -    documentable: Documentable, -    channel: SendChannel<Pair<DRI, Callable>> -): 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 : Documentable> T.findExtensions( +        classGraph: SourceSetDependent<Map<DRI, List<DRI>>>, +        extensionMap: Map<DRI, List<Callable>> +    ): CallableExtensions? { +        val resultSet = mutableSetOf<Callable>() -        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<Pair<DRI, Callable>> = +        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> 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<DRI> = 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<Pair<DRI, Callable>> = -    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> 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<DRI> = 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 <T, U> Iterable<Pair<T, U>>.toMultiMap(): Map<T, List<U>> = +        groupBy(Pair<T, *>::first, Pair<*, U>::second)  } -private fun <T, U> Iterable<Pair<T, U>>.toMultiMap(): Map<T, List<U>> = -    groupBy(Pair<T, *>::first, Pair<*, U>::second) -  data class CallableExtensions(val extensions: Set<Callable>) : ExtraProperty<Documentable> {      companion object Key : ExtraProperty.Key<Documentable, CallableExtensions> {          override fun mergeStrategyFor(left: CallableExtensions, right: CallableExtensions) = @@ -116,14 +153,3 @@ data class CallableExtensions(val extensions: Set<Callable>) : ExtraProperty<Doc      override val key = Key  } - -//TODO IMPORTANT remove this terrible hack after updating to 1.4-M3 -fun <T : Any> ReceiveChannel<T>.consumeAsFlow(): Flow<T> = 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 00000000..d657fa32 --- /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<DRI> +typealias ClassHierarchy = SourceSetDependent<Map<DRI, Supertypes>> + +class FullClassHierarchyBuilder { +    suspend operator fun invoke(original: DModule): ClassHierarchy = coroutineScope { +        val map = original.sourceSets.associateWith { ConcurrentHashMap<DRI, List<DRI>>() } +        original.packages.parallelForEach { visitDocumentable(it, map) } +        map +    } + +    private suspend fun collectSupertypesFromKotlinType( +        driWithKType: Pair<DRI, KotlinType>, +        supersMap: MutableMap<DRI, Supertypes> +    ): 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<DRI, PsiClass>, +        supersMap: MutableMap<DRI, Supertypes> +    ): 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<MutableMap<DRI, List<DRI>>> +    ): 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 00000000..f2657ef8 --- /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 <T : WithExtraProperties<R>, 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<DClass>()) { +                checkExtension() +            } +            with((this / "classes" / "C").cast<DClass>()) { +                checkExtension() +            } +            with((this / "classes" / "D").cast<DClass>()) { +                checkExtension() +            } +            with((this / "classes" / "A").cast<DClass>()) { +                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<DClass>()) { +                checkExtension() +            } +            with((this / "classes" / "I2").cast<DInterface>()) { +                checkExtension() +            } +            with((this / "classes" / "I").cast<DInterface>()) { +                checkExtension() +            } +        } +    } + +    @Test +    fun `should be extension for external classes`() { +        inlineModelTest( +            """ +            |abstract class A<T>: AbstractList<T>() +            |fun<T> AbstractCollection<T>.extension() {} +            | +            |class B:Exception() +            |fun Throwable.extension() = "" +            """ +        ) { +            with((this / "classes" / "A").cast<DClass>()) { +                checkExtension() +            } +            with((this / "classes" / "B").cast<DClass>()) { +                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<DClass>()) { +                checkExtension() +            } +            with((this / "classes" / "C").cast<DClass>()) { +                checkExtension() +            } +            with((this / "classes" / "D").cast<DClass>()) { +                checkExtension() +            } +            with((this / "classes" / "A").cast<DClass>()) { +                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<DClass>()) { +                        checkExtension() +                    } +                    with((this / "classes" / "A").cast<DClass>()) { +                        checkExtension() +                    } +                } +            } +        } +    } +}
\ No newline at end of file | 
