From 12a386bb7185f862a1cbd831e6856c4235953833 Mon Sep 17 00:00:00 2001 From: Vadim Mishenev Date: Fri, 30 Jun 2023 16:44:20 +0300 Subject: [PATCH] Generate dedicated pages for typealiases (#3051) --- plugins/base/api/base.api | 3 +- .../renderers/html/NavigationDataProvider.kt | 1 + .../kotlin/renderers/html/NavigationPage.kt | 1 + .../renderers/html/htmlPreprocessors.kt | 1 + .../documentables/DefaultPageCreator.kt | 51 +++++++----- .../images/nav-icons/typealias-kotlin.svg | 9 +++ .../src/main/resources/dokka/styles/style.css | 4 + .../content/functions/ContentForBriefTest.kt | 37 +++++++++ .../content/typealiases/TypealiasTest.kt | 78 +++++++++++++++++++ .../renderers/html/NavigationIconTest.kt | 12 ++- .../test/kotlin/signatures/SignatureTest.kt | 6 +- ...rgeImplicitExpectActualDeclarationsTest.kt | 32 ++++++++ 12 files changed, 212 insertions(+), 23 deletions(-) create mode 100644 plugins/base/src/main/resources/dokka/images/nav-icons/typealias-kotlin.svg create mode 100644 plugins/base/src/test/kotlin/content/typealiases/TypealiasTest.kt diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index 7875800efe..0af1b46a0a 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -432,6 +432,7 @@ public final class org/jetbrains/dokka/base/renderers/html/NavigationNodeIcon : public static final field INTERFACE Lorg/jetbrains/dokka/base/renderers/html/NavigationNodeIcon; public static final field INTERFACE_KT Lorg/jetbrains/dokka/base/renderers/html/NavigationNodeIcon; public static final field OBJECT Lorg/jetbrains/dokka/base/renderers/html/NavigationNodeIcon; + public static final field TYPEALIAS_KT Lorg/jetbrains/dokka/base/renderers/html/NavigationNodeIcon; public static final field VAL Lorg/jetbrains/dokka/base/renderers/html/NavigationNodeIcon; public static final field VAR Lorg/jetbrains/dokka/base/renderers/html/NavigationNodeIcon; public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/dokka/base/renderers/html/NavigationNodeIcon; @@ -1464,7 +1465,7 @@ public class org/jetbrains/dokka/base/translators/documentables/DefaultPageCreat public final fun getLogger ()Lorg/jetbrains/dokka/utilities/DokkaLogger; protected final fun getMergeImplicitExpectActualDeclarations ()Z protected final fun getSeparateInheritedMembers ()Z - public fun pageForClasslike (Lorg/jetbrains/dokka/model/DClasslike;)Lorg/jetbrains/dokka/pages/ClasslikePageNode; + public fun pageForClasslike (Lorg/jetbrains/dokka/model/Documentable;)Lorg/jetbrains/dokka/pages/ClasslikePageNode; public fun pageForClasslikes (Ljava/util/List;)Lorg/jetbrains/dokka/pages/ClasslikePageNode; public fun pageForEnumEntries (Ljava/util/List;)Lorg/jetbrains/dokka/pages/ClasslikePageNode; public fun pageForEnumEntry (Lorg/jetbrains/dokka/model/DEnumEntry;)Lorg/jetbrains/dokka/pages/ClasslikePageNode; diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt index 4dae21c8b4..be1b0fcf3a 100644 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt @@ -41,6 +41,7 @@ abstract class NavigationDataProvider { val isJava = documentable?.hasAnyJavaSources() ?: false when (documentable) { + is DTypeAlias -> NavigationNodeIcon.TYPEALIAS_KT is DClass -> when { documentable.isException -> NavigationNodeIcon.EXCEPTION documentable.isAbstract() -> { diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt index fc17983d62..9543c388db 100644 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt @@ -109,6 +109,7 @@ enum class NavigationNodeIcon( FUNCTION("function"), EXCEPTION("exception-class"), OBJECT("object"), + TYPEALIAS_KT("typealias-kt"), VAL("val"), VAR("var"); diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt index 7ffcd9e350..a213bce9e4 100644 --- a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt +++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt @@ -120,6 +120,7 @@ object AssetsInstaller : PageTransformer { "images/nav-icons/interface.svg", "images/nav-icons/interface-kotlin.svg", "images/nav-icons/object.svg", + "images/nav-icons/typealias-kotlin.svg", ) override fun invoke(input: RootPageNode) = input.modified( diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt index 1abd344111..ffc7fd8534 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -42,21 +42,34 @@ open class DefaultPageCreator( open fun pageForModule(m: DModule): ModulePageNode = ModulePageNode(m.name.ifEmpty { "" }, contentForModule(m), listOf(m), m.packages.map(::pageForPackage)) + /** + * We want to generate separated pages for no-actual typealias. + * Actual typealias are displayed on pages for their expect class (trough [ActualTypealias] extra). + * + * @see ActualTypealias + */ + private fun List.filterOutActualTypeAlias(): List { + 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) } + } + open fun pageForPackage(p: DPackage): PackagePageNode = PackagePageNode( p.name, contentForPackage(p), setOf(p.dri), listOf(p), if (mergeImplicitExpectActualDeclarations) - p.classlikes.mergeClashingDocumentable().map(::pageForClasslikes) + + (p.classlikes + p.typealiases).filterOutActualTypeAlias() + .mergeClashingDocumentable().map(::pageForClasslikes) + p.functions.mergeClashingDocumentable().map(::pageForFunctions) + p.properties.mergeClashingDocumentable().map(::pageForProperties) else - p.classlikes.renameClashingDocumentable().map(::pageForClasslike) + + (p.classlikes + p.typealiases).filterOutActualTypeAlias() + .renameClashingDocumentable().map(::pageForClasslike) + p.functions.renameClashingDocumentable().map(::pageForFunction) + p.properties.mapNotNull(::pageForProperty) ) open fun pageForEnumEntry(e: DEnumEntry): ClasslikePageNode = pageForEnumEntries(listOf(e)) - open fun pageForClasslike(c: DClasslike): ClasslikePageNode = pageForClasslikes(listOf(c)) + open fun pageForClasslike(c: Documentable): ClasslikePageNode = pageForClasslikes(listOf(c)) open fun pageForEnumEntries(documentables: List): ClasslikePageNode { val dri = documentables.dri.also { @@ -83,33 +96,38 @@ open class DefaultPageCreator( ) } - open fun pageForClasslikes(documentables: List): ClasslikePageNode { + /** + * @param documentables a list of [DClasslike] and [DTypeAlias] with the same dri in different sourceSets + */ + open fun pageForClasslikes(documentables: List): 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.filterIsInstance() + val constructors = - if (documentables.shouldDocumentConstructors()) { - documentables.flatMap { (it as? WithConstructors)?.constructors ?: emptyList() } + if (classlikes.shouldDocumentConstructors()) { + classlikes.flatMap { (it as? WithConstructors)?.constructors ?: emptyList() } } 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 nestedClasslikes = classlikes.flatMap { it.classlikes } + val functions = classlikes.flatMap { it.filteredFunctions } + val props = classlikes.flatMap { it.filteredProperties } + val entries = classlikes.flatMap { if (it is DEnum) it.entries else emptyList() } val childrenPages = constructors.map(::pageForFunction) + if (mergeImplicitExpectActualDeclarations) - classlikes.mergeClashingDocumentable().map(::pageForClasslikes) + + nestedClasslikes.mergeClashingDocumentable().map(::pageForClasslikes) + functions.mergeClashingDocumentable().map(::pageForFunctions) + props.mergeClashingDocumentable().map(::pageForProperties) + entries.mergeClashingDocumentable().map(::pageForEnumEntries) else - classlikes.renameClashingDocumentable().map(::pageForClasslike) + + nestedClasslikes.renameClashingDocumentable().map(::pageForClasslike) + functions.renameClashingDocumentable().map(::pageForFunction) + props.renameClashingDocumentable().mapNotNull(::pageForProperty) + entries.renameClashingDocumentable().map(::pageForEnumEntry) @@ -329,7 +347,7 @@ open class DefaultPageCreator( sortedWith(compareBy({ it.name }, { it.parameters.size }, { it.dri.toString() })) /** - * @param documentables a list of [DClasslike] and [DEnumEntry] with the same dri in different sourceSets + * @param documentables a list of [DClasslike] and [DEnumEntry] and [DTypeAlias] with the same dri in different sourceSets */ protected open fun contentForClasslikesAndEntries(documentables: List): ContentGroup = contentBuilder.contentFor(documentables.dri, documentables.sourceSets) { @@ -477,8 +495,7 @@ open class DefaultPageCreator( .takeIf { documentable is DProperty } }?.let { group(sourceSets = setOf(sourceSet), kind = ContentKind.BriefComment) { - if (documentable.hasSeparatePage) createBriefComment(documentable, sourceSet, it) - else comment(it.root) + createBriefComment(documentable, sourceSet, it) } } } @@ -689,6 +706,7 @@ open class DefaultPageCreator( protected open fun TagWrapper.toHeaderString() = this.javaClass.toGenericString().split('.').last() } + internal val List.sourceSets: Set get() = flatMap { it.sourceSets }.toSet() @@ -706,9 +724,6 @@ internal val Documentable.descriptions: SourceSetDependent internal val Documentable.customTags: Map> get() = groupedTags.withTypeNamed() -private val Documentable.hasSeparatePage: Boolean - get() = this !is DTypeAlias - /** * @see DefaultPageCreator.sortDivergentElementsDeterministically for usage */ diff --git a/plugins/base/src/main/resources/dokka/images/nav-icons/typealias-kotlin.svg b/plugins/base/src/main/resources/dokka/images/nav-icons/typealias-kotlin.svg new file mode 100644 index 0000000000..4795069b26 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/images/nav-icons/typealias-kotlin.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plugins/base/src/main/resources/dokka/styles/style.css b/plugins/base/src/main/resources/dokka/styles/style.css index 30f3b176f8..ab48a7db38 100644 --- a/plugins/base/src/main/resources/dokka/styles/style.css +++ b/plugins/base/src/main/resources/dokka/styles/style.css @@ -913,6 +913,10 @@ code:not(.block) { content: url("../images/nav-icons/object.svg"); } +.nav-icon.typealias-kt::before { + content: url("../images/nav-icons/typealias-kotlin.svg"); +} + .nav-icon.val::before { content: url("../images/nav-icons/field-value.svg"); } diff --git a/plugins/base/src/test/kotlin/content/functions/ContentForBriefTest.kt b/plugins/base/src/test/kotlin/content/functions/ContentForBriefTest.kt index f86506af9a..c09a0e4d21 100644 --- a/plugins/base/src/test/kotlin/content/functions/ContentForBriefTest.kt +++ b/plugins/base/src/test/kotlin/content/functions/ContentForBriefTest.kt @@ -4,6 +4,7 @@ import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.links.TypeConstructor import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DPackage import org.jetbrains.dokka.model.dfs import org.jetbrains.dokka.pages.* import org.junit.jupiter.api.Assertions.assertEquals @@ -131,6 +132,32 @@ class ContentForBriefTest : BaseAbstractTest() { } } + @Test + fun `brief should work for typealias`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |/** + |* This is an example of html + |* + |* This is definitely not a brief + |*/ + |typealias A = Int + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionBriefDocs = module.singleTypeAliasesDescription("test") + + assertEquals( + "This is an example of html", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + @Test fun `brief for functions should work with html`() { testInline( @@ -344,4 +371,14 @@ class ContentForBriefTest : BaseAbstractTest() { val function = functionsTable.children.first() return function.dfs { it is ContentGroup && it.dci.kind == ContentKind.Comment && it.children.all { it is ContentText } } as ContentGroup } + private fun RootPageNode.singleTypeAliasesDescription(packageName: String): ContentGroup { + val packagePage = + dfs { it.name == packageName && (it as WithDocumentables).documentables.firstOrNull() is DPackage } as ContentPage + val contentTable = + packagePage.content.dfs { it is ContentTable && it.dci.kind == ContentKind.Classlikes } as ContentTable + + assertEquals(1, contentTable.children.size) + val row = contentTable.children.first() + return row.dfs { it is ContentGroup && it.dci.kind == ContentKind.Comment && it.children.all { it is ContentText } } as ContentGroup + } } \ No newline at end of file diff --git a/plugins/base/src/test/kotlin/content/typealiases/TypealiasTest.kt b/plugins/base/src/test/kotlin/content/typealiases/TypealiasTest.kt new file mode 100644 index 0000000000..c2f58cb9f5 --- /dev/null +++ b/plugins/base/src/test/kotlin/content/typealiases/TypealiasTest.kt @@ -0,0 +1,78 @@ +package content.typealiases + +import matchers.content.* +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.pages.* +import org.junit.jupiter.api.Test +import utils.assertNotNull + + +class TypealiasTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(commonStdlibPath!!) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + } + } + + @Test + fun `typealias should have a dedicated page with full documentation`() { + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package example + | + | /** + | * Brief text + | * + | * some text + | * + | * @see String + | * @throws Unit + | */ + | typealias A = String + """, + configuration + ) { + pagesTransformationStage = { module -> + val content = (module.dfs { it.name == "A" } as ClasslikePageNode).content + val platformHinted = content.dfs { it is PlatformHintedContent } + platformHinted.assertNotNull("platformHinted").assertNode { + group { + group { + group { + +"typealias " + group { group { link { +"A" } } } + +" = " + group { link { +"String" } } + } + } + + group { + group { + group { + group { +"Brief text" } + group { +"some text" } + } + } + } + + header { +"See also" } + table { + group { link { +"String" } } + } + + header { +"Throws" } + table { + group { group { link { +"Unit" } } } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt b/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt index a7a7bacf62..e83f70d5f7 100644 --- a/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt +++ b/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt @@ -37,7 +37,7 @@ class NavigationIconTest : BaseAbstractTest() { .filterKeys { it.startsWith("images/nav-icons") } .keys.sorted() - assertEquals(15, navIconAssets.size) + assertEquals(16, navIconAssets.size) assertEquals("images/nav-icons/abstract-class-kotlin.svg", navIconAssets[0]) assertEquals("images/nav-icons/abstract-class.svg", navIconAssets[1]) assertEquals("images/nav-icons/annotation-kotlin.svg", navIconAssets[2]) @@ -53,6 +53,7 @@ class NavigationIconTest : BaseAbstractTest() { assertEquals("images/nav-icons/interface-kotlin.svg", navIconAssets[12]) assertEquals("images/nav-icons/interface.svg", navIconAssets[13]) assertEquals("images/nav-icons/object.svg", navIconAssets[14]) + assertEquals("images/nav-icons/typealias-kotlin.svg", navIconAssets[15]) } } } @@ -99,6 +100,15 @@ class NavigationIconTest : BaseAbstractTest() { ) } + @Test + fun `should add icon styles to kotlin typealias navigation item`() { + assertNavigationIcon( + source = kotlinSource("typealias KotlinTypealias = String"), + expectedIconClass = "typealias-kt", + expectedNavLinkText = "KotlinTypealias" + ) + } + @Test fun `should add icon styles to kotlin enum navigation item`() { assertNavigationIcon( diff --git a/plugins/base/src/test/kotlin/signatures/SignatureTest.kt b/plugins/base/src/test/kotlin/signatures/SignatureTest.kt index 3a263fd077..d271be2e37 100644 --- a/plugins/base/src/test/kotlin/signatures/SignatureTest.kt +++ b/plugins/base/src/test/kotlin/signatures/SignatureTest.kt @@ -603,7 +603,7 @@ class SignatureTest : BaseAbstractTest() { pluginOverrides = listOf(writerPlugin) ) { renderingStage = { _, _ -> - writerPlugin.writer.renderedContent("root/example.html").firstSignature().match( + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( "typealias ", A("PlainTypealias"), " = ", A("Int"), ignoreSpanWithTokenStyle = true ) @@ -663,7 +663,7 @@ class SignatureTest : BaseAbstractTest() { pluginOverrides = listOf(writerPlugin) ) { renderingStage = { _, _ -> - writerPlugin.writer.renderedContent("root/example.html").firstSignature().match( + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( "typealias ", A("PlainTypealias"), " = ", A("Comparable"), "<", A("Int"), ">", ignoreSpanWithTokenStyle = true @@ -690,7 +690,7 @@ class SignatureTest : BaseAbstractTest() { pluginOverrides = listOf(writerPlugin) ) { renderingStage = { _, _ -> - writerPlugin.writer.renderedContent("root/example.html").firstSignature().match( + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( "typealias ", A("GenericTypealias"), "<", A("T"), "> = ", A("Comparable"), "<", A("T"), ">", ignoreSpanWithTokenStyle = true diff --git a/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt b/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt index 5e3352095d..241fb48150 100644 --- a/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt +++ b/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt @@ -113,6 +113,38 @@ class MergeImplicitExpectActualDeclarationsTest : BaseAbstractTest() { } } + @Test + fun `should merge class and typealias`() { + testInline( + """ + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class A { + | fun method1(): String + |} + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |typealias A = String + | + """.trimMargin(), + configuration(true), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val classPage = root.dfs { it.name == "A" } as? ClasslikePageNode + assertNotNull(classPage, "Tested class not found!") + + val platformHintedContent = classPage.content.dfs { it is PlatformHintedContent }.assertNotNull("platformHintedContent") + assertEquals(2, platformHintedContent.sourceSets.size) + + platformHintedContent.dfs { it is ContentText && it.text == "class " }.assertNotNull("class keyword") + platformHintedContent.dfs { it is ContentText && it.text == "typealias " }.assertNotNull("typealias keyword") + } + } + } @Test fun `should merge method and prop`() { testInline(