diff options
Diffstat (limited to 'plugins/base/src')
11 files changed, 784 insertions, 236 deletions
diff --git a/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt b/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt index 21757d70..8ea8818d 100644 --- a/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt +++ b/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt @@ -8,12 +8,14 @@ data class DokkaBaseConfiguration( var customStyleSheets: List<File> = defaultCustomStyleSheets, var customAssets: List<File> = defaultCustomAssets, var separateInheritedMembers: Boolean = separateInheritedMembersDefault, - var footerMessage: String = defaultFooterMessage + var footerMessage: String = defaultFooterMessage, + var mergeImplicitExpectActualDeclarations: Boolean = mergeImplicitExpectActualDeclarationsDefault ) : ConfigurableBlock { companion object { val defaultFooterMessage = "© ${Year.now().value} Copyright" val defaultCustomStyleSheets: List<File> = emptyList() val defaultCustomAssets: List<File> = emptyList() const val separateInheritedMembersDefault: Boolean = false + const val mergeImplicitExpectActualDeclarationsDefault: Boolean = false } }
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt index 43526dc3..3297d09f 100644 --- a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt +++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt @@ -1,14 +1,14 @@ package org.jetbrains.dokka.base.renderers.html -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.DokkaBaseConfiguration import org.jetbrains.dokka.base.renderers.sourceSets -import org.jetbrains.dokka.base.templating.AddToSearch import org.jetbrains.dokka.base.templating.AddToSourcesetDependencies import org.jetbrains.dokka.base.templating.toJsonString -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.DEnumEntry +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.withDescendants import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.configuration @@ -30,19 +30,18 @@ abstract class NavigationDataProvider { when { this !is ClasslikePageNode -> children.filterIsInstance<ContentPage>().map { visit(it) } - documentable is DEnum -> - children.filter { it is ContentPage && it.documentable is DEnumEntry }.map { visit(it as ContentPage) } + documentables.any { it is DEnum } -> + children.filter { it is WithDocumentables && it.documentables.any { it is DEnumEntry } } + .map { visit(it as ContentPage) } else -> emptyList() }.sortedBy { it.name.toLowerCase() } /** - * Parenthesis is applied in 2 cases: + * Parenthesis is applied in 1 case: * - page only contains functions (therefore documentable from this page is [DFunction]) - * - page merges only functions (documentable is null because [SameMethodNamePageMergerStrategy][org.jetbrains.dokka.base.transformers.pages.merger.SameMethodNamePageMergerStrategy] - * removes it from page) */ private val ContentPage.displayableName: String - get() = if (documentable is DFunction || (documentable == null && dri.all { it.callable != null })) { + get() = if (this is WithDocumentables && documentables.all { it is DFunction }) { "$name()" } else { name diff --git a/plugins/base/src/main/kotlin/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt b/plugins/base/src/main/kotlin/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt index 2fb70fc8..6c12c719 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt @@ -23,7 +23,7 @@ class SameMethodNamePageMergerStrategy(val logger: DokkaLogger) : PageMergerStra children = members.flatMap { it.children }.distinct(), content = squashDivergentInstances(members).withSourceSets(members.allSourceSets()), embeddedResources = members.flatMap { it.embeddedResources }.distinct(), - documentable = null + documentables = members.flatMap { it.documentables } ) return (pages - members) + listOf(merged) @@ -37,9 +37,9 @@ class SameMethodNamePageMergerStrategy(val logger: DokkaLogger) : PageMergerStra .reduce { acc, node -> acc.mapTransform<ContentDivergentGroup, ContentNode> { g -> g.copy(children = (g.children + - (node.dfs { it is ContentDivergentGroup && it.groupID == g.groupID } as? ContentDivergentGroup) - ?.children?.single() - ).filterNotNull() + ((node.dfs { it is ContentDivergentGroup && it.groupID == g.groupID } as? ContentDivergentGroup) + ?.children ?: emptyList()) + ) ) } } diff --git a/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt index 3ff7a77d..db133bb8 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt @@ -35,14 +35,18 @@ abstract class SamplesTransformer(val context: DokkaContext) : PageTransformer { "<script src=\"https://unpkg.com/kotlin-playground@1\"></script>" return input.transformContentPagesTree { page -> - page.documentable?.documentation?.entries?.fold(page) { acc, entry -> - entry.value.children.filterIsInstance<Sample>().fold(acc) { acc, sample -> + val samples = (page as? WithDocumentables)?.documentables?.flatMap { + it.documentation.entries.flatMap { entry -> + entry.value.children.filterIsInstance<Sample>().map { entry.key to it } + } + } + + samples?.fold(page as ContentPage) { acc, (sampleSourceSet, sample) -> acc.modified( - content = acc.content.addSample(page, entry.key, sample.name, analysis), + content = acc.content.addSample(page, sampleSourceSet, sample.name, analysis), embeddedResources = acc.embeddedResources + kotlinPlaygroundScript ) - } - } ?: page + } ?: page } } diff --git a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt index f66ff222..93305055 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt @@ -1,15 +1,15 @@ package org.jetbrains.dokka.base.transformers.pages.sourcelinks -import com.intellij.psi.PsiElement import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder -import org.jetbrains.dokka.model.DocumentableSource import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet import org.jetbrains.dokka.analysis.DescriptorDocumentableSource import org.jetbrains.dokka.analysis.PsiDocumentableSource import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.model.DocumentableSource import org.jetbrains.dokka.model.WithSources import org.jetbrains.dokka.model.toDisplaySourceSets import org.jetbrains.dokka.pages.* @@ -32,8 +32,9 @@ class SourceLinksTransformer(val context: DokkaContext) : PageTransformer { override fun invoke(input: RootPageNode) = input.transformContentPagesTree { node -> - when (val documentable = node.documentable) { - is WithSources -> resolveSources(documentable) + when (node) { + is WithDocumentables -> + node.documentables.filterIsInstance<WithSources>().flatMap { resolveSources(it) } .takeIf { it.isNotEmpty() } ?.let { node.addSourcesContent(it) } ?: node @@ -65,23 +66,26 @@ class SourceLinksTransformer(val context: DokkaContext) : PageTransformer { private fun PageContentBuilder.buildSourcesContent( node: ContentPage, sources: List<Pair<DokkaSourceSet, String>> - ) = contentFor( - node.dri.first(), - node.documentable!!.sourceSets.toSet() - ) { - header(2, "Sources", kind = ContentKind.Source) - +ContentTable( - header = emptyList(), - children = sources.map { - buildGroup(node.dri, setOf(it.first), kind = ContentKind.Source, extra = mainExtra + SymbolAnchorHint(it.second, ContentKind.Source)) { - link("${it.first.displayName} source", it.second) - } - }, - dci = DCI(node.dri, ContentKind.Source), - sourceSets = node.documentable!!.sourceSets.toDisplaySourceSets(), - style = emptySet(), - extra = mainExtra + SimpleAttr.header("Sources") - ) + ): ContentGroup { + val documentables = (node as? WithDocumentables)?.documentables.orEmpty() + return contentFor( + node.dri, + documentables.flatMap { it.sourceSets }.toSet() + ) { + header(2, "Sources", kind = ContentKind.Source) + +ContentTable( + header = emptyList(), + children = sources.map { + buildGroup(node.dri, setOf(it.first), kind = ContentKind.Source, extra = mainExtra + SymbolAnchorHint(it.second, ContentKind.Source)) { + link("${it.first.displayName} source", it.second) + } + }, + dci = DCI(node.dri, ContentKind.Source), + sourceSets = documentables.flatMap { it.sourceSets }.toDisplaySourceSets(), + style = emptySet(), + extra = mainExtra + SimpleAttr.header("Sources") + ) + } } private fun DocumentableSource.toLink(sourceLink: SourceLink): String { diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt index e2aca6f9..c5136c27 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -1,9 +1,14 @@ package org.jetbrains.dokka.base.translators.documentables +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint import org.jetbrains.dokka.base.signatures.SignatureProvider import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions +import org.jetbrains.dokka.base.transformers.documentables.ClashingDriIdentifier import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.* @@ -15,11 +20,6 @@ import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.kotlin.utils.addToStdlib.safeAs import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf -import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet -import org.jetbrains.dokka.base.DokkaBaseConfiguration -import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint -import org.jetbrains.dokka.base.transformers.documentables.ClashingDriIdentifier -import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider private typealias GroupedTags = Map<KClass<out TagWrapper>, List<Pair<DokkaSourceSet?, TagWrapper>>> @@ -35,66 +35,139 @@ open class DefaultPageCreator( ) { protected open val contentBuilder = PageContentBuilder(commentsToContentConverter, signatureProvider, logger) + protected val mergeImplicitExpectActualDeclarations = + configuration?.mergeImplicitExpectActualDeclarations + ?: DokkaBaseConfiguration.mergeImplicitExpectActualDeclarationsDefault + protected val separateInheritedMembers = configuration?.separateInheritedMembers ?: DokkaBaseConfiguration.separateInheritedMembersDefault - open fun pageForModule(m: DModule) = - ModulePageNode(m.name.ifEmpty { "<root>" }, contentForModule(m), m, m.packages.map(::pageForPackage)) + open fun pageForModule(m: DModule): ModulePageNode = + ModulePageNode(m.name.ifEmpty { "<root>" }, contentForModule(m), listOf(m), m.packages.map(::pageForPackage)) open fun pageForPackage(p: DPackage): PackagePageNode = PackagePageNode( - p.name, contentForPackage(p), setOf(p.dri), p, - p.classlikes.renameClashingDocumentable().map(::pageForClasslike) + - p.functions.renameClashingDocumentable() - .map(::pageForFunction) + p.properties.mapNotNull(::pageForProperty) + p.name, contentForPackage(p), setOf(p.dri), listOf(p), + if (mergeImplicitExpectActualDeclarations) + p.classlikes.mergeClashingDocumentable().map(::pageForClasslikes) + + p.functions.mergeClashingDocumentable().map(::pageForFunctions) + + p.properties.mergeClashingDocumentable().map(::pageForProperties) + else + p.classlikes.renameClashingDocumentable().map(::pageForClasslike) + + p.functions.renameClashingDocumentable().map(::pageForFunction) + + p.properties.mapNotNull(::pageForProperty) ) - open fun pageForEnumEntry(e: DEnumEntry): ClasslikePageNode = - ClasslikePageNode( - e.nameAfterClash(), contentForEnumEntry(e), setOf(e.dri), e, - e.classlikes.renameClashingDocumentable().map(::pageForClasslike) + - e.filteredFunctions.renameClashingDocumentable().map(::pageForFunction) + - e.filteredProperties.renameClashingDocumentable().mapNotNull(::pageForProperty) - ) + open fun pageForEnumEntry(e: DEnumEntry): ClasslikePageNode = pageForEnumEntries(listOf(e)) - open fun pageForClasslike(c: DClasslike): ClasslikePageNode { - val constructors = if (c is WithConstructors) c.constructors else emptyList() + open fun pageForClasslike(c: DClasslike): ClasslikePageNode = pageForClasslikes(listOf(c)) + + open fun pageForEnumEntries(documentables: List<DEnumEntry>): ClasslikePageNode { + val dri = documentables.dri.also { + if (it.size != 1) { + logger.error("Documentable dri should have the same one ${it.first()} inside the one page!") + } + } + + val classlikes = documentables.flatMap { it.classlikes } + val functions = documentables.flatMap { it.filteredFunctions } + val props = documentables.flatMap { it.filteredProperties } + + val childrenPages = if (mergeImplicitExpectActualDeclarations) + functions.mergeClashingDocumentable().map(::pageForFunctions) + + props.mergeClashingDocumentable().map(::pageForProperties) + else + classlikes.renameClashingDocumentable().map(::pageForClasslike) + + functions.renameClashingDocumentable().map(::pageForFunction) + + props.renameClashingDocumentable().mapNotNull(::pageForProperty) return ClasslikePageNode( - c.nameAfterClash(), contentForClasslike(c), setOf(c.dri), c, - constructors.map(::pageForFunction) + - c.classlikes.renameClashingDocumentable().map(::pageForClasslike) + - c.filteredFunctions.renameClashingDocumentable().map(::pageForFunction) + - c.filteredProperties.renameClashingDocumentable().mapNotNull(::pageForProperty) + - if (c is DEnum) c.entries.map(::pageForEnumEntry) else emptyList() + documentables.first().nameAfterClash(), contentForClasslikesAndEntries(documentables), dri, documentables, + childrenPages + ) + } + + open fun pageForClasslikes(documentables: List<DClasslike>): ClasslikePageNode { + val dri = documentables.dri.also { + if (it.size != 1) { + logger.error("Documentable dri should have the same one ${it.first()} inside the one page!") + } + } + + val constructors = documentables.flatMap { if (it is WithConstructors) it.constructors else emptyList() } + + val classlikes = documentables.flatMap { it.classlikes } + val functions = documentables.flatMap { it.filteredFunctions } + val props = documentables.flatMap { it.filteredProperties } + val entries = documentables.flatMap { if (it is DEnum) it.entries else emptyList() } + + val childrenPages = constructors.map(::pageForFunction) + + if (mergeImplicitExpectActualDeclarations) + classlikes.mergeClashingDocumentable().map(::pageForClasslikes) + + functions.mergeClashingDocumentable().map(::pageForFunctions) + + props.mergeClashingDocumentable().map(::pageForProperties) + + entries.mergeClashingDocumentable().map(::pageForEnumEntries) + else + classlikes.renameClashingDocumentable().map(::pageForClasslike) + + functions.renameClashingDocumentable().map(::pageForFunction) + + props.renameClashingDocumentable().mapNotNull(::pageForProperty) + + entries.renameClashingDocumentable().map(::pageForEnumEntry) + + return ClasslikePageNode( + documentables.first().nameAfterClash(), contentForClasslikesAndEntries(documentables), dri, documentables, + childrenPages ) } private fun <T> T.toClashedName() where T : Documentable, T : WithExtraProperties<T> = (extra[ClashingDriIdentifier]?.value?.joinToString(", ", "[", "]") { it.displayName } ?: "") + name.orEmpty() - @Suppress("UNCHECKED_CAST") private fun <T : Documentable> List<T>.renameClashingDocumentable(): List<T> = groupBy { it.dri }.values.flatMap { elements -> if (elements.size == 1) elements else elements.mapNotNull { element -> - when (element) { - is DClass -> element.copy(extra = element.extra + DriClashAwareName(element.toClashedName())) - is DObject -> element.copy(extra = element.extra + DriClashAwareName(element.toClashedName())) - is DAnnotation -> element.copy(extra = element.extra + DriClashAwareName(element.toClashedName())) - is DInterface -> element.copy(extra = element.extra + DriClashAwareName(element.toClashedName())) - is DEnum -> element.copy(extra = element.extra + DriClashAwareName(element.toClashedName())) - is DFunction -> element.copy(extra = element.extra + DriClashAwareName(element.toClashedName())) - is DProperty -> element.copy(extra = element.extra + DriClashAwareName(element.toClashedName())) - is DTypeAlias -> element.copy(extra = element.extra + DriClashAwareName(element.toClashedName())) - else -> null - } as? T? + element.renameClashingDocumentable() } } - open fun pageForFunction(f: DFunction) = MemberPageNode(f.nameAfterClash(), contentForFunction(f), setOf(f.dri), f) + @Suppress("UNCHECKED_CAST") + private fun <T : Documentable> T.renameClashingDocumentable(): T? = when (this) { + is DClass -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DObject -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DAnnotation -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DInterface -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DEnum -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DFunction -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DProperty -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DTypeAlias -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + else -> null + } as? T? + + private fun <T : Documentable> List<T>.mergeClashingDocumentable(): List<List<T>> = + groupBy { it.dri }.values.toList() + + open fun pageForFunction(f: DFunction) = + MemberPageNode(f.nameAfterClash(), contentForFunction(f), setOf(f.dri), listOf(f)) + + open fun pageForFunctions(fs: List<DFunction>): MemberPageNode { + val dri = fs.dri.also { + if (it.size != 1) { + logger.error("Function dri should have the same one ${it.first()} inside the one page!") + } + } + return MemberPageNode(fs.first().nameAfterClash(), contentForMembers(fs), dri, fs) + } open fun pageForProperty(p: DProperty): MemberPageNode? = - MemberPageNode(p.nameAfterClash(), contentForProperty(p), setOf(p.dri), p) + MemberPageNode(p.nameAfterClash(), contentForProperty(p), setOf(p.dri), listOf(p)) + + open fun pageForProperties(ps: List<DProperty>): MemberPageNode { + val dri = ps.dri.also { + if (it.size != 1) { + logger.error("Property dri should have the same one ${it.first()} inside the one page!") + } + } + return MemberPageNode(ps.first().nameAfterClash(), contentForMembers(ps), dri, ps) + } private fun <T> T.isInherited(): Boolean where T : Documentable, T : WithExtraProperties<T> = sourceSets.all { sourceSet -> extra[InheritedMember]?.isInherited(sourceSet) == true } @@ -168,148 +241,193 @@ open class DefaultPageCreator( } } + protected open fun contentForScopes( + scopes: List<WithScope>, + sourceSets: Set<DokkaSourceSet> + ): ContentGroup { + val types = scopes.flatMap { it.classlikes } + scopes.filterIsInstance<DPackage>().flatMap { it.typealiases } + val inheritors = scopes.fold(mutableMapOf<DokkaSourceSet, List<DRI>>()) { acc, scope -> + val inheritorsForScope = + scope.safeAs<WithExtraProperties<Documentable>>()?.let { it.extra[InheritorsInfo] }?.let { inheritors -> + inheritors.value.filter { it.value.isNotEmpty() } + }.orEmpty() + inheritorsForScope.forEach { (k, v) -> + acc.compute(k) { _, value -> value?.plus(v) ?: v } + } + acc + } + + return contentForScope( + @Suppress("UNCHECKED_CAST") + (scopes as List<Documentable>).dri, + sourceSets, + types, + scopes.flatMap { it.functions }, + scopes.flatMap { it.properties }, + inheritors + ) + } + protected open fun contentForScope( s: WithScope, dri: DRI, sourceSets: Set<DokkaSourceSet> - ) = contentBuilder.contentFor(s as Documentable) { + ): ContentGroup { val types = listOf( s.classlikes, (s as? DPackage)?.typealiases ?: emptyList() ).flatten() + val inheritors = + s.safeAs<WithExtraProperties<Documentable>>()?.let { it.extra[InheritorsInfo] }?.let { inheritors -> + inheritors.value.filter { it.value.isNotEmpty() } + }.orEmpty() + + return contentForScope(setOf(dri), sourceSets, types, s.functions, s.properties, inheritors) + } + + protected open fun contentForScope( + dri: Set<DRI>, + sourceSets: Set<DokkaSourceSet>, + types: List<Documentable>, + functions: List<DFunction>, + properties: List<DProperty>, + inheritors: SourceSetDependent<List<DRI>> + ) = contentBuilder.contentFor(dri, sourceSets) { divergentBlock("Types", types, ContentKind.Classlikes, extra = mainExtra + SimpleAttr.header("Types")) if (separateInheritedMembers) { - val (inheritedFunctions, memberFunctions) = s.functions.splitInherited() - val (inheritedProperties, memberProperties) = s.properties.splitInherited() + val (inheritedFunctions, memberFunctions) = functions.splitInherited() + val (inheritedProperties, memberProperties) = properties.splitInherited() propertiesBlock("Properties", memberProperties, sourceSets) propertiesBlock("Inherited properties", inheritedProperties, sourceSets) functionsBlock("Functions", memberFunctions) functionsBlock("Inherited functions", inheritedFunctions) } else { - functionsBlock("Functions", s.functions) - propertiesBlock("Properties", s.properties, sourceSets) + functionsBlock("Functions", functions) + propertiesBlock("Properties", properties, sourceSets) } - s.safeAs<WithExtraProperties<Documentable>>()?.let { it.extra[InheritorsInfo] }?.let { inheritors -> - val map = inheritors.value.filter { it.value.isNotEmpty() } - if (map.values.any()) { - header(2, "Inheritors") { } - +ContentTable( - header = listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData) { - text("Name") - }), - children = map.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } } - .groupBy({ it.second }, { it.first }).map { (classlike, platforms) -> - val label = classlike.classNames?.substringAfterLast(".") ?: classlike.toString() - .also { logger.warn("No class name found for DRI $classlike") } - buildGroup( - setOf(classlike), - platforms.toSet(), - ContentKind.Inheritors, - extra = mainExtra + SymbolAnchorHint(label, ContentKind.Inheritors) - ) { - link(label, classlike) - } - }, - dci = DCI(setOf(dri), ContentKind.Inheritors), - sourceSets = sourceSets.toDisplaySourceSets(), - style = emptySet(), - extra = mainExtra + SimpleAttr.header("Inheritors") - ) - } + if (inheritors.values.any()) { + header(2, "Inheritors") { } + +ContentTable( + header = listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData) { + text("Name") + }), + children = inheritors.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } } + .groupBy({ it.second }, { it.first }).map { (classlike, platforms) -> + val label = classlike.classNames?.substringAfterLast(".") ?: classlike.toString() + .also { logger.warn("No class name found for DRI $classlike") } + buildGroup( + setOf(classlike), + platforms.toSet(), + ContentKind.Inheritors, + extra = mainExtra + SymbolAnchorHint(label, ContentKind.Inheritors) + ) { + link(label, classlike) + } + }, + dci = DCI(dri, ContentKind.Inheritors), + sourceSets = sourceSets.toDisplaySourceSets(), + style = emptySet(), + extra = mainExtra + SimpleAttr.header("Inheritors") + ) } } private fun Iterable<DFunction>.sorted() = sortedWith(compareBy({ it.name }, { it.parameters.size }, { it.dri.toString() })) - protected open fun contentForEnumEntry(e: DEnumEntry) = contentBuilder.contentFor(e) { - group(kind = ContentKind.Cover) { - cover(e.name) - sourceSetDependentHint(e.dri, e.sourceSets.toSet()) { - +buildSignature(e) - +contentForDescription(e) - } - } - group(styles = setOf(ContentStyle.TabbedContent)) { - +contentForComments(e) - +contentForScope(e, e.dri, e.sourceSets) - } - } - - protected open fun contentForClasslike(c: DClasslike) = contentBuilder.contentFor(c) { - @Suppress("UNCHECKED_CAST") - val extensions = (c as WithExtraProperties<DClasslike>) - .extra[CallableExtensions]?.extensions - ?.filterIsInstance<Documentable>().orEmpty() - // Extensions are added to sourceSets since they can be placed outside the sourceSets from classlike - // Example would be an Interface in common and extension function in jvm - group(kind = ContentKind.Cover, sourceSets = mainSourcesetData + extensions.sourceSets) { - cover(c.name.orEmpty()) - sourceSetDependentHint(c.dri, c.sourceSets) { - +buildSignature(c) - +contentForDescription(c) + /** + * @param documentables a list of [DClasslike] and [DEnumEntry] with the same dri in different sourceSets + */ + protected open fun contentForClasslikesAndEntries(documentables: List<Documentable>): ContentGroup = + contentBuilder.contentFor(documentables.dri, documentables.sourceSets) { + val classlikes = documentables.filterIsInstance<DClasslike>() + + @Suppress("UNCHECKED_CAST") + val extensions = (classlikes as List<WithExtraProperties<DClasslike>>).flatMap { + it.extra[CallableExtensions]?.extensions + ?.filterIsInstance<Documentable>().orEmpty() } - } - group(styles = setOf(ContentStyle.TabbedContent), sourceSets = mainSourcesetData + extensions.sourceSets) { - +contentForComments(c) - if (c is WithConstructors) { - block( - "Constructors", - 2, - ContentKind.Constructors, - c.constructors, - c.sourceSets, - needsAnchors = true, - extra = PropertyContainer.empty<ContentNode>() + SimpleAttr.header("Constructors") - ) { - link(it.name, it.dri, kind = ContentKind.Main, styles = setOf(ContentStyle.RowTitle)) - sourceSetDependentHint( - it.dri, - it.sourceSets.toSet(), - kind = ContentKind.SourceSetDependentHint, - styles = emptySet(), - extra = PropertyContainer.empty() - ) { + // Extensions are added to sourceSets since they can be placed outside the sourceSets from classlike + // Example would be an Interface in common and extension function in jvm + group(kind = ContentKind.Cover, sourceSets = mainSourcesetData + extensions.sourceSets) { + cover(documentables.first().name.orEmpty()) + sourceSetDependentHint(documentables.dri, documentables.sourceSets) { + documentables.forEach { +buildSignature(it) - contentForBrief(it) + +contentForDescription(it) } } } - if (c is DEnum) { - block( - "Entries", - 2, - ContentKind.Classlikes, - c.entries, - c.sourceSets.toSet(), - needsSorting = false, - needsAnchors = true, - extra = mainExtra + SimpleAttr.header("Entries"), - styles = emptySet() - ) { - link(it.name, it.dri) - sourceSetDependentHint( - it.dri, - it.sourceSets.toSet(), - kind = ContentKind.SourceSetDependentHint, - extra = PropertyContainer.empty() - ) { - +buildSignature(it) - contentForBrief(it) + + group(styles = setOf(ContentStyle.TabbedContent), sourceSets = mainSourcesetData + extensions.sourceSets) { + +contentForComments(documentables) + val csWithConstructor = classlikes.filterIsInstance<WithConstructors>() + if (csWithConstructor.isNotEmpty()) { + val constructorsToDocumented = csWithConstructor.flatMap { it.constructors } + multiBlock( + "Constructors", + 2, + ContentKind.Constructors, + constructorsToDocumented.groupBy { it.parameters.map { it.dri } } + .map { (_, v) -> v.first().name to v }, + @Suppress("UNCHECKED_CAST") + (csWithConstructor as List<Documentable>).sourceSets, + needsAnchors = true, + extra = PropertyContainer.empty<ContentNode>() + SimpleAttr.header("Constructors") + ) { key, ds -> + link(key, ds.first().dri, kind = ContentKind.Main, styles = setOf(ContentStyle.RowTitle)) + sourceSetDependentHint( + ds.dri, + ds.sourceSets, + kind = ContentKind.SourceSetDependentHint, + styles = emptySet(), + extra = PropertyContainer.empty<ContentNode>() + ) { + ds.forEach { + +buildSignature(it) + contentForBrief(it) + } + } } } - } - +contentForScope(c, c.dri, c.sourceSets) + val csEnum = classlikes.filterIsInstance<DEnum>() + if (csEnum.isNotEmpty()) { + multiBlock( + "Entries", + 2, + ContentKind.Classlikes, + csEnum.flatMap { it.entries }.groupBy { it.name }.toList(), + csEnum.sourceSets, + needsSorting = false, + needsAnchors = true, + extra = mainExtra + SimpleAttr.header("Entries"), + styles = emptySet() + ) { key, ds -> + link(key, ds.first().dri) + sourceSetDependentHint( + ds.dri, + ds.sourceSets, + kind = ContentKind.SourceSetDependentHint, + extra = PropertyContainer.empty<ContentNode>() + ) { + ds.forEach { + +buildSignature(it) + contentForBrief(it) + } + } + } + } + +contentForScopes(documentables.filterIsInstance<WithScope>(), documentables.sourceSets) - divergentBlock( - "Extensions", - extensions, - ContentKind.Extensions, - extra = mainExtra + SimpleAttr.header("Extensions") - ) + divergentBlock( + "Extensions", + extensions, + ContentKind.Extensions, + extra = mainExtra + SimpleAttr.header("Extensions") + ) + } } - } @Suppress("UNCHECKED_CAST") private inline fun <reified T : TagWrapper> GroupedTags.withTypeUnnamed(): SourceSetDependent<T> = @@ -347,7 +465,7 @@ open class DefaultPageCreator( if (customTags.isNotEmpty()) { group(styles = setOf(TextStyle.Block)) { platforms.forEach { platform -> - customTags.forEach { (tagName, sourceSetTag) -> + customTags.forEach { (_, sourceSetTag) -> sourceSetTag[platform]?.let { tag -> customTagContentProviders.filter { it.isApplicable(tag) }.forEach { provider -> with(provider) { @@ -379,8 +497,8 @@ open class DefaultPageCreator( }.children } - private fun Documentable.getPossibleFallbackSourcesets(sourceSet: DokkaSourceSet) = - this.sourceSets.filter { it.sourceSetID in sourceSet.dependentSourceSets } + private fun Set<DokkaSourceSet>.getPossibleFallbackSourcesets(sourceSet: DokkaSourceSet) = + this.filter { it.sourceSetID in sourceSet.dependentSourceSets } private fun <V> Map<DokkaSourceSet, V>.fallback(sourceSets: List<DokkaSourceSet>): V? = sourceSets.firstOrNull { it in this.keys }.let { this[it] } @@ -388,8 +506,19 @@ open class DefaultPageCreator( protected open fun contentForComments( d: Documentable, isPlatformHintedContent: Boolean = true + ) = contentForComments(d.dri, d.sourceSets, d.groupedTags, isPlatformHintedContent) + + protected open fun contentForComments( + d: List<Documentable>, + isPlatformHintedContent: Boolean = true + ) = contentForComments(d.first().dri, d.sourceSets, d.groupedTags, isPlatformHintedContent) + + protected open fun contentForComments( + dri: DRI, + sourceSets: Set<DokkaSourceSet>, + tags: GroupedTags, + isPlatformHintedContent: Boolean = true ): List<ContentNode> { - val tags = d.groupedTags fun DocumentableContentBuilder.buildContent( platforms: Set<DokkaSourceSet>, @@ -417,7 +546,7 @@ open class DefaultPageCreator( buildContent(availablePlatforms) { table(kind = ContentKind.Parameters, sourceSets = availablePlatforms) { availablePlatforms.forEach { platform -> - val possibleFallbacks = d.getPossibleFallbackSourcesets(platform) + val possibleFallbacks = sourceSets.getPossibleFallbackSourcesets(platform) params.mapNotNull { (_, param) -> (param[platform] ?: param.fallback(possibleFallbacks))?.let { row(sourceSets = setOf(platform), kind = ContentKind.Parameters) { @@ -451,7 +580,7 @@ open class DefaultPageCreator( buildContent(availablePlatforms) { table(kind = ContentKind.Sample) { availablePlatforms.forEach { platform -> - val possibleFallbacks = d.getPossibleFallbackSourcesets(platform) + val possibleFallbacks = sourceSets.getPossibleFallbackSourcesets(platform) seeAlsoTags.forEach { (_, see) -> (see[platform] ?: see.fallback(possibleFallbacks))?.let { row( @@ -537,12 +666,11 @@ open class DefaultPageCreator( } } - return contentBuilder.contentFor(d) { + return contentBuilder.contentFor(dri, sourceSets) { if (tags.isNotEmpty()) { contentForSamples() contentForSeeAlso() - if (d !is DProperty) - contentForParams() + contentForParams() contentForThrows() } }.children @@ -558,7 +686,8 @@ open class DefaultPageCreator( We purposefully ignore all other tags as they should not be visible in brief */ - it.firstMemberOfTypeOrNull<Description>() ?: it.firstMemberOfTypeOrNull<Property>().takeIf { documentable is DProperty } + it.firstMemberOfTypeOrNull<Description>() ?: it.firstMemberOfTypeOrNull<Property>() + .takeIf { documentable is DProperty } }?.let { group(sourceSets = setOf(sourceSet), kind = ContentKind.BriefComment) { if (documentable.hasSeparatePage) createBriefComment(documentable, sourceSet, it) @@ -568,9 +697,13 @@ open class DefaultPageCreator( } } - private fun DocumentableContentBuilder.createBriefComment(documentable: Documentable, sourceSet: DokkaSourceSet, tag: TagWrapper){ + private fun DocumentableContentBuilder.createBriefComment( + documentable: Documentable, + sourceSet: DokkaSourceSet, + tag: TagWrapper + ) { (documentable as? WithSources)?.documentableLanguage(sourceSet)?.let { - when(it){ + when (it) { DocumentableLanguage.KOTLIN -> firstParagraphComment(tag.root) DocumentableLanguage.JAVA -> firstSentenceComment(tag.root) } @@ -581,22 +714,27 @@ open class DefaultPageCreator( protected open fun contentForProperty(p: DProperty) = contentForMember(p) - protected open fun contentForMember(d: Documentable) = contentBuilder.contentFor(d) { - group(kind = ContentKind.Cover) { - cover(d.name.orEmpty()) - } - divergentGroup(ContentDivergentGroup.GroupID("member")) { - instance(setOf(d.dri), d.sourceSets.toSet()) { - divergent { - +buildSignature(d) - } - after { - +contentForDescription(d) - +contentForComments(d, isPlatformHintedContent = false) + protected open fun contentForMember(d: Documentable) = contentForMembers(listOf(d)) + + protected open fun contentForMembers(doumentables: List<Documentable>) = + contentBuilder.contentFor(doumentables.dri, doumentables.sourceSets) { + group(kind = ContentKind.Cover) { + cover(doumentables.first().name.orEmpty()) + } + divergentGroup(ContentDivergentGroup.GroupID("member")) { + doumentables.forEach { d -> + instance(setOf(d.dri), d.sourceSets) { + divergent { + +buildSignature(d) + } + after { + +contentForDescription(d) + +contentForComments(d, isPlatformHintedContent = false) + } + } } } } - } private fun DocumentableContentBuilder.functionsBlock(name: String, list: Collection<DFunction>) = divergentBlock( name, @@ -610,22 +748,24 @@ open class DefaultPageCreator( list: Collection<DProperty>, sourceSets: Set<DokkaSourceSet> ) { - block( + multiBlock( name, 2, ContentKind.Properties, - list, + list.groupBy { it.name }.toList(), sourceSets, needsAnchors = true, extra = mainExtra + SimpleAttr.header(name), headers = listOf( headers("Name", "Summary") ) - ) { - link(it.name, it.dri, kind = ContentKind.Main) - sourceSetDependentHint(it.dri, it.sourceSets.toSet(), kind = ContentKind.SourceSetDependentHint) { - +buildSignature(it) - contentForBrief(it) + ) { key, props -> + link(key, props.first().dri, kind = ContentKind.Main) + sourceSetDependentHint(props.dri, props.sourceSets, kind = ContentKind.SourceSetDependentHint) { + props.forEach { + +buildSignature(it) + contentForBrief(it) + } } } } @@ -699,7 +839,7 @@ open class DefaultPageCreator( if (customTags.isEmpty()) return documentable.sourceSets.forEach { sourceSet -> - customTags.forEach { (tagName, sourceSetTag) -> + customTags.forEach { (_, sourceSetTag) -> sourceSetTag[sourceSet]?.let { tag -> customTagContentProviders.filter { it.isApplicable(tag) }.forEach { provider -> with(provider) { @@ -716,9 +856,19 @@ open class DefaultPageCreator( private val List<Documentable>.sourceSets: Set<DokkaSourceSet> get() = flatMap { it.sourceSets }.toSet() + private val List<Documentable>.dri: Set<DRI> + get() = map { it.dri }.toSet() + private val Documentable.groupedTags: GroupedTags get() = documentation.flatMap { (pd, doc) -> - doc.children.asSequence().map { pd to it }.toList() + doc.children.map { pd to it }.toList() + }.groupBy { it.second::class } + + private val List<Documentable>.groupedTags: GroupedTags + get() = this.flatMap { + it.documentation.flatMap { (pd, doc) -> + doc.children.map { pd to it }.toList() + } }.groupBy { it.second::class } private val Documentable.descriptions: SourceSetDependent<Description> @@ -730,6 +880,7 @@ open class DefaultPageCreator( private val Documentable.hasSeparatePage: Boolean get() = this !is DTypeAlias + @Suppress("UNCHECKED_CAST") private fun <T : Documentable> T.nameAfterClash(): String = ((this as? WithExtraProperties<out Documentable>)?.extra?.get(DriClashAwareName)?.value ?: name).orEmpty() } diff --git a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt index 8c3fcd84..7b6fbc7a 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt @@ -227,6 +227,45 @@ open class PageContentBuilder( } } + fun <T : Pair<String, List<Documentable>>> multiBlock( + name: String, + level: Int, + kind: Kind = ContentKind.Main, + groupedElements: Iterable<T>, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + renderWhenEmpty: Boolean = false, + needsSorting: Boolean = true, + headers: List<ContentGroup> = emptyList(), + needsAnchors: Boolean = false, + operation: DocumentableContentBuilder.(String, List<Documentable>) -> Unit + ) { + if (renderWhenEmpty || groupedElements.any()) { + header(level, name, kind = kind) { } + contents += ContentTable( + header = headers, + children = groupedElements + .let { + if (needsSorting) + it.sortedWith(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)) { it.first }) + else it + } + .map { + val newExtra = if (needsAnchors) extra + SymbolAnchorHint(it.first, kind) else extra + val documentables = it.second + buildGroup(documentables.map { it.dri }.toSet(), documentables.flatMap { it.sourceSets }.toSet(), kind, styles, newExtra) { + operation(it.first, documentables) + } + }, + dci = DCI(mainDRI, kind), + sourceSets = sourceSets.toDisplaySourceSets(), + style = styles, + extra = extra + ) + } + } + fun <T> list( elements: List<T>, prefix: String = "", diff --git a/plugins/base/src/test/kotlin/content/functions/ContentForBriefTest.kt b/plugins/base/src/test/kotlin/content/functions/ContentForBriefTest.kt index 7d8a169b..50f9e357 100644 --- a/plugins/base/src/test/kotlin/content/functions/ContentForBriefTest.kt +++ b/plugins/base/src/test/kotlin/content/functions/ContentForBriefTest.kt @@ -1,11 +1,11 @@ package content.functions -import org.junit.Assert.assertEquals import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.links.TypeConstructor import org.jetbrains.dokka.model.DClass import org.jetbrains.dokka.model.dfs import org.jetbrains.dokka.pages.* +import org.junit.Assert.assertEquals import org.junit.jupiter.api.Test import kotlin.test.assertNull @@ -60,7 +60,7 @@ class ContentForBriefTest : BaseAbstractTest() { testInline(codeWithSecondaryAndPrimaryConstructorsDocumented, testConfiguration) { pagesTransformationStage = { module -> val classPage = - module.dfs { it.name == "Example" && (it as ContentPage).documentable is DClass } as ContentPage + module.dfs { it.name == "Example" && (it as WithDocumentables).documentables.firstOrNull() is DClass } as ContentPage val constructorsTable = classPage.content.dfs { it is ContentTable && it.dci.kind == ContentKind.Constructors } as ContentTable @@ -84,7 +84,7 @@ class ContentForBriefTest : BaseAbstractTest() { testInline(codeWithSecondaryAndPrimaryConstructorsDocumented, testConfiguration) { pagesTransformationStage = { module -> val classPage = - module.dfs { it.name == "Example" && (it as ContentPage).documentable is DClass } as ContentPage + module.dfs { it.name == "Example" && (it as WithDocumentables).documentables.firstOrNull() is DClass } as ContentPage val constructorsTable = classPage.content.dfs { it is ContentTable && it.dci.kind == ContentKind.Constructors } as ContentTable @@ -108,7 +108,7 @@ class ContentForBriefTest : BaseAbstractTest() { testInline(codeWithDocumentedParameter, testConfiguration) { pagesTransformationStage = { module -> val classPage = - module.dfs { it.name == "Example" && (it as ContentPage).documentable is DClass } as ContentPage + module.dfs { it.name == "Example" && (it as WithDocumentables).documentables.firstOrNull() is DClass } as ContentPage val constructorsTable = classPage.content.dfs { it is ContentTable && it.dci.kind == ContentKind.Constructors } as ContentTable @@ -319,7 +319,7 @@ class ContentForBriefTest : BaseAbstractTest() { } private fun RootPageNode.singleFunctionDescription(className: String): ContentGroup { - val classPage = dfs { it.name == className && (it as ContentPage).documentable is DClass } as ContentPage + val classPage = dfs { it.name == className && (it as WithDocumentables).documentables.firstOrNull() is DClass } as ContentPage val functionsTable = classPage.content.dfs { it is ContentTable && it.dci.kind == ContentKind.Functions } as ContentTable diff --git a/plugins/base/src/test/kotlin/content/functions/ContentForConstructors.kt b/plugins/base/src/test/kotlin/content/functions/ContentForConstructors.kt index 832fa6f6..8da3be54 100644 --- a/plugins/base/src/test/kotlin/content/functions/ContentForConstructors.kt +++ b/plugins/base/src/test/kotlin/content/functions/ContentForConstructors.kt @@ -32,7 +32,7 @@ class ContentForConstructors : BaseAbstractTest() { """.trimIndent(), testConfiguration) { pagesTransformationStage = { module -> val classPage = - module.dfs { it.name == "Example" && (it as ContentPage).documentable is DClass } as ContentPage + module.dfs { it.name == "Example" && (it as WithDocumentables).documentables.firstOrNull() is DClass } as ContentPage val constructorsTable = classPage.content.dfs { it is ContentTable && it.dci.kind == ContentKind.Constructors } as ContentTable diff --git a/plugins/base/src/test/kotlin/locationProvider/DokkaLocationProviderTest.kt b/plugins/base/src/test/kotlin/locationProvider/DokkaLocationProviderTest.kt index 8c96eab3..59406e1e 100644 --- a/plugins/base/src/test/kotlin/locationProvider/DokkaLocationProviderTest.kt +++ b/plugins/base/src/test/kotlin/locationProvider/DokkaLocationProviderTest.kt @@ -53,8 +53,7 @@ class DokkaLocationProviderTest : BaseAbstractTest() { ModulePageNode( name = name, children = packages.pages, - content = stubContentNode, - documentable = null + content = stubContentNode ) ) } @@ -69,7 +68,6 @@ class DokkaLocationProviderTest : BaseAbstractTest() { name = name, children = packages.pages, content = stubContentNode, - documentable = null, dri = emptySet() ) ) @@ -84,7 +82,6 @@ class DokkaLocationProviderTest : BaseAbstractTest() { name = name, children = emptyList(), content = stubContentNode, - documentable = null, dri = emptySet() ) ) diff --git a/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt b/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt new file mode 100644 index 00000000..ad3b5d38 --- /dev/null +++ b/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt @@ -0,0 +1,352 @@ +package transformers + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.PluginConfigurationImpl +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.childrenOfType +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.firstChildOfType +import org.jetbrains.dokka.pages.* +import org.jetbrains.kotlin.utils.addIfNotNull +import org.junit.jupiter.api.Test +import utils.assertNotNull +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class MergeImplicitExpectActualDeclarationsTest : BaseAbstractTest() { + + @Suppress("UNUSED_VARIABLE") + private fun configuration(switchOn: Boolean) = dokkaConfiguration { + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/pageMerger/Test.kt") + } + val js = sourceSet { + name = "js" + displayName = "js" + analysisPlatform = "js" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jsMain/kotlin/pageMerger/Test.kt") + } + val jvm = sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + sourceRoots = listOf("src/jvmMain/kotlin/pageMerger/Test.kt") + } + } + pluginsConfigurations.addIfNotNull( + PluginConfigurationImpl( + DokkaBase::class.qualifiedName!!, + DokkaConfiguration.SerializationFormat.JSON, + """{ "mergeImplicitExpectActualDeclarations": $switchOn }""", + ) + ) + } + + private fun ClasslikePageNode.findSectionWithName(name: String) : ContentNode? { + var sectionHeader: ContentHeader? = null + return content.dfs { node -> + node.children.filterIsInstance<ContentHeader>().any { header -> + header.children.firstOrNull { it is ContentText && it.text == name }?.also { sectionHeader = header } != null + } + }?.children?.dropWhile { child -> child != sectionHeader }?.drop(1)?.firstOrNull() + } + + @Test + fun `should merge fun`() { + testInline( + """ + + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | fun method1(): String + |} + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | fun method1(): Int + |} + | + """.trimMargin(), + configuration(true), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val classPage = root.dfs { it.name == "classA" } as? ClasslikePageNode + assertNotNull(classPage, "Tested class not found!") + + val functions = classPage.findSectionWithName("Functions").assertNotNull("Functions") + val method1 = functions.children.singleOrNull().assertNotNull("method1") + + assertEquals( + 2, + method1.firstChildOfType<ContentDivergentGroup>().childrenOfType<ContentDivergentInstance>().size, + "Incorrect number of divergent instances found" + ) + + val methodPage = root.dfs { it.name == "method1" } as? MemberPageNode + assertNotNull(methodPage, "Tested method not found!") + + val divergentGroup = methodPage.content.dfs { it is ContentDivergentGroup } as? ContentDivergentGroup + + assertEquals( + 2, + divergentGroup?.childrenOfType<ContentDivergentInstance>()?.size, + "Incorrect number of divergent instances found in method page" + ) + } + } + } + + @Test + fun `should merge method and prop`() { + testInline( + """ + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | fun method1(): String + |} + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | val prop1: Int + |} + | + """.trimMargin(), + configuration(true), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val classPage = root.dfs { it.name == "classA" } as? ClasslikePageNode + assertNotNull(classPage, "Tested class not found!") + + val props = classPage.findSectionWithName("Properties").assertNotNull("Properties") + val prop1 = props.children.singleOrNull().assertNotNull("prop1") + + val functions = classPage.findSectionWithName("Functions").assertNotNull("Functions") + val method1 = functions.children.singleOrNull().assertNotNull("method1") + } + } + } + + @Test + fun `should merge prop`() { + testInline( + """ + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | val prop1: String + |} + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | val prop1: Int + |} + | + """.trimMargin(), + configuration(true), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val classPage = root.dfs { it.name == "classA" } as? ClasslikePageNode + assertNotNull(classPage, "Tested class not found!") + + val props = classPage.findSectionWithName("Properties").assertNotNull("Properties") + val prop1 = props.children.singleOrNull().assertNotNull("prop1") + + assertEquals( + 2, + prop1.firstChildOfType<PlatformHintedContent>().inner.children.size, + "Incorrect number of divergent instances found" + ) + + val propPage = root.dfs { it.name == "prop1" } as? MemberPageNode + assertNotNull(propPage, "Tested method not found!") + + val divergentGroup = propPage.content.dfs { it is ContentDivergentGroup } as? ContentDivergentGroup + + assertEquals( + 2, + divergentGroup?.childrenOfType<ContentDivergentInstance>()?.size, + "Incorrect number of divergent instances found in method page" + ) + } + } + } + + @Test + fun `should merge enum and class`() { + testInline( + """ + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | val prop1: String + |} + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |enum class classA { + | ENTRY + |} + | + """.trimMargin(), + configuration(true), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val classPage = root.dfs { it.name == "classA" } as? ClasslikePageNode + assertNotNull(classPage, "Tested class not found!") + + val entries = classPage.findSectionWithName("Entries").assertNotNull("Entries") + val entry = entries.children.singleOrNull().assertNotNull("ENTRY") + + val props = classPage.findSectionWithName("Properties").assertNotNull("Properties") + assertEquals( + 3, + props.children.size, + "Incorrect number of properties found in method page" + ) + } + } + } + + fun PageNode.childrenRec(): List<PageNode> = listOf(this) + children.flatMap { it.childrenRec() } + + @Test + fun `should merge enum entries`() { + testInline( + """ + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |enum class classA { + | SMTH; + | fun method1(): Int + |} + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |enum class classA { + | SMTH; + | fun method1(): Int + |} + | + """.trimMargin(), + configuration(true), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val classPage = root.dfs { it.name == "SMTH" } as? ClasslikePageNode + assertNotNull(classPage, "Tested class not found!") + + val functions = classPage.findSectionWithName("Functions").assertNotNull("Functions") + val method1 = functions.children.singleOrNull().assertNotNull("method1") + + assertEquals( + 2, + method1.firstChildOfType<ContentDivergentGroup>().childrenOfType<ContentDivergentInstance>().size, + "Incorrect number of divergent instances found" + ) + } + } + } + + /** + * There is a case when a property and fun from different source sets + * have the same name so pages have the same urls respectively. + */ + @Test + fun `should no merge prop and method with the same name`() { + testInline( + """ + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | fun merged():String + |} + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class classA { + | val merged:String + |} + | + """.trimMargin(), + configuration(true), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val allChildren = root.childrenRec().filterIsInstance<MemberPageNode>() + + assertEquals( + 1, + allChildren.filter { it.name == "merged" }.size, + "Incorrect number of fun pages" + ) + } + } + } + + @Test + fun `should always merge constructor`() { + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |expect class classA(a: Int) + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual class classA(a: Int) + """.trimMargin(), + configuration(false), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val classPage = root.dfs { it.name == "classA" } as? ClasslikePageNode + assertNotNull(classPage, "Tested class not found!") + + val constructors = classPage.findSectionWithName("Constructors").assertNotNull("Constructors") + + assertEquals( + 1, + constructors.children.size, + "Incorrect number of constructors" + ) + + val platformHinted = constructors.dfs { it is PlatformHintedContent } as? PlatformHintedContent + + assertEquals( + 2, + platformHinted?.sourceSets?.size, + "Incorrect number of source sets" + ) + } + } + } +}
\ No newline at end of file |