/* * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package org.jetbrains.dokka.base.translators.documentables import org.jetbrains.dokka.DokkaConfiguration import org.jetbrains.dokka.Platform import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.links.PointingToDeclaration import org.jetbrains.dokka.model.Documentable import org.jetbrains.dokka.model.SourceSetDependent import org.jetbrains.dokka.model.WithScope import org.jetbrains.dokka.model.doc.* import org.jetbrains.dokka.model.orEmpty import org.jetbrains.dokka.model.properties.WithExtraProperties import org.jetbrains.dokka.pages.ContentKind import org.jetbrains.dokka.pages.ContentStyle import org.jetbrains.dokka.pages.TextStyle import org.jetbrains.dokka.utilities.DokkaLogger import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf internal const val KDOC_TAG_HEADER_LEVEL = 4 private val unnamedTagsExceptions: Set> = setOf(Property::class, Description::class, Constructor::class, Param::class, See::class) internal fun PageContentBuilder.DocumentableContentBuilder.descriptionSectionContent( documentable: Documentable, sourceSets: Set, ) { val descriptions = documentable.descriptions if (descriptions.any { it.value.root.children.isNotEmpty() }) { sourceSets.forEach { sourceSet -> descriptions[sourceSet]?.also { group(sourceSets = setOf(sourceSet), styles = emptySet()) { comment(it.root) } } } } } /** * Custom tags are tags which are not part of the [KDoc specification](https://kotlinlang.org/docs/kotlin-doc.html). For instance, a user-defined tag * which is specific to the user's code base would be considered a custom tag. * * For details, see [CustomTagContentProvider] */ internal fun PageContentBuilder.DocumentableContentBuilder.customTagSectionContent( documentable: Documentable, sourceSets: Set, customTagContentProviders: List, ) { val customTags = documentable.customTags if (customTags.isEmpty()) return sourceSets.forEach { sourceSet -> customTags.forEach { (_, sourceSetTag) -> sourceSetTag[sourceSet]?.let { tag -> customTagContentProviders.filter { it.isApplicable(tag) }.forEach { provider -> group(sourceSets = setOf(sourceSet), styles = setOf(ContentStyle.KDocTag)) { with(provider) { contentForDescription(sourceSet, tag) } } } } } } } /** * Tags in KDoc are used in form of "@tag name value". * This function handles tags that have only value parameter without name. * List of such tags: `@return`, `@author`, `@since`, `@receiver` */ internal fun PageContentBuilder.DocumentableContentBuilder.unnamedTagSectionContent( documentable: Documentable, sourceSets: Set, toHeaderString: TagWrapper.() -> String, ) { val unnamedTags = documentable.groupedTags .filterNot { (k, _) -> k.isSubclassOf(NamedTagWrapper::class) || k in unnamedTagsExceptions } .values.flatten().groupBy { it.first } .mapValues { it.value.map { it.second } } .takeIf { it.isNotEmpty() } ?: return sourceSets.forEach { sourceSet -> unnamedTags[sourceSet]?.let { tags -> if (tags.isNotEmpty()) { tags.groupBy { it::class }.forEach { (_, sameCategoryTags) -> group(sourceSets = setOf(sourceSet), styles = setOf(ContentStyle.KDocTag)) { header( level = KDOC_TAG_HEADER_LEVEL, text = sameCategoryTags.first().toHeaderString(), styles = setOf() ) sameCategoryTags.forEach { comment(it.root, styles = setOf()) } } } } } } } internal fun PageContentBuilder.DocumentableContentBuilder.paramsSectionContent(tags: GroupedTags) { val params = tags.withTypeNamed() if (params.isEmpty()) return val availableSourceSets = params.availableSourceSets() tableSectionContentBlock( blockName = "Parameters", kind = ContentKind.Parameters, sourceSets = availableSourceSets ) { availableSourceSets.forEach { sourceSet -> val possibleFallbacks = availableSourceSets.getPossibleFallback(sourceSet) params.mapNotNull { (_, param) -> (param[sourceSet] ?: param.fallback(possibleFallbacks))?.let { row(sourceSets = setOf(sourceSet), kind = ContentKind.Parameters) { text( it.name, kind = ContentKind.Parameters, styles = mainStyles + setOf(ContentStyle.RowTitle, TextStyle.Underlined) ) if (it.isNotEmpty()) { comment(it.root) } } } } } } } internal fun PageContentBuilder.DocumentableContentBuilder.seeAlsoSectionContent(tags: GroupedTags) { val seeAlsoTags = tags.withTypeNamed() if (seeAlsoTags.isEmpty()) return val availableSourceSets = seeAlsoTags.availableSourceSets() tableSectionContentBlock( blockName = "See also", kind = ContentKind.Comment, sourceSets = availableSourceSets ) { availableSourceSets.forEach { sourceSet -> val possibleFallbacks = availableSourceSets.getPossibleFallback(sourceSet) seeAlsoTags.forEach { (_, see) -> (see[sourceSet] ?: see.fallback(possibleFallbacks))?.let { seeTag -> row( sourceSets = setOf(sourceSet), kind = ContentKind.Comment ) { seeTag.address?.let { dri -> link( text = seeTag.name.removePrefix("${dri.packageName}."), address = dri, kind = ContentKind.Comment, styles = mainStyles + ContentStyle.RowTitle ) } ?: text( text = seeTag.name, kind = ContentKind.Comment, styles = mainStyles + ContentStyle.RowTitle ) if (seeTag.isNotEmpty()) { comment(seeTag.root) } } } } } } } /** * Used for multi-value tags (e.g. params) when values are missed on some platforms. * It this case description is inherited from parent platform. * E.g. if param hasn't description in JVM, the description is taken from common. */ private fun Set.getPossibleFallback(sourceSet: DokkaConfiguration.DokkaSourceSet) = this.filter { it.sourceSetID in sourceSet.dependentSourceSets } private fun Map.fallback(sourceSets: List): V? = sourceSets.firstOrNull { it in this.keys }.let { this[it] } internal fun PageContentBuilder.DocumentableContentBuilder.throwsSectionContent(tags: GroupedTags) { val throwsTags = tags.withTypeNamed() if (throwsTags.isEmpty()) return val availableSourceSets = throwsTags.availableSourceSets() tableSectionContentBlock( blockName = "Throws", kind = ContentKind.Main, sourceSets = availableSourceSets ) { throwsTags.forEach { (throwsName, throwsPerSourceSet) -> throwsPerSourceSet.forEach { (sourceSet, throws) -> row(sourceSets = setOf(sourceSet)) { group(styles = mainStyles + ContentStyle.RowTitle) { throws.exceptionAddress?.let { val className = it.takeIf { it.target is PointingToDeclaration }?.classNames link(text = className ?: throwsName, address = it) } ?: text(throwsName) } if (throws.isNotEmpty()) { comment(throws.root) } } } } } } private fun TagWrapper.isNotEmpty() = this.children.isNotEmpty() internal fun PageContentBuilder.DocumentableContentBuilder.samplesSectionContent(tags: GroupedTags) { val samples = tags.withTypeNamed() if (samples.isEmpty()) return val availableSourceSets = samples.availableSourceSets() header(KDOC_TAG_HEADER_LEVEL, "Samples", kind = ContentKind.Sample, sourceSets = availableSourceSets) availableSourceSets.forEach { sourceSet -> group( sourceSets = setOf(sourceSet), kind = ContentKind.Sample, styles = setOf(TextStyle.Monospace, ContentStyle.RunnableSample), ) { samples.filter { it.value.isEmpty() || sourceSet in it.value } .forEach { text(text = it.key, sourceSets = setOf(sourceSet)) } } } } internal fun PageContentBuilder.DocumentableContentBuilder.inheritorsSectionContent( documentable: Documentable, logger: DokkaLogger, ) { val inheritors = if (documentable is WithScope) documentable.inheritors() else return if (inheritors.values.none()) return // split content section for the case: // parent is in the shared source set (without expect-actual) and inheritor is in the platform code if (documentable.isDefinedInSharedSourceSetOnly(inheritors.keys.toSet())) sharedSourceSetOnlyInheritorsSectionContent(inheritors, logger) else multiplatformInheritorsSectionContent(documentable, inheritors, logger) } private fun WithScope.inheritors(): SourceSetDependent> { @Suppress("UNCHECKED_CAST") val withExtra = this as? WithExtraProperties return withExtra ?.let { it.extra[InheritorsInfo] } ?.let { inheritors -> inheritors.value.filter { it.value.isNotEmpty() } } .orEmpty() } /** * Detect that documentable is located only in the shared code without expect-actuals * Value of `analysisPlatform` will be [Platform.common] in cases if a source set shared between 2 different platforms. * But if it shared between 2 same platforms (e.g. jvm("awt") and jvm("android")) * then the source set will be still marked as jvm platform. * * So, we also try to check if any of inheritors source sets depends on current documentable source set. * that will mean that the source set is shared. */ private fun Documentable.isDefinedInSharedSourceSetOnly(inheritorsSourceSets: Set) = sourceSets.size == 1 && (sourceSets.first().analysisPlatform == Platform.common || sourceSets.first().hasDependentSourceSet(inheritorsSourceSets)) private fun DokkaConfiguration.DokkaSourceSet.hasDependentSourceSet( sourceSets: Set, ) = sourceSets.any { sourceSet -> sourceSet.dependentSourceSets.any { it == this.sourceSetID } } private fun PageContentBuilder.DocumentableContentBuilder.multiplatformInheritorsSectionContent( documentable: Documentable, inheritors: Map>, logger: DokkaLogger, ) { // intersect is used for removing duplication in case of merged classlikes from different platforms val availableSourceSets = inheritors.keys.toSet().intersect(documentable.sourceSets) tableSectionContentBlock( blockName = "Inheritors", kind = ContentKind.Inheritors, sourceSets = availableSourceSets ) { availableSourceSets.forEach { sourceSet -> inheritors[sourceSet]?.forEach { classlike: DRI -> inheritorRow(classlike, logger, sourceSet) } } } } private fun PageContentBuilder.DocumentableContentBuilder.sharedSourceSetOnlyInheritorsSectionContent( inheritors: Map>, logger: DokkaLogger, ) { val uniqueInheritors = inheritors.values.flatten().toSet() tableSectionContentBlock( blockName = "Inheritors", kind = ContentKind.Inheritors, ) { uniqueInheritors.forEach { classlike -> inheritorRow(classlike, logger) } } } private fun PageContentBuilder.TableBuilder.inheritorRow( classlike: DRI, logger: DokkaLogger, sourceSet: DokkaConfiguration.DokkaSourceSet? = null, ) = row { link( text = classlike.friendlyClassName() ?: classlike.toString().also { logger.warn("No class name found for DRI $classlike") }, address = classlike, sourceSets = sourceSet?.let { setOf(it) } ?: mainSourcesetData ) } private fun PageContentBuilder.DocumentableContentBuilder.tableSectionContentBlock( blockName: String, kind: ContentKind, sourceSets: Set = mainSourcesetData, body: PageContentBuilder.TableBuilder.() -> Unit, ) { header(KDOC_TAG_HEADER_LEVEL, text = blockName, kind = kind, sourceSets = sourceSets) table( kind = kind, sourceSets = sourceSets, ) { body() } } private fun DRI.friendlyClassName() = classNames?.substringAfterLast(".") private fun Map>.availableSourceSets() = values.flatMap { it.keys }.toSet()