diff --git a/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/pages/contentNodeProperties.kt b/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/pages/contentNodeProperties.kt index 64f1957291..f3699ee23c 100644 --- a/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/pages/contentNodeProperties.kt +++ b/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/pages/contentNodeProperties.kt @@ -16,7 +16,30 @@ public class SimpleAttr( } public enum class BasicTabbedContentType : TabbedContentType { - TYPE, CONSTRUCTOR, FUNCTION, PROPERTY, ENTRY, EXTENSION_PROPERTY, EXTENSION_FUNCTION + TYPE, CONSTRUCTOR, + + // property/function here means a different things depending on parent: + // - if parent=package - describes just `top-level` property/function without receiver + // - if parent=classlike - describes `member` property/function, + // it could have receiver (becoming member extension property/function) or not (ordinary member property/function) + // for examples look at docs for `EXTENSION_PROPERTY`, `EXTENSION_FUNCTION` + FUNCTION, PROPERTY, + + ENTRY, + + // property/function here means a different things depending on parent, + // and not just `an extension property/function`: + // example 1: `fun Foo.bar()` - top-level extension function + // - on a page describing `Foo` class `bar` will have type=`EXTENSION_FUNCTION` + // - on a page describing package declarations `bar` will have type=`EXTENSION_FUNCTION` + // example 2: `object Namespace { fun Foo.bar() }` - member extension function + // - on a page describing `Foo` class `bar` will have type=`EXTENSION_FUNCTION` + // - on a page describing `Namespace` object `bar` will have type=`FUNCTION` + // + // These types are needed to separate member functions and extension function on classlike pages. + // The same split rules are also used + // when grouping functions/properties with the same name on pages for classlike and package + EXTENSION_PROPERTY, EXTENSION_FUNCTION } /** diff --git a/dokka-subprojects/plugin-base/api/plugin-base.api b/dokka-subprojects/plugin-base/api/plugin-base.api index 8d768b4222..bdcbdc2718 100644 --- a/dokka-subprojects/plugin-base/api/plugin-base.api +++ b/dokka-subprojects/plugin-base/api/plugin-base.api @@ -1375,8 +1375,6 @@ public class org/jetbrains/dokka/base/translators/documentables/DefaultPageCreat public static synthetic fun contentForScope$default (Lorg/jetbrains/dokka/base/translators/documentables/DefaultPageCreator;Lorg/jetbrains/dokka/model/WithScope;Lorg/jetbrains/dokka/links/DRI;Ljava/util/Set;Ljava/util/List;ILjava/lang/Object;)Lorg/jetbrains/dokka/pages/ContentGroup; protected fun contentForScopes (Ljava/util/List;Ljava/util/Set;Ljava/util/List;)Lorg/jetbrains/dokka/pages/ContentGroup; public static synthetic fun contentForScopes$default (Lorg/jetbrains/dokka/base/translators/documentables/DefaultPageCreator;Ljava/util/List;Ljava/util/Set;Ljava/util/List;ILjava/lang/Object;)Lorg/jetbrains/dokka/pages/ContentGroup; - protected fun divergentBlock (Lorg/jetbrains/dokka/base/translators/documentables/PageContentBuilder$DocumentableContentBuilder;Ljava/lang/String;Ljava/util/Collection;Lorg/jetbrains/dokka/pages/ContentKind;Lorg/jetbrains/dokka/model/properties/PropertyContainer;)V - public static synthetic fun divergentBlock$default (Lorg/jetbrains/dokka/base/translators/documentables/DefaultPageCreator;Lorg/jetbrains/dokka/base/translators/documentables/PageContentBuilder$DocumentableContentBuilder;Ljava/lang/String;Ljava/util/Collection;Lorg/jetbrains/dokka/pages/ContentKind;Lorg/jetbrains/dokka/model/properties/PropertyContainer;ILjava/lang/Object;)V protected fun getContentBuilder ()Lorg/jetbrains/dokka/base/translators/documentables/PageContentBuilder; public final fun getCustomTagContentProviders ()Ljava/util/List; public final fun getDocumentableAnalyzer ()Lorg/jetbrains/dokka/analysis/kotlin/internal/DocumentableSourceLanguageParser; diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultPageCreator.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultPageCreator.kt index dac5144d8c..a8b3639011 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultPageCreator.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultPageCreator.kt @@ -71,7 +71,8 @@ public open class DefaultPageCreator( * @see ActualTypealias */ private fun List.filterOutActualTypeAlias(): List { - fun List.hasExpectClass(dri: DRI) = find { it is DClasslike && it.dri == dri && it.expectPresentInSet != null } != null + fun List.hasExpectClass(dri: DRI) = + find { it is DClasslike && it.dri == dri && it.expectPresentInSet != null } != null return this.filterNot { it is DTypeAlias && this.hasExpectClass(it.dri) } } @@ -371,7 +372,14 @@ public open class DefaultPageCreator( } } group(styles = setOf(ContentStyle.TabbedContent), extra = mainExtra) { - +contentForScope(p, p.dri, p.sourceSets) + val (functions, extensionFunctions) = p.functions.partition { it.receiver == null } + val (properties, extensionProperties) = p.properties.partition { it.receiver == null } + +contentForScope( + s = p.copy(functions = functions, properties = properties), + dri = p.dri, + sourceSets = p.sourceSets, + extensions = extensionFunctions + extensionProperties + ) } } } @@ -380,31 +388,22 @@ public open class DefaultPageCreator( scopes: List, sourceSets: Set, extensions: List = emptyList() - ): ContentGroup { - val types = scopes.flatMap { it.classlikes } + scopes.filterIsInstance().flatMap { it.typealiases } - return contentForScope( - @Suppress("UNCHECKED_CAST") - (scopes as List).dri, - sourceSets, - types, - scopes.flatMap { it.functions }, - scopes.flatMap { it.properties }, - extensions - ) - } + ): ContentGroup = contentForScope( + dri = @Suppress("UNCHECKED_CAST") (scopes as List).dri, + sourceSets = sourceSets, + types = scopes.flatMap { it.classlikes } + + scopes.filterIsInstance().flatMap { it.typealiases }, + functions = scopes.flatMap { it.functions }, + properties = scopes.flatMap { it.properties }, + extensions = extensions, + ) protected open fun contentForScope( s: WithScope, dri: DRI, sourceSets: Set, extensions: List = emptyList() - ): ContentGroup { - val types = listOf( - s.classlikes, - (s as? DPackage)?.typealiases ?: emptyList() - ).flatten() - return contentForScope(setOf(dri), sourceSets, types, s.functions, s.properties, extensions) - } + ): ContentGroup = contentForScopes(listOf(s), sourceSets, extensions) private fun contentForScope( dri: Set, @@ -412,41 +411,35 @@ public open class DefaultPageCreator( types: List, functions: List, properties: List, - extensions: List + extensions: List, ) = contentBuilder.contentFor(dri, sourceSets) { - divergentBlock( - "Types", - types, - ContentKind.Classlikes - ) - val (extensionProps, extensionFuns) = extensions.splitPropsAndFuns() + typesBlock(types) + val (extensionProperties, extensionFunctions) = extensions.splitPropsAndFuns() if (separateInheritedMembers) { val (inheritedFunctions, memberFunctions) = functions.splitInherited() val (inheritedProperties, memberProperties) = properties.splitInherited() - val (inheritedExtensionFunctions, extensionFunctions) = extensionFuns.splitInheritedExtension(dri) - val (inheritedExtensionProperties, extensionProperties) = extensionProps.splitInheritedExtension(dri) - propertiesBlock( - "Properties", memberProperties + extensionProperties - ) - propertiesBlock( - "Inherited properties", inheritedProperties + inheritedExtensionProperties - ) - functionsBlock("Functions", memberFunctions + extensionFunctions) - functionsBlock( - "Inherited functions", inheritedFunctions + inheritedExtensionFunctions - ) + val ( + inheritedExtensionFunctions, + directExtensionFunctions + ) = extensionFunctions.splitInheritedExtension(dri) + + val ( + inheritedExtensionProperties, + directExtensionProperties + ) = extensionProperties.splitInheritedExtension(dri) + + propertiesBlock("Properties", memberProperties, directExtensionProperties) + propertiesBlock("Inherited properties", inheritedProperties, inheritedExtensionProperties) + + functionsBlock("Functions", memberFunctions, directExtensionFunctions) + functionsBlock("Inherited functions", inheritedFunctions, inheritedExtensionFunctions) } else { - propertiesBlock( - "Properties", properties + extensionProps - ) - functionsBlock("Functions", functions + extensionFuns) + propertiesBlock("Properties", properties, extensionProperties) + functionsBlock("Functions", functions, extensionFunctions) } } - private fun Iterable.sorted() = - sortedWith(compareBy({ it.name }, { it.parameters.size }, { it.dri.toString() })) - /** * @param documentables a list of [DClasslike] and [DEnumEntry] and [DTypeAlias] with the same dri in different sourceSets */ @@ -491,6 +484,7 @@ public open class DefaultPageCreator( +contentForScopes(scopes, documentables.sourceSets, extensions) } } + protected open fun contentForConstructors( constructorsToDocumented: List, dri: Set, @@ -560,7 +554,6 @@ public open class DefaultPageCreator( } - protected open fun contentForDescription( d: Documentable ): List { @@ -611,7 +604,7 @@ public open class DefaultPageCreator( tag: TagWrapper ) { val language = documentableAnalyzer.getLanguage(documentable, sourceSet) - when(language) { + when (language) { DocumentableLanguage.JAVA -> firstSentenceComment(tag.root) DocumentableLanguage.KOTLIN -> firstParagraphComment(tag.root) else -> firstParagraphComment(tag.root) @@ -643,128 +636,206 @@ public open class DefaultPageCreator( } } + private fun DocumentableContentBuilder.typesBlock(types: List) { + if (types.isEmpty()) return + + val grouped = types + // This groupBy should probably use LocationProvider + .groupBy(Documentable::name) + .mapValues { (_, elements) -> + // This hacks displaying actual typealias signatures along classlike ones + if (elements.any { it is DClasslike }) elements.filter { it !is DTypeAlias } else elements + } + + val groups = grouped.entries + .sortedWith(compareBy(nullsFirst(canonicalAlphabeticalOrder)) { it.key }) + .map { (name, elements) -> + DivergentElementGroup( + name = name, + kind = ContentKind.Classlikes, + elements = elements + ) + } + + divergentBlock( + name = "Types", + kind = ContentKind.Classlikes, + extra = mainExtra, + contentType = BasicTabbedContentType.TYPE, + groups = groups + ) + } + private fun DocumentableContentBuilder.functionsBlock( name: String, - list: Collection + declarations: List, + extensions: List ) { - divergentBlock( - name, - list.sorted(), - ContentKind.Functions, - extra = mainExtra + functionsOrPropertiesBlock( + name = name, + contentKind = ContentKind.Functions, + contentType = when { + declarations.isEmpty() -> BasicTabbedContentType.EXTENSION_FUNCTION + else -> BasicTabbedContentType.FUNCTION + }, + declarations = declarations, + extensions = extensions ) } private fun DocumentableContentBuilder.propertiesBlock( name: String, - list: Collection + declarations: List, + extensions: List ) { - divergentBlock( - name, - list, - ContentKind.Properties, - extra = mainExtra + functionsOrPropertiesBlock( + name = name, + contentKind = ContentKind.Properties, + contentType = when { + declarations.isEmpty() -> BasicTabbedContentType.EXTENSION_PROPERTY + else -> BasicTabbedContentType.PROPERTY + }, + declarations = declarations, + extensions = extensions ) - } - private data class NameAndIsExtension(val name:String?, val isExtension: Boolean) - - private fun groupAndSortDivergentCollection(collection: Collection): List>> { - val groupKeyComparator: Comparator>> = - compareBy>, String?>( - nullsFirst(canonicalAlphabeticalOrder) - ) { it.key.name } - .thenBy { it.key.isExtension } - - return collection - .groupBy { - NameAndIsExtension( - it.name, - it.isExtension() + + private fun DocumentableContentBuilder.functionsOrPropertiesBlock( + name: String, + contentKind: ContentKind, + contentType: BasicTabbedContentType, + declarations: List, + extensions: List + ) { + if (declarations.isEmpty() && extensions.isEmpty()) return + + // This groupBy should probably use LocationProvider + val grouped = declarations.groupBy { + NameAndIsExtension(it.name, isExtension = false) + } + extensions.groupBy { + NameAndIsExtension(it.name, isExtension = true) + } + + val groups = grouped.entries + .sortedWith(compareBy(NameAndIsExtension.comparator) { it.key }) + .map { (nameAndIsExtension, elements) -> + DivergentElementGroup( + name = nameAndIsExtension.name, + kind = when { + nameAndIsExtension.isExtension -> ContentKind.Extensions + else -> contentKind + }, + elements = elements ) - } // This groupBy should probably use LocationProvider - // This hacks displaying actual typealias signatures along classlike ones - .mapValues { if (it.value.any { it is DClasslike }) it.value.filter { it !is DTypeAlias } else it.value } - .entries.sortedWith(groupKeyComparator) + } + + divergentBlock( + name = name, + kind = contentKind, + extra = mainExtra, + contentType = contentType, + groups = groups + ) } - protected open fun DocumentableContentBuilder.divergentBlock( + private data class NameAndIsExtension(val name: String?, val isExtension: Boolean) { + companion object { + val comparator = compareBy( + comparator = nullsFirst(canonicalAlphabeticalOrder), + selector = NameAndIsExtension::name + ).thenBy(NameAndIsExtension::isExtension) + } + } + + private class DivergentElementGroup( + val name: String?, + val kind: ContentKind, + val elements: List + ) + + private fun DocumentableContentBuilder.divergentBlock( name: String, - collection: Collection, kind: ContentKind, - extra: PropertyContainer = mainExtra + extra: PropertyContainer, + contentType: BasicTabbedContentType, + groups: List, ) { - if (collection.any()) { - val onlyExtensions = collection.all { it.isExtension() } - val groupExtra = when(kind) { - ContentKind.Functions -> extra + TabbedContentTypeExtra(if (onlyExtensions) BasicTabbedContentType.EXTENSION_FUNCTION else BasicTabbedContentType.FUNCTION) - ContentKind.Properties -> extra + TabbedContentTypeExtra(if (onlyExtensions) BasicTabbedContentType.EXTENSION_PROPERTY else BasicTabbedContentType.PROPERTY) - ContentKind.Classlikes -> extra + TabbedContentTypeExtra(BasicTabbedContentType.TYPE) - else -> extra - } + if (groups.isEmpty()) return + + // be careful: extra here will be applied for children by default + group(extra = extra + TabbedContentTypeExtra(contentType)) { + header(2, name, kind = kind, extra = extra) { } + table(kind, extra = extra, styles = emptySet()) { + header { + group { text("Name") } + group { text("Summary") } + } + groups.forEach { group -> + val elementName = group.name + val rowKind = group.kind + val sortedElements = sortDivergentElementsDeterministically(group.elements) + + // This override here is needed to be able to split members and extensions into separate tabs in HTML renderer. + // The idea is that `contentType` is set to the `tab group` itself to `FUNCTION` or `PROPERTY` (above in the code), + // and then for `extensions` we override it - in this case we are able to create 2 tabs in HTML renderer: + // - `Members` - which show ONLY member functions/properties + // - `Members & Extensions` - which show BOTH member functions/properties and extensions for this classlike + val rowContentTypeOverride = when (rowKind) { + ContentKind.Extensions -> when (contentType) { + BasicTabbedContentType.FUNCTION -> BasicTabbedContentType.EXTENSION_FUNCTION + BasicTabbedContentType.PROPERTY -> BasicTabbedContentType.EXTENSION_PROPERTY + else -> null + } - group(extra = groupExtra) { - // be careful: groupExtra will be applied for children by default - header(2, name, kind = kind, extra = extra) { } - val isFunctions = collection.any { it is DFunction } - table(kind, extra = extra, styles = emptySet()) { - header { - group { text("Name") } - group { text("Summary") } + else -> null } - groupAndSortDivergentCollection(collection) - .forEach { (elementNameAndIsExtension, elements) -> // This groupBy should probably use LocationProvider - val elementName = elementNameAndIsExtension.name - val isExtension = elementNameAndIsExtension.isExtension - val rowExtra = - if (isExtension) extra + TabbedContentTypeExtra(if(isFunctions) BasicTabbedContentType.EXTENSION_FUNCTION else BasicTabbedContentType.EXTENSION_PROPERTY) else extra - val rowKind = if (isExtension) ContentKind.Extensions else kind - val sortedElements = sortDivergentElementsDeterministically(elements) - row( - dri = sortedElements.map { it.dri }.toSet(), - sourceSets = sortedElements.flatMap { it.sourceSets }.toSet(), - kind = rowKind, - styles = emptySet(), - extra = elementName?.let { name -> rowExtra + SymbolAnchorHint(name, kind) } ?: rowExtra - ) { - link( - text = elementName.orEmpty(), - address = sortedElements.first().dri, - kind = rowKind, - styles = setOf(ContentStyle.RowTitle), - sourceSets = sortedElements.sourceSets.toSet(), - extra = extra - ) - divergentGroup( - ContentDivergentGroup.GroupID(name), - sortedElements.map { it.dri }.toSet(), - kind = rowKind, - extra = extra + + row( + dri = sortedElements.map { it.dri }.toSet(), + sourceSets = sortedElements.flatMap { it.sourceSets }.toSet(), + kind = rowKind, + styles = emptySet(), + extra = extra.addAll( + listOfNotNull( + rowContentTypeOverride?.let(::TabbedContentTypeExtra), + elementName?.let { name -> SymbolAnchorHint(name, kind) } + ) + ) + ) { + link( + text = elementName.orEmpty(), + address = sortedElements.first().dri, + kind = rowKind, + styles = setOf(ContentStyle.RowTitle), + sourceSets = sortedElements.sourceSets.toSet(), + extra = extra + ) + divergentGroup( + ContentDivergentGroup.GroupID(name), + sortedElements.map { it.dri }.toSet(), + kind = rowKind, + extra = extra + ) { + sortedElements.map { element -> + instance( + setOf(element.dri), + element.sourceSets.toSet() ) { - sortedElements.map { element -> - instance( - setOf(element.dri), - element.sourceSets.toSet(), - extra = PropertyContainer.withAll( - SymbolAnchorHint(element.name ?: "", rowKind) - ) - ) { - divergent(extra = PropertyContainer.empty()) { - group { - +buildSignature(element) - } - } - after( - extra = PropertyContainer.empty() - ) { - contentForBrief(element) - contentForCustomTagsBrief(element) - } + divergent(extra = PropertyContainer.empty()) { + group { + +buildSignature(element) } } + after( + extra = PropertyContainer.empty() + ) { + contentForBrief(element) + contentForCustomTagsBrief(element) + } } } } + } } } } diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/ExtensionsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/ExtensionsTest.kt new file mode 100644 index 0000000000..fe6ace5948 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/ExtensionsTest.kt @@ -0,0 +1,714 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content + +import matchers.content.* +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.pages.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class ExtensionsTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + // function tests + + @Test + fun `should render top-level extension function`() { + testInline( + """ + /src/A.kt + fun String.extension() {} + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() as ContentPage + + pkgNode.content.assertTabbedNode { + group { + assertTabGroup("Functions", BasicTabbedContentType.EXTENSION_FUNCTION) { + assertTableWithTabs( + "extension" to null + ) + } + } + } + } + } + } + + @Test + fun `should differentiate top-level function and extension function`() { + testInline( + """ + /src/A.kt + fun function() {} + fun String.extension() {} + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() as ContentPage + + pkgNode.content.assertTabbedNode { + group { + assertTabGroup("Functions", BasicTabbedContentType.FUNCTION) { + assertTableWithTabs( + "extension" to BasicTabbedContentType.EXTENSION_FUNCTION, + "function" to null + ) + } + } + } + } + } + } + + @Test + fun `should render member extension function for object`() { + testInline( + """ + /src/A.kt + object A { + fun String.memberExtension() {} + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Functions", BasicTabbedContentType.FUNCTION) { + assertTableWithTabs( + "memberExtension" to null, + ) + } + } + } + } + } + } + + @Test + fun `should render top-level extension function for object`() { + testInline( + """ + /src/A.kt + object A + fun A.topLevelExtension() {} + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Functions", BasicTabbedContentType.EXTENSION_FUNCTION) { + assertTableWithTabs( + "topLevelExtension" to null + ) + } + } + } + } + } + } + + @Test + fun `should differentiate functions and extension functions for object`() { + testInline( + """ + /src/A.kt + object A { + fun function() {} + fun String.memberExtension() {} + } + fun A.extension() {} + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Functions", BasicTabbedContentType.FUNCTION) { + assertTableWithTabs( + "extension" to BasicTabbedContentType.EXTENSION_FUNCTION, + "function" to null, + "memberExtension" to null + ) + } + } + } + } + } + } + + @Test + fun `should render member extensions function for class`() { + testInline( + """ + /src/A.kt + class A { + fun String.memberExtension() {} + fun A.memberSelfExtension() {} + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Functions", BasicTabbedContentType.FUNCTION) { + assertTableWithTabs( + "memberExtension" to null, + "memberSelfExtension" to null, + ) + } + } + } + } + } + } + + @Test + fun `should render top-level extension functions for class`() { + testInline( + """ + /src/A.kt + class A + fun A.topLevelExtension() {} + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Functions", BasicTabbedContentType.EXTENSION_FUNCTION) { + assertTableWithTabs( + "topLevelExtension" to null + ) + } + } + } + } + } + } + + @Test + fun `should render extension functions from object for class`() { + testInline( + """ + /src/A.kt + class A + object B { + fun A.extensionFromB() {} + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Functions", BasicTabbedContentType.EXTENSION_FUNCTION) { + assertTableWithTabs( + "extensionFromB" to null + ) + } + } + } + } + } + } + + @Test + fun `should differentiate functions and extension functions for class`() { + testInline( + """ + /src/A.kt + class A { + fun function() {} + fun String.memberExtension() {} + fun A.memberSelfExtension() {} + } + fun A.extension() {} + object B { + fun A.extensionFromB() {} + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Functions", BasicTabbedContentType.FUNCTION) { + assertTableWithTabs( + "extension" to BasicTabbedContentType.EXTENSION_FUNCTION, + "extensionFromB" to BasicTabbedContentType.EXTENSION_FUNCTION, + "function" to null, + "memberExtension" to null, + "memberSelfExtension" to null, + ) + } + } + } + } + } + } + + @Test + fun `should render member extension functions for companion object of class`() { + testInline( + """ + /src/A.kt + class A { + companion object { + fun Int.companionMemberExtensionForInt() {} + fun A.companionMemberExtensionForA() {} + } + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Types", BasicTabbedContentType.TYPE) { skipAllNotMatching() } + assertTabGroup("Functions", BasicTabbedContentType.EXTENSION_FUNCTION) { + assertTableWithTabs( + "companionMemberExtensionForA" to null, + ) + } + } + } + + val companionPage = objectNode.children.single { it.name == "Companion" } as ContentPage + + companionPage.content.assertTabbedNode { + group { + assertTabGroup("Functions", BasicTabbedContentType.FUNCTION) { + assertTableWithTabs( + "companionMemberExtensionForA" to null, + "companionMemberExtensionForInt" to null + ) + } + } + } + } + } + } + + // property tests + + @Test + fun `should render top-level extension property`() { + testInline( + """ + /src/A.kt + val String.extension: String get() = "" + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() as ContentPage + + pkgNode.content.assertTabbedNode { + group { + assertTabGroup("Properties", BasicTabbedContentType.EXTENSION_PROPERTY) { + assertTableWithTabs( + "extension" to null + ) + } + } + } + } + } + } + + @Test + fun `should differentiate top-level property and extension property`() { + testInline( + """ + /src/A.kt + val property: String get() = "" + val String.extension: String get() = "" + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() as ContentPage + + pkgNode.content.assertTabbedNode { + group { + assertTabGroup("Properties", BasicTabbedContentType.PROPERTY) { + assertTableWithTabs( + "extension" to BasicTabbedContentType.EXTENSION_PROPERTY, + "property" to null + ) + } + } + } + } + } + } + + @Test + fun `should render member extension property for object`() { + testInline( + """ + /src/A.kt + object A { + val String.memberExtension: String get() = "" + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Properties", BasicTabbedContentType.PROPERTY) { + assertTableWithTabs( + "memberExtension" to null, + ) + } + } + } + } + } + } + + @Test + fun `should render top-level extension property for object`() { + testInline( + """ + /src/A.kt + object A + val A.topLevelExtension: String get() = "" + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Properties", BasicTabbedContentType.EXTENSION_PROPERTY) { + assertTableWithTabs( + "topLevelExtension" to null + ) + } + } + } + } + } + } + + @Test + fun `should differentiate properties and extension properties for object`() { + testInline( + """ + /src/A.kt + object A { + val property: String get() = "" + val String.memberExtension: String get() = "" + } + val A.extension: String get() = "" + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Properties", BasicTabbedContentType.PROPERTY) { + assertTableWithTabs( + "extension" to BasicTabbedContentType.EXTENSION_PROPERTY, + "memberExtension" to null, + "property" to null + ) + } + } + } + } + } + } + + @Test + fun `should render member extension properties for class`() { + testInline( + """ + /src/A.kt + class A { + val String.memberExtension: String get() = "" + val A.memberSelfExtension: String get() = "" + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Properties", BasicTabbedContentType.PROPERTY) { + assertTableWithTabs( + "memberExtension" to null, + "memberSelfExtension" to null, + ) + } + } + } + } + } + } + + @Test + fun `should render top-level extension properties for class`() { + testInline( + """ + /src/A.kt + class A + val A.topLevelExtension: String get() = "" + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Properties", BasicTabbedContentType.EXTENSION_PROPERTY) { + assertTableWithTabs( + "topLevelExtension" to null + ) + } + } + } + } + } + } + + @Test + fun `should render extension properties from object for class`() { + testInline( + """ + /src/A.kt + class A + object B { + val A.extensionFromB: String get() = "" + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Properties", BasicTabbedContentType.EXTENSION_PROPERTY) { + assertTableWithTabs( + "extensionFromB" to null + ) + } + } + } + } + } + } + + @Test + fun `should differentiate properties and extension properties for class`() { + testInline( + """ + /src/A.kt + class A { + val property: String get() = "" + val String.memberExtension: String get() = "" + val A.memberSelfExtension: String get() = "" + } + val A.extension: String get() = "" + object B { + val A.extensionFromB: String get() = "" + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Properties", BasicTabbedContentType.PROPERTY) { + assertTableWithTabs( + "extension" to BasicTabbedContentType.EXTENSION_PROPERTY, + "extensionFromB" to BasicTabbedContentType.EXTENSION_PROPERTY, + "memberExtension" to null, + "memberSelfExtension" to null, + "property" to null, + ) + } + } + } + } + } + } + + @Test + fun `should render member extension properties for companion object of class`() { + testInline( + """ + /src/A.kt + class A { + companion object { + val Int.companionMemberExtensionForInt: String get() = "" + val A.companionMemberExtensionForA: String get() = "" + } + } + """.trimIndent(), + configuration + ) { + pagesGenerationStage = { modulePage -> + val pkgNode = modulePage.children.single() + val objectNode = pkgNode.children.single { it.name == "A" } as ContentPage + + objectNode.content.assertTabbedNode { + group { + assertTabGroup("Constructors", BasicTabbedContentType.CONSTRUCTOR) { skipAllNotMatching() } + } + group { + assertTabGroup("Types", BasicTabbedContentType.TYPE) { skipAllNotMatching() } + assertTabGroup("Properties", BasicTabbedContentType.EXTENSION_PROPERTY) { + assertTableWithTabs( + "companionMemberExtensionForA" to null, + ) + } + } + } + + val companionPage = objectNode.children.single { it.name == "Companion" } as ContentPage + + companionPage.content.assertTabbedNode { + group { + assertTabGroup("Properties", BasicTabbedContentType.PROPERTY) { + assertTableWithTabs( + "companionMemberExtensionForA" to null, + "companionMemberExtensionForInt" to null + ) + } + } + } + } + } + } + + private fun ContentMatcherBuilder.assertTableWithTabs( + vararg expected: Pair + ) { + table { + expected.forEach { (name, tabType) -> + group { + assertTabbedContentType(tabType) + link { +name } + skipAllNotMatching() + } + } + } + } + + private fun ContentMatcherBuilder.assertTabbedContentType(expected: TabbedContentType?) { + check { + assertEquals(expected, extra[TabbedContentTypeExtra]?.value) + } + } + + private fun ContentNode.assertTabbedNode(block: ContentMatcherBuilder.() -> Unit) { + assertNode { + group { + header { } + skipAllNotMatching() + } + tabbedGroup(block) + } + } + + private fun ContentMatcherBuilder.assertTabGroup( + name: String, + type: TabbedContentType, + block: ContentMatcherBuilder.() -> Unit + ) { + group { + assertTabbedContentType(type) + header { +name } + block() + } + } + +}