diff options
author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2023-11-10 11:46:54 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-10 11:46:54 +0100 |
commit | 8e5c63d035ef44a269b8c43430f43f5c8eebfb63 (patch) | |
tree | 1b915207b2b9f61951ddbf0ff2e687efd053d555 /dokka-subprojects/plugin-base/src/test | |
parent | a44efd4ba0c2e4ab921ff75e0f53fc9335aa79db (diff) | |
download | dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.gz dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.bz2 dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.zip |
Restructure the project to utilize included builds (#3174)
* Refactor and simplify artifact publishing
* Update Gradle to 8.4
* Refactor and simplify convention plugins and build scripts
Fixes #3132
---------
Co-authored-by: Adam <897017+aSemy@users.noreply.github.com>
Co-authored-by: Oleg Yukhnevich <whyoleg@gmail.com>
Diffstat (limited to 'dokka-subprojects/plugin-base/src/test')
152 files changed, 31982 insertions, 0 deletions
diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/basic/AbortGracefullyOnMissingDocumentablesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/basic/AbortGracefullyOnMissingDocumentablesTest.kt new file mode 100644 index 00000000..693174ec --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/basic/AbortGracefullyOnMissingDocumentablesTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package basic + +import org.jetbrains.dokka.DokkaGenerator +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import kotlin.test.Test +import kotlin.test.assertTrue + +class AbortGracefullyOnMissingDocumentablesTest: BaseAbstractTest() { + @Test + fun `Generation aborts Gracefully with no Documentables`() { + DokkaGenerator(dokkaConfiguration { }, logger).generate() + + assertTrue( + logger.progressMessages.any { message -> "Exiting Generation: Nothing to document" == message }, + "Expected graceful exit message. Found: ${logger.progressMessages}" + ) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/basic/DRITest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/basic/DRITest.kt new file mode 100644 index 00000000..6fd9d4b0 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/basic/DRITest.kt @@ -0,0 +1,351 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package basic + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.Nullable +import org.jetbrains.dokka.links.TypeConstructor +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.pages.ClasslikePageNode +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.MemberPageNode +import kotlin.test.Test +import kotlin.test.assertEquals + +class DRITest : BaseAbstractTest() { + @Test + fun issue634() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package toplevel + | + |inline fun <T, R : Comparable<R>> Array<out T>.mySortBy( + | crossinline selector: (T) -> R?): Array<out T> = TODO() + |} + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val expected = TypeConstructor( + "kotlin.Function1", listOf( + TypeParam(listOf(Nullable(TypeConstructor("kotlin.Any", emptyList())))), + Nullable(TypeParam(listOf(TypeConstructor("kotlin.Comparable", listOf(RecursiveType(0)))))) + ) + ) + val actual = module.packages.single() + .functions.single() + .dri.callable?.params?.single() + assertEquals(expected, actual) + } + } + } + + @Test + fun issue634WithImmediateNullableSelf() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package toplevel + | + |fun <T : Comparable<T>> Array<T>.doSomething(t: T?): Array<T> = TODO() + |} + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val expected = Nullable(TypeParam(listOf(TypeConstructor("kotlin.Comparable", listOf(RecursiveType(0)))))) + val actual = module.packages.single() + .functions.single() + .dri.callable?.params?.single() + assertEquals(expected, actual) + } + } + } + + @Test + fun issue634WithGenericNullableReceiver() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package toplevel + | + |fun <T : Comparable<T>> T?.doSomethingWithNullable() = TODO() + |} + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val expected = Nullable(TypeParam(listOf(TypeConstructor("kotlin.Comparable", listOf(RecursiveType(0)))))) + val actual = module.packages.single() + .functions.single() + .dri.callable?.receiver + assertEquals(expected, actual) + } + } + } + + @Test + fun issue642WithStarAndAny() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + analysisPlatform = "js" + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + | + |open class Bar<Z> + |class ReBarBar : Bar<StringBuilder>() + |class Foo<out T : Comparable<*>, R : List<Bar<*>>> + | + |fun <T : Comparable<Any?>> Foo<T, *>.qux(): String = TODO() + |fun <T : Comparable<*>> Foo<T, *>.qux(): String = TODO() + | + """.trimMargin(), + configuration + ) { + pagesGenerationStage = { module -> + // DRI(//qux/Foo[TypeParam(bounds=[kotlin.Comparable[kotlin.Any?]]),*]#/PointingToFunctionOrClasslike/) + val expectedDRI = DRI( + "", + null, + Callable( + "qux", TypeConstructor( + "Foo", listOf( + TypeParam( + listOf( + TypeConstructor( + "kotlin.Comparable", listOf( + Nullable(TypeConstructor("kotlin.Any", emptyList())) + ) + ) + ) + ), + StarProjection + ) + ), + emptyList() + ) + ) + + val driCount = module + .withDescendants() + .filterIsInstance<ContentPage>() + .sumBy { it.dri.count { dri -> dri == expectedDRI } } + + assertEquals(1, driCount) + } + } + } + + @Test + fun driForGenericClass(){ + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/Test.kt + |package example + | + |class Sample<S>(first: S){ } + | + | + """.trimMargin(), + configuration + ) { + pagesGenerationStage = { module -> + val sampleClass = module.dfs { it.name == "Sample" } as ClasslikePageNode + val classDocumentable = sampleClass.documentables.firstOrNull() as DClass + + assertEquals( "example/Sample///PointingToDeclaration/", sampleClass.dri.first().toString()) + assertEquals("example/Sample///PointingToGenericParameters(0)/", classDocumentable.generics.first().dri.toString()) + } + } + } + + @Test + fun driForGenericFunction(){ + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + } + } + } + testInline( + """ + |/src/main/kotlin/Test.kt + |package example + | + |class Sample<S>(first: S){ + | fun <T> genericFun(param1: String): Tuple<S,T> = TODO() + |} + | + | + """.trimMargin(), + configuration + ) { + pagesGenerationStage = { module -> + val sampleClass = module.dfs { it.name == "Sample" } as ClasslikePageNode + val functionNode = sampleClass.children.first { it.name == "genericFun" } as MemberPageNode + val functionDocumentable = functionNode.documentables.firstOrNull() as DFunction + val parameter = functionDocumentable.parameters.first() + + assertEquals("example/Sample/genericFun/#kotlin.String/PointingToDeclaration/", functionNode.dri.first().toString()) + + assertEquals(1, functionDocumentable.parameters.size) + assertEquals("example/Sample/genericFun/#kotlin.String/PointingToCallableParameters(0)/", parameter.dri.toString()) + //1 since from the function's perspective there is only 1 new generic declared + //The other one is 'inherited' from class + assertEquals( 1, functionDocumentable.generics.size) + assertEquals( "T", functionDocumentable.generics.first().name) + assertEquals( "example/Sample/genericFun/#kotlin.String/PointingToGenericParameters(0)/", functionDocumentable.generics.first().dri.toString()) + } + } + } + + @Test + fun driForFunctionNestedInsideInnerClass() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + } + } + } + testInline( + """ + |/src/main/kotlin/Test.kt + |package example + | + |class Sample<S>(first: S){ + | inner class SampleInner { + | fun foo(): S = TODO() + | } + |} + | + | + """.trimMargin(), + configuration + ) { + pagesGenerationStage = { module -> + val sampleClass = module.dfs { it.name == "Sample" } as ClasslikePageNode + val sampleInner = sampleClass.children.first { it.name == "SampleInner" } as ClasslikePageNode + val foo = sampleInner.children.first { it.name == "foo" } as MemberPageNode + val documentable = foo.documentables.firstOrNull() as DFunction + + val generics = (sampleClass.documentables.firstOrNull() as WithGenerics).generics + assertEquals(generics.first().dri.toString(), (documentable.type as TypeParameter).dri.toString()) + assertEquals(0, documentable.generics.size) + } + } + } + + @Test + fun driForGenericExtensionFunction(){ + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/Test.kt + |package example + | + | fun <T> List<T>.extensionFunction(): String = "" + | + """.trimMargin(), + configuration + ) { + pagesGenerationStage = { module -> + val extensionFunction = module.dfs { it.name == "extensionFunction" } as MemberPageNode + val documentable = extensionFunction.documentables.firstOrNull() as DFunction + + assertEquals( + "example//extensionFunction/kotlin.collections.List[TypeParam(bounds=[kotlin.Any?])]#/PointingToDeclaration/", + extensionFunction.dri.first().toString() + ) + assertEquals(1, documentable.generics.size) + assertEquals("T", documentable.generics.first().name) + assertEquals( + "example//extensionFunction/kotlin.collections.List[TypeParam(bounds=[kotlin.Any?])]#/PointingToGenericParameters(0)/", + documentable.generics.first().dri.toString() + ) + + } + } + } + + @Test + fun `deep recursive typebound #1342`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/Test.kt + |package example + | + | fun <T, S, R> recursiveBound(t: T, s: S, r: R) where T: List<S>, S: List<R>, R: List<S> = Unit + | + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.dfs { it.name == "recursiveBound" } + assertEquals( + "example//recursiveBound/#TypeParam(bounds=[kotlin.collections.List[TypeParam(bounds=[kotlin.collections.List[TypeParam(bounds=[kotlin.collections.List[^^]])]])]])#TypeParam(bounds=[kotlin.collections.List[TypeParam(bounds=[kotlin.collections.List[^]])]])#TypeParam(bounds=[kotlin.collections.List[TypeParam(bounds=[kotlin.collections.List[^]])]])/PointingToDeclaration/", + function?.dri?.toString(), + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/basic/DokkaBasicTests.kt b/dokka-subprojects/plugin-base/src/test/kotlin/basic/DokkaBasicTests.kt new file mode 100644 index 00000000..2b353ad8 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/basic/DokkaBasicTests.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package basic + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.pages.ClasslikePageNode +import org.jetbrains.dokka.pages.ModulePageNode +import kotlin.test.Test +import kotlin.test.assertEquals + +class DokkaBasicTests : BaseAbstractTest() { + + @Test + fun basic1() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package basic + | + |class Test { + | val tI = 1 + | fun tF() = 2 + |} + """.trimMargin(), + configuration + ) { + pagesGenerationStage = { + val root = it as ModulePageNode + assertEquals(3, root.getClasslikeToMemberMap().filterKeys { it.name == "Test" }.entries.firstOrNull()?.value?.size) + } + } + } + + private fun ModulePageNode.getClasslikeToMemberMap() = + this.parentMap.filterValues { it is ClasslikePageNode }.entries.groupBy({ it.value }) { it.key } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/basic/FailOnWarningTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/basic/FailOnWarningTest.kt new file mode 100644 index 00000000..ebdf7860 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/basic/FailOnWarningTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package basic + +import org.jetbrains.dokka.DokkaException +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.testApi.logger.TestLogger +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class FailOnWarningTest : BaseAbstractTest() { + + @Test + fun `throws exception if one or more warnings were emitted`() { + val configuration = dokkaConfiguration { + failOnWarning = true + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + assertFailsWith<DokkaException> { + testInline( + """ + |/src/main/kotlin/Bar.kt + |package sample + |class Bar {} + """.trimIndent(), configuration + ) { + pluginsSetupStage = { + logger.warn("Warning!") + } + } + } + } + + @Test + fun `throws exception if one or more error were emitted`() { + val configuration = dokkaConfiguration { + failOnWarning = true + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + assertFailsWith<DokkaException> { + testInline( + """ + |/src/main/kotlin/Bar.kt + |package sample + |class Bar {} + """.trimIndent(), configuration + ) { + pluginsSetupStage = { + logger.error("Error!") + } + } + } + } + + @Test + fun `does not throw if now warning or error was emitted`() { + + val configuration = dokkaConfiguration { + failOnWarning = true + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + + testInline( + """ + |/src/main/kotlin/Bar.kt + |package sample + |class Bar {} + """.trimIndent(), + configuration, + loggerForTest = TestLogger(ZeroErrorOrWarningCountDokkaLogger()) + ) { + /* We expect no Exception */ + } + } + + @Test + fun `does not throw if disabled`() { + val configuration = dokkaConfiguration { + failOnWarning = false + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + + testInline( + """ + |/src/main/kotlin/Bar.kt + |package sample + |class Bar {} + """.trimIndent(), configuration + ) { + pluginsSetupStage = { + logger.warn("Error!") + logger.error("Error!") + } + } + } +} + +private class ZeroErrorOrWarningCountDokkaLogger( + logger: DokkaLogger = DokkaConsoleLogger(LoggingLevel.DEBUG) +) : DokkaLogger by logger { + override var warningsCount: Int = 0 + override var errorsCount: Int = 0 +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/basic/LoggerTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/basic/LoggerTest.kt new file mode 100644 index 00000000..12c39690 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/basic/LoggerTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package basic + +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import org.jetbrains.dokka.utilities.MessageEmitter +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class LoggerTest { + class AccumulatingEmitter : MessageEmitter { + val messages: MutableList<String> = mutableListOf() + override fun invoke(message: String) { + messages.add(message) + } + } + + @Test + fun `should display info messages if logging is info`(){ + val emitter = AccumulatingEmitter() + val logger = DokkaConsoleLogger(LoggingLevel.INFO, emitter) + + logger.debug("Debug!") + logger.info("Info!") + + assertTrue(emitter.messages.size > 0) + assertTrue(emitter.messages.any { it == "Info!" }) + assertFalse(emitter.messages.any { it == "Debug!" }) + } + + @Test + fun `should not display info messages if logging is warn`(){ + val emitter = AccumulatingEmitter() + val logger = DokkaConsoleLogger(LoggingLevel.WARN, emitter) + + logger.warn("Warning!") + logger.info("Info!") + + + assertTrue(emitter.messages.size > 0) + assertFalse(emitter.messages.any { it.contains("Info!") }) + assertTrue(emitter.messages.any { it.contains("Warning!") }) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/ContentInDescriptionTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/ContentInDescriptionTest.kt new file mode 100644 index 00000000..a278795d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/ContentInDescriptionTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.doc.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ContentInDescriptionTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + classpath += jvmStdlibPath!! + } + } + } + + val expectedDescription = Description( + CustomDocTag( + listOf( + P( + listOf( + Text("Hello World! Docs with period issue, e.g."), + Text(String(Character.toChars(160)), params = mapOf("content-type" to "html")), + Text("this.") + ) + ) + ), + params = emptyMap(), + name = "MARKDOWN_FILE" + ) + ) + + @Test + fun `nbsp is handled as code in kotlin`() { + testInline( + """ + |/src/main/kotlin/sample/ParentKt.kt + |package sample; + |/** + | * Hello World! Docs with period issue, e.g. this. + | */ + |public class ParentKt { + |} + """.trimIndent(), configuration + ) { + documentablesMergingStage = { + val classlike = it.packages.flatMap { it.classlikes }.find { it.name == "ParentKt" } + + assertTrue(classlike != null) + assertEquals(expectedDescription, classlike.documentation.values.first().children.first()) + } + } + } + + @Test + fun `nbsp is handled as code in java`() { + testInline( + """ + |/src/main/kotlin/sample/Parent.java + |package sample; + |/** + | * Hello World! Docs with period issue, e.g. this. + | */ + |public class Parent { + |} + """.trimIndent(), configuration + ) { + documentablesMergingStage = { + val classlike = it.packages.flatMap { it.classlikes }.find { it.name == "Parent" } + + assertTrue(classlike != null) + assertEquals(expectedDescription, classlike.documentation.values.first().children.first()) + } + } + } + + @Test + fun `same documentation in java and kotlin when nbsp is present`() { + testInline( + """ + |/src/main/kotlin/sample/Parent.java + |package sample; + |/** + | * Hello World! Docs with period issue, e.g. this. + | */ + |public class Parent { + |} + | + |/src/main/kotlin/sample/ParentKt.kt + |package sample; + |/** + | * Hello World! Docs with period issue, e.g. this. + | */ + |public class ParentKt { + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val java = module.packages.flatMap { it.classlikes }.first { it.name == "Parent" } + val kotlin = module.packages.flatMap { it.classlikes }.first { it.name == "ParentKt" } + + assertEquals(java.documentation.values.first(), kotlin.documentation.values.first()) + } + } + } + + @Test + fun `text surrounded by angle brackets is not removed`() { + testInline( + """ + |/src/main/kotlin/sample/Foo.kt + |package sample + |/** + | * My example `CodeInline<Bar>` + | * ``` + | * CodeBlock<Bar> + | * ``` + | */ + |class Foo { + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val cls = module.packages.flatMap { it.classlikes }.first { it.name == "Foo" } + val documentation = cls.documentation.values.first() + val docTags = documentation.children.single().root.children + + assertEquals("CodeInline<Bar>", ((docTags[0].children[1] as CodeInline).children.first() as Text).body) + assertEquals("CodeBlock<Bar>", ((docTags[1] as CodeBlock).children.first() as Text).body) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/HighlightingTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/HighlightingTest.kt new file mode 100644 index 00000000..a7fb2bde --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/HighlightingTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.pages.* +import kotlin.test.Test +import kotlin.test.assertTrue + +class HighlightingTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(commonStdlibPath!!, jvmStdlibPath!!) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + } + } + + @Test + fun `open suspend fun`() { + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package example + | + | open suspend fun simpleFun(): String = "Celebrimbor" + """, + configuration + ) { + pagesTransformationStage = { module -> + val symbol = (module.dfs { it.name == "simpleFun" } as MemberPageNode).content + .dfs { it is ContentGroup && it.dci.kind == ContentKind.Symbol } + val children = symbol?.children + + for (it in listOf( + Pair(0, TokenStyle.Keyword), Pair(1, TokenStyle.Keyword), Pair(2, TokenStyle.Keyword), + Pair(4, TokenStyle.Punctuation), Pair(5, TokenStyle.Punctuation), Pair(6, TokenStyle.Operator) + )) + assertTrue(children?.get(it.first)?.style?.contains(it.second) == true) + assertTrue(children?.get(3)?.children?.first()?.style?.contains(TokenStyle.Function) == true) + } + } + } + + @Test + fun `plain typealias of plain class with annotation`() { + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |@MustBeDocumented + |@Target(AnnotationTarget.TYPEALIAS) + |annotation class SomeAnnotation + | + |@SomeAnnotation + |typealias PlainTypealias = Int + | + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { module -> + val symbol = (module.dfs { it.name == "example" } as PackagePageNode).content + .dfs { it is ContentGroup && it.dci.kind == ContentKind.Symbol } + val children = symbol?.children + + for (it in listOf( + Pair(1, TokenStyle.Keyword), Pair(3, TokenStyle.Operator) + )) + assertTrue(children?.get(it.first)?.style?.contains(it.second) == true) + val annotation = children?.first()?.children?.first() + + assertTrue(annotation?.children?.get(0)?.style?.contains(TokenStyle.Annotation) == true) + assertTrue(annotation?.children?.get(1)?.children?.first()?.style?.contains(TokenStyle.Annotation) == true) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/ContentForAnnotationsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/ContentForAnnotationsTest.kt new file mode 100644 index 00000000..7293b53c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/ContentForAnnotationsTest.kt @@ -0,0 +1,351 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.annotations + +import matchers.content.* +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.base.utils.firstNotNullOfOrNull +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.ContentText +import org.jetbrains.dokka.pages.MemberPageNode +import org.jetbrains.dokka.pages.PackagePageNode +import utils.ParamAttributes +import utils.assertNotNull +import utils.bareSignature +import utils.propertySignature +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + + +class ContentForAnnotationsTest : BaseAbstractTest() { + + + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + classpath += jvmStdlibPath!! + } + } + } + + @Test + fun `function with documented annotation`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, + | AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD + |) + |@Retention(AnnotationRetention.SOURCE) + |@MustBeDocumented + |annotation class Fancy + | + | + |@Fancy + |fun function(@Fancy abc: String): String { + | return "Hello, " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + mapOf("Fancy" to emptySet()), + "", + "", + emptySet(), + "function", + "String", + "abc" to ParamAttributes(mapOf("Fancy" to emptySet()), emptySet(), "String") + ) + } + } + } + + } + } + } + } + + @Test + fun `function with undocumented annotation`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, + | AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD + |) + |@Retention(AnnotationRetention.SOURCE) + |annotation class Fancy + | + |@Fancy + |fun function(@Fancy abc: String): String { + | return "Hello, " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + "String", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + + } + } + } + } + + @Test + fun `property with undocumented annotation`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |@Suppress + |val property: Int = 6 + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + propertySignature(emptyMap(), "", "", emptySet(), "val", "property", "Int", "6") + } + } + } + } + + @Test + fun `property with documented annotation`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |@MustBeDocumented + |annotation class Fancy + | + |@Fancy + |val property: Int = 6 + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + propertySignature(mapOf("Fancy" to emptySet()), "", "", emptySet(), "val", "property", "Int", "6") + } + } + } + } + + + @Test + fun `rich documented annotation`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |@MustBeDocumented + |@Retention(AnnotationRetention.SOURCE) + |@Target(AnnotationTarget.PROPERTY) + |annotation class BugReport( + | val assignedTo: String = "[none]", + | val testCase: KClass<ABC> = ABC::class, + | val status: Status = Status.UNCONFIRMED, + | val ref: Reference = Reference(value = 1), + | val reportedBy: Array<Reference>, + | val showStopper: Boolean = false + | val previousReport: BugReport? + |) { + | enum class Status { + | UNCONFIRMED, CONFIRMED, FIXED, NOTABUG + | } + | class ABC + |} + |annotation class Reference(val value: Long) + |annotation class ReferenceReal(val value: Double) + | + | + |@BugReport( + | assignedTo = "me", + | testCase = BugReport.ABC::class, + | status = BugReport.Status.FIXED, + | ref = Reference(value = 2u), + | reportedBy = [Reference(value = 2UL), Reference(value = 4L), + | ReferenceReal(value = 4.9), ReferenceReal(value = 2f)], + | showStopper = true, + | previousReport = null + |) + |val ltint: Int = 5 + """.trimIndent(), testConfiguration + ) { + documentablesCreationStage = { modules -> + + fun expectedAnnotationValue(name: String, value: AnnotationParameterValue) = AnnotationValue(Annotations.Annotation( + dri = DRI("test", name), + params = mapOf("value" to value), + scope = Annotations.AnnotationScope.DIRECT, + mustBeDocumented = false + )) + val property = modules.flatMap { it.packages }.flatMap { it.properties }.first() + val annotation = property.extra[Annotations]?.let { + it.directAnnotations.entries.firstNotNullOfOrNull { (_, annotations) -> annotations.firstOrNull() } + } + val annotationParams = annotation?.params ?: emptyMap() + + assertEquals(expectedAnnotationValue("Reference", IntValue(2)), annotationParams["ref"]) + + val reportedByParam = ArrayValue(listOf( + expectedAnnotationValue("Reference", LongValue(2)), + expectedAnnotationValue("Reference", LongValue(4)), + expectedAnnotationValue("ReferenceReal", DoubleValue(4.9)), + expectedAnnotationValue("ReferenceReal", FloatValue(2f)) + )) + assertEquals(reportedByParam, annotationParams["reportedBy"]) + assertEquals(BooleanValue(true), annotationParams["showStopper"]) + assertEquals(NullValue, annotationParams["previousReport"]) + } + + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + propertySignature( + mapOf( + "BugReport" to setOf( + "assignedTo", + "testCase", + "status", + "ref", + "reportedBy", + "showStopper", + "previousReport" + ) + ), "", "", emptySet(), "val", "ltint", "Int", "5" + ) + } + } + } + } + + @Test + fun `JvmName for property with setter and getter`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + |@get:JvmName("xd") + |@set:JvmName("asd") + |var property: String + | get() = "" + | set(value) {} + """.trimIndent(), testConfiguration + ) { + documentablesCreationStage = { modules -> + fun expectedAnnotation(name: String) = Annotations.Annotation( + dri = DRI("kotlin.jvm", "JvmName"), + params = mapOf("name" to StringValue(name)), + scope = Annotations.AnnotationScope.DIRECT, + mustBeDocumented = true + ) + + val property = modules.flatMap { it.packages }.flatMap { it.properties }.first() + val getterAnnotation = property.getter?.extra?.get(Annotations)?.let { + it.directAnnotations.entries.firstNotNullOfOrNull { (_, annotations) -> annotations.firstOrNull() } + } + val setterAnnotation = property.getter?.extra?.get(Annotations)?.let { + it.directAnnotations.entries.firstNotNullOfOrNull { (_, annotations) -> annotations.firstOrNull() } + } + + assertEquals(expectedAnnotation("xd"), getterAnnotation) + assertTrue(getterAnnotation?.mustBeDocumented!!) + assertEquals(Annotations.AnnotationScope.DIRECT, getterAnnotation.scope) + + assertEquals(expectedAnnotation("asd"), setterAnnotation) + assertTrue(setterAnnotation?.mustBeDocumented!!) + assertEquals(Annotations.AnnotationScope.DIRECT, setterAnnotation.scope) + } + } + } + + @Test + fun `annotated bounds in Kotlin`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |@MustBeDocumented + |@Target(AnnotationTarget.TYPE_PARAMETER) + |annotation class Hello(val bar: String) + |fun <T: @Hello("abc") String> foo(arg: String): List<T> = TODO() + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { root -> + val fooPage = root.dfs { it.name == "foo" } as MemberPageNode + fooPage.content.dfs { it is ContentText && it.text == "Hello" }.assertNotNull() + } + } + } + + @Test + fun `annotated bounds in Java`() { + testInline( + """ + |/src/main/java/demo/AnnotationTest.java + |package demo; + |import java.lang.annotation.*; + |import java.util.List; + |@Documented + |@Target({ElementType.TYPE_USE, ElementType.TYPE}) + |@interface Hello { + | public String bar() default ""; + |} + |public class AnnotationTest { + | public <T extends @Hello(bar = "baz") String> List<T> foo() { + | return null; + | } + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { root -> + val fooPage = root.dfs { it.name == "foo" } as MemberPageNode + fooPage.content.dfs { it is ContentText && it.text == "Hello" }.assertNotNull() + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/FileLevelJvmNameTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/FileLevelJvmNameTest.kt new file mode 100644 index 00000000..5809d7df --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/FileLevelJvmNameTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.annotations + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Annotations +import org.jetbrains.dokka.model.StringValue +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.test.assertEquals + +class FileLevelJvmNameTest : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + classpath += jvmStdlibPath!! + } + } + } + + companion object { + private const val functionTest = + """ + |/src/main/kotlin/test/source.kt + |@file:JvmName("CustomJvmName") + |package test + | + |fun function(abc: String): String { + | return "Hello, " + abc + |} + """ + + private const val extensionFunctionTest = + """ + |/src/main/kotlin/test/source.kt + |@file:JvmName("CustomJvmName") + |package test + | + |fun String.function(abc: String): String { + | return "Hello, " + abc + |} + """ + + private const val propertyTest = + """ + |/src/main/kotlin/test/source.kt + |@file:JvmName("CustomJvmName") + |package test + | + |val property: String + | get() = "" + """ + + private const val extensionPropertyTest = + """ + |/src/main/kotlin/test/source.kt + |@file:JvmName("CustomJvmName") + |package test + | + |val String.property: String + | get() = "" + """ + } + + @ParameterizedTest + @ValueSource(strings = [functionTest, extensionFunctionTest]) + fun `jvm name should be included in functions extra`(query: String) { + testInline( + query.trimIndent(), testConfiguration + ) { + documentablesCreationStage = { modules -> + val expectedAnnotation = Annotations.Annotation( + dri = DRI("kotlin.jvm", "JvmName"), + params = mapOf("name" to StringValue("CustomJvmName")), + scope = Annotations.AnnotationScope.FILE, + mustBeDocumented = true + ) + val function = modules.flatMap { it.packages }.first().functions.first() + val annotation = function.extra[Annotations]?.fileLevelAnnotations?.entries?.first()?.value?.single() + assertEquals(emptyMap(), function.extra[Annotations]?.directAnnotations) + assertEquals(expectedAnnotation, annotation) + assertEquals(expectedAnnotation.scope, annotation?.scope) + assertEquals(expectedAnnotation.mustBeDocumented, annotation?.mustBeDocumented) + } + } + } + + @ParameterizedTest + @ValueSource(strings = [propertyTest, extensionPropertyTest]) + fun `jvm name should be included in properties extra`(query: String) { + testInline( + query.trimIndent(), testConfiguration + ) { + documentablesCreationStage = { modules -> + val expectedAnnotation = Annotations.Annotation( + dri = DRI("kotlin.jvm", "JvmName"), + params = mapOf("name" to StringValue("CustomJvmName")), + scope = Annotations.AnnotationScope.FILE, + mustBeDocumented = true + ) + val properties = modules.flatMap { it.packages }.first().properties.first() + val annotation = properties.extra[Annotations]?.fileLevelAnnotations?.entries?.first()?.value?.single() + assertEquals(emptyMap(), properties.extra[Annotations]?.directAnnotations) + assertEquals(expectedAnnotation, annotation) + assertEquals(expectedAnnotation.scope, annotation?.scope) + assertEquals(expectedAnnotation.mustBeDocumented, annotation?.mustBeDocumented) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/JavaDeprecatedTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/JavaDeprecatedTest.kt new file mode 100644 index 00000000..5a2ff93e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/JavaDeprecatedTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.annotations + +import matchers.content.* +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.base.transformers.documentables.deprecatedAnnotation +import org.jetbrains.dokka.base.transformers.documentables.isDeprecated +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.ContentStyle +import utils.pWrapped +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class JavaDeprecatedTest : BaseAbstractTest() { + + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + } + } + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `should assert util functions for deprecation`() { + testInline( + """ + |/src/main/kotlin/deprecated/DeprecatedJavaClass.java + |package deprecated + | + |@Deprecated(forRemoval = true) + |public class DeprecatedJavaClass {} + """.trimIndent(), + testConfiguration + ) { + documentablesTransformationStage = { module -> + val deprecatedClass = module.children + .single { it.name == "deprecated" }.children + .single { it.name == "DeprecatedJavaClass" } + + val isDeprecated = (deprecatedClass as WithExtraProperties<out Documentable>).isDeprecated() + assertTrue(isDeprecated) + + val deprecatedAnnotation = (deprecatedClass as WithExtraProperties<out Documentable>).deprecatedAnnotation + assertNotNull(deprecatedAnnotation) + + assertTrue(deprecatedAnnotation.isDeprecated()) + assertEquals("java.lang", deprecatedAnnotation.dri.packageName) + assertEquals("Deprecated", deprecatedAnnotation.dri.classNames) + } + } + } + + @Test + fun `should change deprecated header if marked for removal`() { + testInline( + """ + |/src/main/kotlin/deprecated/DeprecatedJavaClass.java + |package deprecated + | + |/** + | * Average function description + | */ + |@Deprecated(forRemoval = true) + |public class DeprecatedJavaClass {} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val deprecatedJavaClass = module.children + .single { it.name == "deprecated" }.children + .single { it.name == "DeprecatedJavaClass" } as ContentPage + + deprecatedJavaClass.content.assertNode { + group { + header(1) { +"DeprecatedJavaClass" } + platformHinted { + skipAllNotMatching() + group { + header(3) { + +"Deprecated (for removal)" + } + } + group { pWrapped("Average function description") } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `should add footnote for 'since' param`() { + testInline( + """ + |/src/main/kotlin/deprecated/DeprecatedJavaClass.java + |package deprecated + | + |/** + | * Average function description + | */ + |@Deprecated(since = "11") + |public class DeprecatedJavaClass {} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val deprecatedJavaClass = module.children + .single { it.name == "deprecated" }.children + .single { it.name == "DeprecatedJavaClass" } as ContentPage + + deprecatedJavaClass.content.assertNode { + group { + header(1) { +"DeprecatedJavaClass" } + platformHinted { + skipAllNotMatching() + group { + header(3) { + +"Deprecated" + } + group { + check { assertEquals(ContentStyle.Footnote, this.style.firstOrNull()) } + +"Since version 11" + } + } + group { pWrapped("Average function description") } + } + } + skipAllNotMatching() + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/KotlinDeprecatedTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/KotlinDeprecatedTest.kt new file mode 100644 index 00000000..7612aff8 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/KotlinDeprecatedTest.kt @@ -0,0 +1,401 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.annotations + +import matchers.content.* +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.base.transformers.documentables.deprecatedAnnotation +import org.jetbrains.dokka.base.transformers.documentables.isDeprecated +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.ContentStyle +import utils.ParamAttributes +import utils.bareSignature +import utils.pWrapped +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + + +class KotlinDeprecatedTest : BaseAbstractTest() { + + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + analysisPlatform = "jvm" + } + } + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `should assert util functions for deprecation`() { + testInline( + """ + |/src/main/kotlin/kotlin/KotlinFile.kt + |package deprecated + | + |@Deprecated( + | message = "Fancy message" + |) + |fun simpleFunction() {} + """.trimIndent(), + testConfiguration + ) { + documentablesTransformationStage = { module -> + val deprecatedFunction = module.children + .single { it.name == "deprecated" }.children + .single { it.name == "simpleFunction" } + + val isDeprecated = (deprecatedFunction as WithExtraProperties<out Documentable>).isDeprecated() + assertTrue(isDeprecated) + + val deprecatedAnnotation = (deprecatedFunction as WithExtraProperties<out Documentable>).deprecatedAnnotation + assertNotNull(deprecatedAnnotation) + + assertTrue(deprecatedAnnotation.isDeprecated()) + assertEquals("kotlin", deprecatedAnnotation.dri.packageName) + assertEquals("Deprecated", deprecatedAnnotation.dri.classNames) + } + } + } + + @Test + fun `should change header if deprecation level is not default`() { + testInline( + """ + |/src/main/kotlin/kotlin/DeprecatedKotlin.kt + |package deprecated + | + |/** + | * Average function description + | */ + |@Deprecated( + | message = "Reason for deprecation bla bla", + | level = DeprecationLevel.ERROR + |) + |fun oldLegacyFunction(typedParam: SomeOldType, someLiteral: String): String {} + | + |fun newShinyFunction(typedParam: SomeOldType, someLiteral: String, newTypedParam: SomeNewType): String {} + |class SomeOldType {} + |class SomeNewType {} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionWithDeprecatedFunction = module.children + .single { it.name == "deprecated" }.children + .single { it.name == "oldLegacyFunction" } as ContentPage + + functionWithDeprecatedFunction.content.assertNode { + group { + header(1) { +"oldLegacyFunction" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + annotations = emptyMap(), + visibility = "", + modifier = "", + keywords = emptySet(), + name = "oldLegacyFunction", + returnType = "String", + params = arrayOf( + "typedParam" to ParamAttributes(emptyMap(), emptySet(), "SomeOldType"), + "someLiteral" to ParamAttributes(emptyMap(), emptySet(), "String"), + ) + ) + } + after { + group { + header(3) { + +"Deprecated (with error)" + } + p { + +"Reason for deprecation bla bla" + } + } + group { pWrapped("Average function description") } + } + } + } + } + } + } + } + + @Test + fun `should display repalceWith param with imports as code blocks`() { + testInline( + """ + |/src/main/kotlin/kotlin/DeprecatedKotlin.kt + |package deprecated + | + |/** + | * Average function description + | */ + |@Deprecated( + | message = "Reason for deprecation bla bla", + | replaceWith = ReplaceWith( + | "newShinyFunction(typedParam, someLiteral, SomeNewType())", + | imports = [ + | "com.example.dokka.debug.newShinyFunction", + | "com.example.dokka.debug.SomeOldType", + | "com.example.dokka.debug.SomeNewType", + | ] + | ), + |) + |fun oldLegacyFunction(typedParam: SomeOldType, someLiteral: String): String {} + | + |fun newShinyFunction(typedParam: SomeOldType, someLiteral: String, newTypedParam: SomeNewType): String {} + |class SomeOldType {} + |class SomeNewType {} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionWithDeprecatedFunction = module.children + .single { it.name == "deprecated" }.children + .single { it.name == "oldLegacyFunction" } as ContentPage + + functionWithDeprecatedFunction.content.assertNode { + group { + header(1) { +"oldLegacyFunction" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + annotations = emptyMap(), + visibility = "", + modifier = "", + keywords = emptySet(), + name = "oldLegacyFunction", + returnType = "String", + params = arrayOf( + "typedParam" to ParamAttributes(emptyMap(), emptySet(), "SomeOldType"), + "someLiteral" to ParamAttributes(emptyMap(), emptySet(), "String"), + ) + ) + } + after { + group { + header(3) { + +"Deprecated" + } + p { + +"Reason for deprecation bla bla" + } + + header(4) { + +"Replace with" + } + codeBlock { + +"import com.example.dokka.debug.newShinyFunction" + br() + +"import com.example.dokka.debug.SomeOldType" + br() + +"import com.example.dokka.debug.SomeNewType" + br() + } + codeBlock { + +"newShinyFunction(typedParam, someLiteral, SomeNewType())" + } + } + group { pWrapped("Average function description") } + } + } + } + } + } + } + } + + @Test + fun `should add footnote for DeprecatedSinceKotlin annotation`() { + testInline( + """ + |/src/main/kotlin/kotlin/DeprecatedKotlin.kt + |package deprecated + | + |/** + | * Average function description + | */ + |@DeprecatedSinceKotlin( + | warningSince = "1.4", + | errorSince = "1.5", + | hiddenSince = "1.6" + |) + |@Deprecated( + | message = "Deprecation reason bla bla" + |) + |fun oldLegacyFunction(typedParam: SomeOldType, someLiteral: String): String {} + | + |fun newShinyFunction(typedParam: SomeOldType, someLiteral: String, newTypedParam: SomeNewType): String {} + |class SomeOldType {} + |class SomeNewType {} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionWithDeprecatedFunction = module.children + .single { it.name == "deprecated" }.children + .single { it.name == "oldLegacyFunction" } as ContentPage + + functionWithDeprecatedFunction.content.assertNode { + group { + header(1) { +"oldLegacyFunction" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + annotations = emptyMap(), + visibility = "", + modifier = "", + keywords = emptySet(), + name = "oldLegacyFunction", + returnType = "String", + params = arrayOf( + "typedParam" to ParamAttributes(emptyMap(), emptySet(), "SomeOldType"), + "someLiteral" to ParamAttributes(emptyMap(), emptySet(), "String"), + ) + ) + } + after { + group { + header(3) { + +"Deprecated" + } + group { + check { assertEquals(ContentStyle.Footnote, this.style.firstOrNull()) } + p { + +"Warning since 1.4" + } + p { + +"Error since 1.5" + } + p { + +"Hidden since 1.6" + } + } + p { + +"Deprecation reason bla bla" + } + } + group { pWrapped("Average function description") } + } + } + } + } + } + } + } + + @Test + fun `should generate deprecation block with all parameters present and long description`() { + testInline( + """ + |/src/main/kotlin/kotlin/DeprecatedKotlin.kt + |package deprecated + | + |/** + | * Average function description + | */ + |@Deprecated( + | message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vel vulputate risus. " + + | "Etiam dictum odio vel vulputate auctor.Nulla facilisi. Duis ullamcorper ullamcorper lectus " + + | "nec rutrum. Quisque eu risus eu purus bibendum ultricies. Maecenas tincidunt dui in sodales " + + | "faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin id sem felis. " + + | "Praesent et libero lacinia, egestas libero in, ultrices lectus. Suspendisse eget volutpat " + + | "velit. Phasellus laoreet mi eu egestas mattis.", + | replaceWith = ReplaceWith( + | "newShinyFunction(typedParam, someLiteral, SomeNewType())", + | imports = [ + | "com.example.dokka.debug.newShinyFunction", + | "com.example.dokka.debug.SomeOldType", + | "com.example.dokka.debug.SomeNewType", + | ] + | ), + | level = DeprecationLevel.ERROR + |) + |fun oldLegacyFunction(typedParam: SomeOldType, someLiteral: String): String {} + | + |fun newShinyFunction(typedParam: SomeOldType, someLiteral: String, newTypedParam: SomeNewType): String {} + |class SomeOldType {} + |class SomeNewType {} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionWithDeprecatedFunction = module.children + .single { it.name == "deprecated" }.children + .single { it.name == "oldLegacyFunction" } as ContentPage + + functionWithDeprecatedFunction.content.assertNode { + group { + header(1) { +"oldLegacyFunction" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + annotations = emptyMap(), + visibility = "", + modifier = "", + keywords = emptySet(), + name = "oldLegacyFunction", + returnType = "String", + params = arrayOf( + "typedParam" to ParamAttributes(emptyMap(), emptySet(), "SomeOldType"), + "someLiteral" to ParamAttributes(emptyMap(), emptySet(), "String"), + ) + ) + } + after { + group { + header(3) { + +"Deprecated (with error)" + } + p { + +("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Maecenas vel vulputate risus. Etiam dictum odio vel " + + "vulputate auctor.Nulla facilisi. Duis ullamcorper " + + "ullamcorper lectus nec rutrum. Quisque eu risus eu " + + "purus bibendum ultricies. Maecenas tincidunt dui in sodales faucibus. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Proin id sem felis. Praesent et libero lacinia, egestas " + + "libero in, ultrices lectus. Suspendisse eget volutpat velit. " + + "Phasellus laoreet mi eu egestas mattis.") + } + header(4) { + +"Replace with" + } + codeBlock { + +"import com.example.dokka.debug.newShinyFunction" + br() + +"import com.example.dokka.debug.SomeOldType" + br() + +"import com.example.dokka.debug.SomeNewType" + br() + } + codeBlock { + +"newShinyFunction(typedParam, someLiteral, SomeNewType())" + } + } + group { pWrapped("Average function description") } + } + } + } + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/SinceKotlinTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/SinceKotlinTest.kt new file mode 100644 index 00000000..6ee95bbd --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/SinceKotlinTest.kt @@ -0,0 +1,350 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.annotations + +import matchers.content.* +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.transformers.pages.annotations.SinceKotlinTransformer +import org.jetbrains.dokka.base.transformers.pages.annotations.SinceKotlinVersion +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.CustomTagWrapper +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.pages.ContentPage +import signatures.AbstractRenderingTest +import utils.* +import kotlin.test.* + + +class SinceKotlinTest : AbstractRenderingTest() { + + val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + analysisPlatform = "jvm" + } + } + } + + @BeforeTest + fun setSystemProperty() { + System.setProperty(SinceKotlinTransformer.SHOULD_DISPLAY_SINCE_KOTLIN_SYS_PROP, "true") + } + @AfterTest + fun clearSystemProperty() { + System.clearProperty(SinceKotlinTransformer.SHOULD_DISPLAY_SINCE_KOTLIN_SYS_PROP) + } + + @Test + fun versionsComparing() { + assertTrue(SinceKotlinVersion("1.0").compareTo(SinceKotlinVersion("1.0")) == 0) + assertTrue(SinceKotlinVersion("1.0.0").compareTo(SinceKotlinVersion("1")) == 0) + assertTrue(SinceKotlinVersion("1.0") >= SinceKotlinVersion("1.0")) + assertTrue(SinceKotlinVersion("1.1") > SinceKotlinVersion("1")) + assertTrue(SinceKotlinVersion("1.0") < SinceKotlinVersion("2.0")) + assertTrue(SinceKotlinVersion("1.0") < SinceKotlinVersion("2.2")) + } + + @Test + fun `rendered SinceKotlin custom tag for typealias, extensions, functions, properties`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |@SinceKotlin("1.5") + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |@SinceKotlin("1.5") + |fun String.extension(abc: String): String { + | return "My precious " + abc + |} + |@SinceKotlin("1.5") + |typealias Str = String + |@SinceKotlin("1.5") + |val str = "str" + """.trimIndent(), + testConfiguration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.renderedContent("root/test/index.html") + assertEquals(4, content.getElementsContainingOwnText("Since Kotlin").count()) + } + } + } + + @Test + fun `should propagate SinceKotlin`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |@SinceKotlin("1.5") + |class A { + | fun ring(abc: String): String { + | return "My precious " + abc + | } + |} + """.trimIndent(), testConfiguration + ) { + documentablesTransformationStage = { module -> + @Suppress("UNCHECKED_CAST") val funcs = module.children.single { it.name == "test" } + .children.single { it.name == "A" } + .children.filter { it.name == "ring" && it is DFunction } as List<DFunction> + with(funcs) { + val sinceKotlin = mapOf( + Platform.jvm to SinceKotlinVersion("1.5"), + ) + + for(i in sinceKotlin) { + val tag = + find { it.sourceSets.first().analysisPlatform == i.key }?.documentation?.values?.first() + ?.dfs { it is CustomTagWrapper && it.name == "Since Kotlin" } + .assertNotNull("SinceKotlin[${i.key}]") + assertEquals((tag.children.first() as Text).body, i.value.toString()) + } + } + } + } + } + + @Test + fun `mpp fun without SinceKotlin annotation`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/jvm/") + analysisPlatform = "jvm" + } + sourceSet { + sourceRoots = listOf("src/native/") + analysisPlatform = "native" + name = "native" + } + sourceSet { + sourceRoots = listOf("src/common/") + analysisPlatform = "common" + name = "common" + } + sourceSet { + sourceRoots = listOf("src/js/") + analysisPlatform = "js" + name = "js" + } + sourceSet { + sourceRoots = listOf("src/wasm/") + analysisPlatform = "wasm" + name = "wasm" + } + } + } + testInline( + """ + |/src/jvm/kotlin/test/source.kt + |package test + | + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |/src/native/kotlin/test/source.kt + |package test + | + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |/src/common/kotlin/test/source.kt + |package test + | + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |/src/js/kotlin/test/source.kt + |package test + | + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |/src/wasm/kotlin/test/source.kt + |package test + | + |fun ring(abc: String): String { + | return "My precious " + abc + |} + """.trimIndent(), configuration + ) { + documentablesTransformationStage = { module -> + @Suppress("UNCHECKED_CAST") val funcs = module.children.single { it.name == "test" } + .children.filter { it.name == "ring" && it is DFunction } as List<DFunction> + with(funcs) { + val sinceKotlin = mapOf( + Platform.common to SinceKotlinVersion("1.0"), + Platform.jvm to SinceKotlinVersion("1.0"), + Platform.js to SinceKotlinVersion("1.1"), + Platform.native to SinceKotlinVersion("1.3"), + Platform.wasm to SinceKotlinVersion("1.8"), + ) + + for(i in sinceKotlin) { + val tag = + find { it.sourceSets.first().analysisPlatform == i.key }?.documentation?.values?.first() + ?.dfs { it is CustomTagWrapper && it.name == "Since Kotlin" } + .assertNotNull("SinceKotlin[${i.key}]") + assertEquals((tag.children.first() as Text).body, i.value.toString()) + } + } + } + } + } + + @Test + fun `mpp fun with SinceKotlin annotation`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/jvm/") + classpath = listOfNotNull(jvmStdlibPath) + analysisPlatform = "jvm" + } + sourceSet { + sourceRoots = listOf("src/native/") + analysisPlatform = "native" + name = "native" + } + sourceSet { + sourceRoots = listOf("src/common/") + classpath = listOfNotNull(commonStdlibPath) + analysisPlatform = "common" + name = "common" + } + sourceSet { + sourceRoots = listOf("src/js/") + classpath = listOfNotNull(jsStdlibPath) + analysisPlatform = "js" + name = "js" + } + sourceSet { + sourceRoots = listOf("src/wasm/") + analysisPlatform = "wasm" + name = "wasm" + } + } + } + testInline( + """ + |/src/jvm/kotlin/test/source.kt + |package test + | + |/** dssdd */ + |@SinceKotlin("1.3") + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |/src/native/kotlin/test/source.kt + |package test + | + |/** dssdd */ + |@SinceKotlin("1.3") + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |/src/common/kotlin/test/source.kt + |package test + | + |/** dssdd */ + |@SinceKotlin("1.3") + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |/src/js/kotlin/test/source.kt + |package test + | + |/** dssdd */ + |@SinceKotlin("1.3") + |fun ring(abc: String): String { + | return "My precious " + abc + |} + |/src/wasm/kotlin/test/source.kt + |package test + | + |/** dssdd */ + |@SinceKotlin("1.3") + |fun ring(abc: String): String { + | return "My precious " + abc + |} + """.trimIndent(), configuration + ) { + documentablesTransformationStage = { module -> + @Suppress("UNCHECKED_CAST") val funcs = module.children.single { it.name == "test" } + .children.filter { it.name == "ring" && it is DFunction } as List<DFunction> + with(funcs) { + val sinceKotlin = mapOf( + Platform.common to SinceKotlinVersion("1.3"), + Platform.jvm to SinceKotlinVersion("1.3"), + Platform.js to SinceKotlinVersion("1.3"), + Platform.native to SinceKotlinVersion("1.3"), + Platform.wasm to SinceKotlinVersion("1.8"), + ) + + for(i in sinceKotlin) { + val tag = + find { it.sourceSets.first().analysisPlatform == i.key }?.documentation?.values?.first() + ?.dfs { it is CustomTagWrapper && it.name == "Since Kotlin" } + .assertNotNull("SinceKotlin[${i.key}]") + assertEquals(i.value.toString(), (tag.children.first() as Text).body , "Platform ${i.key}") + } + } + } + } + } + + @Test + fun `should do not render since kotlin tag when flag is unset`() { + clearSystemProperty() + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |@SinceKotlin("1.3") + |fun ring(abc: String): String { + | return "My precious " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "ring" } as ContentPage + page.content.assertNode { + group { + header(1) { +"ring" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "ring", + "String", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/exceptions/ContentForExceptions.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/exceptions/ContentForExceptions.kt new file mode 100644 index 00000000..22becb93 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/exceptions/ContentForExceptions.kt @@ -0,0 +1,439 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.exceptions + +import matchers.content.* +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.DisplaySourceSet +import utils.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContentForExceptions : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + analysisPlatform = "jvm" + } + } + } + + private val mppTestConfiguration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/pageMerger/Test.kt") + classpath = listOfNotNull(commonStdlibPath) + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jvmMain/kotlin/pageMerger/Test.kt") + classpath = listOfNotNull(jvmStdlibPath) + } + sourceSet { + name = "linuxX64" + displayName = "linuxX64" + analysisPlatform = "native" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/linuxX64Main/kotlin/pageMerger/Test.kt") + } + } + pluginsConfigurations.add( + PluginConfigurationImpl( + DokkaBase::class.qualifiedName!!, + DokkaConfiguration.SerializationFormat.JSON, + """{ "mergeImplicitExpectActualDeclarations": true }""", + ) + ) + } + + @OnlyDescriptors("Fixed in 1.9.20 (IMPORT STAR)") + @Test + fun `function with navigatable thrown exception`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |/** + |* @throws Exception + |*/ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"Throws" } + table { + group { + group { + link { +"Exception" } + } + } + } + } + } + } + } + } + } + } + + @Test + fun `function with non-navigatable thrown exception`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |/** + |* @throws UnavailableException + |*/ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"Throws" } + table { + group { + group { + +"UnavailableException" + } + } + } + } + } + } + } + } + } + } + + @Test + fun `multiplatofrm class with throws`() { + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @throws CommonException + |*/ + |expect open class Parent + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @throws JvmException + |*/ + |actual open class Parent + | + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @throws LinuxException + |*/ + |actual open class Parent + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"expect open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + header(4) { +"Throws" } + table { + group { + group { + +"CommonException" + } + check { + assertEquals(1, sourceSets.size) + assertEquals( + "common", + this.sourceSets.first().name + ) + } + } + group { + group { + +"JvmException" + } + check { + sourceSets.assertSourceSet("jvm") + } + } + group { + group { + +"LinuxException" + } + check { + sourceSets.assertSourceSet("linuxX64") + } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `multiplatofrm class with throws in few platforms`() { + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @throws CommonException + |*/ + |expect open class Parent + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @throws JvmException + |*/ + |actual open class Parent + | + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual open class Parent + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"expect open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + header(4) { +"Throws" } + table { + group { + group { + +"CommonException" + } + check { + sourceSets.assertSourceSet("common") + } + } + group { + group { + +"JvmException" + } + check { + sourceSets.assertSourceSet("jvm") + } + } + check { + assertEquals(2, sourceSets.size) + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `throws in merged functions`() { + testInline( + """ + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @throws LinuxException + |*/ + |fun function() { + | println() + |} + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @throws JvmException + |*/ + |fun function() { + | println() + |} + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + ) + } + after { + header(4) { +"Throws" } + table { + group { + group { + +"JvmException" + } + } + check { + sourceSets.assertSourceSet("jvm") + } + } + } + check { + sourceSets.assertSourceSet("jvm") + } + } + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + ) + } + after { + header(4) { +"Throws" } + table { + group { + group { + +"LinuxException" + } + } + } + } + check { + sourceSets.assertSourceSet("linuxX64") + } + } + } + } + } + } + } +} + +private fun Set<DisplaySourceSet>.assertSourceSet(expectedName: String) { + assertEquals(1, this.size) + assertEquals(expectedName, this.first().name) +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/functions/ContentForBriefTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/functions/ContentForBriefTest.kt new file mode 100644 index 00000000..d93a6c27 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/functions/ContentForBriefTest.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.functions + +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 kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + + +class ContentForBriefTest : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + } + } + } + + private val codeWithSecondaryAndPrimaryConstructorsDocumented = + """ + |/src/main/kotlin/test/source.kt + |package test + | + |/** + | * Dummy text. + | * + | * @constructor constructor docs + | * @param exampleParameter dummy parameter. + | */ + |class Example(val exampleParameter: Int) { + | + | /** + | * secondary constructor + | * @param param1 param1 docs + | */ + | constructor(param1: String) : this(1) + |} + """.trimIndent() + + private val codeWithDocumentedParameter = + """ + |/src/main/kotlin/test/source.kt + |package test + | + |/** + | * Dummy text. + | * + | * @param exampleParameter dummy parameter. + | */ + |class Example(val exampleParameter: Int) { + |} + """.trimIndent() + + + @Test + fun `primary constructor should not inherit docs from its parameter`() { + testInline(codeWithSecondaryAndPrimaryConstructorsDocumented, testConfiguration) { + pagesTransformationStage = { module -> + val classPage = module.findClassPage("Example") + + val constructorsWithBriefs = classPage.findConstructorsWithBriefs() + val constructorDocs = constructorsWithBriefs.findConstructorDocs { + it.callable?.params?.first() == TypeConstructor("kotlin.Int", emptyList()) + } + + assertEquals("constructor docs", constructorDocs.text) + } + } + } + + @Test + fun `secondary constructor should not inherit docs from its parameter`() { + testInline(codeWithSecondaryAndPrimaryConstructorsDocumented, testConfiguration) { + pagesTransformationStage = { module -> + val classPage = module.findClassPage("Example") + + val constructorsWithBriefs = classPage.findConstructorsWithBriefs() + val constructorDocs = constructorsWithBriefs.findConstructorDocs { + it.callable?.params?.first() == TypeConstructor("kotlin.String", emptyList()) + } + + assertEquals("secondary constructor", constructorDocs.text) + } + } + } + + /** + * All constructors are merged in one block (like overloaded functions). + * That leads to the structure where content block (`constructorsWithBriefs`) consist of plain list + * of constructors and briefs. In that list constructor is above, brief is below. + */ + private fun ContentPage.findConstructorsWithBriefs(): List<ContentNode> { + val constructorsTable = this.content.dfs { + it is ContentTable && it.dci.kind == ContentKind.Constructors + } as ContentTable + + val constructorsWithBriefs = constructorsTable.dfs { + it is ContentGroup && it.dci.kind == ContentKind.SourceSetDependentHint + }?.children + assertNotNull(constructorsWithBriefs, "Content node with constructors and briefs is not found") + + return constructorsWithBriefs + } + + private fun List<ContentNode>.findConstructorDocs(constructorMatcher: (DRI) -> Boolean): ContentText { + val constructorIndex = this.indexOfFirst { constructorMatcher(it.dci.dri.first()) } + return this[constructorIndex + 1] // expect that the relevant comment is below the constructor + .dfs { it is ContentText && it.dci.kind == ContentKind.Comment } as ContentText + } + + @Test + fun `primary constructor should not inherit docs from its parameter when no specific docs are provided`() { + testInline(codeWithDocumentedParameter, testConfiguration) { + pagesTransformationStage = { module -> + val classPage = module.findClassPage("Example") + val constructorsTable = + classPage.content.dfs { it is ContentTable && it.dci.kind == ContentKind.Constructors } as ContentTable + + assertEquals(1, constructorsTable.children.size) + val primary = constructorsTable.children.first() + val primaryConstructorDocs = primary.dfs { it is ContentText && it.dci.kind == ContentKind.Comment } + + assertNull(primaryConstructorDocs, "Expected no primary constructor docs to be present") + } + } + } + + @Test + fun `brief should work for typealias`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |/** + |* This is an example <!-- not visible --> 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 <!-- not visible --> of html", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + + @Test + fun `brief for functions should work with html`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class Example(val exampleParameter: Int) { + | /** + | * This is an example <!-- not visible --> of html + | * + | * This is definitely not a brief + | */ + | fun test(): String = "TODO" + |} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionBriefDocs = module.singleFunctionDescription("Example") + + assertEquals( + "This is an example <!-- not visible --> of html", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + + @Test + fun `brief for functions should work with ie`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class Example(val exampleParameter: Int) { + | /** + | * The user token, i.e. "Bearer xyz". Throw an exception if not available. + | * + | * This is definitely not a brief + | */ + | fun test(): String = "TODO" + |} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionBriefDocs = module.singleFunctionDescription("Example") + + assertEquals( + "The user token, i.e. \"Bearer xyz\". Throw an exception if not available.", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + + @Test + fun `brief for functions should work with eg`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class Example(val exampleParameter: Int) { + | /** + | * The user token, e.g. "Bearer xyz". Throw an exception if not available. + | * + | * This is definitely not a brief + | */ + | fun test(): String = "TODO" + |} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionBriefDocs = module.singleFunctionDescription("Example") + + assertEquals( + "The user token, e.g. \"Bearer xyz\". Throw an exception if not available.", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + + @Test + fun `brief for functions should be first sentence for Java`() { + testInline( + """ + |/src/main/java/test/Example.java + |package test; + | + |public class Example { + | /** + | * The user token, or not. This is definitely not a brief in java + | */ + | public static String test() { + | return "TODO"; + | } + |} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionBriefDocs = module.singleFunctionDescription("Example") + + assertEquals( + "The user token, or not.", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + + @Test + fun `brief for functions should work with ie for Java`() { + testInline( + """ + |/src/main/java/test/Example.java + |package test; + | + |public class Example { + | /** + | * The user token, e.g. "Bearer xyz". This is definitely not a brief in java + | */ + | public static String test() { + | return "TODO"; + | } + |} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionBriefDocs = module.singleFunctionDescription("Example") + + assertEquals( + "The user token, e.g. \"Bearer xyz\".", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + + //Source: https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html#exampleresult + @Test + fun `brief for functions should work with html comment for Java`() { + testInline( + """ + |/src/main/java/test/Example.java + |package test; + | + |public class Example { + | /** + | * This is a simulation of Prof.<!-- --> Knuth's MIX computer. This is definitely not a brief in java + | */ + | public static String test() { + | return "TODO"; + | } + |} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionBriefDocs = module.singleFunctionDescription("Example") + + assertEquals( + "This is a simulation of Prof.<!-- --> Knuth's MIX computer.", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + + @Test + fun `brief for functions should work with html comment at the end for Java`() { + testInline( + """ + |/src/main/java/test/Example.java + |package test; + | + |public class Example { + | /** + | * This is a simulation of Prof.<!-- --> Knuth's MIX computer. This is definitely not a brief in java <!-- --> + | */ + | public static String test() { + | return "TODO"; + | } + |} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val functionBriefDocs = module.singleFunctionDescription("Example") + + assertEquals( + "This is a simulation of Prof.<!-- --> Knuth's MIX computer.", + functionBriefDocs.children.joinToString("") { (it as ContentText).text }) + } + } + } + + private fun RootPageNode.findClassPage(className: String): ContentPage { + return this.dfs { + it.name == className && (it as WithDocumentables).documentables.firstOrNull() is DClass + } as ContentPage + } + + private fun RootPageNode.singleFunctionDescription(className: String): ContentGroup { + 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 + + assertEquals(1, functionsTable.children.size) + 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 + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/functions/ContentForConstructors.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/functions/ContentForConstructors.kt new file mode 100644 index 00000000..d1ed93dc --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/functions/ContentForConstructors.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.functions + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.pages.* +import utils.assertContains +import utils.assertNotNull +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContentForConstructors : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + } + } + } + + @Test + fun `constructor name should have RowTitle style`() { + testInline(""" + |/src/main/kotlin/test/source.kt + |package test + | + |/** + | * Dummy text. + | */ + |class Example(val exampleParameter: Int) { + |} + """.trimIndent(), testConfiguration) { + pagesTransformationStage = { module -> + val classPage = + 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 + + assertEquals(1, constructorsTable.children.size) + val primary = constructorsTable.children.first() + val constructorName = + primary.dfs { (it as? ContentText)?.text == "Example" }.assertNotNull("constructorName") + + assertContains(constructorName.style, ContentStyle.RowTitle) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/inheritors/ContentForInheritorsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/inheritors/ContentForInheritorsTest.kt new file mode 100644 index 00000000..245592cc --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/inheritors/ContentForInheritorsTest.kt @@ -0,0 +1,499 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.inheritors + +import matchers.content.* +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 utils.OnlyDescriptors +import utils.classSignature +import utils.findTestType +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContentForInheritorsTest : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + } + } + } + + private val mppTestConfiguration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jvmMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "linuxX64" + displayName = "linuxX64" + analysisPlatform = "native" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/linuxX64Main/kotlin/pageMerger/Test.kt") + } + } + pluginsConfigurations.add( + PluginConfigurationImpl( + DokkaBase::class.qualifiedName!!, + DokkaConfiguration.SerializationFormat.JSON, + """{ "mergeImplicitExpectActualDeclarations": true }""", + ) + ) + } + + + //Case from skiko library + private val mppTestConfigurationSharedAsPlatform = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/pageMerger/Test.kt") + } + val jvm = sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jvmMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "android" + displayName = "android" + analysisPlatform = "jvm" + dependentSourceSets = setOf(jvm.value.sourceSetID) + sourceRoots = listOf("src/androidMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "awt" + displayName = "awt" + analysisPlatform = "jvm" + dependentSourceSets = setOf(jvm.value.sourceSetID) + sourceRoots = listOf("src/awtMain/kotlin/pageMerger/Test.kt") + } + + } + } + + @Test + fun `class with one inheritor has table in description`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class Parent + | + |class Foo : Parent() + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + classSignature( + emptyMap(), + "", + "", + emptySet(), + "Parent" + ) + header(4) { +"Inheritors" } + table { + group { + link { +"Foo" } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @OnlyDescriptors("Order of inheritors is different in K2") + @Test + fun `interface with few inheritors has table in description`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |interface Parent + | + |class Foo : Parent() + |class Bar : Parent() + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"interface " + link { + +"Parent" + } + } + header(4) { +"Inheritors" } + table { + group { + link { +"Foo" } + } + group { + link { +"Bar" } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `inherit from one of multiplatoforms actuals`() { + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |expect open class Parent + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual open class Parent + | + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual open class Parent + |class Child: Parent() + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"expect open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + header(4) { +"Inheritors" } + table { + group { + link { +"Child" } + } + check { + assertEquals(1, sourceSets.size) + assertEquals( + "linuxX64", + this.sourceSets.first().name + ) + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `inherit from class in common code`() { + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |open class Parent + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class Child : Parent() + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"open class " + link { + +"Parent" + } + } + header(4) { +"Inheritors" } + table { + group { + link { +"Child" } + } + check { + assertEquals(1, sourceSets.size) + assertEquals( + "common", + this.sourceSets.first().name + ) + } + } + } + } + skipAllNotMatching() + } + } + } + } + + + @Test + fun `inheritors from merged classes`() { + testInline( + """ + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |open class Parent + |class LChild : Parent() + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |open class Parent + |class JChild : Parent() + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"open class " + link { + +"Parent" + } + } + header(4) { +"Inheritors" } + table { + group { + link { +"JChild" } + } + check { + assertEquals(1, sourceSets.size) + assertEquals( + "jvm", + this.sourceSets.first().name + ) + } + } + group { + +"open class " + link { + +"Parent" + } + } + header(4) { +"Inheritors" } + table { + group { + link { +"LChild" } + } + check { + assertEquals(1, sourceSets.size) + assertEquals( + "linuxX64", + this.sourceSets.first().name + ) + } + } + } + } + skipAllNotMatching() + } + } + } + } + + + @Test + fun `merged inheritors from merged classes`() { + testInline( + """ + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |open class Parent + |class Child : Parent() + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |open class Parent + |class Child : Parent() + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"open class " + link { + +"Parent" + } + } + header(4) { +"Inheritors" } + table { + group { + link { +"Child" } + } + check { + assertEquals(1, sourceSets.size) + assertEquals( + "jvm", + this.sourceSets.first().name + ) + } + } + group { + +"open class " + link { + +"Parent" + } + } + header(4) { +"Inheritors" } + table { + group { + link { +"Child" } + } + check { + assertEquals(1, sourceSets.size) + assertEquals( + "linuxX64", + this.sourceSets.first().name + ) + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `parent in shared source set that analyse as platform`() { + testInline( + """ + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |interface Parent + | + |/src/androidMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class Child : Parent + | + |/src/awtMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |class AwtChild : Parent + |class Child : Parent + | + """.trimMargin(), + mppTestConfigurationSharedAsPlatform + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"interface " + link { + +"Parent" + } + } + header(4) { +"Inheritors" } + table { + group { + link { +"Child" } + } + group { + link { +"AwtChild" } + } + check { + assertEquals(1, sourceSets.size) + assertEquals( + "jvm", + this.sourceSets.first().name + ) + } + } + } + } + skipAllNotMatching() + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/params/ContentForParamsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/params/ContentForParamsTest.kt new file mode 100644 index 00000000..d0c6ac9d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/params/ContentForParamsTest.kt @@ -0,0 +1,1529 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.params + +import matchers.content.* +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.Param +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.firstIsInstanceOrNull +import utils.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContentForParamsTest : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + analysisPlatform = "jvm" + } + } + } + + @Test + fun `undocumented function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), "", "", emptySet(), "function", null, "abc" to ParamAttributes( + emptyMap(), + emptySet(), + "String" + ) + ) + } + } + } + } + } + } + } + + @Test + fun `undocumented parameter`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + group { pWrapped("comment to function") } + } + } + } + } + } + } + } + + @Test + fun `undocumented parameter and other tags without function comment`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @author Kordyjan + | * @author Woolfy + | * @since 0.11 + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + unnamedTag("Author") { + comment { + +"Kordyjan" + } + comment { + +"Woolfy" + } + } + unnamedTag("Since") { comment { +"0.11" } } + } + } + } + } + } + } + } + + @Test + fun `multiple authors`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + |/** + | * Annotation processor which visits all classes. + | * + | * @author googler1@google.com (Googler 1) + | * @author googler2@google.com (Googler 2) + | */ + | public class DocGenProcessor { } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val classPage = module.findTestType("sample", "DocGenProcessor") + classPage.content.assertNode { + group { + header { +"DocGenProcessor" } + platformHinted { + group { + skipAllNotMatching() //Signature + } + group { + group { + group { + +"Annotation processor which visits all classes." + } + } + } + group { + header(4) { +"Author" } + comment { +"googler1@google.com (Googler 1)" } + comment { +"googler2@google.com (Googler 2)" } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `author delimetered by space`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + |/** + | * Annotation processor which visits all classes. + | * + | * @author Marcin Aman Senior + | */ + | public class DocGenProcessor { } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val classPage = module.findTestType("sample", "DocGenProcessor") + classPage.content.assertNode { + group { + header { +"DocGenProcessor" } + platformHinted { + group { + skipAllNotMatching() //Signature + } + group { + group { + group { + +"Annotation processor which visits all classes." + } + } + } + group { + header(4) { +"Author" } + comment { +"Marcin Aman Senior" } + } + } + } + skipAllNotMatching() + } + } + } + } + + + @Test + fun `deprecated with multiple links inside`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + |/** + | * Return the target fragment set by {@link #setTargetFragment} or {@link + | * #setTargetFragment}. + | * + | * @deprecated Instead of using a target fragment to pass results, the fragment requesting a + | * result should use + | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentManager#setFragmentResult(String, Bundle)} to deliver results to + | * {@link java.util.HashMap#containsKey(java.lang.Object) + | * FragmentResultListener} instances registered by other fragments via + | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentManager#setFragmentResultListener(String, LifecycleOwner, + | * FragmentResultListener)}. + | */ + | public class DocGenProcessor { + | public String setTargetFragment(){ + | return ""; + | } + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val classPage = + module.findTestType("sample", "DocGenProcessor") + classPage.content.assertNode { + group { + header { +"DocGenProcessor" } + platformHinted { + group { + skipAllNotMatching() //Signature + } + group { + comment { + +"Return the target fragment set by " + link { +"setTargetFragment" } + +" or " + link { +"setTargetFragment" } + +"." + } + } + group { + header(4) { +"Deprecated" } + comment { + +"Instead of using a target fragment to pass results, the fragment requesting a result should use " + link { +"FragmentManager#setFragmentResult(String, Bundle)" } + +" to deliver results to " + link { +"FragmentResultListener" } + +" instances registered by other fragments via " + link { +"FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener)" } + +"." + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `deprecated with an html link in multiple lines`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + |/** + | * @deprecated Use + | * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view "> + | * TabLayout and ViewPager</a> instead. + | */ + | public class DocGenProcessor { } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val classPage = + module.findTestType("sample", "DocGenProcessor") + classPage.content.assertNode { + group { + header { +"DocGenProcessor" } + platformHinted { + group { + skipAllNotMatching() //Signature + } + group { + header(4) { +"Deprecated" } + comment { + +"Use " + link { +"TabLayout and ViewPager" } + +" instead." + } + } + } + } + skipAllNotMatching() + } + } + } + } + + + @Test + fun `deprecated with an multiple inline links`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + |/** + | * FragmentManagerNonConfig stores the retained instance fragments across + | * activity recreation events. + | * + | * <p>Apps should treat objects of this type as opaque, returned by + | * and passed to the state save and restore process for fragments in + | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentController#retainNestedNonConfig()} and + | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)}.</p> + | * + | * @deprecated Have your {@link java.util.HashMap FragmentHostCallback} implement + | * {@link java.util.HashMap } to automatically retain the Fragment's + | * non configuration state. + | */ + | public class DocGenProcessor { } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val classPage = + module.findTestType("sample", "DocGenProcessor") + classPage.content.assertNode { + group { + header { +"DocGenProcessor" } + platformHinted { + group { + skipAllNotMatching() //Signature + } + group { + comment { + group { + +"FragmentManagerNonConfig stores the retained instance fragments across activity recreation events. " + } + group { + +"Apps should treat objects of this type as opaque, returned by and passed to the state save and restore process for fragments in " + link { +"FragmentController#retainNestedNonConfig()" } + +" and " + link { +"FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)" } + +"." + } + } + } + group { + header(4) { +"Deprecated" } + comment { + +"Have your " + link { +"FragmentHostCallback" } + +" implement " + link { +"java.util.HashMap" } + +" to automatically retain the Fragment's non configuration state." + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `multiline throws with comment`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + | public class DocGenProcessor { + | /** + | * a normal comment + | * + | * @throws java.lang.IllegalStateException if the Dialog has not yet been created (before + | * onCreateDialog) or has been destroyed (after onDestroyView). + | * @throws java.lang.RuntimeException when {@link java.util.HashMap#containsKey(java.lang.Object) Hash + | * Map} doesn't contain value. + | */ + | public static void sample(){ } + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val functionPage = + module.findTestType( + "sample", + "DocGenProcessor" + ).children.single { it.name == "sample" } as ContentPage + functionPage.content.assertNode { + group { + header(1) { +"sample" } + } + divergentGroup { + divergentInstance { + divergent { + skipAllNotMatching() //Signature + } + after { + group { pWrapped("a normal comment") } + header(4) { +"Throws" } + table { + group { + group { + link { +"IllegalStateException" } + } + comment { +"if the Dialog has not yet been created (before onCreateDialog) or has been destroyed (after onDestroyView)." } + } + group { + group { + link { +"RuntimeException" } + } + comment { + +"when " + link { +"Hash Map" } + +" doesn't contain value." + } + } + } + } + } + } + } + } + } + } + + @OnlyDescriptors("Fixed in 1.9.20 (IMPORT STAR)") + @Test + fun `multiline kotlin throws with comment`() { + testInline( + """ + |/src/main/kotlin/sample/sample.kt + |package sample; + | /** + | * a normal comment + | * + | * @throws java.lang.IllegalStateException if the Dialog has not yet been created (before + | * onCreateDialog) or has been destroyed (after onDestroyView). + | * @exception RuntimeException when [Hash Map][java.util.HashMap.containsKey] doesn't contain value. + | */ + | fun sample(){ } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val functionPage = module.findTestType("sample", "sample") + functionPage.content.assertNode { + group { + header(1) { +"sample" } + } + divergentGroup { + divergentInstance { + divergent { + skipAllNotMatching() //Signature + } + after { + group { pWrapped("a normal comment") } + header(4) { +"Throws" } + table { + group { + group { + link { + check { + assertEquals( + "java.lang/IllegalStateException///PointingToDeclaration/", + (this as ContentDRILink).address.toString() + ) + } + +"IllegalStateException" + } + } + comment { +"if the Dialog has not yet been created (before onCreateDialog) or has been destroyed (after onDestroyView)." } + } + group { + group { + link { + check { + assertEquals( + "kotlin/RuntimeException///PointingToDeclaration/", + (this as ContentDRILink).address.toString() + ) + } + +"RuntimeException" + } + } + comment { + +"when " + link { +"Hash Map" } + +" doesn't contain value." + } + } + } + } + } + } + } + } + } + } + + @Test + fun `should display fully qualified throws name for unresolved class`() { + testInline( + """ + |/src/main/kotlin/sample/sample.kt + |package sample; + | /** + | * a normal comment + | * + | * @throws com.example.UnknownException description for non-resolved + | */ + | fun sample(){ } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val functionPage = + module.findTestType("sample", "sample") + functionPage.content.assertNode { + group { + header(1) { +"sample" } + } + divergentGroup { + divergentInstance { + divergent { + skipAllNotMatching() //Signature + } + after { + group { pWrapped("a normal comment") } + header(4) { +"Throws" } + table { + group { + group { + +"com.example.UnknownException" + } + comment { +"description for non-resolved" } + } + } + } + } + } + } + } + } + } + + @Test + fun `multiline throws where exception is not in the same line as description`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + | public class DocGenProcessor { + | /** + | * a normal comment + | * + | * @throws java.lang.IllegalStateException if the Dialog has not yet been created (before + | * onCreateDialog) or has been destroyed (after onDestroyView). + | * @throws java.lang.RuntimeException when + | * {@link java.util.HashMap#containsKey(java.lang.Object) Hash + | * Map} + | * doesn't contain value. + | */ + | public static void sample(){ } + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val functionPage = + module.findTestType( + "sample", + "DocGenProcessor" + ).children.single { it.name == "sample" } as ContentPage + functionPage.content.assertNode { + group { + header(1) { +"sample" } + } + divergentGroup { + divergentInstance { + divergent { + skipAllNotMatching() //Signature + } + after { + group { pWrapped("a normal comment") } + header(4) { +"Throws" } + table { + group { + group { + link { + check { + assertEquals( + "java.lang/IllegalStateException///PointingToDeclaration/", + (this as ContentDRILink).address.toString() + ) + } + +"IllegalStateException" + } + } + comment { +"if the Dialog has not yet been created (before onCreateDialog) or has been destroyed (after onDestroyView)." } + } + group { + group { + link { + check { + assertEquals( + "java.lang/RuntimeException///PointingToDeclaration/", + (this as ContentDRILink).address.toString() + ) + } + +"RuntimeException" + } + } + comment { + +"when " + link { +"Hash Map" } + +" doesn't contain value." + } + } + } + } + } + + } + } + } + } + } + + + + @Test + fun `documentation splitted in 2 using enters`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + |/** + | * Listener for handling fragment results. + | * + | * This object should be passed to + | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener)} + | * and it will listen for results with the same key that are passed into + | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentManager#setFragmentResult(String, Bundle)}. + | * + | */ + | public class DocGenProcessor { } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val classPage = + module.findTestType("sample", "DocGenProcessor") + classPage.content.assertNode { + group { + header { +"DocGenProcessor" } + platformHinted { + group { + skipAllNotMatching() //Signature + } + group { + comment { + +"Listener for handling fragment results. This object should be passed to " + link { +"FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener)" } + +" and it will listen for results with the same key that are passed into " + link { +"FragmentManager#setFragmentResult(String, Bundle)" } + +"." + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `multiline return tag with param`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + | public class DocGenProcessor { + | /** + | * a normal comment + | * + | * @param testParam Sample description for test param that has a type of {@link java.lang.String String} + | * @return empty string when + | * {@link java.util.HashMap#containsKey(java.lang.Object) Hash + | * Map} + | * doesn't contain value. + | */ + | public static String sample(String testParam){ + | return ""; + | } + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val functionPage = + module.findTestType( + "sample", + "DocGenProcessor" + ).children.single { it.name == "sample" } as ContentPage + functionPage.content.assertNode { + group { + header(1) { +"sample" } + } + divergentGroup { + divergentInstance { + divergent { + skipAllNotMatching() //Signature + } + after { + group { pWrapped("a normal comment") } + group { + header(4) { +"Return" } + comment { + +"empty string when " + link { +"Hash Map" } + +" doesn't contain value." + } + } + header(4) { +"Parameters" } + table { + group { + +"testParam" + comment { + +"Sample description for test param that has a type of " + link { +"String" } + } + } + } + } + } + } + } + } + } + } + + @Test + fun `return tag in kotlin`() { + testInline( + """ + |/src/main/kotlin/sample/sample.kt + |package sample; + | /** + | * a normal comment + | * + | * @return empty string when [Hash Map][java.util.HashMap.containsKey] doesn't contain value. + | * + | */ + |fun sample(): String { + | return "" + | } + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val functionPage = module.findTestType("sample", "sample") + functionPage.content.assertNode { + group { + header(1) { +"sample" } + } + divergentGroup { + divergentInstance { + divergent { + skipAllNotMatching() //Signature + } + after { + group { pWrapped("a normal comment") } + group { + header(4) { +"Return" } + comment { + +"empty string when " + link { +"Hash Map" } + +" doesn't contain value." + } + } + } + } + } + } + } + } + } + + + @Test + fun `list with links and description`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + |/** + | * Static library support version of the framework's {@link java.lang.String}. + | * Used to write apps that run on platforms prior to Android 3.0. When running + | * on Android 3.0 or above, this implementation is still used; it does not try + | * to switch to the framework's implementation. See the framework {@link java.lang.String} + | * documentation for a class overview. + | * + | * <p>The main differences when using this support version instead of the framework version are: + | * <ul> + | * <li>Your activity must extend {@link java.lang.String FragmentActivity} + | * <li>You must call {@link java.util.HashMap#containsKey(java.lang.Object) FragmentActivity#getSupportFragmentManager} to get the + | * {@link java.util.HashMap FragmentManager} + | * </ul> + | * + | */ + |public class DocGenProcessor { } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val classPage = module.findTestType("sample", "DocGenProcessor") + classPage.content.assertNode { + group { + header { +"DocGenProcessor" } + platformHinted { + group { + skipAllNotMatching() //Signature + } + group { + comment { + group { + +"Static library support version of the framework's " + link { +"java.lang.String" } + +". Used to write apps that run on platforms prior to Android 3.0." + +" When running on Android 3.0 or above, this implementation is still used; it does not try to switch to the framework's implementation. See the framework " + link { +"java.lang.String" } + +" documentation for a class overview. " //TODO this probably shouldnt have a space but it is minor + } + group { + +"The main differences when using this support version instead of the framework version are: " + } + list { + group { + +"Your activity must extend " + link { +"FragmentActivity" } + } + group { + +"You must call " + link { +"FragmentActivity#getSupportFragmentManager" } + +" to get the " + link { +"FragmentManager" } + } + } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `documentation with table`() { + testInline( + """ + |/src/main/java/sample/DocGenProcessor.java + |package sample; + |/** + | * <table> + | * <caption>List of supported types</caption> + | * <tr> + | * <td>cell 11</td> <td>cell 21</td> + | * </tr> + | * <tr> + | * <td>cell 12</td> <td>cell 22</td> + | * </tr> + | * </table> + | */ + | public class DocGenProcessor { } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val classPage = + module.findTestType("sample", "DocGenProcessor") + classPage.content.assertNode { + group { + header { +"DocGenProcessor" } + platformHinted { + group { + skipAllNotMatching() //Signature + } + comment { + table { + check { + caption!!.assertNode { + caption { + +"List of supported types" + } + } + } + group { + group { + +"cell 11" + } + group { + +"cell 21" + } + } + group { + group { + +"cell 12" + } + group { + +"cell 22" + } + } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + + @Test + fun `undocumented parameter and other tags`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @author Kordyjan + | * @since 0.11 + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + group { pWrapped("comment to function") } + unnamedTag("Author") { comment { +"Kordyjan" } } + unnamedTag("Since") { comment { +"0.11" } } + } + } + } + } + } + } + } + + @Test + fun `single parameter`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param abc comment to param + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + group { pWrapped("comment to function") } + header(4) { +"Parameters" } + table { + group { + +"abc" + check { + val textStyles = children.single { it is ContentText }.style + assertContains(textStyles, TextStyle.Underlined) + } + group { group { +"comment to param" } } + } + } + } + } + } + } + } + } + } + + @Test + fun `single parameter in class`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to class + | * @param abc comment to param + | */ + |class Foo(abc: String) + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "Foo") + println(page.content) + page.content.assertNode { + group { + header(1) { +"Foo" } + platformHinted { + classSignature( + emptyMap(), + "", + "", + emptySet(), + "Foo", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + group { + pWrapped("comment to class") + } + header(4) { +"Parameters" } + table { + group { + +"abc" + check { + val textStyles = children.single { it is ContentText }.style + assertContains(textStyles, TextStyle.Underlined) + } + group { group { +"comment to param" } } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `multiple parameters`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param first comment to first param + | * @param second comment to second param + | * @param[third] comment to third param + | */ + |fun function(first: String, second: Int, third: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), "", "", emptySet(), "function", null, + "first" to ParamAttributes(emptyMap(), emptySet(), "String"), + "second" to ParamAttributes(emptyMap(), emptySet(), "Int"), + "third" to ParamAttributes(emptyMap(), emptySet(), "Double") + ) + } + after { + group { group { group { +"comment to function" } } } + header(4) { +"Parameters" } + table { + group { + +"first" + check { + val textStyles = children.single { it is ContentText }.style + assertContains(textStyles, TextStyle.Underlined) + } + group { group { +"comment to first param" } } + } + group { + +"second" + check { + val textStyles = children.single { it is ContentText }.style + assertContains(textStyles, TextStyle.Underlined) + } + group { group { +"comment to second param" } } + } + group { + +"third" + check { + val textStyles = children.single { it is ContentText }.style + assertContains(textStyles, TextStyle.Underlined) + } + group { group { +"comment to third param" } } + } + } + } + } + } + } + } + } + } + + + @Test + fun `multiple parameters with not natural order`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param c comment to c param + | * @param b comment to b param + | * @param[a] comment to a param + | */ + |fun function(c: String, b: Int, a: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), "", "", emptySet(), "function", null, + "c" to ParamAttributes(emptyMap(), emptySet(), "String"), + "b" to ParamAttributes(emptyMap(), emptySet(), "Int"), + "a" to ParamAttributes(emptyMap(), emptySet(), "Double") + ) + } + after { + group { group { group { +"comment to function" } } } + header(4) { +"Parameters" } + table { + group { + +"c" + group { group { +"comment to c param" } } + } + group { + +"b" + group { group { +"comment to b param" } } + } + group { + +"a" + group { group { +"comment to a param" } } + } + } + + } + } + } + } + } + } + } + + @Test + fun `multiple parameters without function description`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @param first comment to first param + | * @param second comment to second param + | * @param[third] comment to third param + | */ + |fun function(first: String, second: Int, third: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), "", "", emptySet(), "function", null, + "first" to ParamAttributes(emptyMap(), emptySet(), "String"), + "second" to ParamAttributes(emptyMap(), emptySet(), "Int"), + "third" to ParamAttributes(emptyMap(), emptySet(), "Double") + ) + } + after { + header(4) { +"Parameters" } + table { + group { + +"first" + group { group { +"comment to first param" } } + } + group { + +"second" + group { group { +"comment to second param" } } + } + group { + +"third" + group { group { +"comment to third param" } } + } + } + } + } + } + } + } + } + } + + @Test + fun `function with receiver`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param abc comment to param + | * @receiver comment to receiver + | */ + |fun String.function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignatureWithReceiver( + emptyMap(), + "", + "", + emptySet(), + "String", + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + group { pWrapped("comment to function") } + group { + header(4) { +"Receiver" } + pWrapped("comment to receiver") + } + header(4) { +"Parameters" } + table { + group { + +"abc" + group { group { +"comment to param" } } + } + } + + } + } + } + } + } + } + } + + @Test + fun `missing parameter documentation`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param first comment to first param + | * @param[third] comment to third param + | */ + |fun function(first: String, second: Int, third: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), "", "", emptySet(), "function", null, + "first" to ParamAttributes(emptyMap(), emptySet(), "String"), + "second" to ParamAttributes(emptyMap(), emptySet(), "Int"), + "third" to ParamAttributes(emptyMap(), emptySet(), "Double") + ) + } + after { + group { group { group { +"comment to function" } } } + header(4) { +"Parameters" } + table { + group { + +"first" + group { group { +"comment to first param" } } + } + group { + +"third" + group { group { +"comment to third param" } } + } + } + } + } + } + } + } + } + } + + @Test + fun `parameters mixed with other tags`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param first comment to first param + | * @author Kordyjan + | * @param second comment to second param + | * @since 0.11 + | * @param[third] comment to third param + | */ + |fun function(first: String, second: Int, third: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), "", "", emptySet(), "function", null, + "first" to ParamAttributes(emptyMap(), emptySet(), "String"), + "second" to ParamAttributes(emptyMap(), emptySet(), "Int"), + "third" to ParamAttributes(emptyMap(), emptySet(), "Double") + ) + } + after { + group { pWrapped("comment to function") } + unnamedTag("Author") { comment { +"Kordyjan" } } + unnamedTag("Since") { comment { +"0.11" } } + header(4) { +"Parameters" } + + table { + group { + +"first" + group { group { +"comment to first param" } } + } + group { + +"second" + group { group { +"comment to second param" } } + } + group { + +"third" + group { group { +"comment to third param" } } + } + } + } + } + } + } + } + } + } + + @Test + fun javaDocCommentWithDocumentedParameters() { + testInline( + """ + |/src/main/java/test/Main.java + |package test + | public class Main { + | + | /** + | * comment to function + | * @param first comment to first param + | * @param second comment to second param + | */ + | public void sample(String first, String second) { + | + | } + | } + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val sampleFunction = module.dfs { + it is MemberPageNode && it.dri.first() + .toString() == "test/Main/sample/#java.lang.String#java.lang.String/PointingToDeclaration/" + } as MemberPageNode + val forJvm = (sampleFunction.documentables.firstOrNull() as DFunction).parameters.mapNotNull { + val jvm = it.documentation.keys.first { it.analysisPlatform == Platform.jvm } + it.documentation[jvm] + } + + assertEquals(2, forJvm.size) + val (first, second) = forJvm.map { it.paramsDescription() } + assertEquals("comment to first param", first) + assertEquals("comment to second param", second) + } + } + } + + private fun DocumentationNode.paramsDescription(): String = + children.firstIsInstanceOrNull<Param>()?.root?.children?.first()?.children?.firstIsInstanceOrNull<Text>()?.body.orEmpty() + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/properties/ContentForClassWithParamsAndPropertiesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/properties/ContentForClassWithParamsAndPropertiesTest.kt new file mode 100644 index 00000000..d244567f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/properties/ContentForClassWithParamsAndPropertiesTest.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.properties + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.pages.ClasslikePageNode +import org.jetbrains.dokka.pages.RootPageNode +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContentForClassWithParamsAndPropertiesTest : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + } + } + } + + @Test + fun `should work for a simple property`() { + propertyTest { rootPage -> + val node = rootPage.dfs { it.name == "LoadInitialParams" } as ClasslikePageNode + val actualDocsForPlaceholdersEnabled = + (node.documentables.firstOrNull() as DClass).constructors.first().parameters.find { it.name == "placeholdersEnabled" } + ?.documentation?.entries?.first()?.value + assertEquals(DocumentationNode(listOf(docsForPlaceholdersEnabled)), actualDocsForPlaceholdersEnabled) + } + } + + @Test + fun `should work for a simple with linebreak`() { + propertyTest { rootPage -> + val node = rootPage.dfs { it.name == "LoadInitialParams" } as ClasslikePageNode + val actualDocsForRequestedLoadSize = + (node.documentables.firstOrNull() as DClass).constructors.first().parameters.find { it.name == "requestedLoadSize" } + ?.documentation?.entries?.first()?.value + assertEquals(DocumentationNode(listOf(docsForRequestedLoadSize)), actualDocsForRequestedLoadSize) + } + } + + @Test + fun `should work with multiline property inline code`() { + propertyTest { rootPage -> + val node = rootPage.dfs { it.name == "LoadInitialParams" } as ClasslikePageNode + + val actualDocsForRequestedInitialKey = + (node.documentables.firstOrNull() as DClass).constructors.first().parameters.find { it.name == "requestedInitialKey" } + ?.documentation?.entries?.first()?.value + assertEquals(DocumentationNode(listOf(docsForRequestedInitialKey)), actualDocsForRequestedInitialKey) + } + } + + @Test + fun `constructor should only the param and constructor tags`() { + propertyTest { rootPage -> + val constructorDocs = Description( + root = CustomDocTag( + children = listOf( + P( + children = listOf( + Text("Creates an empty group.") + ) + ) + ), + emptyMap(), "MARKDOWN_FILE" + ) + ) + val node = rootPage.dfs { it.name == "LoadInitialParams" } as ClasslikePageNode + + val actualDocs = + (node.documentables.firstOrNull() as DClass).constructors.first().documentation.entries.first().value + assertEquals(DocumentationNode(listOf(constructorDocs, docsForParam)), actualDocs) + } + } + + @Test + fun `class should have all tags`() { + propertyTest { rootPage -> + val ownDescription = Description( + root = CustomDocTag( + children = listOf( + P( + children = listOf( + Text("Holder object for inputs to loadInitial.") + ) + ) + ), + emptyMap(), "MARKDOWN_FILE" + ) + ) + val node = rootPage.dfs { it.name == "LoadInitialParams" } as ClasslikePageNode + + val actualDocs = + (node.documentables.firstOrNull() as DClass).documentation.entries.first().value + assertEquals( + DocumentationNode( + listOf( + ownDescription, + docsForParam, + docsForRequestedInitialKey, + docsForRequestedLoadSize, + docsForPlaceholdersEnabled, + docsForConstructor + ) + ), + actualDocs + ) + } + } + + @Test + fun `property should also work with own docs that override the param tag`() { + propertyTest { rootPage -> + val ownDescription = Description( + root = CustomDocTag( + children = listOf( + P( + children = listOf( + Text("Own docs") + ) + ) + ), + emptyMap(), "MARKDOWN_FILE" + ) + ) + val node = rootPage.dfs { it.name == "ItemKeyedDataSource" } as ClasslikePageNode + + val actualDocs = + (node.documentables.firstOrNull() as DClass).properties.first().documentation.entries.first().value + assertEquals( + DocumentationNode(listOf(ownDescription)), + actualDocs + ) + } + } + + + private fun propertyTest(block: (RootPageNode) -> Unit) { + testInline( + """ |/src/main/kotlin/test/source.kt + |package test + |/** + | * @property tested Docs from class + | */ + |abstract class ItemKeyedDataSource<Key : Any, Value : Any> : DataSource<Key, Value>(ITEM_KEYED) { + | /** + | * Own docs + | */ + | val tested = "" + | + | /** + | * Holder object for inputs to loadInitial. + | * + | * @param Key Type of data used to query Value types out of the DataSource. + | * @property requestedInitialKey Load items around this key, or at the beginning of the data set + | * if `null` is passed. + | * + | * Note that this key is generally a hint, and may be ignored if you want to always load from + | * the beginning. + | * @property requestedLoadSize Requested number of items to load. + | * + | * Note that this may be larger than available data. + | * @property placeholdersEnabled Defines whether placeholders are enabled, and whether the + | * loaded total count will be ignored. + | * + | * @constructor Creates an empty group. + | */ + | open class LoadInitialParams<Key : Any>( + | @JvmField + | val requestedInitialKey: Key?, + | @JvmField + | val requestedLoadSize: Int, + | @JvmField + | val placeholdersEnabled: Boolean + | ) + |}""".trimIndent(), testConfiguration + ) { + pagesGenerationStage = block + } + } + + private val docsForPlaceholdersEnabled = Property( + root = CustomDocTag( + listOf( + P( + children = listOf( + Text("Defines whether placeholders are enabled, and whether the loaded total count will be ignored.") + ) + ) + ), emptyMap(), "MARKDOWN_FILE" + ), + name = "placeholdersEnabled" + ) + + private val docsForRequestedInitialKey = Property( + root = CustomDocTag( + listOf( + P( + children = listOf( + Text("Load items around this key, or at the beginning of the data set if "), + CodeInline( + listOf( + Text("null") + ) + ), + Text(" is passed.") + ), + params = emptyMap() + ), + P( + children = listOf( + Text("Note that this key is generally a hint, and may be ignored if you want to always load from the beginning.") + ) + ) + ), emptyMap(), "MARKDOWN_FILE" + ), + name = "requestedInitialKey" + ) + + private val docsForRequestedLoadSize = Property( + root = CustomDocTag( + listOf( + P( + children = listOf( + Text("Requested number of items to load.") + ) + ), + P( + children = listOf( + Text("Note that this may be larger than available data.") + ) + ) + ), emptyMap(), "MARKDOWN_FILE" + ), + name = "requestedLoadSize" + ) + + private val docsForConstructor = Constructor( + root = CustomDocTag( + children = listOf( + P( + children = listOf( + Text("Creates an empty group.") + ) + ) + ), + emptyMap(), "MARKDOWN_FILE" + ) + ) + + private val docsForParam = Param( + root = CustomDocTag( + children = listOf( + P( + children = listOf( + Text("Type of data used to query Value types out of the DataSource.") + ) + ) + ), + emptyMap(), "MARKDOWN_FILE" + ), + name = "Key" + ) +} + diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/receiver/ContentForReceiverTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/receiver/ContentForReceiverTest.kt new file mode 100644 index 00000000..d94c1106 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/receiver/ContentForReceiverTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.receiver + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.Receiver +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.pages.ContentHeader +import org.jetbrains.dokka.pages.ContentText +import org.jetbrains.dokka.pages.MemberPageNode +import utils.docs +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class ContentForReceiverTest: BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + } + } + } + + @Test + fun `should have docs for receiver`(){ + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + |/** + | * docs + | * @receiver docs for string + | */ + |fun String.asd2(): String = this + """.trimIndent(), + testConfiguration + ){ + documentablesTransformationStage = { module -> + with(module.packages.flatMap { it.functions }.first()){ + val receiver = docs().firstOrNull { it is Receiver } + assertNotNull(receiver) + val content = receiver.dfs { it is Text } as Text + assertEquals("docs for string", content.body) + } + } + pagesTransformationStage = { rootPageNode -> + val functionPage = rootPageNode.dfs { it is MemberPageNode } as MemberPageNode + val header = functionPage.content.dfs { it is ContentHeader && it.children.firstOrNull() is ContentText } + val text = functionPage.content.dfs { it is ContentText && it.text == "docs for string" } + + assertNotNull(header) + assertNotNull(text) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/samples/ContentForSamplesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/samples/ContentForSamplesTest.kt new file mode 100644 index 00000000..d166d8f8 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/samples/ContentForSamplesTest.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.samples + +import matchers.content.* +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.base.transformers.pages.KOTLIN_PLAYGROUND_SCRIPT +import org.jetbrains.dokka.model.DisplaySourceSet +import utils.TestOutputWriterPlugin +import utils.assertContains +import utils.classSignature +import utils.findTestType +import java.nio.file.Paths +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class ContentForSamplesTest : BaseAbstractTest() { + private val testDataDir = getTestDataDir("content/samples").toAbsolutePath() + + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + samples = listOf( + Paths.get("$testDataDir/samples.kt").toString(), + ) + } + } + } + + private val mppTestConfiguration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/pageMerger/Test.kt") + samples = listOf( + Paths.get("$testDataDir/samples.kt").toString(), + ) + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jvmMain/kotlin/pageMerger/Test.kt") + samples = listOf( + Paths.get("$testDataDir/samples.kt").toString(), + ) + } + sourceSet { + name = "linuxX64" + displayName = "linuxX64" + analysisPlatform = "native" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/linuxX64Main/kotlin/pageMerger/Test.kt") + samples = listOf( + Paths.get("$testDataDir/samples.kt").toString(), + ) + } + } + } + + @Test + fun `samples block is rendered in the description`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + | /** + | * @sample [test.sampleForClassDescription] + | */ + |class Foo + """.trimIndent(), testConfiguration, + pluginOverrides = listOf(writerPlugin) + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "Foo") + assertContains(page.embeddedResources, KOTLIN_PLAYGROUND_SCRIPT) + page.content.assertNode { + group { + header(1) { +"Foo" } + platformHinted { + classSignature( + emptyMap(), + "", + "", + emptySet(), + "Foo" + ) + header(4) { +"Samples" } + group { + codeBlock { + +"""| + |fun main() { + | //sampleStart + | print("Hello") + | //sampleEnd + |}""".trimMargin() + } + } + } + } + skipAllNotMatching() + } + } + renderingStage = { _, _ -> + assertNotEquals(-1, writerPlugin.writer.contents["root/test/-foo/index.html"]?.indexOf(KOTLIN_PLAYGROUND_SCRIPT)) + } + } + } + + @Test + fun `multiplatofrm class with samples in few platforms`() { + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @sample [test.sampleForClassDescription] + |*/ + |expect open class Parent + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @sample unresolved + |*/ + |actual open class Parent + | + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual open class Parent + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + assertContains(page.embeddedResources, KOTLIN_PLAYGROUND_SCRIPT) + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"expect open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + header(4) { +"Samples" } + group { + codeBlock { + +"""| + |fun main() { + | //sampleStart + | print("Hello") + | //sampleEnd + |}""".trimMargin() + } + check { + sourceSets.assertSourceSet("common") + } + } + group { + +"unresolved" + check { + sourceSets.assertSourceSet("jvm") + } + } + } + } + skipAllNotMatching() + } + } + } + } +} + + +private fun Set<DisplaySourceSet>.assertSourceSet(expectedName: String) { + assertEquals(1, this.size) + assertEquals(expectedName, this.first().name) +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt new file mode 100644 index 00000000..fb72178b --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt @@ -0,0 +1,866 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.seealso + +import matchers.content.* +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.pages.ContentDRILink +import utils.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContentForSeeAlsoTest : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + analysisPlatform = "jvm" + } + } + } + + private val mppTestConfiguration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jvmMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "linuxX64" + displayName = "linuxX64" + analysisPlatform = "native" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/linuxX64Main/kotlin/pageMerger/Test.kt") + } + } + } + + @Test + fun `undocumented function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + } + } + } + } + + @Test + fun `undocumented seealso`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see abc + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"See also" } + table { + group { + //DRI should be "test//abc/#/-1/" + link { +"abc" } + } + } + } + } + } + } + } + } + } + + @Test + fun `undocumented seealso without reference for class`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see abc + | */ + |class Foo() + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "Foo") + println(page.content) + page.content.assertNode { + group { + header(1) { +"Foo" } + platformHinted { + classSignature( + emptyMap(), + "", + "", + emptySet(), + "Foo" + ) + header(4) { +"See also" } + table { + group { + +"abc" + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @OnlyDescriptors("No link for `abc` in K1") + @Test + fun `undocumented seealso with reference to parameter for class`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see abc + | */ + |class Foo(abc: String) + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "Foo") + println(page.content) + page.content.assertNode { + group { + header(1) { +"Foo" } + platformHinted { + classSignature( + emptyMap(), + "", + "", + emptySet(), + "Foo", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + header(4) { +"See also" } + table { + group { + +"abc" // link { +"abc" } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @OnlyDescriptors("issue #3179") + @Test + fun `undocumented seealso with reference to property for class`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see abc + | */ + |class Foo(val abc: String) + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "Foo") + println(page.content) + page.content.assertNode { + group { + header(1) { +"Foo" } + platformHinted { + classSignature( + emptyMap(), + "", + "", + emptySet(), + "Foo", + "val abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + header(4) { +"See also" } + table { + group { + link { +"Foo.abc" } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `documented seealso`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see abc Comment to abc + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"See also" } + table { + group { + //DRI should be "test//abc/#/-1/" + link { +"abc" } + group { + group { +"Comment to abc" } + } + } + } + } + } + } + } + } + } + } + + @OnlyDescriptors("issue #3179") + @Test + fun `documented seealso with reference to property for class`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see abc Comment to abc + | */ + |class Foo(val abc: String) + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "Foo") + println(page.content) + page.content.assertNode { + group { + header(1) { +"Foo" } + platformHinted { + classSignature( + emptyMap(), + "", + "", + emptySet(), + "Foo", + "val abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + header(4) { +"See also" } + table { + group { + link { +"Foo.abc" } + group { + group { +"Comment to abc" } + } + } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `should use fully qualified name for unresolved link`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see com.example.NonExistingClass description for non-existing + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"See also" } + table { + group { + +"com.example.NonExistingClass" + group { + group { +"description for non-existing" } + } + } + } + } + } + } + } + } + } + } + + @Test + fun `undocumented seealso with stdlib link`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see Collection + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"See also" } + table { + group { + link { + check { + assertEquals( + "kotlin.collections/Collection///PointingToDeclaration/", + (this as ContentDRILink).address.toString() + ) + } + +"Collection" + } + } + } + + } + } + } + } + } + } + } + + @Test + fun `documented seealso with stdlib link`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see Collection Comment to stdliblink + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"See also" } + table { + group { + //DRI should be "test//abc/#/-1/" + link { +"Collection" } + group { + group { +"Comment to stdliblink" } + } + } + + } + } + } + } + } + } + } + } + + @Test + fun `documented seealso with stdlib link with other tags`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * random comment + | * @see Collection Comment to stdliblink + | * @author pikinier20 + | * @since 0.11 + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + group { comment { +"random comment" } } + unnamedTag("Author") { comment { +"pikinier20" } } + unnamedTag("Since") { comment { +"0.11" } } + + header(4) { +"See also" } + table { + group { + //DRI should be "test//abc/#/-1/" + link { +"Collection" } + group { + group { +"Comment to stdliblink" } + } + } + } + + } + } + } + } + } + } + } + + @Test + fun `documented multiple see also`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see abc Comment to abc1 + | * @see abc Comment to abc2 + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"See also" } + table { + group { + //DRI should be "test//abc/#/-1/" + link { +"abc" } + group { + group { +"Comment to abc2" } + } + } + } + + } + } + } + } + } + } + } + + @Test + fun `documented multiple see also mixed source`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @see abc Comment to abc1 + | * @see[Collection] Comment to collection + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("test", "function") + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + null, + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + after { + header(4) { +"See also" } + table { + group { + //DRI should be "test//abc/#/-1/" + link { +"abc" } + group { + group { +"Comment to abc1" } + } + } + group { + //DRI should be "test//abc/#/-1/" + link { +"Collection" } + group { group { +"Comment to collection" } } + } + } + } + } + } + } + } + } + } + + @Test + fun `should prefix static function and property links with class name`() { + testInline( + """ + |/src/main/kotlin/com/example/package/CollectionExtensions.kt + |package com.example.util + | + |object CollectionExtensions { + | val property = "Hi" + | fun emptyList() {} + |} + | + |/src/main/kotlin/com/example/foo.kt + |package com.example + | + |import com.example.util.CollectionExtensions.property + |import com.example.util.CollectionExtensions.emptyList + | + |/** + | * @see [property] static property + | * @see [emptyList] static emptyList + | */ + |fun function() {} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("com.example", "function") + + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + annotations = emptyMap(), + visibility = "", + modifier = "", + keywords = emptySet(), + name = "function", + returnType = null, + ) + } + after { + header(4) { +"See also" } + table { + group { + link { +"CollectionExtensions.property" } + group { + group { +"static property" } + } + } + group { + link { +"CollectionExtensions.emptyList" } + group { + group { +"static emptyList" } + } + } + } + } + } + } + } + } + } + } + + @Test + fun `multiplatform class with seealso in few platforms`() { + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/** + |* @see Unit + |*/ + |expect open class Parent + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |val x = 0 + |/** + |* @see x resolved + |* @see y unresolved + |*/ + |actual open class Parent + | + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual open class Parent + | + """.trimMargin(), + mppTestConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.findTestType("pageMerger", "Parent") + page.content.assertNode { + group { + header(1) { +"Parent" } + platformHinted { + group { + +"expect open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + group { + +"actual open class " + link { + +"Parent" + } + } + header(4) { + +"See also" + check { + assertEquals(2, sourceSets.size) + } + } + table { + group { + link { +"Unit" } + check { + sourceSets.assertSourceSet("common") + } + } + group { + link { +"Unit" } + check { + sourceSets.assertSourceSet("jvm") + } + } + group { + link { +"x" } + group { group { +"resolved" } } + check { + sourceSets.assertSourceSet("jvm") + } + } + group { + +"y" + group { group { +"unresolved" } } + check { + sourceSets.assertSourceSet("jvm") + } + } + + check { + assertEquals(2, sourceSets.size) + } + } + } + } + skipAllNotMatching() + } + } + } + } +} + +private fun Set<DisplaySourceSet>.assertSourceSet(expectedName: String) { + assertEquals(1, this.size) + assertEquals(expectedName, this.first().name) +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/signatures/ConstructorsSignaturesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/signatures/ConstructorsSignaturesTest.kt new file mode 100644 index 00000000..9a413e0e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/signatures/ConstructorsSignaturesTest.kt @@ -0,0 +1,469 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.signatures + +import matchers.content.* +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.pages.BasicTabbedContentType +import org.jetbrains.dokka.pages.ContentPage +import kotlin.test.Test +import utils.OnlyDescriptors + +class ConstructorsSignaturesTest : BaseAbstractTest() { + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + } + } + } + + @Test + fun `class name without parenthesis`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class SomeClass + | + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "SomeClass" } as ContentPage + page.content.assertNode { + group { + header(1) { +"SomeClass" } + platformHinted { + group { + +"class " + link { +"SomeClass" } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `class name with empty parenthesis`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class SomeClass() + | + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "SomeClass" } as ContentPage + page.content.assertNode { + group { + header(1) { +"SomeClass" } + platformHinted { + group { + +"class " + link { +"SomeClass" } + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `class with a parameter`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class SomeClass(a: String) + | + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "SomeClass" } as ContentPage + page.content.assertNode { + group { + header(1) { +"SomeClass" } + platformHinted { + group { + +"class " + link { +"SomeClass" } + +"(" + group { + group { + +"a: " + group { link { +"String" } } + } + } + +")" + } + } + } + skipAllNotMatching() + } + } + } + } + + @Test + fun `class with a val parameter`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class SomeClass(val a: String, var i: Int) + | + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "SomeClass" } as ContentPage + page.content.assertNode { + group { + header(1) { +"SomeClass" } + platformHinted { + group { + +"class " + link { +"SomeClass" } + +"(" + group { + group { + +"val a: " + group { link { +"String" } } + +", " + } + group { + +"var i: " + group { link { +"Int" } } + } + } + +")" + } + } + } + skipAllNotMatching() + } + } + } + } + + @OnlyDescriptors("Order of constructors is different in K2") + @Test + fun `class with a parameterless secondary constructor`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class SomeClass(a: String) { + | constructor() + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "SomeClass" } as ContentPage + page.content.assertNode { + group { + header(1) { +"SomeClass" } + platformHinted { + group { + +"class " + link { +"SomeClass" } + +"(" + group { + group { + +"a: " + group { link { +"String" } } + } + } + +")" + } + } + } + tabbedGroup { + group { + tab(BasicTabbedContentType.CONSTRUCTOR) { + header { +"Constructors" } + table { + group { + link { +"SomeClass" } + platformHinted { + group { + +"constructor" + +"(" + +")" + } + group { + +"constructor" + +"(" + group { + group { + +"a: " + group { link { +"String" } } + } + } + +")" + } + } + } + } + } + } + skipAllNotMatching() + } + } + } + } + } + + + @OnlyDescriptors("Order of constructors is different in K2") + @Test + fun `class with a few documented constructors`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + | /** + | * some comment + | * @constructor ctor comment + | **/ + |class SomeClass(a: String){ + | /** + | * ctor one + | **/ + | constructor(): this("") + | + | /** + | * ctor two + | **/ + | constructor(b: Int): this("") + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "SomeClass" } as ContentPage + page.content.assertNode { + group { + header(1) { +"SomeClass" } + platformHinted { + group { + +"class " + link { +"SomeClass" } + +"(" + group { + group { + +"a: " + group { link { +"String" } } + } + } + +")" + } + skipAllNotMatching() + } + } + tabbedGroup { + group { + tab(BasicTabbedContentType.CONSTRUCTOR) { + header { +"Constructors" } + table { + group { + link { +"SomeClass" } + platformHinted { + group { + +"constructor" + +"(" + +")" + } + group { + group { + group { +"ctor one" } + } + } + group { + +"constructor" + +"(" + group { + group { + +"b: " + group { + link { +"Int" } + } + } + } + +")" + } + group { + group { + group { +"ctor two" } + } + } + group { + +"constructor" + +"(" + group { + group { + +"a: " + group { + link { +"String" } + } + } + } + +")" + } + group { + group { + group { +"ctor comment" } + } + } + } + } + } + } + } + skipAllNotMatching() + } + } + } + } + } + + @Test + fun `class with explicitly documented constructor`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + | /** + | * some comment + | * @constructor ctor comment + | **/ + |class SomeClass(a: String) + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "SomeClass" } as ContentPage + page.content.assertNode { + group { + header(1) { +"SomeClass" } + platformHinted { + group { + +"class " + link { +"SomeClass" } + +"(" + group { + group { + +"a: " + group { link { +"String" } } + } + } + +")" + } + skipAllNotMatching() + } + } + tabbedGroup { + group { + tab(BasicTabbedContentType.CONSTRUCTOR) { + header { +"Constructors" } + table { + group { + link { +"SomeClass" } + platformHinted { + group { + +"constructor" + +"(" + group { + group { + +"a: " + group { + link { +"String" } + } + } + } + +")" + } + group { + group { + group { +"ctor comment" } + } + } + } + } + } + } + } + skipAllNotMatching() + } + } + } + } + } + + @Test + fun `should render primary constructor, but not constructors block for annotation class`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |annotation class MyAnnotation(val param: String) {} + """.trimIndent(), + testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "MyAnnotation" } as ContentPage + page.content.assertNode { + group { + header(1) { +"MyAnnotation" } + platformHinted { + group { + +"annotation class " + link { +"MyAnnotation" } + +"(" + group { + group { + +"val param: " + group { link { +"String" } } + } + } + +")" + } + } + } + group { + group { + group { + header { +"Properties" } + table { + skipAllNotMatching() + } + } + } + } + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/signatures/ContentForSignaturesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/signatures/ContentForSignaturesTest.kt new file mode 100644 index 00000000..8af9e082 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/signatures/ContentForSignaturesTest.kt @@ -0,0 +1,515 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package content.signatures + +import matchers.content.* +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.PackagePageNode +import utils.ParamAttributes +import utils.bareSignature +import utils.propertySignature +import utils.typealiasSignature +import kotlin.test.Test + +class ContentForSignaturesTest : BaseAbstractTest() { + + private val testConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PRIVATE, + DokkaConfiguration.Visibility.PROTECTED, + DokkaConfiguration.Visibility.INTERNAL, + ) + } + } + } + + @Test + fun `function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |fun function(abc: String): String { + | return "Hello, " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + emptySet(), + "function", + "String", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + } + } + } + } + + @Test + fun `private function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |private fun function(abc: String): String { + | return "Hello, " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "private", + "", + emptySet(), + "function", + "String", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + } + } + } + } + + @Test + fun `open function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |open fun function(abc: String): String { + | return "Hello, " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "open", + emptySet(), + "function", + "String", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + } + } + } + } + + @Test + fun `function without parameters`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |fun function(): String { + | return "Hello" + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + annotations = emptyMap(), + visibility = "", + modifier = "", + keywords = emptySet(), + name = "function", + returnType = "String", + ) + } + } + } + + } + } + } + } + + + @Test + fun `suspend function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |suspend fun function(abc: String): String { + | return "Hello, " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "", + "", + setOf("suspend"), + "function", + "String", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + } + } + } + } + + @Test + fun `protected open suspend function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |protected open suspend fun function(abc: String): String { + | return "Hello, " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "protected", + "open", + setOf("suspend"), + "function", + "String", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + } + } + } + } + + @Test + fun `protected open suspend inline function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |protected open suspend inline fun function(abc: String): String { + | return "Hello, " + abc + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + group { + header(1) { +"function" } + } + divergentGroup { + divergentInstance { + divergent { + bareSignature( + emptyMap(), + "protected", + "open", + setOf("inline", "suspend"), + "function", + "String", + "abc" to ParamAttributes(emptyMap(), emptySet(), "String") + ) + } + } + } + } + } + } + } + + @Test + fun `property`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |val property: Int = 6 + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + propertySignature(emptyMap(), "", "", emptySet(), "val", "property", "Int", "6") + } + } + } + } + + @Test + fun `const property`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |const val property: Int = 6 + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + propertySignature(emptyMap(), "", "", setOf("const"), "val", "property", "Int", "6") + } + } + } + } + + @Test + fun `protected property`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |protected val property: Int = 6 + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + propertySignature(emptyMap(), "protected", "", emptySet(), "val", "property", "Int", "6") + } + } + } + } + + @Test + fun `protected lateinit property`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |protected lateinit var property: Int = 6 + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + propertySignature(emptyMap(), "protected", "", setOf("lateinit"), "var", "property", "Int", null) + } + } + } + } + + @Test + fun `should not display default value for mutable property`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |var property: Int = 6 + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + propertySignature( + annotations = emptyMap(), + visibility = "", + modifier = "", + keywords = setOf(), + preposition = "var", + name = "property", + type = "Int", + value = null + ) + } + } + } + } + + @Test + fun `typealias to String`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |typealias Alias = String + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + typealiasSignature("Alias", "String") + } + } + } + } + + @Test + fun `typealias to Int`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |typealias Alias = Int + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + typealiasSignature("Alias", "Int") + } + } + } + } + + @Test + fun `typealias to type in same package`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |typealias Alias = X + |class X + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + typealiasSignature("Alias", "X") + } + } + } + } + + @Test + fun `typealias to type in different package`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + |import other.X + |typealias Alias = X + | + |/src/main/kotlin/test/source2.kt + |package other + |class X + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + typealiasSignature("Alias", "X") + } + } + } + } + + @Test + fun `typealias to type in different package with same name`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + |typealias Alias = other.Alias + | + |/src/main/kotlin/test/source2.kt + |package other + |class Alias + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } as PackagePageNode + page.content.assertNode { + typealiasSignature("Alias", "other.Alias") + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/content/typealiases/TypealiasTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/content/typealiases/TypealiasTest.kt new file mode 100644 index 00000000..4015e0f4 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/content/typealiases/TypealiasTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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.ClasslikePageNode +import org.jetbrains.dokka.pages.PlatformHintedContent +import utils.assertNotNull +import kotlin.test.Test + + +class TypealiasTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(commonStdlibPath!!, jvmStdlibPath!!) + 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" } } } + } + } + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/enums/JavaEnumsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/enums/JavaEnumsTest.kt new file mode 100644 index 00000000..39c893e9 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/enums/JavaEnumsTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package enums + +import org.jetbrains.dokka.SourceLinkDefinitionImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import signatures.renderedContent +import utils.TestOutputWriterPlugin +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class JavaEnumsTest : BaseAbstractTest() { + + private val basicConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + // Shouldn't try to give source links to synthetic methods (values, valueOf) if any are present + // https://github.com/Kotlin/dokka/issues/2544 + @Test + fun `java enum with configured source links should not fail build due to any synthetic methods`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + sourceLinks = listOf( + SourceLinkDefinitionImpl( + localDirectory = "src/main/java", + remoteUrl = URL("https://github.com/user/repo/tree/master/src/main/java"), + remoteLineSuffix = "#L" + ) + ) + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/java/basic/JavaEnum.java + |package testpackage + | + |/** + |* doc + |*/ + |public enum JavaEnum { + | ONE, TWO, THREE + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val enumPage = writerPlugin.writer.renderedContent("root/testpackage/-java-enum/index.html") + val sourceLink = enumPage.select(".symbol .floating-right") + .select("a[href]") + .attr("href") + + + assertEquals( + "https://github.com/user/repo/tree/master/src/main/java/basic/JavaEnum.java#L6", + sourceLink + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/enums/KotlinEnumsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/enums/KotlinEnumsTest.kt new file mode 100644 index 00000000..c32a5cc2 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/enums/KotlinEnumsTest.kt @@ -0,0 +1,471 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package enums + +import matchers.content.* +import org.jetbrains.dokka.SourceLinkDefinitionImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.pages.ClasslikePage +import org.jetbrains.dokka.pages.ClasslikePageNode +import org.jetbrains.dokka.pages.ContentGroup +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import signatures.renderedContent +import utils.* +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class KotlinEnumsTest : BaseAbstractTest() { + + @Test + fun `should preserve enum source ordering for documentables`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package testpackage + | + |enum class TestEnum { + | ZERO, + | ONE, + | TWO, + | THREE, + | FOUR, + | FIVE, + | SIX, + | SEVEN, + | EIGHT, + | NINE + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + documentablesTransformationStage = { module -> + val testPackage = module.packages[0] + assertEquals("testpackage", testPackage.name) + + val testEnum = testPackage.classlikes[0] as DEnum + assertEquals("TestEnum", testEnum.name) + + val enumEntries = testEnum.entries + assertEquals(10, enumEntries.count()) + + assertEquals("ZERO", enumEntries[0].name) + assertEquals("ONE", enumEntries[1].name) + assertEquals("TWO", enumEntries[2].name) + assertEquals("THREE", enumEntries[3].name) + assertEquals("FOUR", enumEntries[4].name) + assertEquals("FIVE", enumEntries[5].name) + assertEquals("SIX", enumEntries[6].name) + assertEquals("SEVEN", enumEntries[7].name) + assertEquals("EIGHT", enumEntries[8].name) + assertEquals("NINE", enumEntries[9].name) + } + } + } + + @Test + fun `should preserve enum source ordering for generated pages`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package testpackage + | + |enum class TestEnum { + | ZERO, + | ONE, + | TWO, + | THREE, + | FOUR, + | FIVE, + | SIX, + | SEVEN, + | EIGHT, + | NINE + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + pagesGenerationStage = { rootPage -> + val packagePage = rootPage.children[0] + assertEquals("testpackage", packagePage.name) + + val testEnumNode = packagePage.children[0] + assertEquals("TestEnum", testEnumNode.name) + + val enumEntries = testEnumNode.children.filterIsInstance<ClasslikePage>() + assertEquals(10, enumEntries.size) + + assertEquals("ZERO", enumEntries[0].name) + assertEquals("ONE", enumEntries[1].name) + assertEquals("TWO", enumEntries[2].name) + assertEquals("THREE", enumEntries[3].name) + assertEquals("FOUR", enumEntries[4].name) + assertEquals("FIVE", enumEntries[5].name) + assertEquals("SIX", enumEntries[6].name) + assertEquals("SEVEN", enumEntries[7].name) + assertEquals("EIGHT", enumEntries[8].name) + assertEquals("NINE", enumEntries[9].name) + } + } + } + + @Test + fun `should preserve enum source ordering for rendered entries`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package testpackage + | + |enum class TestEnum { + | ZERO, + | ONE, + | TWO, + | THREE, + | FOUR, + | FIVE, + | SIX, + | SEVEN, + | EIGHT, + | NINE + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val enumEntriesOnPage = writerPlugin.writer.renderedContent("root/testpackage/-test-enum/index.html") + .select("div[data-togglable=ENTRY] .table") + .select("div.table-row") + .select("div.keyValue") + .select("div.title") + .select("a") + + val enumEntries = enumEntriesOnPage.map { it.text() } + assertEquals(10, enumEntries.size) + + assertEquals("ZERO", enumEntries[0]) + assertEquals("ONE", enumEntries[1]) + assertEquals("TWO", enumEntries[2]) + assertEquals("THREE", enumEntries[3]) + assertEquals("FOUR", enumEntries[4]) + assertEquals("FIVE", enumEntries[5]) + assertEquals("SIX", enumEntries[6]) + assertEquals("SEVEN", enumEntries[7]) + assertEquals("EIGHT", enumEntries[8]) + assertEquals("NINE", enumEntries[9]) + } + } + } + + @Test + fun `should preserve enum source ordering for navigation menu`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package testpackage + | + |enum class TestEnum { + | ZERO, + | ONE, + | TWO, + | THREE, + | FOUR, + | FIVE, + | SIX, + | SEVEN, + | EIGHT, + | NINE + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val sideMenu = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + + assertEquals("ZERO", sideMenu.select("#root-nav-submenu-0-0-0").text()) + assertEquals("ONE", sideMenu.select("#root-nav-submenu-0-0-1").text()) + assertEquals("TWO", sideMenu.select("#root-nav-submenu-0-0-2").text()) + assertEquals("THREE", sideMenu.select("#root-nav-submenu-0-0-3").text()) + assertEquals("FOUR", sideMenu.select("#root-nav-submenu-0-0-4").text()) + assertEquals("FIVE", sideMenu.select("#root-nav-submenu-0-0-5").text()) + assertEquals("SIX", sideMenu.select("#root-nav-submenu-0-0-6").text()) + assertEquals("SEVEN", sideMenu.select("#root-nav-submenu-0-0-7").text()) + assertEquals("EIGHT", sideMenu.select("#root-nav-submenu-0-0-8").text()) + assertEquals("NINE", sideMenu.select("#root-nav-submenu-0-0-9").text()) + } + } + } + + fun TestOutputWriter.navigationHtml(): Element = contents.getValue("navigation.html").let { Jsoup.parse(it) } + + @Test + fun `should handle companion object within enum`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package testpackage + | + |enum class TestEnum { + | E1, + | E2; + | companion object {} + |} + """.trimMargin(), + configuration + ) { + documentablesTransformationStage = { m -> + m.packages.let { p -> + assertTrue(p.isNotEmpty(), "Package list cannot be empty") + p.first().classlikes.let { c -> + assertTrue(c.isNotEmpty(), "Classlikes list cannot be empty") + + val enum = c.first() as DEnum + assertNotNull(enum.companion) + } + } + } + } + } + + @Test + fun enumWithMethods() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/TestEnum.kt + |package testpackage + | + | + |interface Sample { + | fun toBeImplemented(): String + |} + | + |enum class TestEnum: Sample { + | E1 { + | override fun toBeImplemented(): String = "e1" + | } + |} + """.trimMargin(), + configuration + ) { + documentablesTransformationStage = { m -> + m.packages.let { p -> + p.first().classlikes.let { c -> + val enum = c.first { it is DEnum } as DEnum + val first = enum.entries.first() + + assertNotNull(first.functions.find { it.name == "toBeImplemented" }) + } + } + } + } + } + + @Test + @OnlyDescriptors("K2 has `compareTo`, that should be suppressed, due to #3196") + fun `enum should have functions on page`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/TestEnum.kt + |package testpackage + | + | + |interface Sample { + | fun toBeImplemented(): String + |} + | + |enum class TestEnum: Sample { + | E1 { + | override fun toBeImplemented(): String = "e1" + | } + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { root -> + root.contentPage<ClasslikePageNode>("E1") { + assertHasFunctions("toBeImplemented") + } + + root.contentPage<ClasslikePageNode>("TestEnum") { + assertHasFunctions("toBeImplemented", "valueOf", "values") + } + } + } + } + + @Test + fun enumWithAnnotationsOnEntries() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/TestEnum.kt + |package testpackage + | + |enum class TestEnum { + | /** + | Sample docs for E1 + | **/ + | @SinceKotlin("1.3") // This annotation is transparent due to lack of @MustBeDocumented annotation + | E1 + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { m -> + val entryNode = m.children.first { it.name == "testpackage" }.children.first { it.name == "TestEnum" }.children.filterIsInstance<ClasslikePageNode>().first() + val signature = (entryNode.content as ContentGroup).dfs { it is ContentGroup && it.dci.toString() == "[testpackage/TestEnum.E1///PointingToDeclaration/{\"org.jetbrains.dokka.links.EnumEntryDRIExtra\":{\"key\":\"org.jetbrains.dokka.links.EnumEntryDRIExtra\"}}][Cover]" } as ContentGroup + + signature.assertNode { + header(1) { +"E1" } + platformHinted { + group { + group { + link { +"E1" } + } + } + group { + group { + group { + +"Sample docs for E1" + } + } + } + } + } + } + } + } + + // Shouldn't try to give source links to synthetic methods (values, valueOf) if any are present + // Initially reported for Java, making sure it doesn't fail for Kotlin either + // https://github.com/Kotlin/dokka/issues/2544 + @Test + fun `kotlin enum with configured source links should not fail the build due to synthetic methods`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + sourceLinks = listOf( + SourceLinkDefinitionImpl( + localDirectory = "src/main/kotlin", + remoteUrl = URL("https://github.com/user/repo/tree/master/src/main/kotlin"), + remoteLineSuffix = "#L" + ) + ) + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/basic/KotlinEnum.kt + |package testpackage + | + |/** + |* Doc + |*/ + |enum class KotlinEnum { + | ONE, TWO, THREE + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val sourceLink = writerPlugin.writer.renderedContent("root/testpackage/-kotlin-enum/index.html") + .select(".symbol .floating-right") + .select("a[href]") + .attr("href") + + assertEquals( + "https://github.com/user/repo/tree/master/src/main/kotlin/basic/KotlinEnum.kt#L6", + sourceLink + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/expect/AbstractExpectTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/expect/AbstractExpectTest.kt new file mode 100644 index 00000000..7f187127 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/expect/AbstractExpectTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package expect + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +abstract class AbstractExpectTest( + val testDir: Path? = Paths.get("src/test", "resources", "expect"), + val formats: List<String> = listOf("html") +) : BaseAbstractTest() { + + protected fun generateOutput(path: Path, outFormat: String): Path? { + val config = dokkaConfiguration { + format = outFormat + sourceSets { + sourceSet { + sourceRoots = listOf(path.toAbsolutePath().asString()) + } + } + } + + var result: Path? = null + testFromData(config, cleanupOutput = false) { + renderingStage = { _, context -> result = context.configuration.outputDir.toPath() } + } + return result + } + + protected fun compareOutput(expected: Path, obtained: Path?, gitTimeout: Long = 500) { + obtained?.let { path -> + val gitCompare = ProcessBuilder( + "git", + "--no-pager", + "diff", + expected.asString(), + path.asString() + ).also { logger.info("git diff command: ${it.command().joinToString(" ")}") } + .also { it.redirectErrorStream() }.start() + + assertTrue(gitCompare.waitFor(gitTimeout, TimeUnit.MILLISECONDS), "Git timed out after $gitTimeout") + gitCompare.inputStream.bufferedReader().lines().forEach { logger.info(it) } + assertEquals(0, gitCompare.exitValue(), "${path.fileName}: outputs don't match") + } ?: throw AssertionError("obtained path is null") + } + + protected fun compareOutputWithExcludes( + expected: Path, + obtained: Path?, + excludes: List<String>, + timeout: Long = 500 + ) { + obtained?.let { _ -> + val (res, out, err) = runDiff(expected, obtained, excludes, timeout) + assertEquals(0, res, "Outputs differ:\nstdout - $out\n\nstderr - ${err ?: ""}") + } ?: throw AssertionError("obtained path is null") + } + + protected fun runDiff(exp: Path, obt: Path, excludes: List<String>, timeout: Long): ProcessResult = + ProcessBuilder().command( + listOf("diff", "-ru") + excludes.flatMap { listOf("-x", it) } + listOf("--", exp.asString(), obt.asString()) + ).also { + it.redirectErrorStream() + }.start().also { assertTrue(it.waitFor(timeout, TimeUnit.MILLISECONDS), "diff timed out") }.let { + ProcessResult(it.exitValue(), it.inputStream.bufferResult()) + } + + + protected fun testOutput(p: Path, outFormat: String) { + val expectOut = p.resolve("out/$outFormat") + val testOut = generateOutput(p.resolve("src"), outFormat) + .also { logger.info("Test out: ${it?.asString()}") } + + compareOutput(expectOut.toAbsolutePath(), testOut?.toAbsolutePath()) + testOut?.deleteRecursively() + } + + protected fun testOutputWithExcludes( + p: Path, + outFormat: String, + ignores: List<String> = emptyList(), + timeout: Long = 500 + ) { + val expected = p.resolve("out/$outFormat") + generateOutput(p.resolve("src"), outFormat) + ?.let { obtained -> + compareOutputWithExcludes(expected, obtained, ignores, timeout) + + obtained.deleteRecursively() + } ?: throw AssertionError("Output not generated for ${p.fileName}") + } + + protected fun generateExpect(p: Path, outFormat: String) { + val out = p.resolve("out/$outFormat/") + Files.createDirectories(out) + + val ret = generateOutput(p.resolve("src"), outFormat) + Files.list(out).forEach { it.deleteRecursively() } + ret?.let { Files.list(it).forEach { f -> f.copyRecursively(out.resolve(f.fileName)) } } + } + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectGenerator.kt b/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectGenerator.kt new file mode 100644 index 00000000..0568ba74 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectGenerator.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package expect + +import kotlin.test.Ignore +import kotlin.test.Test + +class ExpectGenerator : AbstractExpectTest() { + + @Ignore + @Test + fun generateAll() = testDir?.dirsWithFormats(formats).orEmpty().forEach { (p, f) -> + generateExpect(p, f) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectTest.kt new file mode 100644 index 00000000..f1eb2a77 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package expect + +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.TestFactory +import kotlin.test.Ignore + +class ExpectTest : AbstractExpectTest() { + private val ignores: List<String> = listOf( + "images", + "scripts", + "images", + "styles", + "*.js", + "*.css", + "*.svg", + "*.map" + ) + + @Ignore + @TestFactory + fun expectTest() = testDir?.dirsWithFormats(formats).orEmpty().map { (p, f) -> + dynamicTest("${p.fileName}-$f") { testOutputWithExcludes(p, f, ignores) } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectUtils.kt b/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectUtils.kt new file mode 100644 index 00000000..a8b1b187 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectUtils.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package expect + +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Path +import kotlin.streams.toList + +data class ProcessResult(val code: Int, val out: String, val err: String? = null) + +internal fun Path.dirsWithFormats(formats: List<String>): List<Pair<Path, String>> = + Files.list(this).toList().filter { Files.isDirectory(it) }.flatMap { p -> formats.map { p to it } } + +internal fun Path.asString() = normalize().toString() +internal fun Path.deleteRecursively() = toFile().deleteRecursively() + +internal fun Path.copyRecursively(target: Path) = toFile().copyRecursively(target.toFile()) + +internal fun Path.listRecursively(filter: (Path) -> Boolean): List<Path> = when { + Files.isDirectory(this) -> listOfNotNull(takeIf(filter)) + Files.list(this).toList().flatMap { + it.listRecursively( + filter + ) + } + Files.isRegularFile(this) -> listOfNotNull(this.takeIf(filter)) + else -> emptyList() + } + +internal fun InputStream.bufferResult(): String = this.bufferedReader().lines().toList().joinToString("\n") diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/expectActuals/ExpectActualsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/expectActuals/ExpectActualsTest.kt new file mode 100644 index 00000000..3fc6e5c5 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/expectActuals/ExpectActualsTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package expectActuals + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.ClasslikePageNode +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + + +class ExpectActualsTest : BaseAbstractTest() { + + @Test + fun `three same named expect actual classes`() { + + val configuration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/pageMerger/Test.kt") + } + val commonJ = sourceSet { + name = "commonJ" + displayName = "commonJ" + analysisPlatform = "common" + sourceRoots = listOf("src/commonJMain/kotlin/pageMerger/Test.kt") + dependentSourceSets = setOf(common.value.sourceSetID) + } + val commonN1 = sourceSet { + name = "commonN1" + displayName = "commonN1" + analysisPlatform = "common" + sourceRoots = listOf("src/commonN1Main/kotlin/pageMerger/Test.kt") + dependentSourceSets = setOf(common.value.sourceSetID) + } + val commonN2 = sourceSet { + name = "commonN2" + displayName = "commonN2" + analysisPlatform = "common" + sourceRoots = listOf("src/commonN2Main/kotlin/pageMerger/Test.kt") + dependentSourceSets = setOf(common.value.sourceSetID) + } + sourceSet { + name = "js" + displayName = "js" + analysisPlatform = "js" + dependentSourceSets = setOf(commonJ.value.sourceSetID) + sourceRoots = listOf("src/jsMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(commonJ.value.sourceSetID) + sourceRoots = listOf("src/jvmMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "linuxX64" + displayName = "linuxX64" + analysisPlatform = "native" + dependentSourceSets = setOf(commonN1.value.sourceSetID) + sourceRoots = listOf("src/linuxX64Main/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "mingwX64" + displayName = "mingwX64" + analysisPlatform = "native" + dependentSourceSets = setOf(commonN1.value.sourceSetID) + sourceRoots = listOf("src/mingwX64Main/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "iosArm64" + displayName = "iosArm64" + analysisPlatform = "native" + dependentSourceSets = setOf(commonN2.value.sourceSetID) + sourceRoots = listOf("src/iosArm64Main/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "iosX64" + displayName = "iosX64" + analysisPlatform = "native" + dependentSourceSets = setOf(commonN2.value.sourceSetID) + sourceRoots = listOf("src/iosX64Main/kotlin/pageMerger/Test.kt") + } + } + } + + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/src/commonJMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |expect class A + | + |/src/commonN1Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |expect class A + | + |/src/commonN2Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |expect class A + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual class A + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual class A + | + |/src/linuxX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual class A + | + |/src/mingwX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual class A + | + |/src/iosArm64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual class A + | + |/src/iosX64Main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |actual class A + | + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + val allChildren = it.withDescendants().filterIsInstance<ClasslikePageNode>().toList() + val commonJ = allChildren.filter { it.name == "[jvm, js]A" } + val commonN1 = allChildren.filter { it.name == "[mingwX64, linuxX64]A" } + val commonN2 = allChildren.filter { it.name == "[iosX64, iosArm64]A" } + val noClass = allChildren.filter { it.name == "A" } + assertEquals(1, commonJ.size, "There can be only one [jvm, js]A page") + assertTrue( + commonJ.first().documentables.firstOrNull()?.sourceSets?.map { it.displayName } + ?.containsAll(listOf("commonJ", "js", "jvm")) ?: false, + "A(jvm, js)should have commonJ, js, jvm sources" + ) + + assertEquals(1, commonN1.size, "There can be only one [mingwX64, linuxX64]A page") + assertTrue( + commonN1.first().documentables.firstOrNull()?.sourceSets?.map { it.displayName } + ?.containsAll(listOf("commonN1", "linuxX64", "mingwX64")) ?: false, + "[mingwX64, linuxX64]A should have commonN1, linuxX64, mingwX64 sources" + ) + + assertEquals(1, commonN2.size, "There can be only one [iosX64, iosArm64]A page") + assertTrue( + commonN2.first().documentables.firstOrNull()?.sourceSets?.map { it.displayName } + ?.containsAll(listOf("commonN2", "iosArm64", "iosX64")) ?: false, + "[iosX64, iosArm64]A should have commonN2, iosArm64, iosX64 sources" + ) + + assertTrue(noClass.isEmpty(), "There can't be any A page") + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/filter/DeprecationFilterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/filter/DeprecationFilterTest.kt new file mode 100644 index 00000000..75d82e9b --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/filter/DeprecationFilterTest.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package filter + +import org.jetbrains.dokka.DokkaDefaults +import org.jetbrains.dokka.PackageOptionsImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import kotlin.test.Test +import kotlin.test.assertTrue + +class DeprecationFilterTest : BaseAbstractTest() { + + @Test + fun `should skip hidden deprecated level regardless of skipDeprecated`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + classpath = listOfNotNull(jvmStdlibPath) + skipDeprecated = false + perPackageOptions = mutableListOf( + PackageOptionsImpl( + "example.*", + true, + false, + false, + false, + DokkaDefaults.documentedVisibilities + ) + ) + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |@Deprecated("dep", level = DeprecationLevel.HIDDEN) + |fun testFunction() { } + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.isEmpty() + ) + } + } + } + + @Test + fun `function with false global skipDeprecated`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + skipDeprecated = false + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |fun testFunction() { } + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.size == 1 + ) + } + } + } + + @Test + fun `deprecated function with false global skipDeprecated`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + skipDeprecated = false + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |@Deprecated("dep") + |fun testFunction() { } + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.size == 1 + ) + } + } + } + + @Test + fun `deprecated function with true global skipDeprecated`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + skipDeprecated = true + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |@Deprecated("dep") + |fun testFunction() { } + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.isEmpty() + ) + } + } + } + + @Test + fun `should skip deprecated companion object`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + skipDeprecated = true + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |class Test { + | @Deprecated("dep") + | companion object { + | fun method() {} + | } + |} + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().classlikes.first().classlikes.isEmpty() + ) + } + } + } + + @Test + fun `deprecated function with false global true package skipDeprecated`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + skipDeprecated = false + perPackageOptions = mutableListOf( + PackageOptionsImpl( + "example.*", + true, + false, + true, + false, + DokkaDefaults.documentedVisibilities + ) + ) + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |@Deprecated("dep") + |fun testFunction() { } + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.isEmpty() + ) + } + } + } + + @Test + fun `deprecated function with true global false package skipDeprecated`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + skipDeprecated = true + perPackageOptions = mutableListOf( + PackageOptionsImpl("example", + false, + false, + false, + false, + DokkaDefaults.documentedVisibilities + ) + ) + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |@Deprecated("dep") + |fun testFunction() { } + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.size == 1 + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/filter/EmptyPackagesFilterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/filter/EmptyPackagesFilterTest.kt new file mode 100644 index 00000000..c6c6b160 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/filter/EmptyPackagesFilterTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package filter + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class EmptyPackagesFilterTest : BaseAbstractTest() { + @Test + fun `empty package with false skipEmptyPackages`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + skipEmptyPackages = false + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.isNotEmpty() + ) + } + } + } + @Test + fun `empty package with true skipEmptyPackages`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + skipEmptyPackages = true + sourceRoots = listOf("src/main/kotlin") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + | class ThisShouldBePresent { } + |/src/main/kotlin/empty/TestEmpty.kt + |package empty + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + modules.forEach { module -> + assertEquals(listOf("example"), module.packages.map { it.name }) + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/filter/JavaFileFilterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/filter/JavaFileFilterTest.kt new file mode 100644 index 00000000..1c74c7ce --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/filter/JavaFileFilterTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package filter + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import kotlin.test.Test +import kotlin.test.assertTrue + +class JavaFileFilterTest : BaseAbstractTest() { + @Test + fun `java file should be included`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + skipEmptyPackages = false + sourceRoots = listOf("src/main/java/basic/Test.java") + } + } + } + + testInline( + """ + |/src/main/java/basic/Test.java + |package example; + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.isNotEmpty() + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/filter/JavaVisibilityFilterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/filter/JavaVisibilityFilterTest.kt new file mode 100644 index 00000000..b648f802 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/filter/JavaVisibilityFilterTest.kt @@ -0,0 +1,308 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package filter + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.DokkaDefaults +import org.jetbrains.dokka.PackageOptionsImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DModule +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import testApi.testRunner.dokkaConfiguration +import kotlin.test.Test +import kotlin.test.assertEquals + +class JavaVisibilityFilterTest : BaseAbstractTest() { + + @Test + fun `should document nothing private if no visibilities are included`() { + testVisibility( + """ + | public class JavaVisibilityTest { + | public String publicProperty = "publicProperty"; + | private String privateProperty = "privateProperty"; + | + | public void publicFunction() { } + | private void privateFunction() { } + | } + """.trimIndent(), + includedVisibility = DokkaDefaults.documentedVisibilities + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(1, it.size) + assertEquals("publicProperty", it[0].name) + } + clazz.functions.also { + assertEquals(1, it.size) + assertEquals("publicFunction", it[0].name) + } + } + } + + @Test + fun `should document private within public class`() { + testVisibility( + """ + | public class JavaVisibilityTest { + | public String publicProperty = "publicProperty"; + | protected String noise = "noise"; + | + | private String privateProperty = "privateProperty"; + | + | public void publicFunction() { } + | private void privateFunction() { } + | } + """.trimIndent(), + includedVisibility = setOf(DokkaConfiguration.Visibility.PUBLIC, DokkaConfiguration.Visibility.PRIVATE) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(2, it.size) + assertEquals("publicProperty", it[0].name) + assertEquals("privateProperty", it[1].name) + } + clazz.functions.also { + assertEquals(2, it.size) + assertEquals("publicFunction", it[0].name) + assertEquals("privateFunction", it[1].name) + } + } + } + + @Test + fun `should document package private within private class`() { + testVisibility( + """ + | public class JavaVisibilityTest { + | public String publicProperty = "publicProperty"; + | protected String noise = "noise"; + | + | String packagePrivateProperty = "packagePrivateProperty"; + | + | public void publicFunction() { } + | void packagePrivateFunction() { } + | } + """.trimIndent(), + includedVisibility = setOf(DokkaConfiguration.Visibility.PUBLIC, DokkaConfiguration.Visibility.PACKAGE) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(2, it.size) + assertEquals("publicProperty", it[0].name) + assertEquals("packagePrivateProperty", it[1].name) + } + clazz.functions.also { + assertEquals(2, it.size) + assertEquals("publicFunction", it[0].name) + assertEquals("packagePrivateFunction", it[1].name) + } + } + } + + @Test + fun `should document protected within public class`() { + testVisibility( + """ + | public class JavaVisibilityTest { + | public String publicProperty = "publicProperty"; + | String noise = "noise"; + | + | protected String protectedProperty = "protectedProperty"; + | + | public void publicFunction() { } + | protected void protectedFunction() { } + | } + """.trimIndent(), + includedVisibility = setOf(DokkaConfiguration.Visibility.PUBLIC, DokkaConfiguration.Visibility.PROTECTED) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(2, it.size) + assertEquals("publicProperty", it[0].name) + assertEquals("protectedProperty", it[1].name) + } + clazz.functions.also { + assertEquals(2, it.size) + assertEquals("publicFunction", it[0].name) + assertEquals("protectedFunction", it[1].name) + } + } + } + + @Test + fun `should include all visibilities`() { + testVisibility( + """ + | public class JavaVisibilityTest { + | public String publicProperty = "publicProperty"; + | private String privateProperty = "privateProperty"; + | String packagePrivateProperty = "packagePrivateProperty"; + | protected String protectedProperty = "protectedProperty"; + | + | public void publicFunction() { } + | private void privateFunction() { } + | void packagePrivateFunction() { } + | protected void protectedFunction() { } + | } + """.trimIndent(), + includedVisibility = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PRIVATE, + DokkaConfiguration.Visibility.PROTECTED, + DokkaConfiguration.Visibility.PACKAGE, + ) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(4, it.size) + assertEquals("publicProperty", it[0].name) + assertEquals("privateProperty", it[1].name) + assertEquals("packagePrivateProperty", it[2].name) + assertEquals("protectedProperty", it[3].name) + } + clazz.functions.also { + assertEquals(4, it.size) + assertEquals("publicFunction", it[0].name) + assertEquals("privateFunction", it[1].name) + assertEquals("packagePrivateFunction", it[2].name) + assertEquals("protectedFunction", it[3].name) + } + } + } + + private fun testVisibility(body: String, includedVisibility: Set<DokkaConfiguration.Visibility>, asserts: (List<DModule>) -> Unit) { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + documentedVisibilities = includedVisibility + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/java/basic/JavaVisibilityTest.java + |package example; + | + $body + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = asserts + } + } + + @ParameterizedTest + @MethodSource(value = ["nonPublicPermutations", "publicPermutations"]) + fun `includeNonPublic - should include package private java class`(configuration: ConfigurationWithVisibility) { + testInline( + """ + |/src/main/java/basic/VisibilityTest.java + |package basic; + | + |${configuration.visibilityKeyword} class VisibilityTest { + | static void test() { + | + | } + |} + """.trimMargin(), + configuration.configuration + ) { + preMergeDocumentablesTransformationStage = { + assertEquals(configuration.expectedClasslikes, it.first().packages.first().classlikes.size) + } + } + } + + data class ConfigurationWithVisibility( + val visibilityKeyword: String, + val configuration: DokkaConfigurationImpl, + val expectedClasslikes: Int + ) + + companion object TestDataSources { + @Suppress("DEPRECATION") // for includeNonPublic + val globalExcludes = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = false + sourceRoots = listOf("src/") + } + } + } + + @Suppress("DEPRECATION") // for includeNonPublic + val globalIncludes = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = true + sourceRoots = listOf("src/") + } + } + } + + @Suppress("DEPRECATION") // for includeNonPublic + val globalIncludesPackageExcludes = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = true + sourceRoots = listOf("src/") + perPackageOptions = mutableListOf( + PackageOptionsImpl( + "basic", + includeNonPublic = false, + reportUndocumented = false, + skipDeprecated = false, + suppress = false, + documentedVisibilities = DokkaDefaults.documentedVisibilities + ) + ) + } + } + } + + @Suppress("DEPRECATION") // for includeNonPublic + val globalExcludesPackageIncludes = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = false + sourceRoots = listOf("src/") + perPackageOptions = mutableListOf( + PackageOptionsImpl( + "basic", + true, + false, + false, + false, + DokkaDefaults.documentedVisibilities + ) + ) + } + } + } + + @JvmStatic + fun nonPublicPermutations() = listOf("protected", "", "private").flatMap { keyword -> + listOf(globalIncludes, globalExcludesPackageIncludes).map { configuration -> + ConfigurationWithVisibility(keyword, configuration, expectedClasslikes = 1) + } + listOf(globalExcludes, globalExcludes).map { configuration -> + ConfigurationWithVisibility(keyword, configuration, expectedClasslikes = 0) + } + } + + @JvmStatic + fun publicPermutations() = + listOf(globalIncludes, globalExcludesPackageIncludes, globalExcludes, globalExcludes).map { configuration -> + ConfigurationWithVisibility("public", configuration, expectedClasslikes = 1) + } + } +} + diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/filter/KotlinArrayDocumentableReplacerTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/filter/KotlinArrayDocumentableReplacerTest.kt new file mode 100644 index 00000000..240982c5 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/filter/KotlinArrayDocumentableReplacerTest.kt @@ -0,0 +1,211 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package filter + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.FunctionalTypeConstructor +import org.jetbrains.dokka.model.GenericTypeConstructor +import org.jetbrains.dokka.model.Invariance +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import utils.OnlyDescriptors + +class KotlinArrayDocumentableReplacerTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + @Test + fun `function with array type params`() { + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |fun testFunction(param1: Array<Int>, param2: Array<Boolean>, + | param3: Array<Float>, param4: Array<Double>, + | param5: Array<Long>, param6: Array<Short>, + | param7: Array<Char>, param8: Array<Byte>) { } + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + val params = it.firstOrNull()?.packages?.firstOrNull()?.functions?.firstOrNull()?.parameters + + val typeArrayNames = listOf("IntArray", "BooleanArray", "FloatArray", "DoubleArray", "LongArray", "ShortArray", + "CharArray", "ByteArray") + + assertEquals(typeArrayNames.size, params?.size) + params?.forEachIndexed{ i, param -> + assertEquals(GenericTypeConstructor(DRI("kotlin", typeArrayNames[i]), emptyList()), + param.type) + } + } + } + } + @Test + fun `function with specific parameters of array type`() { + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |fun testFunction(param1: Array<Array<Int>>, param2: (Array<Int>) -> Array<Int>) { } + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + val params = it.firstOrNull()?.packages?.firstOrNull()?.functions?.firstOrNull()?.parameters + assertEquals( + Invariance(GenericTypeConstructor(DRI("kotlin", "IntArray"), emptyList())), + (params?.firstOrNull()?.type as? GenericTypeConstructor)?.projections?.firstOrNull()) + assertEquals( + Invariance(GenericTypeConstructor(DRI("kotlin", "IntArray"), emptyList())), + (params?.get(1)?.type as? FunctionalTypeConstructor)?.projections?.get(0)) + assertEquals( + Invariance(GenericTypeConstructor(DRI("kotlin", "IntArray"), emptyList())), + (params?.get(1)?.type as? FunctionalTypeConstructor)?.projections?.get(1)) + } + } + } + @Test + fun `property with array type`() { + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |class MyTest { + | val isEmpty: Array<Boolean> + | get() = emptyList + | set(value) { + | field = value + | } + |} + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + val myTestClass = it.firstOrNull()?.packages?.firstOrNull()?.classlikes?.firstOrNull() + val property = myTestClass?.properties?.firstOrNull() + + assertEquals(GenericTypeConstructor(DRI("kotlin", "BooleanArray"), emptyList()), + property?.type) + assertEquals(GenericTypeConstructor(DRI("kotlin", "BooleanArray"), emptyList()), + property?.getter?.type) + assertEquals(GenericTypeConstructor(DRI("kotlin", "BooleanArray"), emptyList()), + property?.setter?.parameters?.firstOrNull()?.type) + } + } + } + @Test + fun `typealias with array type`() { + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |typealias arr = Array<Int> + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + val arrTypealias = it.firstOrNull()?.packages?.firstOrNull()?.typealiases?.firstOrNull() + + assertEquals(GenericTypeConstructor(DRI("kotlin", "IntArray"), emptyList()), + arrTypealias?.underlyingType?.values?.firstOrNull()) + } + } + } + + // Unreal case: Upper bound of a type parameter cannot be an array + @Test + fun `generic fun and class`() { + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |fun<T : Array<Int>> testFunction() { } + |class myTestClass<T : Array<Int>>{ } + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + val testFun = it.firstOrNull()?.packages?.firstOrNull()?.functions?.firstOrNull() + val myTestClass = it.firstOrNull()?.packages?.firstOrNull()?.classlikes?.firstOrNull() as? DClass + + assertEquals(GenericTypeConstructor(DRI("kotlin","IntArray"), emptyList()), + testFun?.generics?.firstOrNull()?.bounds?.firstOrNull()) + assertEquals(GenericTypeConstructor(DRI("kotlin","IntArray"), emptyList()), + myTestClass?.generics?.firstOrNull()?.bounds?.firstOrNull()) + } + } + } + + @OnlyDescriptors("Fix module.contentScope in new Standalone API") // TODO fix module.contentScope [getKtModuleForKtElement] + @Test + fun `no jvm source set`() { + val configurationWithNoJVM = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + analysisPlatform = "jvm" + } + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/TestJS.kt") + analysisPlatform = "js" + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |fun testFunction(param: Array<Int>) + | + | + |/src/main/kotlin/basic/TestJS.kt + |package example + | + |fun testFunction(param: Array<Int>) + """.trimMargin(), + configurationWithNoJVM + ) { + preMergeDocumentablesTransformationStage = { + val paramsJS = it[1].packages.firstOrNull()?.functions?.firstOrNull()?.parameters + assertNotEquals( + GenericTypeConstructor(DRI("kotlin", "IntArray"), emptyList()), + paramsJS?.firstOrNull()?.type) + + val paramsJVM = it.firstOrNull()?.packages?.firstOrNull()?.functions?.firstOrNull()?.parameters + assertEquals( + GenericTypeConstructor(DRI("kotlin", "IntArray"), emptyList()), + paramsJVM?.firstOrNull()?.type) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/filter/VisibilityFilterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/filter/VisibilityFilterTest.kt new file mode 100644 index 00000000..872e5865 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/filter/VisibilityFilterTest.kt @@ -0,0 +1,755 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package filter + +import org.jetbrains.dokka.DokkaConfiguration.Visibility +import org.jetbrains.dokka.DokkaDefaults +import org.jetbrains.dokka.PackageOptionsImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.DModule +import kotlin.test.* + +class VisibilityFilterTest : BaseAbstractTest() { + + @Test + fun `should document only public for defaults`() { + testVisibility( + """ + | val publicProperty: String = "publicProperty" + | private val privateProperty: String = "privateProperty" + | + | fun publicFun() { } + | private fun privateFun() { } + """.trimIndent(), + visibilities = DokkaDefaults.documentedVisibilities + ) { module -> + val pckg = module.first().packages.first() + pckg.properties.also { + assertEquals(1, it.size) + assertEquals("publicProperty", it[0].name) + } + pckg.functions.also { + assertEquals(1, it.size) + assertEquals("publicFun", it[0].name) + } + } + } + + @Test + fun `should document public`() { + testVisibility( + """ + | class TestClass<out V> { + | private var privateToThisVisibility: V? = null + | val publicProperty: String = "publicProperty" + | internal val noise: String = "noise" + | + | private val privateProperty: String = "privateProperty" + | + | fun publicFun() { } + | + | private fun privateFun() { } + | } + """.trimIndent(), + visibilities = setOf(Visibility.PUBLIC) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(1, it.size) + assertEquals("publicProperty", it[0].name) + } + clazz.functions.also { + assertEquals(1, it.size) + assertEquals("publicFun", it[0].name) + } + } + } + + @Test + fun `should document only private`() { + testVisibility( + """ + | public val noiseMember: String = "noise" + | internal fun noiseFun() { } + | class NoisePublicClass { } + | + | private val privateProperty: String = "privateProperty" + | private fun privateFun() { } + """.trimIndent(), + visibilities = setOf(Visibility.PRIVATE) + ) { module -> + val pckg = module.first().packages.first() + + assertTrue(pckg.classlikes.isEmpty()) + pckg.properties.also { + assertEquals(1, it.size) + assertEquals("privateProperty", it[0].name) + } + pckg.functions.also { + assertEquals(1, it.size) + assertEquals("privateFun", it[0].name) + } + } + } + + @Test + fun `should document only internal`() { + testVisibility( + """ + | public val noiseMember: String = "noise" + | private fun noiseFun() { } + | class NoisePublicClass { } + | + | internal val internalProperty: String = "privateProperty" + | internal fun internalFun() { } + """.trimIndent(), + visibilities = setOf(Visibility.INTERNAL) + ) { module -> + val pckg = module.first().packages.first() + + assertTrue(pckg.classlikes.isEmpty()) + pckg.properties.also { + assertEquals(1, it.size) + assertEquals("internalProperty", it[0].name) + } + pckg.functions.also { + assertEquals(1, it.size) + assertEquals("internalFun", it[0].name) + } + } + } + + @Test + fun `should document private within public class`() { + testVisibility( + """ + | class TestClass { + | val publicProperty: String = "publicProperty" + | internal val noise: String = "noise" + | + | private val privateProperty: String = "privateProperty" + | + | fun publicFun() { } + | + | private fun privateFun() { } + | } + """.trimIndent(), + visibilities = setOf(Visibility.PUBLIC, Visibility.PRIVATE) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(2, it.size) + assertEquals("publicProperty", it[0].name) + assertEquals("privateProperty", it[1].name) + } + clazz.functions.also { + assertEquals(2, it.size) + assertEquals("publicFun", it[0].name) + assertEquals("privateFun", it[1].name) + } + } + } + + @Test + fun `should document internal within public class`() { + testVisibility( + """ + | class TestClass { + | val publicProperty: String = "publicProperty" + | protected val noise: String = "noise" + | + | internal val internalProperty: String = "internalProperty" + | + | fun publicFun() { } + | + | internal fun internalFun() { } + | } + """.trimIndent(), + visibilities = setOf(Visibility.PUBLIC, Visibility.INTERNAL) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(2, it.size) + assertEquals("publicProperty", it[0].name) + assertEquals("internalProperty", it[1].name) + } + clazz.functions.also { + assertEquals(2, it.size) + assertEquals("publicFun", it[0].name) + assertEquals("internalFun", it[1].name) + } + } + } + + @Test + fun `should document protected within public class`() { + testVisibility( + """ + | class TestClass { + | val publicProperty: String = "publicProperty" + | internal val noise: String = "noise" + | + | protected val protectedProperty: String = "protectedProperty" + | + | fun publicFun() { } + | + | protected fun protectedFun() { } + | } + """.trimIndent(), + visibilities = setOf(Visibility.PUBLIC, Visibility.PROTECTED) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(2, it.size) + assertEquals("publicProperty", it[0].name) + assertEquals("protectedProperty", it[1].name) + } + clazz.functions.also { + assertEquals(2, it.size) + assertEquals("publicFun", it[0].name) + assertEquals("protectedFun", it[1].name) + } + } + } + + @Test + fun `should document all visibilities`() { + testVisibility( + """ + | class TestClass { + | val publicProperty: String = "publicProperty" + | + | private val privateProperty: String = "privateProperty" + | internal val internalProperty: String = "internalProperty" + | protected val protectedProperty: String = "protectedProperty" + | + | fun publicFun() { } + | + | private fun privateFun() { } + | internal fun internalFun() { } + | protected fun protectedFun() { } + | } + """.trimIndent(), + visibilities = setOf( + Visibility.PUBLIC, + Visibility.PRIVATE, + Visibility.PROTECTED, + Visibility.INTERNAL + ) + ) { module -> + val clazz = module.first().packages.first().classlikes.filterIsInstance<DClass>().first() + clazz.properties.also { + assertEquals(4, it.size) + assertEquals("publicProperty", it[0].name) + assertEquals("privateProperty", it[1].name) + assertEquals("internalProperty", it[2].name) + assertEquals("protectedProperty", it[3].name) + } + clazz.functions.also { + assertEquals(4, it.size) + assertEquals("publicFun", it[0].name) + assertEquals("privateFun", it[1].name) + assertEquals("internalFun", it[2].name) + assertEquals("protectedFun", it[3].name) + } + } + } + + @Test + fun `should ignore visibility settings for another package`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + perPackageOptions = listOf( + PackageOptionsImpl( + matchingRegex = "other", + documentedVisibilities = setOf(Visibility.PRIVATE), + includeNonPublic = false, + reportUndocumented = false, + skipDeprecated = false, + suppress = false + ) + ) + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + | fun publicFun() { } + | + | private fun privateFun() { } + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + val functions = it.first().packages.first().functions + assertEquals(1, functions.size) + assertEquals("publicFun", functions[0].name) + } + } + } + + @Test + fun `should choose package visibility settings over global`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + documentedVisibilities = setOf(Visibility.INTERNAL) + perPackageOptions = listOf( + PackageOptionsImpl( + matchingRegex = "example", + documentedVisibilities = setOf(Visibility.PRIVATE), + includeNonPublic = false, + reportUndocumented = false, + skipDeprecated = false, + suppress = false + ) + ) + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + | internal fun internalFun() { } + | + | private fun privateFun() { } + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + val functions = it.first().packages.first().functions + assertEquals(1, functions.size) + assertEquals("privateFun", functions[0].name) + } + } + } + + @Test + fun `private setter should be hidden if only PUBLIC is documented`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + documentedVisibilities = setOf(Visibility.PUBLIC) + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |var property: Int = 0 + |private set + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertNull( + it.first().packages.first().properties.first().setter + ) + } + } + } + + @Test + fun `should choose new documentedVisibilities over deprecated includeNonPublic`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = true + documentedVisibilities = setOf(Visibility.INTERNAL) + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + | internal fun internalFun() { } + | + | private fun privateFun() { } + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + val functions = it.first().packages.first().functions + assertEquals(1, functions.size) + assertEquals("internalFun", functions[0].name) + } + } + } + + @Test + fun `includeNonPublic - public function with false global`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = false + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |fun testFunction() { } + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.size == 1 + ) + } + } + } + + @Test + fun `includeNonPublic - private function with false global`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = false + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |private fun testFunction() { } + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.isEmpty() + ) + } + } + } + + @Test + fun `includeNonPublic - private function with true global`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + includeNonPublic = true + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |private fun testFunction() { } + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.size == 1 + ) + } + } + } + + @Test + fun `private setter with false global includeNonPublic`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = false + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |var property: Int = 0 + |private set + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertNull( + it.first().packages.first().properties.first().setter + ) + } + } + } + + @Test + fun `includeNonPublic - private function with false global true package`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + includeNonPublic = false + perPackageOptions = mutableListOf( + PackageOptionsImpl( + "example", + true, + false, + false, + false, + DokkaDefaults.documentedVisibilities + ) + ) + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |private fun testFunction() { } + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.size == 1 + ) + } + } + } + + @Test + fun `includeNonPublic - private function with true global false package`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + includeNonPublic = true + perPackageOptions = mutableListOf( + PackageOptionsImpl( + "example", + false, + false, + false, + false, + DokkaDefaults.documentedVisibilities + ) + ) + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |private fun testFunction() { } + | + | + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertTrue( + it.first().packages.first().functions.isEmpty() + ) + } + } + } + + @Test + fun `includeNonPublic - private typealias should be skipped`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = false + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + |private typealias ABC = Int + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = { + assertEquals(0, it.first().packages.first().typealiases.size) + } + } + } + + @Test + fun `includeNonPublic - internal property from enum should be skipped`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = false + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package enums + | + |enum class Test(internal val value: Int) { + | A(0) { + | override fun testFun(): Float = 0.05F + | }, + | B(1) { + | override fun testFun(): Float = 0.1F + | }; + | + | internal open fun testFun(): Float = 0.5F + |} + """.trimMargin(), + configuration + ) { + documentablesTransformationStage = { module -> + val enum = module.packages.flatMap { it.classlikes }.filterIsInstance<DEnum>().first() + val entry = enum.entries.first() + + assertFalse("testFun" in entry.functions.map { it.name }) + assertFalse("value" in entry.properties.map { it.name }) + assertFalse("testFun" in enum.functions.map { it.name }) + } + } + } + + @Test + fun `includeNonPublic - internal property from enum`() { + @Suppress("DEPRECATION") + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + includeNonPublic = true + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + classpath = listOfNotNull(jvmStdlibPath) + } + } + } + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package enums + | + |enum class Test(internal val value: Int) { + | A(0) { + | override fun testFun(): Float = 0.05F + | }, + | B(1) { + | override fun testFun(): Float = 0.1F + | }; + | + | internal open fun testFun(): Float = 0.5F + |} + """.trimMargin(), + configuration + ) { + documentablesTransformationStage = { module -> + val enum = module.packages.flatMap { it.classlikes }.filterIsInstance<DEnum>().first() + val entry = enum.entries.first() + + assertTrue("testFun" in entry.functions.map { it.name }) + assertTrue("value" in entry.properties.map { it.name }) + assertTrue("testFun" in enum.functions.map { it.name }) + } + } + } + + + private fun testVisibility( + body: String, + visibilities: Set<Visibility>, + asserts: (List<DModule>) -> Unit + ) { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + documentedVisibilities = visibilities + sourceRoots = listOf("src/main/kotlin/basic/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/basic/Test.kt + |package example + | + $body + | + """.trimMargin(), + configuration + ) { + preMergeDocumentablesTransformationStage = asserts + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/issues/IssuesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/issues/IssuesTest.kt new file mode 100644 index 00000000..007b01ff --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/issues/IssuesTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package issues + +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DFunction +import utils.AbstractModelTest +import utils.name +import kotlin.test.Test + +class IssuesTest : AbstractModelTest("/src/main/kotlin/issues/Test.kt", "issues") { + + @Test + fun errorClasses() { + inlineModelTest( + """ + |class Test(var value: String) { + | fun test(): List<String> = emptyList() + | fun brokenApply(v: String) = apply { value = v } + | + | fun brokenRun(v: String) = run { + | value = v + | this + | } + | + | fun brokenLet(v: String) = let { + | it.value = v + | it + | } + | + | fun brokenGenerics() = listOf("a", "b", "c") + | + | fun working(v: String) = doSomething() + | + | fun doSomething(): String = "Hello" + |} + """, + configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + } + } + } + ) { + with((this / "issues" / "Test").cast<DClass>()) { + (this / "working").cast<DFunction>().type.name equals "String" + (this / "doSomething").cast<DFunction>().type.name equals "String" + (this / "brokenGenerics").cast<DFunction>().type.name equals "List" + (this / "brokenApply").cast<DFunction>().type.name equals "Test" + (this / "brokenRun").cast<DFunction>().type.name equals "Test" + (this / "brokenLet").cast<DFunction>().type.name equals "Test" + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/linkableContent/LinkableContentTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/linkableContent/LinkableContentTest.kt new file mode 100644 index 00000000..1b73ffee --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/linkableContent/LinkableContentTest.kt @@ -0,0 +1,418 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package linkableContent + +import org.jetbrains.dokka.SourceLinkDefinitionImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.base.transformers.pages.sourcelinks.SourceLinksTransformer +import org.jetbrains.dokka.model.WithGenerics +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.pages.* +import org.jsoup.Jsoup +import utils.TestOutputWriterPlugin +import utils.assertNotNull +import java.net.URL +import java.nio.file.Paths +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import utils.OnlyDescriptorsMPP + +class LinkableContentTest : BaseAbstractTest() { + + @OnlyDescriptorsMPP("#3238") + @Test + fun `Include module and package documentation`() { + + val testDataDir = getTestDataDir("multiplatform/basicMultiplatformTest").toAbsolutePath() + val includesDir = getTestDataDir("linkable/includes").toAbsolutePath() + + val configuration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf(Paths.get("$testDataDir/commonMain/kotlin").toString()) + } + val jvmAndJsSecondCommonMain = sourceSet { + name = "jvmAndJsSecondCommonMain" + displayName = "jvmAndJsSecondCommonMain" + analysisPlatform = "common" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmAndJsSecondCommonMain/kotlin").toString()) + } + sourceSet { + name = "js" + displayName = "js" + analysisPlatform = "js" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jsMain/kotlin").toString()) + includes = listOf(Paths.get("$includesDir/include2.md").toString()) + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmMain/kotlin").toString()) + includes = listOf(Paths.get("$includesDir/include1.md").toString()) + } + } + } + + testFromData(configuration) { + documentablesMergingStage = { + assertEquals(2, it.documentation.size) + assertEquals(2, it.packages.size) + assertEquals(1, it.packages.first().documentation.size) + assertEquals(1, it.packages.last().documentation.size) + } + } + + } + + @Test + fun `Sources multiplatform class documentation`() { + + val testDataDir = getTestDataDir("linkable/sources").toAbsolutePath() + + val configuration = dokkaConfiguration { + moduleName = "example" + + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf(Paths.get("$testDataDir/commonMain/kotlin").toString()) + } + val jvmAndJsSecondCommonMain = sourceSet { + name = "jvmAndJsSecondCommonMain" + displayName = "jvmAndJsSecondCommonMain" + analysisPlatform = "common" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmAndJsSecondCommonMain/kotlin").toString()) + } + sourceSet { + name = "js" + displayName = "js" + analysisPlatform = "js" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jsMain/kotlin").toString()) + sourceLinks = listOf( + SourceLinkDefinitionImpl( + localDirectory = "$testDataDir/jsMain/kotlin", + remoteUrl = URL("https://github.com/user/repo/tree/master/src/jsMain/kotlin"), + remoteLineSuffix = "#L" + ) + ) + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmMain/kotlin").toString()) + sourceLinks = listOf( + SourceLinkDefinitionImpl( + localDirectory = "$testDataDir/jvmMain/kotlin", + remoteUrl = URL("https://github.com/user/repo/tree/master/src/jvmMain/kotlin"), + remoteLineSuffix = "#L" + ) + ) + } + } + } + + testFromData(configuration) { + renderingStage = { rootPageNode, dokkaContext -> + val newRoot = SourceLinksTransformer(dokkaContext).invoke(rootPageNode) + val moduleChildren = newRoot.children + assertEquals(1, moduleChildren.size) + val packageChildren = moduleChildren.first().children + assertEquals(2, packageChildren.size) + packageChildren.forEach { + val name = it.name.substringBefore("Class") + val signature = (it as? ClasslikePageNode)?.content?.dfs { it is ContentGroup && it.dci.kind == ContentKind.Symbol }.assertNotNull("signature") + val crl = signature.children.last().children[1] as? ContentResolvedLink + assertEquals( + "https://github.com/user/repo/tree/master/src/${name.toLowerCase()}Main/kotlin/${name}Class.kt#L7", + crl?.address + ) + } + } + } + } + + @OnlyDescriptorsMPP("#3238") + @Test + fun `Samples multiplatform documentation`() { + + val testDataDir = getTestDataDir("linkable/samples").toAbsolutePath() + + val configuration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf(Paths.get("$testDataDir/commonMain/kotlin").toString()) + } + val jvmAndJsSecondCommonMain = sourceSet { + name = "jvmAndJsSecondCommonMain" + displayName = "jvmAndJsSecondCommonMain" + analysisPlatform = "common" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmAndJsSecondCommonMain/kotlin").toString()) + } + sourceSet { + name = "js" + displayName = "js" + analysisPlatform = "js" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jsMain/kotlin").toString()) + samples = listOf("$testDataDir/jsMain/resources/Samples.kt") + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmMain/kotlin").toString()) + samples = listOf("$testDataDir/jvmMain/resources/Samples.kt") + } + } + } + + testFromData(configuration) { + renderingStage = { rootPageNode, _ -> + // TODO [beresnev] :((( +// val newRoot = DefaultSamplesTransformer(dokkaContext).invoke(rootPageNode) + val newRoot = rootPageNode + val moduleChildren = newRoot.children + assertEquals(1, moduleChildren.size) + val packageChildren = moduleChildren.first().children + assertEquals(2, packageChildren.size) + packageChildren.forEach { pageNode -> + val name = pageNode.name.substringBefore("Class") + val classChildren = pageNode.children + assertEquals(2, classChildren.size) + val function = classChildren.find { it.name == "printWithExclamation" } + val text = (function as MemberPageNode).content.let { it as ContentGroup }.children.last() + .let { it as ContentDivergentGroup }.children.single().after + .let { it as ContentGroup }.children.last() + .let { it as ContentGroup }.children.single() + .let { it as ContentCodeBlock }.children.single() + .let { it as ContentText }.text + assertEquals( + """|import p2.${name}Class + |fun main() { + | //sampleStart + | ${name}Class().printWithExclamation("Hi, $name") + | //sampleEnd + |}""".trimMargin(), + text + ) + } + } + } + } + + @Test + fun `Documenting return type for a function in inner class with generic parent`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |class Sample<S>(first: S){ + | inner class SampleInner { + | fun foo(): S = TODO() + | } + |} + | + """.trimIndent(), + dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "js" + } + } + } + ) { + renderingStage = { module, _ -> + val sample = module.children.single { it.name == "test" } + .children.single { it.name == "Sample" } as ClasslikePageNode + val foo = sample + .children + .single { it.name == "SampleInner" } + .let { it as ClasslikePageNode } + .children + .single { it.name == "foo" } + .let { it as MemberPageNode } + + val returnTypeNode = foo.content.dfs { + val link = (it as? ContentDRILink)?.children + val child = link?.first() as? ContentText + child?.text == "S" + } as? ContentDRILink + + assertEquals( + (sample.documentables.firstOrNull() as WithGenerics).generics.first().dri, + returnTypeNode?.address + ) + } + } + } + + @Test + fun `Include module and package documentation with codeblock`() { + + val testDataDir = getTestDataDir("multiplatform/basicMultiplatformTest").toAbsolutePath() + val includesDir = getTestDataDir("linkable/includes").toAbsolutePath() + + val configuration = dokkaConfiguration { + moduleName = "example" + sourceSets { + sourceSet { + analysisPlatform = "js" + sourceRoots = listOf("jsMain").map { + Paths.get("$testDataDir/$it/kotlin").toString() + } + name = "js" + includes = listOf(Paths.get("$includesDir/include2.md").toString()) + } + sourceSet { + analysisPlatform = "jvm" + sourceRoots = listOf("jvmMain").map { + Paths.get("$testDataDir/$it/kotlin").toString() + } + name = "jvm" + includes = listOf(Paths.get("$includesDir/include1.md").toString()) + } + } + } + + testFromData(configuration) { + documentablesMergingStage = { + assertNotEquals(null, it.packages.first().documentation.values.single().dfs { + (it as? Text)?.body?.contains("@SqlTable") ?: false + }) + } + } + + } + + @Test + fun `Include module with description parted in two files`() { + + val testDataDir = getTestDataDir("multiplatform/basicMultiplatformTest").toAbsolutePath() + val includesDir = getTestDataDir("linkable/includes").toAbsolutePath() + + val configuration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf(Paths.get("$testDataDir/commonMain/kotlin").toString()) + } + val jvmAndJsSecondCommonMain = sourceSet { + name = "jvmAndJsSecondCommonMain" + displayName = "jvmAndJsSecondCommonMain" + analysisPlatform = "common" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmAndJsSecondCommonMain/kotlin").toString()) + } + sourceSet { + name = "js" + displayName = "js" + analysisPlatform = "js" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jsMain/kotlin").toString()) + includes = listOf(Paths.get("$includesDir/include2.md").toString()) + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmMain/kotlin").toString()) + includes = listOf( + Paths.get("$includesDir/include1.md").toString(), + Paths.get("$includesDir/include11.md").toString() + ) + } + } + } + + testFromData(configuration) { + documentablesMergingStage = { module -> + val value = module.documentation.entries.single { + it.key.displayName == "jvm" + }.value + assertNotNull(value.dfs { + (it as? Text)?.body == "This is second JVM documentation for module example" + }) + + assertNotNull(value.dfs { + (it as? Text)?.body == "This is JVM documentation for module example" + }) + } + } + } + + @Test + fun `should have a correct link to declaration from another source set`() { + val writerPlugin = TestOutputWriterPlugin() + val configuration = dokkaConfiguration { + sourceSets { + val common = sourceSet { + sourceRoots = listOf("src/commonMain") + analysisPlatform = "common" + name = "common" + displayName = "common" + } + sourceSet { + sourceRoots = listOf("src/jvmMain/") + analysisPlatform = "jvm" + name = "jvm" + displayName = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID) + } + } + } + + testInline( + """ + /src/commonMain/main.kt + class A + /src/jvmMain/main.kt + /** + * link to [A] + */ + class B + """.trimIndent() + , + pluginOverrides = listOf(writerPlugin), + configuration = configuration + ) { + renderingStage = { _, _ -> + val page = + Jsoup.parse(writerPlugin.writer.contents.getValue("root/[root]/-b/index.html")) + val link = page.select(".paragraph a").single() + assertEquals("../-a/index.html", link.attr("href")) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/linking/EnumValuesLinkingTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/linking/EnumValuesLinkingTest.kt new file mode 100644 index 00000000..6dce09fc --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/linking/EnumValuesLinkingTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package linking + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRIExtraContainer +import org.jetbrains.dokka.links.EnumEntryDRIExtra +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.DocumentationLink +import org.jetbrains.dokka.pages.ContentDRILink +import org.jetbrains.dokka.pages.ContentPage +import org.jsoup.Jsoup +import utils.TestOutputWriterPlugin +import java.nio.file.Paths +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import utils.OnlyDescriptors + +class EnumValuesLinkingTest : BaseAbstractTest() { + + @OnlyDescriptors // TODO + @Test + fun `check if enum values are correctly linked`() { + val writerPlugin = TestOutputWriterPlugin() + val testDataDir = getTestDataDir("linking").toAbsolutePath() + testFromData( + dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf(Paths.get("$testDataDir/jvmMain/kotlin").toString()) + analysisPlatform = "jvm" + name = "jvm" + } + } + }, + pluginOverrides = listOf(writerPlugin) + ) { + documentablesTransformationStage = { + val classlikes = it.packages.single().children + assertEquals(4, classlikes.size) + + val javaLinker = classlikes.single { it.name == "JavaLinker" } + javaLinker.documentation.values.single().children.run { + when (val kotlinLink = this[0].children[1].children[1]) { + is DocumentationLink -> kotlinLink.dri.run { + assertEquals("KotlinEnum.ON_CREATE", this.classNames) + assertEquals(null, this.callable) + assertNotNull(DRIExtraContainer(extra)[EnumEntryDRIExtra]) + } + else -> throw AssertionError("Link node is not DocumentationLink type") + } + + when (val javaLink = this[0].children[2].children[1]) { + is DocumentationLink -> javaLink.dri.run { + assertEquals("JavaEnum.ON_DECEIT", this.classNames) + assertEquals(null, this.callable) + assertNotNull(DRIExtraContainer(extra)[EnumEntryDRIExtra]) + } + else -> throw AssertionError("Link node is not DocumentationLink type") + } + } + + val kotlinLinker = classlikes.single { it.name == "KotlinLinker" } + kotlinLinker.documentation.values.single().children.run { + when (val kotlinLink = this[0].children[0].children[5]) { + is DocumentationLink -> kotlinLink.dri.run { + assertEquals("KotlinEnum.ON_CREATE", this.classNames) + assertEquals(null, this.callable) + assertNotNull(DRIExtraContainer(extra)[EnumEntryDRIExtra]) + } + else -> throw AssertionError("Link node is not DocumentationLink type") + } + + when (val javaLink = this[0].children[0].children[9]) { + is DocumentationLink -> javaLink.dri.run { + assertEquals("JavaEnum.ON_DECEIT", this.classNames) + assertEquals(null, this.callable) + assertNotNull(DRIExtraContainer(extra)[EnumEntryDRIExtra]) + } + else -> throw AssertionError("Link node is not DocumentationLink type") + } + } + + assertEquals( + javaLinker.documentation.values.single().children[0].children[1].children[1].let { it as? DocumentationLink }?.dri, + kotlinLinker.documentation.values.single().children[0].children[0].children[5].let { it as? DocumentationLink }?.dri + ) + + assertEquals( + javaLinker.documentation.values.single().children[0].children[2].children[1].let { it as? DocumentationLink }?.dri, + kotlinLinker.documentation.values.single().children[0].children[0].children[9].let { it as? DocumentationLink }?.dri + ) + } + + renderingStage = { rootPageNode, _ -> + val classlikes = rootPageNode.children.single().children + assertEquals(4, classlikes.size) + + val javaLinker = classlikes.single { it.name == "JavaLinker" } + (javaLinker as ContentPage).run { + assertNotNull(content.dfs { it is ContentDRILink && it.address.classNames == "KotlinEnum.ON_CREATE" }) + assertNotNull(content.dfs { it is ContentDRILink && it.address.classNames == "JavaEnum.ON_DECEIT" }) + } + + val kotlinLinker = classlikes.single { it.name == "KotlinLinker" } + (kotlinLinker as ContentPage).run { + assertNotNull(content.dfs { it is ContentDRILink && it.address.classNames == "KotlinEnum.ON_CREATE" }) + assertNotNull(content.dfs { it is ContentDRILink && it.address.classNames == "JavaEnum.ON_DECEIT" }) + } + + Jsoup + .parse(writerPlugin.writer.contents.getValue("root/linking.source/-java-linker/index.html")) + .select("a[href=\"../-kotlin-enum/-o-n_-c-r-e-a-t-e/index.html\"]") + .assertOnlyOneElement() + + Jsoup + .parse(writerPlugin.writer.contents.getValue("root/linking.source/-java-linker/index.html")) + .select("a[href=\"../-java-enum/-o-n_-d-e-c-e-i-t/index.html\"]") + .assertOnlyOneElement() + + Jsoup + .parse(writerPlugin.writer.contents.getValue("root/linking.source/-kotlin-linker/index.html")) + .select("a[href=\"../-kotlin-enum/-o-n_-c-r-e-a-t-e/index.html\"]") + .assertOnlyOneElement() + + Jsoup + .parse(writerPlugin.writer.contents.getValue("root/linking.source/-kotlin-linker/index.html")) + .select("a[href=\"../-java-enum/-o-n_-d-e-c-e-i-t/index.html\"]") + .assertOnlyOneElement() + } + } + } + + private fun <T> List<T>.assertOnlyOneElement() { + if (isEmpty() || size > 1) { + throw AssertionError("Single element expected in list: $this") + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/AndroidExternalLocationProviderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/AndroidExternalLocationProviderTest.kt new file mode 100644 index 00000000..1d107947 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/AndroidExternalLocationProviderTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package locationProvider + +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.external.javadoc.AndroidExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.TypeConstructor +import org.jetbrains.dokka.plugability.DokkaContext +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class AndroidExternalLocationProviderTest : BaseAbstractTest() { + private val android = ExternalDocumentation( + URL("https://developer.android.com/reference/kotlin"), + PackageList( + RecognizedLinkFormat.DokkaHtml, + mapOf("" to setOf("android.content", "android.net")), + emptyMap(), + URL("file://not-used") + ) + ) + private val androidx = ExternalDocumentation( + URL("https://developer.android.com/reference/kotlin"), + PackageList( + RecognizedLinkFormat.DokkaHtml, + mapOf("" to setOf("androidx.appcompat.app")), + emptyMap(), + URL("file://not-used") + ) + ) + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath += jvmStdlibPath!! + } + } + } + + private fun getTestLocationProvider( + externalDocumentation: ExternalDocumentation, + context: DokkaContext? = null + ): DefaultExternalLocationProvider { + val dokkaContext = context ?: DokkaContext.create(configuration, logger, emptyList()) + return AndroidExternalLocationProvider(externalDocumentation, dokkaContext) + } + + @Test + fun `#1230 anchor to a method from AndroidX`() { + val locationProvider = getTestLocationProvider(androidx) + val dri = DRI( + "androidx.appcompat.app", + "AppCompatActivity", + Callable("findViewById", null, listOf(TypeConstructor("kotlin.Int", emptyList()))) + ) + + assertEquals( + "${androidx.documentationURL}/androidx/appcompat/app/AppCompatActivity.html#findviewbyid", + locationProvider.resolve(dri) + ) + } + + @Test + fun `anchor to a method from Android`() { + val locationProvider = getTestLocationProvider(android) + val dri = DRI( + "android.content", + "ContextWrapper", + Callable( + "checkCallingUriPermission", + null, + listOf( + TypeConstructor("android.net.Uri", emptyList()), + TypeConstructor("kotlin.Int", emptyList()) + ) + ) + ) + + assertEquals( + "${android.documentationURL}/android/content/ContextWrapper.html#checkcallinguripermission", + locationProvider.resolve(dri) + ) + } + + @Test + fun `should return null for method not in list`() { + val locationProvider = getTestLocationProvider(android) + val dri = DRI( + "foo", + "Bar", + Callable( + "baz", + null, + emptyList() + ) + ) + + assertEquals(null, locationProvider.resolve(dri)) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/DefaultExternalLocationProviderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/DefaultExternalLocationProviderTest.kt new file mode 100644 index 00000000..c4c3c1e4 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/DefaultExternalLocationProviderTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package locationProvider + +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.TypeConstructor +import org.jetbrains.dokka.plugability.DokkaContext +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class DefaultExternalLocationProviderTest : BaseAbstractTest() { + private val testDataDir = + getTestDataDir("locationProvider").toAbsolutePath().toString().removePrefix("/").let { "/$it" } + private val kotlinLang = "https://kotlinlang.org/api/latest/jvm/stdlib" + private val packageListURL = URL("file://$testDataDir/stdlib-package-list") + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath += jvmStdlibPath!! + } + } + } + + private fun getTestLocationProvider(context: DokkaContext? = null): DefaultExternalLocationProvider { + val dokkaContext = context ?: DokkaContext.create(configuration, logger, emptyList()) + val packageList = PackageList.load(packageListURL, 8, true)!! + val externalDocumentation = + ExternalDocumentation(URL(kotlinLang), packageList) + return DefaultExternalLocationProvider(externalDocumentation, ".html", dokkaContext) + } + + @Test + fun `ordinary link`() { + val locationProvider = getTestLocationProvider() + val dri = DRI("kotlin.reflect", "KVisibility") + + assertEquals("$kotlinLang/kotlin.reflect/-k-visibility/index.html", locationProvider.resolve(dri)) + } + + @Test + fun `relocation in package list`() { + val locationProvider = getTestLocationProvider() + val dri = DRI( + "", + "", + Callable( + "longArray", + null, + listOf( + TypeConstructor("kotlin.Int", emptyList()), + TypeConstructor("kotlin.Any", emptyList()) + ) + ) + ) + + assertEquals("$kotlinLang/kotlin-stdlib/[JS root]/long-array.html", locationProvider.resolve(dri)) + } + + @Test + fun `should return null for class not in list`() { + val locationProvider = getTestLocationProvider() + val dri = DRI( + "foo", + "Bar" + ) + + assertEquals(null, locationProvider.resolve(dri)) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/Dokka010ExternalLocationProviderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/Dokka010ExternalLocationProviderTest.kt new file mode 100644 index 00000000..338e7495 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/Dokka010ExternalLocationProviderTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package locationProvider + +import org.jetbrains.dokka.base.resolvers.external.Dokka010ExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.TypeConstructor +import org.jetbrains.dokka.plugability.DokkaContext +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class Dokka010ExternalLocationProviderTest : BaseAbstractTest() { + private val testDataDir = + getTestDataDir("locationProvider").toAbsolutePath().toString().removePrefix("/").let { "/$it" } + private val kotlinLang = "https://kotlinlang.org/api/latest/jvm/stdlib" + private val packageListURL = URL("file://$testDataDir/old-package-list") + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath += jvmStdlibPath!! + } + } + } + + private fun getTestLocationProvider(context: DokkaContext? = null): Dokka010ExternalLocationProvider { + val dokkaContext = context ?: DokkaContext.create(configuration, logger, emptyList()) + val packageList = PackageList.load(packageListURL, 8, true)!! + val externalDocumentation = + ExternalDocumentation(URL(kotlinLang), packageList) + return Dokka010ExternalLocationProvider(externalDocumentation, ".html", dokkaContext) + } + + @Test + fun `ordinary link`() { + val locationProvider = getTestLocationProvider() + val dri = DRI("kotlin.reflect", "KVisibility") + + assertEquals("$kotlinLang/kotlin.reflect/-k-visibility/index.html", locationProvider.resolve(dri)) + } + + @Test + fun `relocation in package list`() { + val locationProvider = getTestLocationProvider() + val dri = DRI("kotlin.text", "StringBuilder") + + assertEquals("$kotlinLang/kotlin.relocated.text/-string-builder/index.html", locationProvider.resolve(dri)) + } + + @Test + fun `method relocation in package list`() { + val locationProvider = getTestLocationProvider() + val dri = DRI( + "kotlin", + "", + Callable( + "minus", + null, + listOf( + TypeConstructor("java.math.BigDecimal", emptyList()), + TypeConstructor("java.math.BigDecimal", emptyList()) + ) + ) + ) + + assertEquals("$kotlinLang/kotlin/java.math.-big-decimal/minus.html", locationProvider.resolve(dri)) + } + + @Test + fun `#1268 companion part should be stripped`() { + val locationProvider = getTestLocationProvider() + val dri = DRI( + "kotlin", + "Int.Companion", + Callable( + "MIN_VALUE", + null, + emptyList() + ) + ) + + assertEquals("$kotlinLang/kotlin/-int/-m-i-n_-v-a-l-u-e.html", locationProvider.resolve(dri)) + } + + @Test + fun `companion part should be stripped in relocations`() { + val locationProvider = getTestLocationProvider() + val dri = DRI( + "kotlin", + "Int.Companion", + Callable( + "MAX_VALUE", + null, + emptyList() + ) + ) + + assertEquals("$kotlinLang/kotlin/-int/max-value.html", locationProvider.resolve(dri)) + } + + @Test + fun `should return null for method not in list`() { + val locationProvider = getTestLocationProvider() + val dri = DRI( + "foo", + "Bar", + Callable( + "baz", + null, + emptyList() + ) + ) + + assertEquals(null, locationProvider.resolve(dri)) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/DokkaLocationProviderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/DokkaLocationProviderTest.kt new file mode 100644 index 00000000..dce19f70 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/DokkaLocationProviderTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package locationProvider + +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import kotlin.test.Test +import kotlin.test.assertEquals + +class DokkaLocationProviderTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath += jvmStdlibPath!! + } + } + } + + private fun getTestLocationProvider(root: RootPageNode, context: DokkaContext? = null): DokkaLocationProvider { + val dokkaContext = context ?: DokkaContext.create(configuration, logger, emptyList()) + return DokkaLocationProvider(root, dokkaContext, ".html") + } + + @DslMarker + annotation class TestNavigationDSL + + @TestNavigationDSL + class NavigationDSL { + companion object { + private val stubDCI = DCI( + setOf( + DRI("kotlin", "Any") + ), + ContentKind.Comment + ) + val stubContentNode = ContentText("", stubDCI, emptySet()) + } + + operator fun invoke(name: String, fn: ModulesDsl.() -> Unit): RendererSpecificRootPage { + val modules = ModulesDsl().also { it.fn() } + return RendererSpecificRootPage(name = name, children = modules.pages, RenderingStrategy.DoNothing) + } + + @TestNavigationDSL + class ModulesDsl(val pages: MutableList<ModulePageNode> = mutableListOf()) { + fun modulePage(name: String, fn: PackageDsl.() -> Unit) { + val packages = PackageDsl().also { it.fn() } + pages.add( + ModulePageNode( + name = name, + children = packages.pages, + content = stubContentNode + ) + ) + } + } + + @TestNavigationDSL + class PackageDsl(val pages: MutableList<PackagePageNode> = mutableListOf()) { + fun packagePage(name: String, fn: ClassDsl.() -> Unit) { + val packages = ClassDsl().also { it.fn() } + pages.add( + PackagePageNode( + name = name, + children = packages.pages, + content = stubContentNode, + dri = emptySet() + ) + ) + } + } + + @TestNavigationDSL + class ClassDsl(val pages: MutableList<ClasslikePageNode> = mutableListOf()) { + fun classPage(name: String) { + pages.add( + ClasslikePageNode( + name = name, + children = emptyList(), + content = stubContentNode, + dri = emptySet() + ) + ) + } + } + } + + @Test + fun `links to a package with or without a class`() { + val root = NavigationDSL()("Root") { + modulePage("Module") { + packagePage("Package") {} + } + } + val packagePage = root.children.first().children.first() as PackagePageNode + val locationProvider = getTestLocationProvider(root) + val resolvedLink = locationProvider.resolve(packagePage) + val localToRoot = locationProvider.pathToRoot(packagePage) + + val rootWithClass = NavigationDSL()("Root") { + modulePage("Module") { + packagePage("Package") { + classPage("ClassA") + } + } + } + val packagePageWithClass = rootWithClass.children.first().children.first() as PackagePageNode + + val locationProviderWithClass = getTestLocationProvider(rootWithClass) + val localToRootWithClass = locationProviderWithClass.pathToRoot(packagePageWithClass) + val resolvedLinkWithClass = locationProviderWithClass.resolve(packagePageWithClass) + + assertEquals("-module/Package.html", resolvedLink) + assertEquals("../", localToRoot) + + assertEquals("-module/Package/index.html", resolvedLinkWithClass) + assertEquals("../../", localToRootWithClass) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/JavadocExternalLocationProviderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/JavadocExternalLocationProviderTest.kt new file mode 100644 index 00000000..1a747429 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/JavadocExternalLocationProviderTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package locationProvider + +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.external.javadoc.JavadocExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.DRIExtraContainer +import org.jetbrains.dokka.links.EnumEntryDRIExtra +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.plugability.DokkaContext +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class JavadocExternalLocationProviderTest : BaseAbstractTest() { + private val testDataDir = + getTestDataDir("locationProvider").toAbsolutePath().toString().removePrefix("/").let { "/$it" } + + private val jdk = "https://docs.oracle.com/javase/8/docs/api/" + private val jdkPackageListURL = URL("file://$testDataDir/jdk8-package-list") + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath += jvmStdlibPath!! + } + } + } + + private fun getTestLocationProvider(context: DokkaContext? = null): DefaultExternalLocationProvider { + val dokkaContext = context ?: DokkaContext.create(configuration, logger, emptyList()) + val packageList = PackageList.load(jdkPackageListURL, 8, true)!! + val externalDocumentation = + ExternalDocumentation(URL(jdk), packageList) + return JavadocExternalLocationProvider(externalDocumentation, "--", "-", dokkaContext) + } + + @Test + fun `link to enum entity of javadoc`() { + val locationProvider = getTestLocationProvider() + val ktDri = DRI( + "java.nio.file", + "StandardOpenOption.CREATE", + extra = DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode() + ) + val javaDri = DRI( + "java.nio.file", + "StandardOpenOption.CREATE", + null, + PointingToDeclaration, + DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode() + ) + + assertEquals( + "https://docs.oracle.com/javase/8/docs/api/java/nio/file/StandardOpenOption.html#CREATE", + locationProvider.resolve(ktDri) + ) + + assertEquals( + "https://docs.oracle.com/javase/8/docs/api/java/nio/file/StandardOpenOption.html#CREATE", + locationProvider.resolve(javaDri) + ) + } + + @Test + fun `link to nested class of javadoc`() { + val locationProvider = getTestLocationProvider() + val dri = DRI( + "java.rmi.activation", + "ActivationGroupDesc.CommandEnvironment" + ) + + assertEquals( + "https://docs.oracle.com/javase/8/docs/api/java/rmi/activation/ActivationGroupDesc.CommandEnvironment.html", + locationProvider.resolve(dri) + ) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/MultiModuleLinkingTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/MultiModuleLinkingTest.kt new file mode 100644 index 00000000..17327c4c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/MultiModuleLinkingTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package locationProvider + +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class MultiModuleLinkingTest : BaseAbstractTest() { + private val testDataDir = + getTestDataDir("locationProvider").toAbsolutePath().toString().removePrefix("/").let { "/$it" } + private val exampleDomain = "https://example.com" + private val packageListURL = URL("file://$testDataDir/multi-module-package-list") + private val kotlinLang = "https://kotlinlang.org/api/latest/jvm/stdlib" + private val stdlibPackageListURL = URL("file://$testDataDir/stdlib-package-list") + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath += jvmStdlibPath!! + } + } + } + + private fun getTestLocationProvider(context: DokkaContext? = null): DefaultExternalLocationProvider { + val dokkaContext = context ?: DokkaContext.create(configuration, logger, emptyList()) + val packageList = PackageList.load(packageListURL, 8, true)!! + val externalDocumentation = + ExternalDocumentation(URL(exampleDomain), packageList) + return DefaultExternalLocationProvider(externalDocumentation, ".html", dokkaContext) + } + + private fun getStdlibTestLocationProvider(context: DokkaContext? = null): DefaultExternalLocationProvider { + val dokkaContext = context ?: DokkaContext.create(configuration, logger, emptyList()) + val packageList = PackageList.load(stdlibPackageListURL, 8, true)!! + val externalDocumentation = + ExternalDocumentation(URL(kotlinLang), packageList) + return DefaultExternalLocationProvider(externalDocumentation, ".html", dokkaContext) + } + + @Test + fun `should link to a multi-module declaration`() { + val locationProvider = getTestLocationProvider() + val dri = DRI("baz", "BazClass") + + assertEquals("$exampleDomain/moduleB/baz/-baz-class/index.html", locationProvider.resolve(dri)) + } + + @Test + fun `should not fail on non-present package`() { + val stdlibLocationProvider = getStdlibTestLocationProvider() + val locationProvider = getTestLocationProvider() + val dri = DRI("baz", "BazClass") + + assertEquals(null, stdlibLocationProvider.resolve(dri)) + assertEquals("$exampleDomain/moduleB/baz/-baz-class/index.html", locationProvider.resolve(dri)) + } + + @Test + fun `should handle relocations`() { + val locationProvider = getTestLocationProvider() + val dri = DRI("", "NoPackageClass") + + assertEquals("$exampleDomain/moduleB/[root]/-no-package-class/index.html", locationProvider.resolve(dri)) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/markdown/KDocTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/KDocTest.kt new file mode 100644 index 00000000..89f58f1b --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/KDocTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package markdown + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DPackage +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.pages.ModulePageNode +import kotlin.test.assertEquals + +abstract class KDocTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/example/Test.kt") + } + } + } + + private fun interpolateKdoc(kdoc: String) = """ + |/src/main/kotlin/example/Test.kt + |package example + | /** + ${kdoc.split("\n").joinToString("") { "| *$it\n" } } + | */ + |class Test + """.trimMargin() + + private fun actualDocumentationNode(modulePageNode: ModulePageNode) = + (modulePageNode.documentables.firstOrNull()?.children?.first() as DPackage) + .classlikes.single() + .documentation.values.single() + + + protected fun executeTest(kdoc: String, expectedDocumentationNode: DocumentationNode) { + testInline( + interpolateKdoc(kdoc), + configuration + ) { + pagesGenerationStage = { + assertEquals( + expectedDocumentationNode, + actualDocumentationNode(it as ModulePageNode) + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt new file mode 100644 index 00000000..f783892f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package markdown + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.model.WithGenerics +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.pages.ClasslikePageNode +import org.jetbrains.dokka.pages.ContentDRILink +import org.jetbrains.dokka.pages.MemberPageNode +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class LinkTest : BaseAbstractTest() { + + @Test + fun linkToClassLoader() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/parser") + } + } + } + testInline( + """ + |/src/main/kotlin/parser/Test.kt + |package parser + | + | /** + | * Some docs that link to [ClassLoader.clearAssertionStatus] + | */ + |fun test(x: ClassLoader) = x.clearAssertionStatus() + | + """.trimMargin(), + configuration + ) { + renderingStage = { rootPageNode, _ -> + assertNotNull((rootPageNode.children.single().children.single() as MemberPageNode) + .content + .dfs { node -> + node is ContentDRILink && + node.address.toString() == "parser//test/#java.lang.ClassLoader/PointingToDeclaration/" + } + ) + } + } + } + + @Test + fun returnTypeShouldHaveLinkToOuterClassFromInner() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + displayName = "JVM" + } + } + } + //This does not contain a package to check for situation when the package has to be artificially generated + testInline( + """ + |/src/main/kotlin/parser/Test.kt + | + |class Outer<OUTER> { + | inner class Inner<INNER> { + | fun foo(): OUTER = TODO() + | } + |} + """.trimMargin(), + configuration + ) { + renderingStage = { rootPageNode, _ -> + val root = rootPageNode.children.single().children.single() as ClasslikePageNode + val innerClass = root.children.first { it is ClasslikePageNode } + val foo = innerClass.children.first { it.name == "foo" } as MemberPageNode + val destinationDri = (root.documentables.firstOrNull() as WithGenerics).generics.first().dri.toString() + + assertEquals(destinationDri, "/Outer///PointingToGenericParameters(0)/") + assertNotNull(foo.content.dfs { it is ContentDRILink && it.address.toString() == destinationDri }) + } + } + } + + @Test + fun `link to parameter #238`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package example + | + |/** + |* Link to [waitAMinute] + |*/ + |fun stop(hammerTime: String, waitAMinute: String) {} + | + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val parameter = module.dfs { it.name == "waitAMinute" } + val link = module.dfs { it.name == "stop" }!!.documentation.values.single() + .dfs { it is DocumentationLink } as DocumentationLink + + assertEquals(parameter!!.dri, link.dri) + } + } + } + + @Test + fun `link with exclamation mark`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package example + | + |/** + |* Link to ![waitAMinute] + |*/ + |fun stop(hammerTime: String, waitAMinute: String) {} + | + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val functionDocs = module.packages.flatMap { it.functions }.first().documentation.values.first() + val expected = Description( + root = CustomDocTag( + children = listOf( + P( + children = listOf( + Text("Link to !"), + DocumentationLink( + dri = DRI( + packageName = "example", + callable = Callable( + "stop", + receiver = null, + params = listOf( + TypeConstructor("kotlin.String", emptyList()), + TypeConstructor("kotlin.String", emptyList()) + ) + ), + target = PointingToCallableParameters(1) + ), + children = listOf( + Text("waitAMinute") + ), + params = mapOf("href" to "[waitAMinute]") + ) + ) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expected, functionDocs.children.first()) + } + } + } + + @Test + fun `link to property with exclamation mark`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + testInline( + """ + |/src/main/kotlin/Testing.kt + |package example + | + |/** + |* Link to ![Testing.property] + |*/ + |class Testing { + | var property = "" + |} + | + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val functionDocs = module.packages.flatMap { it.classlikes }.first().documentation.values.first() + val expected = Description( + root = CustomDocTag( + children = listOf( + P( + children = listOf( + Text("Link to !"), + DocumentationLink( + dri = DRI( + packageName = "example", + classNames = "Testing", + callable = Callable("property", null, emptyList()), + target = PointingToDeclaration + ), + children = listOf( + Text("Testing.property") + ), + params = mapOf("href" to "[Testing.property]") + ) + ) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expected, functionDocs.children.first()) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/markdown/ParserTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/ParserTest.kt new file mode 100644 index 00000000..bcca27c4 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/ParserTest.kt @@ -0,0 +1,1633 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.tests + +import markdown.KDocTest + +import org.jetbrains.dokka.analysis.markdown.jb.MARKDOWN_ELEMENT_FILE_NAME +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.model.doc.* +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + + +class ParserTest : KDocTest() { + + private fun parseMarkdownToDocNode(text: String) = + MarkdownParser( { null }, "").parseStringToDocNode(text) + + @Test + fun `Simple text`() { + val kdoc = """ + | This is simple test of string + | Next line + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf(P(listOf(Text("This is simple test of string Next line")))), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Simple text with new line`() { + val kdoc = """ + | This is simple test of string\ + | Next line + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text("This is simple test of string"), + Br, + Text("Next line") + ) + ) + ), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Text with Bold and Emphasis decorators`() { + val kdoc = """ + | This is **simple** test of _string_ + | Next **_line_** + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text("This is "), + B(listOf(Text("simple"))), + Text(" test of "), + I(listOf(Text("string"))), + Text(" Next "), + B(listOf(I(listOf(Text("line"))))) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Text with Colon`() { + val kdoc = """ + | This is simple text with: colon! + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf(P(listOf(Text("This is simple text with: colon!")))), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Multilined text`() { + val kdoc = """ + | Text + | and + | String + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf(P(listOf(Text("Text and String")))), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Paragraphs`() { + val kdoc = """ + | Paragraph number + | one + | + | Paragraph\ + | number two + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P(listOf(Text("Paragraph number one"))), + P(listOf(Text("Paragraph"), Br, Text("number two"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Emphasis with star`() { + val kdoc = " *text*" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf(P(listOf(I(listOf(Text("text")))))), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Underscores that are not Emphasis`() { + val kdoc = "text_with_underscores" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf(P(listOf(Text("text_with_underscores")))), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Emphasis with underscores`() { + val kdoc = "_text_" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf(P(listOf(I(listOf(Text("text")))))), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Stars as italic bounds`() { + val kdoc = "The abstract syntax tree node for a multiplying expression. A multiplying\n" + + "expression is a binary expression where the operator is a multiplying operator\n" + + "such as \"*\", \"/\", or \"mod\". A simple example would be \"5*x\"." + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text( + "The abstract syntax tree node for a multiplying expression. A multiplying " + + "expression is a binary expression where the operator is a multiplying operator " + + "such as \"" + ), + I(listOf(Text("\", \"/\", or \"mod\". A simple example would be \"5"))), + Text("x\".") + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Stars as bold bounds`() { + val kdoc = "The abstract syntax tree node for a multiplying expression. A multiplying\n" + + "expression is a binary expression where the operator is a multiplying operator\n" + + "such as \"**\", \"/\", or \"mod\". A simple example would be \"5**x\"." + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text( + "The abstract syntax tree node for a multiplying expression. A multiplying " + + "expression is a binary expression where the operator is a multiplying operator " + + "such as \"" + ), + B(listOf(Text("\", \"/\", or \"mod\". A simple example would be \"5"))), + Text("x\".") + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Embedded star`() { + val kdoc = "Embedded*Star" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P(listOf(Text("Embedded*Star"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + + @Test + fun `Unordered list`() { + val kdoc = """ + | * list item 1 + | * list item 2 + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ul( + listOf( + Li(listOf(P(listOf(Text("list item 1"))))), + Li(listOf(P(listOf(Text("list item 2"))))) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Unordered list with multilines`() { + val kdoc = """ + | * list item 1 + | continue 1 + | * list item 2\ + | continue 2 + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ul( + listOf( + Li(listOf(P(listOf(Text("list item 1 continue 1"))))), + Li(listOf(P(listOf(Text("list item 2"), Br, Text("continue 2"))))) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Unordered list with Bold`() { + val kdoc = """ + | * list **item** 1 + | continue 1 + | * list __item__ 2 + | continue 2 + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ul( + listOf( + Li( + listOf( + P( + listOf( + Text("list "), + B(listOf(Text("item"))), + Text(" 1 continue 1") + ) + ) + ) + ), + Li( + listOf( + P( + listOf( + Text("list "), + B(listOf(Text("item"))), + Text(" 2 continue 2") + ) + ) + ) + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Unordered list with nested bullets`() { + val kdoc = """ + | * Outer first + | Outer next line + | * Outer second + | - Middle first + | Middle next line + | - Middle second + | + Inner first + | Inner next line + | - Middle third + | * Outer third + | + | New paragraph""".trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ul( + listOf( + Li(listOf(P(listOf(Text("Outer first Outer next line"))))), + Li(listOf(P(listOf(Text("Outer second"))))), + Ul( + listOf( + Li(listOf(P(listOf(Text("Middle first Middle next line"))))), + Li(listOf(P(listOf(Text("Middle second"))))), + Ul( + listOf( + Li(listOf(P(listOf(Text("Inner first Inner next line"))))) + ) + ), + Li(listOf(P(listOf(Text("Middle third"))))) + ) + ), + Li(listOf(P(listOf(Text("Outer third"))))) + ) + ), + P(listOf(Text("New paragraph"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Ordered list`() { + val kdoc = """ + | 1. list item 1 + | 2. list item 2 + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ol( + listOf( + Li(listOf(P(listOf(Text("list item 1"))))), + Li(listOf(P(listOf(Text("list item 2"))))) + ), + mapOf("start" to "1") + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + + @Test + fun `Ordered list beginning from other number`() { + val kdoc = """ + | 9. list item 1 + | 12. list item 2 + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ol( + listOf( + Li(listOf(P(listOf(Text("list item 1"))))), + Li(listOf(P(listOf(Text("list item 2"))))) + ), + mapOf("start" to "9") + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Ordered list with multilines`() { + val kdoc = """ + | 2. list item 1 + | continue 1 + | 3. list item 2 + | continue 2 + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ol( + listOf( + Li(listOf(P(listOf(Text("list item 1 continue 1"))))), + Li(listOf(P(listOf(Text("list item 2 continue 2"))))) + ), + mapOf("start" to "2") + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Ordered list with Bold`() { + val kdoc = """ + | 1. list **item** 1 + | continue 1 + | 2. list __item__ 2 + | continue 2 + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ol( + listOf( + Li( + listOf( + P( + listOf( + Text("list "), + B(listOf(Text("item"))), + Text(" 1 continue 1") + ) + ) + ) + ), + Li( + listOf( + P( + listOf( + Text("list "), + B(listOf(Text("item"))), + Text(" 2 continue 2") + ) + ) + ) + ) + ), + mapOf("start" to "1") + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Ordered list with nested bullets`() { + val kdoc = """ + | 1. Outer first + | Outer next line + | 2. Outer second + | 1. Middle first + | Middle next line + | 2. Middle second + | 1. Inner first + | Inner next line + | 5. Middle third + | 4. Outer third + | + | New paragraph""".trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ol( + listOf( + Li(listOf(P(listOf(Text("Outer first Outer next line"))))), + Li(listOf(P(listOf(Text("Outer second"))))), + Ol( + listOf( + Li(listOf(P(listOf(Text("Middle first Middle next line"))))), + Li(listOf(P(listOf(Text("Middle second"))))), + Ol( + listOf( + Li(listOf(P(listOf(Text("Inner first Inner next line"))))) + ), + mapOf("start" to "1") + ), + Li(listOf(P(listOf(Text("Middle third"))))) + ), + mapOf("start" to "1") + ), + Li(listOf(P(listOf(Text("Outer third"))))) + ), + mapOf("start" to "1") + ), + P(listOf(Text("New paragraph"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Ordered nested in Unordered nested in Ordered list`() { + val kdoc = """ + | 1. Outer first + | Outer next line + | 2. Outer second + | + Middle first + | Middle next line + | + Middle second + | 1. Inner first + | Inner next line + | + Middle third + | 4. Outer third + | + | New paragraph""".trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Ol( + listOf( + Li(listOf(P(listOf(Text("Outer first Outer next line"))))), + Li(listOf(P(listOf(Text("Outer second"))))), + Ul( + listOf( + Li(listOf(P(listOf(Text("Middle first Middle next line"))))), + Li(listOf(P(listOf(Text("Middle second"))))), + Ol( + listOf( + Li(listOf(P(listOf(Text("Inner first Inner next line"))))) + ), + mapOf("start" to "1") + ), + Li(listOf(P(listOf(Text("Middle third"))))) + ) + ), + Li(listOf(P(listOf(Text("Outer third"))))) + ), + mapOf("start" to "1") + ), + P(listOf(Text("New paragraph"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Header and two paragraphs`() { + val kdoc = """ + | # Header 1 + | Following text + | + | New paragraph + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + H1(listOf(Text("Header 1"))), + P(listOf(Text("Following text"))), + P(listOf(Text("New paragraph"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Ignore //TODO: ATX_2 to ATX_6 and sometimes ATX_1 from jetbrains parser consumes white space. Need to handle it in their library + @Test + fun `All headers`() { + val kdoc = """ + | # Header 1 + | Text 1 + | ## Header 2 + | Text 2 + | ### Header 3 + | Text 3 + | #### Header 4 + | Text 4 + | ##### Header 5 + | Text 5 + | ###### Header 6 + | Text 6 + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + H1(listOf(Text("Header 1"))), + P(listOf(Text("Text 1"))), + H2(listOf(Text("Header 2"))), + P(listOf(Text("Text 2"))), + H3(listOf(Text("Header 3"))), + P(listOf(Text("Text 3"))), + H4(listOf(Text("Header 4"))), + P(listOf(Text("Text 4"))), + H5(listOf(Text("Header 5"))), + P(listOf(Text("Text 5"))), + H6(listOf(Text("Header 6"))), + P(listOf(Text("Text 6"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Bold New Line Bold`() { + val kdoc = """ + | **line 1**\ + | **line 2** + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + B(listOf(Text("line 1"))), + Br, + B(listOf(Text("line 2"))) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Horizontal rule`() { + val kdoc = """ + | *** + | text 1 + | ___ + | text 2 + | *** + | text 3 + | ___ + | text 4 + | *** + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + HorizontalRule, + P(listOf(Text("text 1"))), + HorizontalRule, + P(listOf(Text("text 2"))), + HorizontalRule, + P(listOf(Text("text 3"))), + HorizontalRule, + P(listOf(Text("text 4"))), + HorizontalRule + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Blockquote`() { + val kdoc = """ + | > Blockquotes are very handy in email to emulate reply text. + | > This line is part of the same quote. + | + | Quote break. + | + | > Quote + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + BlockQuote( + listOf( + P( + listOf( + Text("Blockquotes are very handy in email to emulate reply text. This line is part of the same quote.") + ) + ) + ) + ), + P(listOf(Text("Quote break."))), + BlockQuote( + listOf( + P(listOf(Text("Quote"))) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + + @Test + fun `Blockquote nested`() { + val kdoc = """ + | > text 1 + | > text 2 + | >> text 3 + | >> text 4 + | > + | > text 5 + | + | Quote break. + | + | > Quote + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + BlockQuote( + listOf( + P(listOf(Text("text 1 text 2"))), + BlockQuote( + listOf( + P(listOf(Text("text 3 text 4"))) + ) + ), + P(listOf(Text("text 5"))) + ) + ), + P(listOf(Text("Quote break."))), + BlockQuote( + listOf( + P(listOf(Text("Quote"))) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Ignore //TODO: Again ATX_1 consumes white space + @Test + fun `Blockquote nested with fancy text enhancement`() { + val kdoc = """ + | > text **1** + | > text 2 + | >> # text 3 + | >> * text 4 + | >> * text 5 + | > + | > text 6 + | + | Quote break. + | + | > Quote + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + BlockQuote( + listOf( + P( + listOf( + Text("text "), + B(listOf(Text("1"))), + Text("\ntext 2") + ) + ), + BlockQuote( + listOf( + H1(listOf(Text("text 3"))), + Ul( + listOf( + Li(listOf(P(listOf(Text("text 4"))))), + Ul( + listOf( + Li(listOf(P(listOf(Text("text 5"))))) + ) + ) + ) + ) + ) + ), + P(listOf(Text("text 6"))) + ) + ), + P(listOf(Text("Quote break."))), + BlockQuote( + listOf( + P(listOf(Text("Quote"))) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Simple Code Block`() { + val kdoc = """ + | `Some code` + | Sample text + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + CodeInline(listOf(Text("Some code"))), + Text(" Sample text") + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Multilined Code Block`() { + val kdoc = """ + | ```kotlin + | @Suppress("UNUSED_VARIABLE") + | val x: Int = 0 + | val y: String = "Text" + | + | val z: Boolean = true + | for(i in 0..10) { + | println(i) + | } + | ``` + | Sample text + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + CodeBlock( + listOf( + Text("@Suppress(\"UNUSED_VARIABLE\")"), Br, + Text("val x: Int = 0"), Br, + Text("val y: String = \"Text\""), Br, Br, + Text(" val z: Boolean = true"), Br, + Text("for(i in 0..10) {"), Br, + Text(" println(i)"), Br, + Text("}") + ), + mapOf("lang" to "kotlin") + ), + P(listOf(Text("Sample text"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + + @Test + fun `Inline link`() { + val kdoc = """ + | [I'm an inline-style link](https://www.google.com) + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + A( + listOf(Text("I'm an inline-style link")), + mapOf("href" to "https://www.google.com") + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Inline link with title`() { + val kdoc = """ + | [I'm an inline-style link with title](https://www.google.com "Google's Homepage") + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + A( + listOf(Text("I'm an inline-style link with title")), + mapOf("href" to "https://www.google.com", "title" to "Google's Homepage") + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Full reference link`() { + val kdoc = """ + | [I'm a reference-style link][Arbitrary case-insensitive reference text] + | + | [arbitrary case-insensitive reference text]: https://www.mozilla.org + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + A( + listOf(Text("I'm a reference-style link")), + mapOf("href" to "https://www.mozilla.org") + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Full reference link with number`() { + val kdoc = """ + | [You can use numbers for reference-style link definitions][1] + | + | [1]: http://slashdot.org + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + A( + listOf(Text("You can use numbers for reference-style link definitions")), + mapOf("href" to "http://slashdot.org") + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Short reference link`() { + val kdoc = """ + | Or leave it empty and use the [link text itself]. + | + | [link text itself]: http://www.reddit.com + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text("Or leave it empty and use the "), + A( + listOf(Text("link text itself")), + mapOf("href" to "http://www.reddit.com") + ), + Text(".") + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Autolink`() { + val kdoc = """ + | URLs and URLs in angle brackets will automatically get turned into links. + | http://www.example.com or <http://www.example.com> and sometimes + | example.com (but not on Github, for example). + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text("URLs and URLs in angle brackets will automatically get turned into links. http://www.example.com or "), + A( + listOf(Text("http://www.example.com")), + mapOf("href" to "http://www.example.com") + ), + Text(" and sometimes example.com (but not on Github, for example).") + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Various links`() { + val kdoc = """ + | [I'm an inline-style link](https://www.google.com) + | + | [I'm an inline-style link with title](https://www.google.com "Google's Homepage") + | + | [I'm a reference-style link][Arbitrary case-insensitive reference text] + | + | [You can use numbers for reference-style link definitions][1] + | + | Or leave it empty and use the [link text itself]. + | + | URLs and URLs in angle brackets will automatically get turned into links. + | http://www.example.com or <http://www.example.com> and sometimes + | example.com (but not on Github, for example). + | + | Some text to show that the reference links can follow later. + | + | [arbitrary case-insensitive reference text]: https://www.mozilla.org + | [1]: http://slashdot.org + | [link text itself]: http://www.reddit.com + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + A( + listOf(Text("I'm an inline-style link")), + mapOf("href" to "https://www.google.com") + ) + ) + ), + P( + listOf( + A( + listOf(Text("I'm an inline-style link with title")), + mapOf("href" to "https://www.google.com", "title" to "Google's Homepage") + ) + ) + ), + P( + listOf( + A( + listOf(Text("I'm a reference-style link")), + mapOf("href" to "https://www.mozilla.org") + ) + ) + ), + P( + listOf( + A( + listOf(Text("You can use numbers for reference-style link definitions")), + mapOf("href" to "http://slashdot.org") + ) + ) + ), + P( + listOf( + Text("Or leave it empty and use the "), + A( + listOf(Text("link text itself")), + mapOf("href" to "http://www.reddit.com") + ), + Text(".") + ) + ), + P( + listOf( + Text("URLs and URLs in angle brackets will automatically get turned into links. http://www.example.com or "), + A( + listOf(Text("http://www.example.com")), + mapOf("href" to "http://www.example.com") + ), + Text(" and sometimes example.com (but not on Github, for example).") + ) + ), + P(listOf(Text("Some text to show that the reference links can follow later."))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Windows Carriage Return Line Feed`() { + val kdoc = "text\r\ntext" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text("text text") + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun image() { + val kdoc = "![Sample image](https://www.google.pl/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png)" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Img( + emptyList(), + mapOf( + "href" to "https://www.google.pl/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png", + "alt" to "Sample image" + ) + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Bold + italic + link`() { + val kdoc = "It's very easy to make some words **bold** and other words *italic* with Markdown.\n" + + "You can even [link to Google!](http://google.com)" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text("It's very easy to make some words "), + B(listOf(Text("bold"))), + Text(" and other words "), + I(listOf(Text("italic"))), + Text(" with Markdown. You can even "), + A(listOf(Text("link to Google!")), mapOf("href" to "http://google.com")) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Codeblock from indent`() { + val kdoc = "Here is some example how to use conditional instructions:\n\n" + + " val x = 1\n" + + " val y = 2\n" + + " if (x == 1) {\n" + + " println(y)\n" + + " }" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P(listOf(Text("Here is some example how to use conditional instructions:"))), + CodeBlock( + listOf( + Text( + "val x = 1\n" + + "val y = 2\n" + + "if (x == 1) {\n" + + " println(y)\n" + + "}" + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Table`() { + val kdoc = "First Header | Second Header\n" + + "------------ | -------------\n" + + "Content from cell 1 | Content from cell 2\n" + + "Content in the first column | Content in the second column" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + Table( + listOf( + Th( + listOf( + Td( + listOf( + Text("First Header") + ) + ), + Td( + listOf( + Text("Second Header") + ) + ) + ) + ), + Tr( + listOf( + Td( + listOf( + Text("Content from cell 1") + ) + ), + Td( + listOf( + Text("Content from cell 2") + ) + ) + ) + ), + Tr( + listOf( + Td( + listOf( + Text("Content in the first column") + ) + ), + Td( + listOf( + Text("Content in the second column") + ) + ) + ) + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `Text with Strikethrough`() { + val kdoc = """ + | This is ~~strikethroughed~~ + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text("This is "), + Strikethrough(listOf(Text("strikethroughed"))) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `short link without destination`() { + val kdoc = """ + | This is [link]() + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + Text("This is "), + A( + listOf(Text("link")), + mapOf("href" to "") + ) + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + + @Test + fun `exception thrown by empty header should point to location of a file`() { + val kdoc = """ + | ### + """.trimMargin() + val expectedDocumentationNode = DocumentationNode(emptyList()) + val exception = runCatching { executeTest(kdoc, expectedDocumentationNode) }.exceptionOrNull() + + val expectedMessage = "Wrong AST Tree. Header does not contain expected content in Test.kt/example.Test, element starts from offset 0 and ends 3: ###" + assertTrue( + exception?.message == expectedMessage + || /* for K2 */ exception?.cause?.cause?.message == expectedMessage + ) + } + + @Test + fun `should ignore html comments`() { + val kdoc = """ + | # Example <!--- not visible in header --> Kdoc + | <!-- not visible alone --> + | Pre <!--- not visible --> visible + """.trimMargin() + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + H1( + listOf( + Text("Example "), + Text("<!--- not visible in header -->", params = mapOf("content-type" to "html")), + Text(" Kdoc") + ) + ), + Text("<!-- not visible alone -->", params = mapOf("content-type" to "html")), + P( + listOf( + Text("Pre "), + Text("<!--- not visible -->", params = mapOf("content-type" to "html")), + Text(" visible") + ) + ) + ), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `code with backticks`() { + val kdoc = "` `` ` ` ``` `" + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + P( + listOf( + CodeInline(listOf(Text("`` "))), + Text(" "), + CodeInline(listOf(Text("``` "))), + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + executeTest(kdoc, expectedDocumentationNode) + } + + @Test + fun `should filter spaces in markdown`() { + val markdown = """ + | sdsdds f,*()hhh + | dssd hf + | + | sdsdsds sdd + | + | + | eweww + | + | + | + """.trimMargin() + val actualDocumentationNode = parseMarkdownToDocNode(markdown).children + val expectedDocumentationNode = listOf( + P(listOf(Text(" sdsdds f,*()hhh dssd hf"))), + P(listOf(Text(" sdsdsds sdd"))), + P(listOf(Text(" eweww "))) + ) + assertEquals(actualDocumentationNode, expectedDocumentationNode) + } + + @Test // exists due to #3231 + fun `should ignore the leading whitespace in header in-between the hash symbol and header text`() { + val markdown = """ + | # first header + | ## second header + | ### third header + """.trimMargin() + val actualDocumentationNode = parseMarkdownToDocNode(markdown).children + val expectedDocumentationNode = listOf( + H1(listOf(Text("first header"))), + H2(listOf(Text("second header"))), + H3(listOf(Text("third header"))), + ) + assertEquals(actualDocumentationNode, expectedDocumentationNode) + } + + @Test // exists due to #3231 + fun `should ignore trailing whitespace in header`() { + val markdown = """ + | # first header + | ## second header + | ### third header + """.trimMargin() + val actualDocumentationNode = parseMarkdownToDocNode(markdown).children + val expectedDocumentationNode = listOf( + H1(listOf(Text("first header"))), + H2(listOf(Text("second header"))), + H3(listOf(Text("third header"))), + ) + assertEquals(actualDocumentationNode, expectedDocumentationNode) + } + + @Test // exists due to #3231 + fun `should ignore leading and trailing whitespace in header, but not whitespace in the middle`() { + val markdown = """ + | # first header + | ## second ~~header~~ in a **long** sentence ending with whitespaces + | ### third header + """.trimMargin() + val actualDocumentationNode = parseMarkdownToDocNode(markdown).children + val expectedDocumentationNode = listOf( + H1(listOf(Text("first header"))), + H2(listOf( + Text("second "), + Strikethrough(listOf(Text("header"))), + Text(" in a "), + B(listOf(Text("long"))), + Text(" sentence ending with whitespaces") + )), + H3(listOf(Text("third header"))), + ) + assertEquals(actualDocumentationNode, expectedDocumentationNode) + } +} + diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/ClassesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/ClassesTest.kt new file mode 100644 index 00000000..c18dfafb --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/ClassesTest.kt @@ -0,0 +1,594 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.TypeConstructor +import org.jetbrains.dokka.links.sureClassNames +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.KotlinModifier.* +import kotlin.test.assertNull +import kotlin.test.Test +import utils.* + + +class ClassesTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "classes") { + + @Test + fun emptyClass() { + inlineModelTest( + """ + |class Klass {}""" + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + children counts 4 + } + } + } + + @Test + fun classWithConstructor() { + inlineModelTest( + """ + |class Klass(name: String) + """ + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + children counts 4 + + with(constructors.firstOrNull().assertNotNull("Constructor")) { + visibility.values allEquals KotlinVisibility.Public + parameters counts 1 + with(parameters.firstOrNull().assertNotNull("Constructor parameter")) { + name equals "name" + type.name equals "String" + } + } + + } + } + } + + @Test + fun classWithFunction() { + inlineModelTest( + """ + |class Klass { + | fun fn() {} + |} + """ + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + children counts 5 + + with((this / "fn").cast<DFunction>()) { + type.name equals "Unit" + parameters counts 0 + visibility.values allEquals KotlinVisibility.Public + } + } + } + } + + @Test + fun classWithProperty() { + inlineModelTest( + """ + |class Klass { + | val name: String = "" + |} + """ + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + children counts 5 + + with((this / "name").cast<DProperty>()) { + name equals "name" + // TODO property name + } + } + } + } + + @Test + fun classWithCompanionObject() { + inlineModelTest( + """ + |class Klass() { + | companion object { + | val x = 1 + | fun foo() {} + | } + |} + """ + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + children counts 5 + + with((this / "Companion").cast<DObject>()) { + name equals "Companion" + children counts 5 + + with((this / "x").cast<DProperty>()) { + name equals "x" + } + + with((this / "foo").cast<DFunction>()) { + name equals "foo" + parameters counts 0 + type.name equals "Unit" + } + } + + with((this.companion).cast<DObject>()) { + name equals "Companion" + children counts 5 + + with((this / "x").cast<DProperty>()) { + name equals "x" + } + + with((this / "foo").cast<DFunction>()) { + name equals "foo" + parameters counts 0 + type.name equals "Unit" + } + } + } + } + } + + @Test + fun dataClass() { + inlineModelTest( + """ + |data class Klass() {} + """ + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + visibility.values allEquals KotlinVisibility.Public + with(extra[AdditionalModifiers]!!.content.entries.single().value.assertNotNull("Extras")) { + this counts 1 + first() equals ExtraModifiers.KotlinOnlyModifiers.Data + } + } + } + } + + @Test + fun sealedClass() { + inlineModelTest( + """ + |sealed class Klass() {} + """ + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + modifier.values.forEach { it equals Sealed } + } + } + } + + @Test + fun annotatedClassWithAnnotationParameters() { + inlineModelTest( + """ + |@Deprecated("should no longer be used") class Foo() {} + """ + ) { + with((this / "classes" / "Foo").cast<DClass>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(first()) { + dri.classNames equals "Deprecated" + params.entries counts 1 + (params["message"].assertNotNull("message") as StringValue).value equals "should no longer be used" + } + } + } + } + } + + @Test + fun notOpenClass() { + inlineModelTest( + """ + |open class C() { + | open fun f() {} + |} + | + |class D() : C() { + | override fun f() {} + |} + """ + ) { + val C = (this / "classes" / "C").cast<DClass>() + val D = (this / "classes" / "D").cast<DClass>() + + with(C) { + modifier.values.forEach { it equals Open } + with((this / "f").cast<DFunction>()) { + modifier.values.forEach { it equals Open } + } + } + with(D) { + modifier.values.forEach { it equals Final } + with((this / "f").cast<DFunction>()) { + modifier.values.forEach { it equals Open } + } + D.supertypes.flatMap { it.component2() }.firstOrNull()?.typeConstructor?.dri equals C.dri + } + } + } + + @Test + fun indirectOverride() { + inlineModelTest( + """ + |abstract class C() { + | abstract fun foo() + |} + | + |abstract class D(): C() + | + |class E(): D() { + | override fun foo() {} + |} + """ + ) { + val C = (this / "classes" / "C").cast<DClass>() + val D = (this / "classes" / "D").cast<DClass>() + val E = (this / "classes" / "E").cast<DClass>() + + with(C) { + modifier.values.forEach { it equals Abstract } + ((this / "foo").cast<DFunction>()).modifier.values.forEach { it equals Abstract } + } + + with(D) { + modifier.values.forEach { it equals Abstract } + } + + with(E) { + modifier.values.forEach { it equals Final } + + } + D.supers.single().typeConstructor.dri equals C.dri + E.supers.single().typeConstructor.dri equals D.dri + } + } + + @Test + fun innerClass() { + inlineModelTest( + """ + |class C { + | inner class D {} + |} + """ + ) { + with((this / "classes" / "C").cast<DClass>()) { + + with((this / "D").cast<DClass>()) { + with(extra[AdditionalModifiers]!!.content.entries.single().value.assertNotNull("AdditionalModifiers")) { + this counts 1 + first() equals ExtraModifiers.KotlinOnlyModifiers.Inner + } + } + } + } + } + + @Test + fun companionObjectExtension() { + inlineModelTest( + """ + |class Klass { + | companion object Default {} + |} + | + |/** + | * The def + | */ + |val Klass.Default.x: Int get() = 1 + """ + ) { + with((this / "classes").cast<DPackage>()) { + properties.single().name equals "x" + (properties.single().receiver?.dri?.callable?.receiver as? TypeConstructor)?.fullyQualifiedName equals "classes.Klass.Default" + } + } + } + + @Test + fun secondaryConstructor() { + inlineModelTest( + """ + |class C() { + | /** This is a secondary constructor. */ + | constructor(s: String): this() {} + |} + """ + ) { + with((this / "classes" / "C").cast<DClass>()) { + name equals "C" + constructors counts 2 + + constructors.map { it.name } allEquals "C" + + with(constructors.find { it.parameters.isEmpty() } notNull "C()") { + parameters counts 0 + } + + with(constructors.find { it.parameters.isNotEmpty() } notNull "C(String)") { + parameters counts 1 + with(parameters.firstOrNull() notNull "Constructor parameter") { + name equals "s" + type.name equals "String" + } + } + } + } + } + + @Test + fun sinceKotlin() { + inlineModelTest( + """ + |/** + | * Useful + | */ + |@SinceKotlin("1.1") + |class C + """ + ) { + with((this / "classes" / "C").cast<DClass>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(first()) { + dri.classNames equals "SinceKotlin" + params.entries counts 1 + (params["version"].assertNotNull("version") as StringValue).value equals "1.1" + } + } + } + } + } + + @Test + fun privateCompanionObject() { + inlineModelTest( + """ + |class Klass { + | private companion object { + | fun fn() {} + | val a = 0 + | } + |} + """ + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + assertNull(companion, "Companion should not be visible by default") + } + } + } + + @Test + fun companionObject() { + inlineModelTest( + """ + |class Klass { + | companion object { + | fun fn() {} + | val a = 0 + | } + |} + """ + ) { + with((this / "classes" / "Klass").cast<DClass>()) { + name equals "Klass" + with((this / "Companion").cast<DObject>()) { + name equals "Companion" + visibility.values allEquals KotlinVisibility.Public + + with((this / "fn").cast<DFunction>()) { + name equals "fn" + parameters counts 0 + receiver equals null + } + } + } + } + } + + @Test + fun annotatedClass() { + inlineModelTest( + """@Suppress("abc") class Foo() {}""" + ) { + with((this / "classes" / "Foo").cast<DClass>()) { + with( + extra[Annotations]?.directAnnotations?.values?.firstOrNull()?.firstOrNull() + .assertNotNull("annotations") + ) { + dri.toString() equals "kotlin/Suppress///PointingToDeclaration/" + (params["names"].assertNotNull("param") as ArrayValue).value equals listOf(StringValue("abc")) + } + } + } + } + + @OnlyDescriptors("Bug in descriptors, DRI of entry should have [EnumEntryDRIExtra]") + @Test + fun javaAnnotationClass() { + inlineModelTest( + """ + |import java.lang.annotation.Retention + |import java.lang.annotation.RetentionPolicy + | + |@Retention(RetentionPolicy.SOURCE) + |public annotation class throws() + """ + ) { + with((this / "classes" / "throws").cast<DAnnotation>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(first()) { + dri.classNames equals "Retention" + params["value"].assertNotNull("value") equals EnumValue( + "RetentionPolicy.SOURCE", + DRI("java.lang.annotation", "RetentionPolicy.SOURCE") + ) + } + } + } + } + } + + @Test + fun genericAnnotationClass() { + inlineModelTest( + """annotation class Foo<A,B,C,D:Number>() {}""" + ) { + with((this / "classes" / "Foo").cast<DAnnotation>()) { + generics.map { it.name to it.bounds.first().name } equals listOf( + "A" to "Any", + "B" to "Any", + "C" to "Any", + "D" to "Number" + ) + } + } + } + + @Test + fun nestedGenericClasses() { + inlineModelTest( + """ + |class Outer<OUTER> { + | inner class Inner<INNER, T : OUTER> { } + |} + """.trimMargin() + ) { + with((this / "classes" / "Outer").cast<DClass>()) { + val inner = classlikes.single().cast<DClass>() + inner.generics.map { it.name to it.bounds.first().name } equals listOf("INNER" to "Any", "T" to "OUTER") + } + } + } + + @Test + fun allImplementedInterfaces() { + inlineModelTest( + """ + | interface Highest { } + | open class HighestImpl: Highest { } + | interface Lower { } + | interface LowerImplInterface: Lower { } + | class Tested : HighestImpl(), LowerImplInterface { } + """.trimIndent() + ) { + with((this / "classes" / "Tested").cast<DClass>()) { + extra[ImplementedInterfaces]?.interfaces?.entries?.single()?.value?.map { it.dri.sureClassNames } + ?.sorted() equals listOf("Highest", "Lower", "LowerImplInterface").sorted() + } + } + } + + @Test + fun multipleClassInheritance() { + inlineModelTest( + """ + | open class A { } + | open class B: A() { } + | class Tested : B() { } + """.trimIndent() + ) { + with((this / "classes" / "Tested").cast<DClass>()) { + supertypes.entries.single().value.map { it.typeConstructor.dri.sureClassNames }.single() equals "B" + } + } + } + + @Test + fun multipleClassInheritanceWithInterface() { + inlineModelTest( + """ + | open class A { } + | open class B: A() { } + | interface X { } + | interface Y : X { } + | class Tested : B(), Y { } + """.trimIndent() + ) { + with((this / "classes" / "Tested").cast<DClass>()) { + supertypes.entries.single().value.map { it.typeConstructor.dri.sureClassNames to it.kind } + .sortedBy { it.first } equals listOf( + "B" to KotlinClassKindTypes.CLASS, + "Y" to KotlinClassKindTypes.INTERFACE + ) + } + } + } + + @Test + fun doublyTypealiasedException() { + inlineModelTest( + """ + | typealias B = RuntimeException + | typealias A = B + """.trimMargin() + ) { + with((this / "classes" / "A").cast<DTypeAlias>()) { + extra[ExceptionInSupertypes].assertNotNull("Typealias A should have ExceptionInSupertypes in its extra field") + } + with((this / "classes" / "B").cast<DTypeAlias>()) { + extra[ExceptionInSupertypes].assertNotNull("Typealias B should have ExceptionInSupertypes in its extra field") + } + } + } + + @Test + fun `inline classes`() { + inlineModelTest( + """ + | inline class X(val example: String) + | + | @JvmInline + | value class InlineTest(val x: String) + """.trimMargin() + ) { + with((this / "classes" / "X").cast<DClass>()) { + name equals "X" + properties.first().name equals "example" + extra[AdditionalModifiers]?.content?.values?.firstOrNull() + ?.firstOrNull() equals ExtraModifiers.KotlinOnlyModifiers.Inline + } + } + } + + @Test + fun `value classes`() { + inlineModelTest( + """ + | @JvmInline + | value class InlineTest(val example: String) + """.trimMargin() + ) { + val classlike = packages.flatMap { it.classlikes }.first() as DClass + classlike.name equals "InlineTest" + classlike.properties.first().name equals "example" + classlike.extra[AdditionalModifiers]?.content?.values?.firstOrNull() + ?.firstOrNull() equals ExtraModifiers.KotlinOnlyModifiers.Value + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/CommentTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/CommentTest.kt new file mode 100644 index 00000000..6b00f2f0 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/CommentTest.kt @@ -0,0 +1,338 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DProperty +import org.jetbrains.dokka.model.doc.* +import utils.AbstractModelTest +import utils.assertNotNull +import utils.comments +import utils.docs +import kotlin.test.Test + +class CommentTest : AbstractModelTest("/src/main/kotlin/comment/Test.kt", "comment") { + + @Test + fun codeBlockComment() { + inlineModelTest( + """ + |/** + | * ```brainfuck + | * ++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>. + | * ``` + | */ + |val prop1 = "" + | + | + |/** + | * ``` + | * a + b - c + | * ``` + | */ + |val prop2 = "" + """ + ) { + with((this / "comment" / "prop1").cast<DProperty>()) { + name equals "prop1" + with(this.docs().firstOrNull()?.children?.firstOrNull()?.assertNotNull("Code")) { + (this?.children?.firstOrNull() as? Text) + ?.body equals "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>." + + this?.params?.get("lang") equals "brainfuck" + } + } + with((this / "comment" / "prop2").cast<DProperty>()) { + name equals "prop2" + with(this.docs().firstOrNull()?.children?.firstOrNull()?.assertNotNull("Code")) { + (this?.children?.firstOrNull() as? Text) + ?.body equals "a + b - c" + + this?.params?.get("lang") equals null + } + } + } + } + + @Test + fun codeBlockWithIndentationComment() { + inlineModelTest( + """ + |/** + | * 1. + | * ``` + | * line 1 + | * line 2 + | * ``` + | */ + |val prop1 = "" + """ + ) { + with((this / "comment" / "prop1").cast<DProperty>()) { + name equals "prop1" + with(this.docs().firstOrNull()?.children?.firstOrNull()?.assertNotNull("Code")) { + val codeBlockChildren = ((this?.children?.firstOrNull() as? Li)?.children?.firstOrNull() as? CodeBlock)?.children + (codeBlockChildren?.get(0) as? Text)?.body equals " line 1" + (codeBlockChildren?.get(1) as? Br) notNull "Br" + (codeBlockChildren?.get(2) as? Text)?.body equals " line 2" + } + } + } + } + + @Test + fun emptyDoc() { + inlineModelTest( + """ + val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + name equals "property" + comments() equals "" + } + } + } + + @Test + fun emptyDocButComment() { + inlineModelTest( + """ + |/* comment */ + |val property = "test" + |fun tst() = property + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "" + } + } + } + + @Test + fun multilineDoc() { + inlineModelTest( + """ + |/** + | * doc1 + | * + | * doc2 + | * doc3 + | */ + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "doc1\ndoc2 doc3\n" + } + } + } + + @Test + fun multilineDocWithComment() { + inlineModelTest( + """ + |/** + | * doc1 + | * + | * doc2 + | * doc3 + | */ + |// comment + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "doc1\ndoc2 doc3\n" + } + } + } + + @Test + fun oneLineDoc() { + inlineModelTest( + """ + |/** doc */ + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "doc\n" + } + } + } + + @Test + fun oneLineDocWithComment() { + inlineModelTest( + """ + |/** doc */ + |// comment + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "doc\n" + } + } + } + + @Test + fun oneLineDocWithEmptyLine() { + inlineModelTest( + """ + |/** doc */ + | + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "doc\n" + } + } + } + + @Test + fun emptySection() { + inlineModelTest( + """ + |/** + | * Summary + | * @one + | */ + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "Summary\n\none: []" + with(docs().find { it is CustomTagWrapper && it.name == "one" }.assertNotNull("'one' entry")) { + root.children counts 0 + root.params.keys counts 0 + } + } + } + } + + @Test + fun quotes() { + inlineModelTest( + """ + |/** it's "useful" */ + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals """it's "useful" +""" + } + } + } + + @Test + fun section1() { + inlineModelTest( + """ + |/** + | * Summary + | * @one section one + | */ + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "Summary\n\none: [section one\n]" + } + } + } + + + @Test + fun section2() { + inlineModelTest( + """ + |/** + | * Summary + | * @one section one + | * @two section two + | */ + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "Summary\n\none: [section one\n]\ntwo: [section two\n]" + } + } + } + + @Test + fun multilineSection() { + inlineModelTest( + """ + |/** + | * Summary + | * @one + | * line one + | * line two + | */ + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "Summary\n\none: [line one line two\n]" + } + } + } + + @Test + fun `should be space between Markdown nodes`() { + inlineModelTest( + """ + |/** + | * Rotates paths by `amount` **radians** around (`x`, `y`). + | */ + |val property = "test" + """ + ) { + with((this / "comment" / "property").cast<DProperty>()) { + comments() equals "Rotates paths by amount radians around (x, y).\n" + } + } + } + + @Test + fun `should remove spaces inside indented code block`() { + inlineModelTest( + """ + |/** + | * Welcome: + | * + | * ```kotlin + | * fun main() { + | * println("Hello World!") + | * } + | * ``` + | * + | * fun thisIsACodeBlock() { + | * val butWhy = "per markdown spec, because four-spaces prefix" + | * } + | */ + |class Foo + """ + ) { + with((this / "comment" / "Foo").cast<DClass>()) { + docs()[0].children[2] equals CodeBlock( + listOf( + Text( + "fun thisIsACodeBlock() {\n" + + " val butWhy = \"per markdown spec, because four-spaces prefix\"\n" + + "}" + ) + ) + ) + } + } + } + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/ExtensionsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/ExtensionsTest.kt new file mode 100644 index 00000000..a428dd1d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/ExtensionsTest.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.DInterface +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.properties.WithExtraProperties +import utils.AbstractModelTest +import kotlin.test.Test + +class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "classes") { + private fun <T : WithExtraProperties<R>, R : Documentable> T.checkExtension(name: String = "extension") = + with(extra[CallableExtensions]?.extensions) { + this notNull "extensions" + this counts 1 + (this?.single() as? DFunction)?.name equals name + } + + @Test + fun `should be extension for subclasses`() { + inlineModelTest( + """ + |open class A + |open class B: A() + |open class C: B() + |open class D: C() + |fun B.extension() = "" + """ + ) { + with((this / "classes" / "B").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "C").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "D").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "A").cast<DClass>()) { + extra[CallableExtensions] equals null + } + } + } + + @Test + fun `should be extension for interfaces`() { + inlineModelTest( + """ + |interface I + |interface I2 : I + |open class A: I2 + |fun I.extension() = "" + """ + ) { + + with((this / "classes" / "A").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "I2").cast<DInterface>()) { + checkExtension() + } + with((this / "classes" / "I").cast<DInterface>()) { + checkExtension() + } + } + } + + @Test + fun `should be extension for external classes`() { + inlineModelTest( + """ + |abstract class A<T>: AbstractList<T>() + |fun<T> AbstractCollection<T>.extension() {} + | + |class B:Exception() + |fun Throwable.extension() = "" + """ + ) { + with((this / "classes" / "A").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "B").cast<DClass>()) { + checkExtension() + } + } + } + + @Test + fun `should be extension for typealias`() { + inlineModelTest( + """ + |open class A + |open class B: A() + |open class C: B() + |open class D: C() + |typealias B2 = B + |fun B2.extension() = "" + """ + ) { + with((this / "classes" / "B").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "C").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "D").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "A").cast<DClass>()) { + extra[CallableExtensions] equals null + } + } + } + + @Test + fun `should be extension for java classes`() { + val testConfiguration = dokkaConfiguration { + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/") + classpath += jvmStdlibPath!! + } + } + } + testInline( + """ + |/src/main/kotlin/classes/Test.kt + | package classes + | fun A.extension() = "" + | + |/src/main/kotlin/classes/A.java + | package classes; + | public class A {} + | + | /src/main/kotlin/classes/B.java + | package classes; + | public class B extends A {} + """, + configuration = testConfiguration + ) { + documentablesTransformationStage = { + it.run { + with((this / "classes" / "B").cast<DClass>()) { + checkExtension() + } + with((this / "classes" / "A").cast<DClass>()) { + checkExtension() + } + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/FunctionsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/FunctionsTest.kt new file mode 100644 index 00000000..a6291bb1 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/FunctionsTest.kt @@ -0,0 +1,403 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import utils.AbstractModelTest +import utils.assertNotNull +import utils.comments +import utils.OnlyDescriptors +import utils.name +import kotlin.test.Test + +class FunctionTest : AbstractModelTest("/src/main/kotlin/function/Test.kt", "function") { + + @Test + fun function() { + inlineModelTest( + """ + |/** + | * Function fn + | */ + |fun fn() {} + """ + ) { + with((this / "function" / "fn").cast<DFunction>()) { + name equals "fn" + type.name equals "Unit" + this.children.assertCount(0, "Function children: ") + } + } + } + + @Test + fun overloads() { + inlineModelTest( + """ + |/** + | * Function fn + | */ + |fun fn() {} + | /** + | * Function fn(Int) + | */ + |fun fn(i: Int) {} + """ + ) { + with((this / "function").cast<DPackage>()) { + val fn1 = functions.find { + it.name == "fn" && it.parameters.isEmpty() + }.assertNotNull("fn()") + val fn2 = functions.find { + it.name == "fn" && it.parameters.isNotEmpty() + }.assertNotNull("fn(Int)") + + with(fn1) { + name equals "fn" + parameters.assertCount(0) + } + + with(fn2) { + name equals "fn" + parameters.assertCount(1) + parameters.first().type.name equals "Int" + } + } + } + } + + @Test + fun functionWithReceiver() { + inlineModelTest( + """ + |/** + | * Function with receiver + | */ + |fun String.fn() {} + | + |/** + | * Function with receiver + | */ + |fun String.fn(x: Int) {} + """ + ) { + with((this / "function").cast<DPackage>()) { + val fn1 = functions.find { + it.name == "fn" && it.parameters.isEmpty() + }.assertNotNull("fn()") + val fn2 = functions.find { + it.name == "fn" && it.parameters.count() == 1 + }.assertNotNull("fn(Int)") + + with(fn1) { + name equals "fn" + parameters counts 0 + receiver.assertNotNull("fn() receiver") + } + + with(fn2) { + name equals "fn" + parameters counts 1 + receiver.assertNotNull("fn(Int) receiver") + parameters.first().type.name equals "Int" + } + } + } + } + + @Test + fun functionWithParams() { + inlineModelTest( + """ + |/** + | * Multiline + | * + | * Function + | * Documentation + | */ + |fun function(/** parameter */ x: Int) { + |} + """ + ) { + with((this / "function" / "function").cast<DFunction>()) { + comments() equals "Multiline\nFunction Documentation\n" + + name equals "function" + parameters counts 1 + parameters.firstOrNull().assertNotNull("Parameter: ").also { + it.name equals "x" + it.type.name equals "Int" + it.comments() equals "parameter\n" + } + + type.assertNotNull("Return type: ").name equals "Unit" + } + } + } + + @Test + fun functionWithNotDocumentedAnnotation() { + inlineModelTest( + """ + |@Suppress("FOO") fun f() {} + """ + ) { + with((this / "function" / "f").cast<DFunction>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(first()) { + dri.classNames equals "Suppress" + params.entries counts 1 + (params["names"].assertNotNull("param") as ArrayValue).value equals listOf(StringValue("FOO")) + } + } + } + } + } + + @Test + fun inlineFunction() { + inlineModelTest( + """ + |inline fun f(a: () -> String) {} + """ + ) { + with((this / "function" / "f").cast<DFunction>()) { + extra[AdditionalModifiers]!!.content.entries.single().value counts 1 + extra[AdditionalModifiers]!!.content.entries.single().value exists ExtraModifiers.KotlinOnlyModifiers.Inline + } + } + } + + @Test + fun suspendFunction() { + inlineModelTest( + """ + |suspend fun f() {} + """ + ) { + with((this / "function" / "f").cast<DFunction>()) { + extra[AdditionalModifiers]!!.content.entries.single().value counts 1 + extra[AdditionalModifiers]!!.content.entries.single().value exists ExtraModifiers.KotlinOnlyModifiers.Suspend + } + } + } + + @Test + fun suspendInlineFunctionOrder() { + inlineModelTest( + """ + |suspend inline fun f(a: () -> String) {} + """ + ) { + with((this / "function" / "f").cast<DFunction>()) { + extra[AdditionalModifiers]!!.content.entries.single().value counts 2 + extra[AdditionalModifiers]!!.content.entries.single().value exists ExtraModifiers.KotlinOnlyModifiers.Suspend + extra[AdditionalModifiers]!!.content.entries.single().value exists ExtraModifiers.KotlinOnlyModifiers.Inline + } + } + } + + @Test + fun inlineSuspendFunctionOrderChanged() { + inlineModelTest( + """ + |inline suspend fun f(a: () -> String) {} + """ + ) { + with((this / "function" / "f").cast<DFunction>()) { + with(extra[AdditionalModifiers]!!.content.entries.single().value.assertNotNull("AdditionalModifiers")) { + this counts 2 + this exists ExtraModifiers.KotlinOnlyModifiers.Suspend + this exists ExtraModifiers.KotlinOnlyModifiers.Inline + } + } + } + } + + @OnlyDescriptors("Bug in descriptors, DRI of entry should have [EnumEntryDRIExtra]") + @Test + fun functionWithAnnotatedParam() { + inlineModelTest( + """ + |@Target(AnnotationTarget.VALUE_PARAMETER) + |@Retention(AnnotationRetention.SOURCE) + |@MustBeDocumented + |public annotation class Fancy + | + |fun function(@Fancy notInlined: () -> Unit) {} + """ + ) { + with((this / "function" / "Fancy").cast<DAnnotation>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 3 + with(associate { it.dri.classNames to it }) { + with(this["Target"].assertNotNull("Target")) { + (params["allowedTargets"].assertNotNull("allowedTargets") as ArrayValue).value equals listOf( + EnumValue( + "AnnotationTarget.VALUE_PARAMETER", + DRI("kotlin.annotation", "AnnotationTarget.VALUE_PARAMETER") + ) + ) + } + with(this["Retention"].assertNotNull("Retention")) { + (params["value"].assertNotNull("value") as EnumValue) equals EnumValue( + "AnnotationRetention.SOURCE", + DRI("kotlin.annotation", "AnnotationRetention.SOURCE") + ) + } + this["MustBeDocumented"].assertNotNull("MustBeDocumented").params.entries counts 0 + } + } + + } + with((this / "function" / "function" / "notInlined").cast<DParameter>()) { + with(this.extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(first()) { + dri.classNames equals "Fancy" + params.entries counts 0 + } + } + } + } + } + + @Test + fun functionWithNoinlineParam() { + inlineModelTest( + """ + |fun f(noinline notInlined: () -> Unit) {} + """ + ) { + with((this / "function" / "f" / "notInlined").cast<DParameter>()) { + extra[AdditionalModifiers]!!.content.entries.single().value counts 1 + extra[AdditionalModifiers]!!.content.entries.single().value exists ExtraModifiers.KotlinOnlyModifiers.NoInline + } + } + } + + @OnlyDescriptors("Bug in descriptors, DRI of entry should have [EnumEntryDRIExtra]") + @Test + fun annotatedFunctionWithAnnotationParameters() { + inlineModelTest( + """ + |@Target(AnnotationTarget.VALUE_PARAMETER) + |@Retention(AnnotationRetention.SOURCE) + |@MustBeDocumented + |public annotation class Fancy(val size: Int) + | + |@Fancy(1) fun f() {} + """ + ) { + with((this / "function" / "Fancy").cast<DAnnotation>()) { + constructors counts 1 + with(constructors.first()) { + parameters counts 1 + with(parameters.first()) { + type.name equals "Int" + name equals "size" + } + } + + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 3 + with(associate { it.dri.classNames to it }) { + with(this["Target"].assertNotNull("Target")) { + (params["allowedTargets"].assertNotNull("allowedTargets") as ArrayValue).value equals listOf( + EnumValue( + "AnnotationTarget.VALUE_PARAMETER", + DRI("kotlin.annotation", "AnnotationTarget.VALUE_PARAMETER") + ) + ) + } + with(this["Retention"].assertNotNull("Retention")) { + (params["value"].assertNotNull("value") as EnumValue) equals EnumValue( + "AnnotationRetention.SOURCE", + DRI("kotlin.annotation", "AnnotationRetention.SOURCE") + ) + } + this["MustBeDocumented"].assertNotNull("MustBeDocumented").params.entries counts 0 + } + } + + } + with((this / "function" / "f").cast<DFunction>()) { + with(this.extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(this.first()) { + dri.classNames equals "Fancy" + params.entries counts 1 + (params["size"] as IntValue).value equals 1 + } + } + } + } + } + + @Test + fun functionWithDefaultStringParameter() { + inlineModelTest( + """ + |/src/main/kotlin/function/Test.kt + |package function + |fun f(x: String = "") {} + """ + ) { + with((this / "function" / "f").cast<DFunction>()) { + parameters.forEach { p -> + p.name equals "x" + p.type.name.assertNotNull("Parameter type: ") equals "String" + p.extra[DefaultValue]?.expression?.get(sourceSets.single()) equals StringConstant("") + } + } + } + } + + @Test + fun functionWithDefaultFloatParameter() { + inlineModelTest( + """ + |/src/main/kotlin/function/Test.kt + |package function + |fun f(x: Float = 3.14f) {} + """ + ) { + with((this / "function" / "f").cast<DFunction>()) { + parameters.forEach { p -> + p.name equals "x" + p.type.name.assertNotNull("Parameter type: ") equals "Float" + p.extra[DefaultValue]?.expression?.get(sourceSets.single()) equals FloatConstant(3.14f) + } + } + } + } + + @Test + fun sinceKotlin() { + inlineModelTest( + """ + |/** + | * Quite useful [String] + | */ + |@SinceKotlin("1.1") + |fun f(): String = "1.1 rulezz" + """ + ) { + with((this / "function" / "f").cast<DFunction>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(first()) { + dri.classNames equals "SinceKotlin" + params.entries counts 1 + (params["version"].assertNotNull("version") as StringValue).value equals "1.1" + } + } + } + } + } + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/InheritorsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/InheritorsTest.kt new file mode 100644 index 00000000..459dd9ac --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/InheritorsTest.kt @@ -0,0 +1,428 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.DInterface +import org.jetbrains.dokka.model.doc.P +import org.jetbrains.dokka.model.doc.Text +import utils.AbstractModelTest +import utils.assertNotNull +import kotlin.test.Test +import kotlin.test.assertTrue + +class InheritorsTest : AbstractModelTest("/src/main/kotlin/inheritors/Test.kt", "inheritors") { + + @Test + fun simple() { + inlineModelTest( + """|interface A{} + |class B() : A {} + """.trimMargin(), + ) { + with((this / "inheritors" / "A").cast<DInterface>()) { + val map = extra[InheritorsInfo].assertNotNull("InheritorsInfo").value + with(map.keys.also { it counts 1 }.find { it.analysisPlatform == Platform.jvm }.assertNotNull("jvm key").let { map[it]!! } + ) { + this counts 1 + first().classNames equals "B" + } + } + } + } + + @Test + fun sealed() { + inlineModelTest( + """|sealed class A {} + |class B() : A() {} + |class C() : A() {} + |class D() + """.trimMargin(), + ) { + with((this / "inheritors" / "A").cast<DClass>()) { + val map = extra[InheritorsInfo].assertNotNull("InheritorsInfo").value + with(map.keys.also { it counts 1 }.find { it.analysisPlatform == Platform.jvm }.assertNotNull("jvm key").let { map[it]!! } + ) { + this counts 2 + mapNotNull { it.classNames }.sorted() equals listOf("B", "C") + } + } + } + } + + @Test + fun multiplatform() { + val configuration = dokkaConfiguration { + sourceSets { + val commonSourceSet = sourceSet { + name = "common" + sourceRoots = listOf("common/src/") + analysisPlatform = "common" + } + sourceSet { + name = "jvm" + sourceRoots = listOf("jvm/src/") + analysisPlatform = "jvm" + dependentSourceSets = setOf(commonSourceSet.value.sourceSetID) + } + sourceSet { + name = "js" + sourceRoots = listOf("js/src/") + analysisPlatform = "js" + dependentSourceSets = setOf(commonSourceSet.value.sourceSetID) + } + } + } + + testInline( + """ + |/common/src/main/kotlin/inheritors/Test.kt + |package inheritors + |interface A{} + |/jvm/src/main/kotlin/inheritors/Test.kt + |package inheritors + |class B() : A {} + |/js/src/main/kotlin/inheritors/Test.kt + |package inheritors + |class B() : A {} + |class C() : A {} + """.trimMargin(), + configuration, + cleanupOutput = false, + ) { + documentablesTransformationStage = { m -> + with((m / "inheritors" / "A").cast<DInterface>()) { + val map = extra[InheritorsInfo].assertNotNull("InheritorsInfo").value + with(map.keys.also { it counts 2 }) { + with(find { it.analysisPlatform == Platform.jvm }.assertNotNull("jvm key").let { map[it]!! }) { + this counts 1 + first().classNames equals "B" + } + with(find { it.analysisPlatform == Platform.js }.assertNotNull("js key").let { map[it]!! }) { + this counts 2 + val classes = listOf("B", "C") + assertTrue(all { classes.contains(it.classNames) }, "One of subclasses missing in js" ) + } + } + + } + } + } + } + + @Test + fun `should inherit docs`() { + val expectedDoc = listOf(P(listOf(Text("some text")))) + inlineModelTest( + """|interface A<out E> { + | /** + | * some text + | */ + | val a: Int + | + | /** + | * some text + | */ + | fun b(): E + |} + |open class C + |class B<out E>() : C(), A<out E> { + | val a = 0 + | override fun b(): E {} + |} + """.trimMargin(), + platform = Platform.common.toString() + ) { + with((this / "inheritors" / "A").cast<DInterface>()) { + with(this / "a") { + val propDoc = this?.documentation?.values?.single()?.children?.first()?.children + propDoc equals expectedDoc + } + with(this / "b") { + val funDoc = this?.documentation?.values?.single()?.children?.first()?.children + funDoc equals expectedDoc + } + + } + + with((this / "inheritors" / "B").cast<DClass>()) { + with(this / "a") { + val propDoc = this?.documentation?.values?.single()?.children?.first()?.children + propDoc equals expectedDoc + } + } + } + } + +// TODO [beresnev] fix, needs access to analysis +// class IgnoreCommonBuiltInsPlugin : DokkaPlugin() { +// private val kotlinAnalysisPlugin by lazy { plugin<DescriptorKotlinAnalysisPlugin>() } +// @Suppress("unused") +// val stdLibKotlinAnalysis by extending { +// kotlinAnalysisPlugin.kotlinAnalysis providing { ctx -> +// ProjectKotlinAnalysis( +// sourceSets = ctx.configuration.sourceSets, +// logger = ctx.logger, +// analysisConfiguration = DokkaAnalysisConfiguration(ignoreCommonBuiltIns = true) +// ) +// } override kotlinAnalysisPlugin.defaultKotlinAnalysis +// } +// +// @OptIn(DokkaPluginApiPreview::class) +// override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = +// PluginApiPreviewAcknowledgement +// } +// @Test +// fun `should inherit docs for stdLib #2638`() { +// val testConfiguration = dokkaConfiguration { +// suppressObviousFunctions = false +// sourceSets { +// sourceSet { +// sourceRoots = listOf("src/") +// analysisPlatform = "common" +// languageVersion = "1.4" +// } +// } +// } +// +// inlineModelTest( +// """ +// package kotlin.collections +// +// import kotlin.internal.PlatformDependent +// +// /** +// * Classes that inherit from this interface can be represented as a sequence of elements that can +// * be iterated over. +// * @param T the type of element being iterated over. The iterator is covariant in its element type. +// */ +// public interface Iterable<out T> { +// /** +// * Returns an iterator over the elements of this object. +// */ +// public operator fun iterator(): Iterator<T> +// } +// +// /** +// * Classes that inherit from this interface can be represented as a sequence of elements that can +// * be iterated over and that supports removing elements during iteration. +// * @param T the type of element being iterated over. The mutable iterator is invariant in its element type. +// */ +// public interface MutableIterable<out T> : Iterable<T> { +// /** +// * Returns an iterator over the elements of this sequence that supports removing elements during iteration. +// */ +// override fun iterator(): MutableIterator<T> +// } +// +// /** +// * A generic collection of elements. Methods in this interface support only read-only access to the collection; +// * read/write access is supported through the [MutableCollection] interface. +// * @param E the type of elements contained in the collection. The collection is covariant in its element type. +// */ +// public interface Collection<out E> : Iterable<E> { +// // Query Operations +// /** +// * Returns the size of the collection. +// */ +// public val size: Int +// +// /** +// * Returns `true` if the collection is empty (contains no elements), `false` otherwise. +// */ +// public fun isEmpty(): Boolean +// +// /** +// * Checks if the specified element is contained in this collection. +// */ +// public operator fun contains(element: @UnsafeVariance E): Boolean +// +// override fun iterator(): Iterator<E> +// +// // Bulk Operations +// /** +// * Checks if all elements in the specified collection are contained in this collection. +// */ +// public fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean +// } +// +// /** +// * A generic collection of elements that supports adding and removing elements. +// * +// * @param E the type of elements contained in the collection. The mutable collection is invariant in its element type. +// */ +// public interface MutableCollection<E> : Collection<E>, MutableIterable<E> { +// // Query Operations +// override fun iterator(): MutableIterator<E> +// +// // Modification Operations +// /** +// * Adds the specified element to the collection. +// * +// * @return `true` if the element has been added, `false` if the collection does not support duplicates +// * and the element is already contained in the collection. +// */ +// public fun add(element: E): Boolean +// +// /** +// * Removes a single instance of the specified element from this +// * collection, if it is present. +// * +// * @return `true` if the element has been successfully removed; `false` if it was not present in the collection. +// */ +// public fun remove(element: E): Boolean +// +// // Bulk Modification Operations +// /** +// * Adds all of the elements of the specified collection to this collection. +// * +// * @return `true` if any of the specified elements was added to the collection, `false` if the collection was not modified. +// */ +// public fun addAll(elements: Collection<E>): Boolean +// +// /** +// * Removes all of this collection's elements that are also contained in the specified collection. +// * +// * @return `true` if any of the specified elements was removed from the collection, `false` if the collection was not modified. +// */ +// public fun removeAll(elements: Collection<E>): Boolean +// +// /** +// * Retains only the elements in this collection that are contained in the specified collection. +// * +// * @return `true` if any element was removed from the collection, `false` if the collection was not modified. +// */ +// public fun retainAll(elements: Collection<E>): Boolean +// +// /** +// * Removes all elements from this collection. +// */ +// public fun clear(): Unit +// } +// +// /** +// * A generic ordered collection of elements. Methods in this interface support only read-only access to the list; +// * read/write access is supported through the [MutableList] interface. +// * @param E the type of elements contained in the list. The list is covariant in its element type. +// */ +// public interface List<out E> : Collection<E> { +// // Query Operations +// +// override val size: Int +// override fun isEmpty(): Boolean +// override fun contains(element: @UnsafeVariance E): Boolean +// override fun iterator(): Iterator<E> +// +// // Bulk Operations +// override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean +// +// // Positional Access Operations +// /** +// * Returns the element at the specified index in the list. +// */ +// public operator fun get(index: Int): E +// +// // Search Operations +// /** +// * Returns the index of the first occurrence of the specified element in the list, or -1 if the specified +// * element is not contained in the list. +// */ +// public fun indexOf(element: @UnsafeVariance E): Int +// +// /** +// * Returns the index of the last occurrence of the specified element in the list, or -1 if the specified +// * element is not contained in the list. +// */ +// public fun lastIndexOf(element: @UnsafeVariance E): Int +// +// // List Iterators +// /** +// * Returns a list iterator over the elements in this list (in proper sequence). +// */ +// public fun listIterator(): ListIterator<E> +// +// /** +// * Returns a list iterator over the elements in this list (in proper sequence), starting at the specified [index]. +// */ +// public fun listIterator(index: Int): ListIterator<E> +// +// // View +// /** +// * Returns a view of the portion of this list between the specified [fromIndex] (inclusive) and [toIndex] (exclusive). +// * The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa. +// * +// * Structural changes in the base list make the behavior of the view undefined. +// */ +// public fun subList(fromIndex: Int, toIndex: Int): List<E> +// } +// +// // etc +// """.trimMargin(), +// platform = Platform.common.toString(), +// configuration = testConfiguration, +// prependPackage = false, +// pluginsOverrides = listOf(IgnoreCommonBuiltInsPlugin()) +// ) { +// with((this / "kotlin.collections" / "List" / "contains").cast<DFunction>()) { +// documentation.size equals 1 +// +// } +// } +// } + + @Test + fun `should inherit docs in case of diamond inheritance`() { + inlineModelTest( + """ + public interface Collection2<out E> { + /** + * Returns `true` if the collection is empty (contains no elements), `false` otherwise. + */ + public fun isEmpty(): Boolean + + /** + * Checks if the specified element is contained in this collection. + */ + public operator fun contains(element: @UnsafeVariance E): Boolean + } + + public interface MutableCollection2<E> : Collection2<E>, MutableIterable2<E> + + + public interface List2<out E> : Collection2<E> { + override fun isEmpty(): Boolean + override fun contains(element: @UnsafeVariance E): Boolean + } + + public interface MutableList2<E> : List2<E>, MutableCollection2<E> + + public class AbstractMutableList2<E> : MutableList2<E> { + protected constructor() + + // From List + + override fun isEmpty(): Boolean = size == 0 + public override fun contains(element: E): Boolean = indexOf(element) != -1 + } + public class ArrayDeque2<E> : AbstractMutableList2<E> { + override fun isEmpty(): Boolean = size == 0 + public override fun contains(element: E): Boolean = indexOf(element) != -1 + + } + """.trimMargin() + ) { + with((this / "inheritors" / "ArrayDeque2" / "isEmpty").cast<DFunction>()) { + documentation.size equals 1 + } + with((this / "inheritors" / "ArrayDeque2" / "contains").cast<DFunction>()) { + documentation.size equals 1 + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/JavaTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/JavaTest.kt new file mode 100644 index 00000000..ff706c5e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/JavaTest.kt @@ -0,0 +1,491 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.Param +import org.jetbrains.dokka.model.doc.Text +import utils.AbstractModelTest +import utils.assertContains +import utils.assertNotNull +import utils.name +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = Platform.jvm.toString() + classpath += jvmStdlibPath!! + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PRIVATE, + DokkaConfiguration.Visibility.PROTECTED, + DokkaConfiguration.Visibility.PACKAGE, + ) + } + } + } + + @Test + fun function() { + inlineModelTest( + """ + |class Test { + | /** + | * Summary for Function + | * @param name is String parameter + | * @param value is int parameter + | */ + | public void fn(String name, int value) {} + |} + """, configuration = configuration + ) { + with((this / "java" / "Test").cast<DClass>()) { + name equals "Test" + children counts 2 // default constructor and function + with((this / "fn").cast<DFunction>()) { + name equals "fn" + val params = parameters.map { it.documentation.values.first().children.first() as Param } + params.map { it.firstMemberOfType<Text>().body } equals listOf( + "is String parameter", + "is int parameter" + ) + } + } + } + } + + @Test fun allImplementedInterfacesInJava() { + inlineModelTest( + """ + |interface Highest { } + |interface Lower extends Highest { } + |class Extendable { } + |class Tested extends Extendable implements Lower { } + """, configuration = configuration){ + with((this / "java" / "Tested").cast<DClass>()){ + extra[ImplementedInterfaces]?.interfaces?.entries?.single()?.value?.map { it.dri.sureClassNames }?.sorted() equals listOf("Highest", "Lower").sorted() + } + } + } + + @Test fun multipleClassInheritanceWithInterface() { + inlineModelTest( + """ + |interface Highest { } + |interface Lower extends Highest { } + |class Extendable { } + |class Tested extends Extendable implements Lower { } + """, configuration = configuration){ + with((this / "java" / "Tested").cast<DClass>()) { + supertypes.entries.single().value.map { it.typeConstructor.dri.sureClassNames to it.kind }.sortedBy { it.first } equals listOf("Extendable" to JavaClassKindTypes.CLASS, "Lower" to JavaClassKindTypes.INTERFACE) + } + } + } + + @Test + fun superClass() { + inlineModelTest( + """ + |public class Foo extends Exception implements Cloneable {} + """, configuration = configuration + ) { + with((this / "java" / "Foo").cast<DClass>()) { + val sups = listOf("Exception", "Cloneable") + assertTrue( + sups.all { s -> supertypes.values.flatten().any { it.typeConstructor.dri.classNames == s } }) + "Foo must extend ${sups.joinToString(", ")}" + } + } + } + + @Test + fun arrayType() { + inlineModelTest( + """ + |class Test { + | public String[] arrayToString(int[] data) { + | return null; + | } + |} + """, configuration = configuration + ) { + with((this / "java" / "Test").cast<DClass>()) { + name equals "Test" + children counts 2 // default constructor and function + + with((this / "arrayToString").cast<DFunction>()) { + name equals "arrayToString" + type.name equals "Array" + with(parameters.firstOrNull().assertNotNull("parameters")) { + name equals "data" + type.name equals "Array" + } + } + } + } + } + + @Test + fun typeParameter() { + inlineModelTest( + """ + |class Foo<T extends Comparable<T>> { + | public <E> E foo(); + |} + """, configuration = configuration + ) { + with((this / "java" / "Foo").cast<DClass>()) { + generics counts 1 + generics[0].dri.classNames equals "Foo" + (functions[0].type as? TypeParameter)?.dri?.run { + packageName equals "java" + name equals "Foo" + callable?.name equals "foo" + } + } + } + } + + @Test + fun typeParameterIntoDifferentClasses2596() { + inlineModelTest( + """ + |class GenericDocument { } + |public interface DocumentClassFactory<T> { + | String getSchemaName(); + | GenericDocument toGenericDocument(T document); + | T fromGenericDocument(GenericDocument genericDoc); + |} + | + |public final class DocumentClassFactoryRegistry { + | public <T> DocumentClassFactory<T> getOrCreateFactory(T documentClass) { + | return null; + | } + |} + """, configuration = configuration + ) { + with((this / "java" / "DocumentClassFactory").cast<DInterface>()) { + generics counts 1 + generics[0].dri.classNames equals "DocumentClassFactory" + } + with((this / "java" / "DocumentClassFactoryRegistry").cast<DClass>()) { + functions.forEach { + (it.type as GenericTypeConstructor).dri.classNames equals "DocumentClassFactory" + ((it.type as GenericTypeConstructor).projections[0] as TypeParameter).dri.classNames equals "DocumentClassFactoryRegistry" + } + } + } + } + + @Test + fun constructors() { + inlineModelTest( + """ + |class Test { + | public Test() {} + | + | public Test(String s) {} + |} + """, configuration = configuration + ) { + with((this / "java" / "Test").cast<DClass>()) { + name equals "Test" + + constructors counts 2 + constructors.forEach { it.name equals "Test" } + constructors.find { it.parameters.isEmpty() }.assertNotNull("Test()") + + with(constructors.find { it.parameters.isNotEmpty() }.assertNotNull("Test(String)")) { + parameters.firstOrNull()?.type?.name equals "String" + } + } + } + } + + @Test + fun innerClass() { + inlineModelTest( + """ + |class InnerClass { + | public class D {} + |} + """, configuration = configuration + ) { + with((this / "java" / "InnerClass").cast<DClass>()) { + children counts 2 // default constructor and inner class + with((this / "D").cast<DClass>()) { + name equals "D" + children counts 1 // default constructor + } + } + } + } + + @Test + fun varargs() { + inlineModelTest( + """ + |class Foo { + | public void bar(String... x); + |} + """, configuration = configuration + ) { + with((this / "java" / "Foo").cast<DClass>()) { + name equals "Foo" + children counts 2 // default constructor and function + + with((this / "bar").cast<DFunction>()) { + name equals "bar" + with(parameters.firstOrNull().assertNotNull("parameter")) { + name equals "x" + type.name equals "Array" + } + } + } + } + } + + @Test + fun fields() { + inlineModelTest( + """ + |class Test { + | public int i; + | public static final String s; + |} + """, configuration = configuration + ) { + with((this / "java" / "Test").cast<DClass>()) { + children counts 3 // default constructor + 2 props + + with((this / "i").cast<DProperty>()) { + getter equals null + setter equals null + } + + with((this / "s").cast<DProperty>()) { + getter equals null + setter equals null + } + } + } + } + + @Test + fun staticMethod() { + inlineModelTest( + """ + |class C { + | public static void foo() {} + |} + """, configuration = configuration + ) { + with((this / "java" / "C" / "foo").cast<DFunction>()) { + with(extra[AdditionalModifiers]!!.content.entries.single().value.assertNotNull("AdditionalModifiers")) { + this counts 1 + first() equals ExtraModifiers.JavaOnlyModifiers.Static + } + } + } + } + + @Test + fun throwsList() { + inlineModelTest( + """ + |class C { + | public void foo() throws java.io.IOException, ArithmeticException {} + |} + """, configuration = configuration + ) { + with((this / "java" / "C" / "foo").cast<DFunction>()) { + with(extra[CheckedExceptions]?.exceptions?.entries?.single()?.value.assertNotNull("CheckedExceptions")) { + this counts 2 + first().packageName equals "java.io" + first().classNames equals "IOException" + get(1).packageName equals "java.lang" + get(1).classNames equals "ArithmeticException" + } + } + } + } + + @Test + fun annotatedAnnotation() { + inlineModelTest( + """ + |import java.lang.annotation.*; + | + |@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD}) + |public @interface Attribute { + | String value() default ""; + |} + """, configuration = configuration + ) { + with((this / "java" / "Attribute").cast<DAnnotation>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + with(single()) { + dri.classNames equals "Target" + (params["value"].assertNotNull("value") as ArrayValue).value equals listOf( + EnumValue("ElementType.FIELD", DRI("java.lang.annotation", "ElementType")), + EnumValue("ElementType.TYPE", DRI("java.lang.annotation", "ElementType")), + EnumValue("ElementType.METHOD", DRI("java.lang.annotation", "ElementType")) + ) + } + } + } + } + } + + @Test + fun javaLangObject() { + inlineModelTest( + """ + |class Test { + | public Object fn() { return null; } + |} + """, configuration = configuration + ) { + with((this / "java" / "Test" / "fn").cast<DFunction>()) { + assertTrue(type is JavaObject) + } + } + } + + @Test + fun enumValues() { + inlineModelTest( + """ + |enum E { + | Foo + |} + """, configuration = configuration + ) { + with((this / "java" / "E").cast<DEnum>()) { + name equals "E" + entries counts 1 + with((this / "Foo").cast<DEnumEntry>()) { + name equals "Foo" + } + } + } + } + + @Test + fun inheritorLinks() { + inlineModelTest( + """ + |public class InheritorLinks { + | public static class Foo {} + | + | public static class Bar extends Foo {} + |} + """, configuration = configuration + ) { + with((this / "java" / "InheritorLinks").cast<DClass>()) { + val dri = (this / "Bar").assertNotNull("Foo dri").dri + with((this / "Foo").cast<DClass>()) { + with(extra[InheritorsInfo].assertNotNull("InheritorsInfo")) { + with(value.values.flatten().distinct()) { + this counts 1 + first() equals dri + } + } + } + } + } + } + + @Test + fun `retention should work with static import`() { + inlineModelTest( + """ + |import java.lang.annotation.Retention; + |import java.lang.annotation.RetentionPolicy; + |import static java.lang.annotation.RetentionPolicy.RUNTIME; + | + |@Retention(RUNTIME) + |public @interface JsonClass { + |}; + """, configuration = configuration + ) { + with((this / "java" / "JsonClass").cast<DAnnotation>()) { + val annotation = extra[Annotations]?.directAnnotations?.entries + ?.firstOrNull()?.value //First sourceset + ?.firstOrNull() + + val expectedDri = DRI("java.lang.annotation", "Retention", null, PointingToDeclaration) + val expectedParams = "value" to EnumValue( + "RUNTIME", + DRI( + "java.lang.annotation", + "RetentionPolicy.RUNTIME", + null, + PointingToDeclaration, + DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode() + ) + ) + + assertEquals(expectedDri, annotation?.dri) + assertEquals(expectedParams.first, annotation?.params?.entries?.first()?.key) + assertEquals(expectedParams.second, annotation?.params?.entries?.first()?.value) + } + } + } + + @Test + fun variances() { + inlineModelTest( + """ + |public class Foo { + | public void superBound(java.util.List<? super String> param) {} + | public void extendsBound(java.util.List<? extends String> param) {} + | public void unbounded(java.util.List<?> param) {} + |} + """, configuration = configuration + ) { + with((this / "java" / "Foo").cast<DClass>()) { + val functionNames = functions.map { it.name } + assertContains(functionNames, "superBound") + assertContains(functionNames, "extendsBound") + assertContains(functionNames, "unbounded") + + for (function in functions) { + val param = function.parameters.single() + val type = param.type as GenericTypeConstructor + val variance = type.projections.single() + + when (function.name) { + "superBound" -> { + assertTrue(variance is Contravariance<*>) + val bound = variance.inner + assertEquals((bound as GenericTypeConstructor).dri.classNames, "String") + } + "extendsBound" -> { + assertTrue(variance is Covariance<*>) + val bound = variance.inner + assertEquals((bound as GenericTypeConstructor).dri.classNames, "String") + } + "unbounded" -> { + assertTrue(variance is Covariance<*>) + val bound = variance.inner + assertTrue(bound is JavaObject) + } + } + } + } + } + } + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt new file mode 100644 index 00000000..9b646f24 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt @@ -0,0 +1,365 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.utilities.firstIsInstanceOrNull +import translators.documentationOf +import utils.docs +import kotlin.test.Test +import kotlin.test.assertEquals + +class MultiLanguageInheritanceTest : BaseAbstractTest() { + val configuration = dokkaConfiguration { + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + @Test + fun `from java to kotlin`() { + testInline( + """ + |/src/main/kotlin/sample/Parent.java + |package sample; + | + |/** + | * Sample description from parent + | */ + |public class Parent { + | /** + | * parent function docs + | * @see java.lang.String for details + | */ + | public void parentFunction(){ + | } + |} + | + |/src/main/kotlin/sample/Child.kt + |package sample + |public class Child : Parent() { + | override fun parentFunction(){ + | + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "Child" }?.functions?.find { it.name == "parentFunction" } + val seeTag = function?.documentation?.values?.first()?.children?.firstIsInstanceOrNull<See>() + + assertEquals("", module.documentationOf("Child")) + assertEquals("parent function docs", module.documentationOf("Child", "parentFunction")) + assertEquals("for details", (seeTag?.root?.dfs { it is Text } as Text).body) + assertEquals("java.lang.String", seeTag.name) + } + } + } + + @Test + fun `from kotlin to java`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent `function docs` + | * + | * ``` + | * code block + | * ``` + | * @see java.lang.String for details + | */ + | public open fun parentFun(){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + |public class ChildInJava extends ParentInKotlin { + | @Override + | public void parentFun() { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" } + val seeTag = function?.documentation?.values?.first()?.children?.firstIsInstanceOrNull<See>() + + val expectedDocs = CustomDocTag( + children = listOf( + P( + listOf( + Text("parent "), + CodeInline( + listOf(Text("function docs")) + ) + ) + ), + CodeBlock( + listOf(Text("code block")) + ) + + ), + params = emptyMap(), + name = "MARKDOWN_FILE" + ) + + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals(expectedDocs, function?.docs()?.firstIsInstanceOrNull<Description>()?.root) + assertEquals("for details", (seeTag?.root?.dfs { it is Text } as Text).body) + assertEquals("java.lang.String", seeTag.name) + } + } + } + + @Test + fun `inherit doc on method`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent `function docs` with a link to [defaultString][java.lang.String] + | * + | * ``` + | * code block + | * ``` + | */ + | public open fun parentFun(){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + |public class ChildInJava extends ParentInKotlin { + | /** + | * {@inheritDoc} + | */ + | @Override + | public void parentFun() { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" } + + val expectedDocs = CustomDocTag( + children = listOf( + P( + listOf( + P( + listOf( + Text("parent "), + CodeInline( + listOf(Text("function docs")) + ), + Text(" with a link to "), + DocumentationLink( + DRI("java.lang", "String", null, PointingToDeclaration), + listOf(Text("defaultString")), + params = mapOf("href" to "[java.lang.String]") + ) + ) + ), + CodeBlock( + listOf(Text("code block")) + ) + ) + ) + ), + params = emptyMap(), + name = "MARKDOWN_FILE" + ) + + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals(expectedDocs, function?.docs()?.firstIsInstanceOrNull<Description>()?.root) + } + } + } + + @Test + fun `inline inherit doc on method`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent function docs + | * @see java.lang.String string + | */ + | public open fun parentFun(){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + |public class ChildInJava extends ParentInKotlin { + | /** + | * Start {@inheritDoc} end + | */ + | @Override + | public void parentFun() { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" }?.documentation?.values?.first()?.children?.first() + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals("Start parent function docs end", function?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + } + } + } + + @Test + fun `inherit doc on multiple throws`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent function docs + | * @throws java.lang.RuntimeException runtime + | * @throws java.lang.Exception exception + | */ + | public open fun parentFun(){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + |public class ChildInJava extends ParentInKotlin { + | /** + | * Start {@inheritDoc} end + | * @throws java.lang.RuntimeException Testing {@inheritDoc} + | */ + | @Override + | public void parentFun() { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" } + val docs = function?.documentation?.values?.first()?.children?.first() + val throwsTag = function?.documentation?.values?.first()?.children?.firstIsInstanceOrNull<Throws>() + + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals("Start parent function docs end", docs?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + assertEquals("Testing runtime", throwsTag?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + assertEquals("RuntimeException", throwsTag?.exceptionAddress?.classNames) + } + } + } + + @Test + fun `inherit doc on params`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent function docs + | * @param fst first docs + | * @param snd second docs + | */ + | public open fun parentFun(fst: String, snd: Int){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + | + |import org.jetbrains.annotations.NotNull; + | + |public class ChildInJava extends ParentInKotlin { + | /** + | * @param fst start {@inheritDoc} end + | * @param snd start {@inheritDoc} end + | */ + | @Override + | public void parentFun(@NotNull String fst, int snd) { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" } + val params = function?.documentation?.values?.first()?.children?.filterIsInstance<Param>() + + val fst = params?.first { it.name == "fst" } + val snd = params?.first { it.name == "snd" } + + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals("", module.documentationOf("ChildInJava", "parentFun")) + assertEquals("start first docs end", fst?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + assertEquals("start second docs end", snd?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/ObjectTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/ObjectTest.kt new file mode 100644 index 00000000..009b406e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/ObjectTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.model.AdditionalModifiers +import org.jetbrains.dokka.model.DObject +import org.jetbrains.dokka.model.ExtraModifiers +import utils.AbstractModelTest +import kotlin.test.Test + +class ObjectTest : AbstractModelTest("/src/main/kotlin/objects/Test.kt", "objects") { + + @Test + fun emptyObject() { + inlineModelTest( + """ + |object Obj {} + """.trimIndent() + ) { + with((this / "objects" / "Obj").cast<DObject>()) { + name equals "Obj" + children counts 3 + } + } + } + + @Test + fun `data object class`() { + inlineModelTest( + """ + |data object KotlinDataObject {} + """.trimIndent() + ) { + with((this / "objects" / "KotlinDataObject").cast<DObject>()) { + name equals "KotlinDataObject" + extra[AdditionalModifiers]?.content?.values?.single() + ?.single() equals ExtraModifiers.KotlinOnlyModifiers.Data + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/PackagesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/PackagesTest.kt new file mode 100644 index 00000000..b32f214d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/PackagesTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.model.DPackage +import utils.AbstractModelTest +import kotlin.test.Test + +class PackagesTest : AbstractModelTest("/src/main/kotlin/packages/Test.kt", "packages") { + + @Test + fun rootPackage() { + inlineModelTest( + """ + | + """.trimIndent(), + prependPackage = false, + configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + displayName = "JVM" + } + } + } + ) { + with((this / "[root]").cast<DPackage>()) { + packageName equals "" + children counts 0 + } + } + } + + @Test + fun simpleNamePackage() { + inlineModelTest( + """ + |package simple + """.trimIndent(), + prependPackage = false + ) { + with((this / "simple").cast<DPackage>()) { + packageName equals "simple" + children counts 0 + } + } + } + + @Test + fun dottedNamePackage() { + inlineModelTest( + """ + |package dot.name + """.trimIndent(), + prependPackage = false + ) { + with((this / "dot.name").cast<DPackage>()) { + packageName equals "dot.name" + children counts 0 + } + } + + } + + @Test + fun multipleFiles() { + inlineModelTest( + """ + |package dot.name + |/src/main/kotlin/packages/Test2.kt + |package simple + """.trimIndent(), + prependPackage = false + ) { + children counts 2 + with((this / "dot.name").cast<DPackage>()) { + packageName equals "dot.name" + children counts 0 + } + with((this / "simple").cast<DPackage>()) { + packageName equals "simple" + children counts 0 + } + } + } + + @Test + fun multipleFilesSamePackage() { + inlineModelTest( + """ + |package simple + |/src/main/kotlin/packages/Test2.kt + |package simple + """.trimIndent(), + prependPackage = false + ) { + children counts 1 + with((this / "simple").cast<DPackage>()) { + packageName equals "simple" + children counts 0 + } + } + } + + @Test + fun classAtPackageLevel() { + inlineModelTest( + """ + |package simple.name + | + |class Foo {} + """.trimIndent(), + prependPackage = false + ) { + with((this / "simple.name").cast<DPackage>()) { + packageName equals "simple.name" + children counts 1 + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/PropertyTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/PropertyTest.kt new file mode 100644 index 00000000..92dc56de --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/PropertyTest.kt @@ -0,0 +1,277 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.model.* +import utils.AbstractModelTest +import utils.assertNotNull +import utils.name +import kotlin.test.Test + +class PropertyTest : AbstractModelTest("/src/main/kotlin/property/Test.kt", "property") { + + @Test + fun valueProperty() { + inlineModelTest( + """ + |val property = "test"""" + ) { + with((this / "property" / "property").cast<DProperty>()) { + name equals "property" + children counts 0 + with(getter.assertNotNull("Getter")) { + type.name equals "String" + } + type.name equals "String" + } + } + } + + @Test + fun variableProperty() { + inlineModelTest( + """ + |var property = "test" + """ + ) { + with((this / "property" / "property").cast<DProperty>()) { + name equals "property" + children counts 0 + setter.assertNotNull("Setter") + with(getter.assertNotNull("Getter")) { + type.name equals "String" + } + type.name equals "String" + } + } + } + + @Test + fun valuePropertyWithGetter() { + inlineModelTest( + """ + |val property: String + | get() = "test" + """ + ) { + with((this / "property" / "property").cast<DProperty>()) { + name equals "property" + children counts 0 + with(getter.assertNotNull("Getter")) { + type.name equals "String" + } + type.name equals "String" + } + } + } + + @Test + fun variablePropertyWithAccessors() { + inlineModelTest( + """ + |var property: String + | get() = "test" + | set(value) {} + """ + ) { + with((this / "property" / "property").cast<DProperty>()) { + name equals "property" + children counts 0 + setter.assertNotNull("Setter") + with(getter.assertNotNull("Getter")) { + type.name equals "String" + } + visibility.values allEquals KotlinVisibility.Public + } + } + } + + @Test + fun propertyWithReceiver() { + inlineModelTest( + """ + |val String.property: Int + | get() = size() * 2 + """ + ) { + with((this / "property" / "property").cast<DProperty>()) { + name equals "property" + children counts 0 + with(receiver.assertNotNull("property receiver")) { + name equals null + type.name equals "String" + } + with(getter.assertNotNull("Getter")) { + type.name equals "Int" + } + visibility.values allEquals KotlinVisibility.Public + } + } + } + + @Test + fun propertyOverride() { + inlineModelTest( + """ + |open class Foo() { + | open val property: Int get() = 0 + |} + |class Bar(): Foo() { + | override val property: Int get() = 1 + |} + """ + ) { + with((this / "property").cast<DPackage>()) { + with((this / "Foo" / "property").cast<DProperty>()) { + dri.classNames equals "Foo" + name equals "property" + children counts 0 + with(getter.assertNotNull("Getter")) { + type.name equals "Int" + } + } + with((this / "Bar" / "property").cast<DProperty>()) { + dri.classNames equals "Bar" + name equals "property" + children counts 0 + with(getter.assertNotNull("Getter")) { + type.name equals "Int" + } + } + } + } + } + + @Test + fun propertyInherited() { + inlineModelTest( + """ + |open class Foo() { + | open val property: Int get() = 0 + |} + |class Bar(): Foo() + """ + ) { + with((this / "property").cast<DPackage>()) { + with((this / "Bar" / "property").cast<DProperty>()) { + dri.classNames equals "Foo" + name equals "property" + children counts 0 + with(getter.assertNotNull("Getter")) { + type.name equals "Int" + } + extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + classNames equals "Foo" + callable equals null + } + } + } + } + } + + @Test + fun sinceKotlin() { + inlineModelTest( + """ + |/** + | * Quite useful [String] + | */ + |@SinceKotlin("1.1") + |val prop: String = "1.1 rulezz" + """ + ) { + with((this / "property" / "prop").cast<DProperty>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(first()) { + dri.classNames equals "SinceKotlin" + params.entries counts 1 + (params["version"].assertNotNull("version") as StringValue).value equals "1.1" + } + } + } + } + } + + @Test + fun annotatedProperty() { + inlineModelTest( + """ + |@Strictfp var property = "test" + """, + configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOfNotNull(jvmStdlibPath) + } + } + } + ) { + with((this / "property" / "property").cast<DProperty>()) { + with(extra[Annotations]!!.directAnnotations.entries.single().value.assertNotNull("Annotations")) { + this counts 1 + with(first()) { + dri.classNames equals "Strictfp" + params.entries counts 0 + } + } + } + } + } + + @Test fun genericTopLevelExtensionProperty(){ + inlineModelTest( + """ | val <T : Number> List<T>.sampleProperty: T + | get() { TODO() } + """.trimIndent() + ){ + with((this / "property" / "sampleProperty").cast<DProperty>()) { + name equals "sampleProperty" + with(receiver.assertNotNull("Property receiver")) { + type.name equals "List" + } + with(getter.assertNotNull("Getter")) { + type.name equals "T" + } + setter equals null + generics counts 1 + generics.forEach { + it.name equals "T" + it.bounds.first().name equals "Number" + } + visibility.values allEquals KotlinVisibility.Public + } + } + } + + @Test fun genericExtensionPropertyInClass(){ + inlineModelTest( + """ | package test + | class XD<T> { + | var List<T>.sampleProperty: T + | get() { TODO() } + | set(value) { TODO() } + | } + """.trimIndent() + ){ + with((this / "property" / "XD" / "sampleProperty").cast<DProperty>()) { + name equals "sampleProperty" + children counts 0 + with(receiver.assertNotNull("Property receiver")) { + type.name equals "List" + } + with(getter.assertNotNull("Getter")) { + type.name equals "T" + } + with(setter.assertNotNull("Setter")){ + type.name equals "Unit" + } + generics counts 0 + visibility.values allEquals KotlinVisibility.Public + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/JavaAnnotationsForParametersTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/JavaAnnotationsForParametersTest.kt new file mode 100644 index 00000000..9800006b --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/JavaAnnotationsForParametersTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model.annotations + +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.annotations +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.utilities.cast +import utils.AbstractModelTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JavaAnnotationsForParametersTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { + + @Test + fun `function with deprecated parameter`() { + inlineModelTest( + """ + |public class Test { + | public void fn(@Deprecated String name) {} + |} + """.trimIndent() + ) { + with((this / "java" / "Test").cast<DClass>()) { + with((this / "fn").cast<DFunction>()) { + val dri = + parameters.first().extra[Annotations]?.directAnnotations?.flatMap { it.value }?.map { it.dri } + assertEquals(listOf(DRI("java.lang", "Deprecated")), dri) + } + } + } + } + + @Test + fun `function with parameter that has custom annotation`() { + inlineModelTest( + """ + |@Retention(RetentionPolicy.RUNTIME) + |@Target(ElementType.PARAMETER) + |public @interface Hello { + | public String bar() default ""; + |} + |public class Test { + | public void foo(@Hello(bar = "baz") String arg){ } + |} + """.trimIndent() + ) { + with((this / "java" / "Test").cast<DClass>()) { + with((this / "foo").cast<DFunction>()) { + val annotations = + parameters.first().extra[Annotations]?.directAnnotations?.flatMap { it.value } + val driOfHello = DRI("java", "Hello") + val annotationsValues = annotations?.flatMap { it.params.values }?.map { it.toString() }?.toList() + + assertEquals(listOf(driOfHello), annotations?.map { it.dri }) + assertEquals(listOf("baz"), annotationsValues) + } + } + } + } + + @Test + fun `function with annotated generic parameter`() { + inlineModelTest( + """ + |@Retention(RetentionPolicy.RUNTIME) + |@Target(ElementType.TYPE_PARAMETER) + |@interface Hello { + | public String bar() default ""; + |} + |public class Test { + | public <@Hello(bar = "baz") T> List<T> foo() { + | return null; + | } + |} + """.trimIndent() + ) { + with((this / "java" / "Test").cast<DClass>()) { + with((this / "foo").cast<DFunction>()) { + val annotations = generics.first().extra[Annotations]?.directAnnotations?.flatMap { it.value } + val driOfHello = DRI("java", "Hello") + val annotationsValues = annotations?.flatMap { it.params.values }?.map { it.toString() }?.toList() + + assertEquals(listOf(driOfHello), annotations?.map { it.dri }) + assertEquals(listOf("baz"), annotationsValues) + } + } + } + } + + @Test + fun `function with generic parameter that has annotated bounds`() { + inlineModelTest( + """ + |@Retention(RetentionPolicy.RUNTIME) + |@Target({ElementType.TYPE_USE}) + |@interface Hello { + | public String bar() default ""; + |} + |public class Test { + | public <T extends @Hello(bar = "baz") String> List<T> foo() { + | return null; + | } + |} + """.trimIndent() + ) { + with((this / "java" / "Test").cast<DClass>()) { + with((this / "foo").cast<DFunction>()) { + val annotations = ((generics.first().bounds.first() as Nullable).inner as GenericTypeConstructor) + .extra[Annotations]?.directAnnotations?.flatMap { it.value } + val driOfHello = DRI("java", "Hello") + val annotationsValues = annotations?.flatMap { it.params.values }?.map { it.toString() }?.toList() + + assertEquals(listOf(driOfHello), annotations?.map { it.dri }) + assertEquals(listOf("baz"), annotationsValues) + } + } + } + } + + @Test + fun `type parameter annotations should be visible even if type declaration has none`() { + inlineModelTest( + """ + |@Retention(RetentionPolicy.RUNTIME) + |@Target(ElementType.PARAMETER) + |public @interface Hello { + | public String bar() default ""; + |} + |public class Test { + | public <T> void foo(java.util.List<@Hello T> param) {} + |} + """.trimIndent() + ) { + with((this / "java" / "Test").cast<DClass>()) { + with((this / "foo").cast<DFunction>()) { + val paramAnnotations = parameters.first() + .type.cast<GenericTypeConstructor>() + .projections.first().cast<TypeParameter>() + .annotations() + .values + .flatten() + + assertEquals(1, paramAnnotations.size) + assertEquals(DRI("java", "Hello"), paramAnnotations[0].dri) + } + } + } + } + + @Test + fun `type parameter annotations should not be propagated from resolved type`() { + inlineModelTest( + """ + |@Retention(RetentionPolicy.RUNTIME) + |@Target(ElementType.PARAMETER) + |public @interface Hello { + | public String bar() default ""; + |} + |public class Test { + | public <@Hello T> void foo(java.util.List<T> param) {} + |} + """.trimIndent() + ) { + with((this / "java" / "Test").cast<DClass>()) { + with((this / "foo").cast<DFunction>()) { + val paramAnnotations = parameters.first() + .type.cast<GenericTypeConstructor>() + .projections.first().cast<TypeParameter>() + .annotations() + + assertTrue(paramAnnotations.isEmpty()) + } + } + } + } +} + diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt new file mode 100644 index 00000000..daab7dc9 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model.annotations + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.* +import translators.findClasslike +import kotlin.test.* + +class JavaAnnotationsTest : BaseAbstractTest() { + + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + } + } + } + + @Test // see https://github.com/Kotlin/dokka/issues/2350 + fun `should hande array used as annotation param value`() { + testInline( + """ + |/src/main/java/annotation/TestClass.java + |package annotation; + |public class TestClass { + | @SimpleAnnotation(clazz = String[].class) + | public boolean simpleAnnotation() { + | return false; + | } + |} + | + |/src/main/java/annotation/SimpleAnnotation.java + |package annotation; + |@Retention(RetentionPolicy.RUNTIME) + |@Target(ElementType.METHOD) + |public @interface SimpleAnnotation { + | Class<?> clazz(); + |} + """.trimIndent(), + configuration + ) { + documentablesTransformationStage = { module -> + val testClass = module.findClasslike("annotation", "TestClass") as DClass + assertNotNull(testClass) + + val annotatedFunction = testClass.functions.single { it.name == "simpleAnnotation" } + val annotation = + annotatedFunction.extra[Annotations]?.directAnnotations?.entries?.single()?.value?.single() + assertNotNull(annotation) { "Expected to find an annotation on simpleAnnotation function, found none" } + assertEquals("annotation", annotation.dri.packageName) + assertEquals("SimpleAnnotation", annotation.dri.classNames) + assertEquals(1, annotation.params.size) + + val param = annotation.params.values.single() + assertTrue(param is ClassValue) + // should probably be Array instead + // String matches parsing of Kotlin sources as of now + assertEquals("String", param.className) + assertEquals("java.lang", param.classDRI.packageName) + assertEquals("String", param.classDRI.classNames) + } + } + } + + @Test // see https://github.com/Kotlin/dokka/issues/2551 + fun `should hande annotation used within annotation params with class param value`() { + testInline( + """ + |/src/main/java/annotation/TestClass.java + |package annotation; + |public class TestClass { + | @XmlElementRefs({ + | @XmlElementRef(name = "NotOffered", namespace = "http://www.gaeb.de/GAEB_DA_XML/DA86/3.3", type = JAXBElement.class, required = false) + | }) + | public List<JAXBElement<Object>> content; + |} + | + |/src/main/java/annotation/XmlElementRefs.java + |package annotation; + |public @interface XmlElementRefs { + | XmlElementRef[] value(); + |} + | + |/src/main/java/annotation/XmlElementRef.java + |package annotation; + |public @interface XmlElementRef { + | String name(); + | + | String namespace(); + | + | boolean required(); + | + | Class<JAXBElement> type(); + |} + | + |/src/main/java/annotation/JAXBElement.java + |package annotation; + |public class JAXBElement<T> { + |} + """.trimIndent(), + configuration + ) { + documentablesTransformationStage = { module -> + val testClass = module.findClasslike("annotation", "TestClass") as DClass + assertNotNull(testClass) + + val contentField = testClass.properties.find { it.name == "content" } + assertNotNull(contentField) + + val annotation = contentField.extra[Annotations]?.directAnnotations?.entries?.single()?.value?.single() + assertNotNull(annotation) { "Expected to find an annotation on content field, found none" } + assertEquals("XmlElementRefs", annotation.dri.classNames) + assertEquals(1, annotation.params.size) + + val arrayParam = annotation.params.values.single() + assertTrue(arrayParam is ArrayValue, "Expected single annotation param to be array") + assertEquals(1, arrayParam.value.size) + + val arrayParamValue = arrayParam.value.single() + assertTrue(arrayParamValue is AnnotationValue) + + val arrayParamAnnotationValue = arrayParamValue.annotation + assertEquals(4, arrayParamAnnotationValue.params.size) + assertEquals("XmlElementRef", arrayParamAnnotationValue.dri.classNames) + + val annotationParams = arrayParamAnnotationValue.params.values.toList() + + val nameParam = annotationParams[0] + assertTrue(nameParam is StringValue) + assertEquals("NotOffered", nameParam.value) + + val namespaceParam = annotationParams[1] + assertTrue(namespaceParam is StringValue) + assertEquals("http://www.gaeb.de/GAEB_DA_XML/DA86/3.3", namespaceParam.value) + + val typeParam = annotationParams[2] + assertTrue(typeParam is ClassValue) + assertEquals("JAXBElement", typeParam.className) + assertEquals("annotation", typeParam.classDRI.packageName) + assertEquals("JAXBElement", typeParam.classDRI.classNames) + + val requiredParam = annotationParams[3] + assertTrue(requiredParam is BooleanValue) + assertFalse(requiredParam.value) + } + } + } + + @Test // see https://github.com/Kotlin/dokka/issues/2509 + fun `should handle generic class in annotation`() { + testInline( + """ + |/src/main/java/annotation/Breaking.java + |package annotation; + |public class Breaking<Y> { + |} + | + |/src/main/java/annotation/TestAnnotate.java + |package annotation; + |public @interface TestAnnotate { + | Class<?> value(); + |} + | + |/src/main/java/annotation/TestClass.java + |package annotation; + |@TestAnnotate(Breaking.class) + |public class TestClass { + |} + """.trimIndent(), + configuration + ) { + documentablesTransformationStage = { module -> + val testClass = module.findClasslike("annotation", "TestClass") as DClass + assertNotNull(testClass) + + val annotation = testClass.extra[Annotations]?.directAnnotations?.entries?.single()?.value?.single() + assertNotNull(annotation) { "Expected to find an annotation on TestClass, found none" } + + assertEquals("TestAnnotate", annotation.dri.classNames) + assertEquals(1, annotation.params.size) + + val valueParameter = annotation.params.values.single() + assertTrue(valueParameter is ClassValue) + + assertEquals("Breaking", valueParameter.className) + + assertEquals("annotation", valueParameter.classDRI.packageName) + assertEquals("Breaking", valueParameter.classDRI.classNames) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/KotlinAnnotationsForParametersTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/KotlinAnnotationsForParametersTest.kt new file mode 100644 index 00000000..e3b17818 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/KotlinAnnotationsForParametersTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model.annotations + +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.annotations +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.utilities.cast +import utils.AbstractModelTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class KotlinAnnotationsForParametersTest : AbstractModelTest("/src/main/kotlin/annotations/Test.kt", "annotations") { + @Test + fun `generic receiver with annotations`() { + inlineModelTest( + """ + |@Target(AnnotationTarget.TYPE_PARAMETER) + |annotation class Hello(val bar: String) + |fun <@Hello("abc") T> foo(arg: String): List<T> = TODO() + """.trimIndent() + ) { + with((this / "annotations" / "foo").cast<DFunction>()) { + val annotations = generics.first().extra[Annotations]?.directAnnotations?.flatMap { it.value } + val driOfHello = DRI("annotations", "Hello") + val annotationsValues = annotations?.flatMap { it.params.values }?.map { it.toString() }?.toList() + + assertEquals(listOf(driOfHello), annotations?.map { it.dri }) + assertEquals(listOf("abc"), annotationsValues) + } + } + } + + @Test + fun `generic receiver with annotated bounds`() { + inlineModelTest( + """ + |@Target(AnnotationTarget.TYPE_PARAMETER) + |annotation class Hello(val bar: String) + |fun <T: @Hello("abc") String> foo(arg: String): List<T> = TODO() + """.trimIndent() + ) { + with((this / "annotations" / "foo").cast<DFunction>()) { + val annotations = (generics.first().bounds.first() as GenericTypeConstructor) + .extra[Annotations]?.directAnnotations?.flatMap { it.value } + val driOfHello = DRI("annotations", "Hello") + val annotationsValues = annotations?.flatMap { it.params.values }?.map { it.toString() }?.toList() + + assertEquals(listOf(driOfHello), annotations?.map { it.dri }) + assertEquals(listOf("abc"), annotationsValues) + } + } + } + + @Test + fun `type parameter annotations should be visible even if type declaration has none`() { + inlineModelTest( + """ + |@Target(AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.TYPE) + |annotation class Hello + | + |fun <T> foo(param: List<@Hello T>) {} + """.trimIndent() + ) { + with((this / "annotations" / "foo").cast<DFunction>()) { + val paramAnnotations = parameters.first() + .type.cast<GenericTypeConstructor>() + .projections + .first().cast<Invariance<TypeParameter>>() + .inner.cast<TypeParameter>() + .annotations() + .values + .flatten() + + assertEquals(1, paramAnnotations.size) + assertEquals(DRI("annotations", "Hello"), paramAnnotations[0].dri) + } + } + } + + @Test + fun `type parameter annotations should not be propagated from resolved type`() { + inlineModelTest( + """ + |@Target(AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.TYPE) + |annotation class Hello + | + |fun <@Hello T> foo(param: List<T>) {} + """.trimIndent() + ) { + with((this / "annotations" / "foo").cast<DFunction>()) { + val paramAnnotations = parameters.first() + .type.cast<GenericTypeConstructor>() + .projections.first().cast<Invariance<TypeParameter>>() + .inner.cast<TypeParameter>() + .annotations() + + assertTrue(paramAnnotations.isEmpty()) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/multiplatform/BasicMultiplatformTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/multiplatform/BasicMultiplatformTest.kt new file mode 100644 index 00000000..5412113e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/multiplatform/BasicMultiplatformTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package multiplatform + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class BasicMultiplatformTest : BaseAbstractTest() { + + @Test + fun dataTestExample() { + val testDataDir = getTestDataDir("multiplatform/basicMultiplatformTest").toAbsolutePath() + + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("$testDataDir/jvmMain/") + } + } + } + + testFromData(configuration) { + pagesTransformationStage = { + assertEquals(7, it.children.firstOrNull()?.children?.count() ?: 0) + } + } + } + + @Test + fun inlineTestExample() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/multiplatform/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/multiplatform/Test.kt + |package multiplatform + | + |object Test { + | fun test2(str: String): Unit {println(str)} + |} + """.trimMargin(), + configuration + ) { + pagesGenerationStage = { + assertEquals(3, it.parentMap.size) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/packageList/PackageListTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/packageList/PackageListTest.kt new file mode 100644 index 00000000..d6033433 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/packageList/PackageListTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package packageList + +import org.jetbrains.dokka.base.renderers.PackageListService +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import kotlin.test.Test +import kotlin.test.assertEquals + +class PackageListTest { + @Test + fun `one module package list is created correctly`() { + val nonStandardLocations = mapOf("//longArrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,kotlin.Long]/PointingToDeclaration/" to "[JS root]/long-array-with-fun.html") + val modules = mapOf("" to setOf("foo", "bar", "baz")) + val format = RecognizedLinkFormat.DokkaHtml + val output = PackageListService.renderPackageList(nonStandardLocations, modules, format.formatName, format.linkExtension) + val expected = """ + |${'$'}dokka.format:html-v1 + |${'$'}dokka.linkExtension:html + |${'$'}dokka.location://longArrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,kotlin.Long]/PointingToDeclaration/[JS root]/long-array-with-fun.html + |bar + |baz + |foo + |""".trimMargin() + assertEquals(expected, output) + } + + @Test + fun `multi-module package list is created correctly`() { + val nonStandardLocations = mapOf("//longArrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,kotlin.Long]/PointingToDeclaration/" to "[JS root]/long-array-with-fun.html") + val modules = mapOf("moduleA" to setOf("foo", "bar"), "moduleB" to setOf("baz"), "moduleC" to setOf("qux")) + val format = RecognizedLinkFormat.DokkaHtml + val output = PackageListService.renderPackageList(nonStandardLocations, modules, format.formatName, format.linkExtension) + val expected = """ + |${'$'}dokka.format:html-v1 + |${'$'}dokka.linkExtension:html + |${'$'}dokka.location://longArrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,kotlin.Long]/PointingToDeclaration/[JS root]/long-array-with-fun.html + |module:moduleA + |bar + |foo + |module:moduleB + |baz + |module:moduleC + |qux + |""".trimMargin() + assertEquals(expected, output) + } + + @Test + fun `empty package set in module`() { + val nonStandardLocations = emptyMap<String, String>() + val modules = mapOf("moduleA" to setOf("foo", "bar"), "moduleB" to emptySet(), "moduleC" to setOf("qux")) + val format = RecognizedLinkFormat.DokkaHtml + val output = PackageListService.renderPackageList(nonStandardLocations, modules, format.formatName, format.linkExtension) + val expected = """ + |${'$'}dokka.format:html-v1 + |${'$'}dokka.linkExtension:html + | + |module:moduleA + |bar + |foo + |module:moduleC + |qux + |""".trimMargin() + assertEquals(expected, output) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/pageMerger/PageNodeMergerTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/pageMerger/PageNodeMergerTest.kt new file mode 100644 index 00000000..983f73ff --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/pageMerger/PageNodeMergerTest.kt @@ -0,0 +1,465 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package pageMerger + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.* +import org.junit.jupiter.api.RepeatedTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class PageNodeMergerTest : BaseAbstractTest() { + + private val defaultConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + @Test + fun sameNameStrategyTest() { + testInline( + """ + |/src/main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |fun testT(): Int = 1 + |fun testT(i: Int): Int = i + | + |object Test { + | fun test(): String = "" + | fun test(str: String): String = str + |} + """.trimMargin(), + defaultConfiguration + ) { + pagesTransformationStage = { + val allChildren = it.childrenRec().filterIsInstance<ContentPage>() + val testT = allChildren.filter { it.name == "testT" } + val test = allChildren.filter { it.name == "test" } + + assertTrue(testT.size == 1, "There can be only one testT page") + assertTrue(testT.first().dri.size == 2, "testT page should have 2 DRI, but has ${testT.first().dri.size}") + + assertTrue(test.size == 1, "There can be only one test page") + assertTrue(test.first().dri.size == 2, "test page should have 2 DRI, but has ${test.first().dri.size}") + } + } + } + + @Ignore("TODO: reenable when we have infrastructure for turning off extensions") + @Test + fun defaultStrategyTest() { + val strList: MutableList<String> = mutableListOf() + + testInline( + """ + |/src/main/kotlin/pageMerger/Test.kt + |package pageMerger + | + |fun testT(): Int = 1 + |fun testT(i: Int): Int = i + | + |object Test { + | fun test(): String = "" + | fun test(str: String): String = str + |} + """.trimMargin(), + defaultConfiguration + ) { + pagesTransformationStage = { root -> + val allChildren = root.childrenRec().filterIsInstance<ContentPage>() + val testT = allChildren.filter { it.name == "testT" } + val test = allChildren.filter { it.name == "test" } + + assertTrue(testT.size == 1, "There can be only one testT page") + assertTrue(testT.first().dri.size == 1, "testT page should have single DRI, but has ${testT.first().dri.size}") + + assertTrue(test.size == 1, "There can be only one test page") + assertTrue(test.first().dri.size == 1, "test page should have single DRI, but has ${test.first().dri.size}") + + assertTrue(strList.count() == 2, "Expected 2 warnings, got ${strList.count()}") + } + } + } + + fun PageNode.childrenRec(): List<PageNode> = listOf(this) + children.flatMap { it.childrenRec() } + + + @Test + fun `should not be merged`() { + + val configuration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "js" + displayName = "js" + analysisPlatform = "js" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jsMain/kotlin/pageMerger/Test.kt") + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jvmMain/kotlin/pageMerger/Test.kt") + } + } + } + + testInline( + """ + |/src/commonMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |/src/jsMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |annotation class DoNotMerge + | + |/src/jvmMain/kotlin/pageMerger/Test.kt + |package pageMerger + | + |annotation class DoNotMerge + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + println(it) + val allChildren = it.childrenRec().filterIsInstance<ClasslikePageNode>() + val jvmClass = allChildren.filter { it.name == "[jvm]DoNotMerge" } + val jsClass = allChildren.filter { it.name == "[js]DoNotMerge" } + val noClass = allChildren.filter { it.name == "DoNotMerge" } + assertTrue(jvmClass.size == 1, "There can be only one DoNotMerge(jvm) page") + assertTrue( + jvmClass.first().documentables.firstOrNull()?.sourceSets?.single()?.analysisPlatform?.key == "jvm", + "[jvm]DoNotMerge should have only jvm sources" + ) + + assertTrue(jsClass.size == 1, "There can be only one DoNotMerge(js) page") + assertTrue( + jsClass.first().documentables.firstOrNull()?.sourceSets?.single()?.analysisPlatform?.key == "js", + "[js]DoNotMerge should have only js sources" + ) + + assertTrue(noClass.isEmpty(), "There can't be any DoNotMerge page") + } + } + } + + @RepeatedTest(3) + fun `should deterministically render same name property extensions`() { + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package test + | + |class ExtensionReceiver + | + |/** + | * Top level val extension + | */ + |val ExtensionReceiver.foo: String get() = "bar" + | + |class Obj { + | companion object { + | /** + | * Companion val extension + | */ + | val ExtensionReceiver.foo: String get() = "bar" + | } + |} + | + |/src/main/kotlin/test/nestedpackage/Pckg.kt + |package test.nestedpackage + | + |import test.ExtensionReceiver + | + |/** + | * From nested package int val extension + | */ + |val ExtensionReceiver.foo: Int get() = 42 + """.trimMargin(), + defaultConfiguration + ) { + renderingStage = { rootPageNode, _ -> + val extensions = rootPageNode.findDivergencesOfClass("ExtensionReceiver", ContentKind.Extensions) + + extensions.assertContainsKDocsInOrder( + "Top level val extension", + "Companion val extension", + "From nested package int val extension" + ) + } + } + } + + @RepeatedTest(3) + fun `should deterministically render parameterless same name function extensions`() { + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package test + | + |class ExtensionReceiver + | + |/** + | * Top level fun extension + | */ + |fun ExtensionReceiver.bar(): String = "bar" + | + |class Obj { + | + | companion object { + | /** + | * Companion fun extension + | */ + | fun ExtensionReceiver.bar(): String = "bar" + | } + |} + | + |/src/main/kotlin/test/nestedpackage/Pckg.kt + |package test.nestedpackage + | + |import test.ExtensionReceiver + | + |/** + | * From nested package fun extension + | */ + |fun ExtensionReceiver.bar(): String = "bar" + """.trimMargin(), + defaultConfiguration + ) { + renderingStage = { rootPageNode, _ -> + val extensions = rootPageNode.findDivergencesOfClass("ExtensionReceiver", ContentKind.Extensions) + extensions.assertContainsKDocsInOrder( + "Top level fun extension", + "Companion fun extension", + "From nested package fun extension" + ) + } + } + } + + @RepeatedTest(3) + fun `should deterministically render same name function extensions with parameters`() { + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package test + | + |class ExtensionReceiver + | + |/** + | * Top level fun extension with one string param + | */ + |fun ExtensionReceiver.bar(one: String): String = "bar" + | + |/** + | * Top level fun extension with one int param + | */ + |fun ExtensionReceiver.bar(one: Int): Int = 42 + | + |class Obj { + | + | companion object { + | /** + | * Companion fun extension with two params + | */ + | fun ExtensionReceiver.bar(one: String, two: String): String = "bar" + | } + |} + | + |/src/main/kotlin/test/nestedpackage/Pckg.kt + |package test.nestedpackage + | + |import test.ExtensionReceiver + | + |/** + | * From nested package fun extension with two params + | */ + |fun ExtensionReceiver.bar(one: String, two: String): String = "bar" + | + |/** + | * From nested package fun extension with three params + | */ + |fun ExtensionReceiver.bar(one: String, two: String, three: String): String = "bar" + | + |/** + | * From nested package fun extension with four params + | */ + |fun ExtensionReceiver.bar(one: String, two: String, three: String, four: String): String = "bar" + """.trimMargin(), + defaultConfiguration + ) { + renderingStage = { rootPageNode, _ -> + val extensions = rootPageNode.findDivergencesOfClass("ExtensionReceiver", ContentKind.Extensions) + extensions.assertContainsKDocsInOrder( + "Top level fun extension with one int param", + "Top level fun extension with one string param", + "Companion fun extension with two params", + "From nested package fun extension with two params", + "From nested package fun extension with three params", + "From nested package fun extension with four params" + ) + } + } + } + + @RepeatedTest(3) + fun `should deterministically render same name function extensions with different receiver and return type`() { + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package test + | + |/** + | * Top level fun extension string + | */ + |fun Int.bar(): String = "bar" + | + |/** + | * Top level fun extension int + | */ + |fun String.bar(): Int = 42 + """.trimMargin(), + defaultConfiguration + ) { + renderingStage = { rootPageNode, _ -> + val packageFunctionBlocks = rootPageNode.findPackageFunctionBlocks(packageName = "test") + assertEquals(1, packageFunctionBlocks.size, "Expected to find only one group for the functions") + + val functionsBlock = packageFunctionBlocks[0] + functionsBlock.assertContainsKDocsInOrder( + "Top level fun extension string", + "Top level fun extension int" + ) + } + } + } + + @Test + fun `should not ignore case when grouping by name`() { + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package test + | + |/** + | * Top level fun bAr + | */ + |fun Int.bAr(): String = "bar" + | + |/** + | * Top level fun BaR + | */ + |fun String.BaR(): Int = 42 + """.trimMargin(), + defaultConfiguration + ) { + renderingStage = { rootPageNode, _ -> + val packageFunctionBlocks = rootPageNode.findPackageFunctionBlocks(packageName = "test") + assertEquals(2, packageFunctionBlocks.size, "Expected two separate function groups") + + val firstGroup = packageFunctionBlocks[0] + firstGroup.assertContainsKDocsInOrder( + "Top level fun BaR", + ) + + val secondGroup = packageFunctionBlocks[1] + secondGroup.assertContainsKDocsInOrder( + "Top level fun bAr", + ) + } + } + } + + @Test + fun `should sort groups alphabetically ignoring case`() { + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package test + | + |/** Sequence builder */ + |fun <T> sequence(): Sequence<T> + | + |/** Sequence SAM constructor */ + |fun <T> Sequence(): Sequence<T> + | + |/** Sequence.any() */ + |fun <T> Sequence<T>.any() {} + | + |/** Sequence interface */ + |interface Sequence<T> + """.trimMargin(), + defaultConfiguration + ) { + renderingStage = { rootPageNode, _ -> + val packageFunctionBlocks = rootPageNode.findPackageFunctionBlocks(packageName = "test") + assertEquals(3, packageFunctionBlocks.size, "Expected 3 separate function groups") + + packageFunctionBlocks[0].assertContainsKDocsInOrder( + "Sequence.any()", + ) + + packageFunctionBlocks[1].assertContainsKDocsInOrder( + "Sequence SAM constructor", + ) + + packageFunctionBlocks[2].assertContainsKDocsInOrder( + "Sequence builder", + ) + } + } + } + + private fun RootPageNode.findDivergencesOfClass(className: String, kind: ContentKind): ContentDivergentGroup { + val extensionReceiverPage = this.dfs { it is ClasslikePageNode && it.name == className } as ClasslikePageNode + return extensionReceiverPage.content + .dfs { it is ContentDivergentGroup && it.dci.kind == kind } as ContentDivergentGroup + } + + private fun RootPageNode.findPackageFunctionBlocks(packageName: String): List<ContentDivergentGroup> { + val packagePage = this.dfs { it is PackagePage && it.name == packageName } as PackagePage + val packageFunctionTable = packagePage.content.dfs { + it is ContentTable && it.dci.kind == ContentKind.Functions + } as ContentTable + + return packageFunctionTable.children.map { packageGroup -> + packageGroup.dfs { it is ContentDivergentGroup } as ContentDivergentGroup + } + } + + private fun ContentDivergentGroup.assertContainsKDocsInOrder(vararg expectedKDocs: String) { + expectedKDocs.forEachIndexed { index, expectedKDoc -> + assertEquals(expectedKDoc, this.getElementKDocText(index)) + } + } + + private fun ContentDivergentGroup.getElementKDocText(index: Int): String { + val element = this.children.getOrNull(index) ?: throw IllegalArgumentException("No element with index $index") + val commentNode = element.after + ?.withDescendants() + ?.singleOrNull { it is ContentText && it.dci.kind == ContentKind.Comment } + ?: throw IllegalStateException("Expected the element to contain a single paragraph of text / comment") + + return (commentNode as ContentText).text + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/parsers/JavadocParserTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/parsers/JavadocParserTest.kt new file mode 100644 index 00000000..b56edc97 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/parsers/JavadocParserTest.kt @@ -0,0 +1,618 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package parsers + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.JavaClassReference +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.utilities.firstIsInstanceOrNull +import utils.docs +import utils.text +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class JavadocParserTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + } + } + } + + private fun performJavadocTest(testOperation: (DModule) -> Unit) { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Date2.java + | + |package docs + |/** + | * class level docs + | */ + |public enum AnEnumType { + | /** + | * content being refreshed, which can be a result of + | * invalidation, refresh that may contain content updates, or the initial load. + | */ + | REFRESH + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = testOperation + } + } + + @Test + fun `correctly parsed list`() { + performJavadocTest { module -> + val docs = + (module.packages.single().classlikes.single() as DEnum).entries.single().documentation.values.single().children.single().root.text() + assertEquals( + "content being refreshed, which can be a result of invalidation, refresh that may contain content updates, or the initial load.", + docs.trimEnd() + ) + } + } + + @Test + fun `code tag`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * Identifies calls to {@code assertThat}. + | * + | * {@code + | * Set<String> s; + | * System.out.println("s1 = " + s); + | * } + | * <pre>{@code + | * Set<String> s2; + | * System.out + | * .println("s2 = " + s2); + | * }</pre> + | * + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + Text(body = "Identifies calls to "), + CodeInline(children = listOf(Text(body = "assertThat"))), + Text(body = ". "), + CodeInline(children = listOf(Text(body = "\nSet<String> s;\nSystem.out.println(\"s1 = \" + s);\n"))) + ), + root.children[0].children + ) + kotlin.test.assertEquals( + CodeBlock(children = listOf(Text(body = "\nSet<String> s2;\nSystem.out\n .println(\"s2 = \" + s2);\n"))), + root.children[1] + ) + } + } + } + + @Test + fun `literal tag`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * An example of using the literal tag + | * {@literal @}Entity + | * public class User {} + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + Text(body = "An example of using the literal tag "), + Text(body = "@"), + Text(body = "Entity public class User {}"), + ), + root.children.first().children + ) + } + } + } + + @Test + fun `literal tag nested under pre tag`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * An example of using the literal tag + | * <pre> + | * {@literal @}Entity + | * public class User {} + | * </pre> + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + P(children = listOf(Text(body = "An example of using the literal tag "))), + Pre( + children = + listOf( + Text(body = "@"), + Text(body = "Entity\npublic class User {}\n") + ) + ) + ), + root.children + ) + } + } + } + + @Test + fun `literal tag containing angle brackets`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * An example of using the literal tag + | * {@literal a<B>c} + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + P( + children = listOf( + Text(body = "An example of using the literal tag "), + Text(body = "a<B>c") + ) + ), + ), + root.children + ) + } + } + } + + @Test + fun `html img tag`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * <img src="/path/to/img.jpg" alt="Alt text"/> + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + P( + children = listOf( + Img( + params = mapOf( + "href" to "/path/to/img.jpg", + "alt" to "Alt text" + ) + ) + ) + ) + ), + root.children + ) + } + } + } + + @Test + fun `description list tag`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * <dl> + | * <dt> + | * <code>name="<i>name</i>"</code> + | * </dt> + | * <dd> + | * A URI path segment. The subdirectory name for this value is contained in the + | * <code>path</code> attribute. + | * </dd> + | * <dt> + | * <code>path="<i>path</i>"</code> + | * </dt> + | * <dd> + | * The subdirectory you're sharing. While the <i>name</i> attribute is a URI path + | * segment, the <i>path</i> value is an actual subdirectory name. + | * </dd> + | * </dl> + | */ + | public class Test {} + """.trimIndent() + + val expected = listOf( + Dl( + listOf( + Dt( + listOf( + CodeInline( + listOf( + Text("name=\""), + I( + listOf( + Text("name") + ) + ), + Text("\"") + ) + ), + ) + ), + Dd( + listOf( + Text(" A URI path segment. The subdirectory name for this value is contained in the "), + CodeInline( + listOf( + Text("path") + ) + ), + Text(" attribute. ") + ) + ), + + Dt( + listOf( + CodeInline( + listOf( + Text("path=\""), + I( + listOf( + Text("path") + ) + ), + Text("\"") + ) + ) + ) + ), + Dd( + listOf( + Text(" The subdirectory you're sharing. While the "), + I( + listOf( + Text("name") + ) + ), + Text(" attribute is a URI path segment, the "), + I( + listOf( + Text("path") + ) + ), + Text(" value is an actual subdirectory name. ") + ) + ) + ) + ) + ) + + testInline(source, configuration) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + assertEquals(expected, docs.children.first().root.children) + } + } + } + + @Test + fun `header tags are handled properly`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * An example of using the header tags + | * <h1>A header</h1> + | * <h2>A second level header</h2> + | * <h3>A third level header</h3> + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + P(children = listOf(Text("An example of using the header tags "))), + H1( + listOf( + Text("A header") + ) + ), + H2( + listOf( + Text("A second level header") + ) + ), + H3( + listOf( + Text("A third level header") + ) + ) + ), + root.children + ) + } + } + } + + @Test + fun `var tag is handled properly`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * An example of using var tag: <var>variable</var> + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + P( + children = listOf( + Text("An example of using var tag: "), + Var(children = listOf(Text("variable"))), + ) + ), + ), + root.children + ) + } + } + } + + @Test + fun `u tag is handled properly`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * An example of using u tag: <u>underlined</u> + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + assertEquals( + listOf( + P( + children = listOf( + Text("An example of using u tag: "), + U(children = listOf(Text("underlined"))), + ) + ), + ), + root.children + ) + } + } + } + + @Test + fun `undocumented see also from java`() { + testInline( + """ + |/src/main/java/example/Source.java + |package example; + | + |public interface Source { + | String getProperty(String k, String v); + | + | /** + | * @see #getProperty(String, String) + | */ + | String getProperty(String k); + |} + """.trimIndent(), configuration + ) { + documentablesTransformationStage = { module -> + val functionWithSeeTag = module.packages.flatMap { it.classlikes }.flatMap { it.functions } + .find { it.name == "getProperty" && it.parameters.count() == 1 } + val seeTag = functionWithSeeTag?.docs()?.firstIsInstanceOrNull<See>() + val expectedLinkDestinationDRI = DRI( + packageName = "example", + classNames = "Source", + callable = Callable( + name = "getProperty", + params = listOf(JavaClassReference("java.lang.String"), JavaClassReference("java.lang.String")) + ) + ) + + assertNotNull(seeTag) + assertEquals("getProperty(String, String)", seeTag.name) + assertEquals(expectedLinkDestinationDRI, seeTag.address) + assertEquals(emptyList<DocTag>(), seeTag.children) + } + } + } + + @Test + fun `documented see also from java`() { + testInline( + """ + |/src/main/java/example/Source.java + |package example; + | + |public interface Source { + | String getProperty(String k, String v); + | + | /** + | * @see #getProperty(String, String) this is a reference to a method that is present on the same class. + | */ + | String getProperty(String k); + |} + """.trimIndent(), configuration + ) { + documentablesTransformationStage = { module -> + val functionWithSeeTag = module.packages.flatMap { it.classlikes }.flatMap { it.functions } + .find { it.name == "getProperty" && it.parameters.size == 1 } + val seeTag = functionWithSeeTag?.docs()?.firstIsInstanceOrNull<See>() + val expectedLinkDestinationDRI = DRI( + packageName = "example", + classNames = "Source", + callable = Callable( + name = "getProperty", + params = listOf(JavaClassReference("java.lang.String"), JavaClassReference("java.lang.String")) + ) + ) + + assertNotNull(seeTag) + assertEquals("getProperty(String, String)", seeTag.name) + assertEquals(expectedLinkDestinationDRI, seeTag.address) + assertEquals( + "this is a reference to a method that is present on the same class.", + seeTag.children.first().text().trim() + ) + assertEquals(1, seeTag.children.size) + } + } + } + + @Test + fun `tags are case-sensitive`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * Java's tag with wrong case + | * {@liTeRal @}Entity + | * public class User {} + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.values.first() + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + Text(body = "Java's tag with wrong case {@liTeRal @}Entity public class User {}"), + ), + root.children.first().children + ) + } + } + } + + // TODO [beresnev] move to java-analysis +// @Test +// fun `test isolated parsing is case sensitive`() { +// // Ensure that it won't accidentally break +// val values = JavadocTag.values().map { it.toString().toLowerCase() } +// val withRandomizedCapitalization = values.map { +// val result = buildString { +// for (char in it) { +// if (Random.nextBoolean()) { +// append(char) +// } else { +// append(char.toLowerCase()) +// } +// } +// } +// if (result == it) result.toUpperCase() else result +// } +// +// for ((index, value) in JavadocTag.values().withIndex()) { +// assertEquals(value, JavadocTag.lowercaseValueOfOrNull(values[index])) +// assertNull(JavadocTag.lowercaseValueOfOrNull(withRandomizedCapitalization[index])) +// } +// } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/BasicTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/BasicTest.kt new file mode 100644 index 00000000..9653b7bb --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/BasicTest.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jetbrains.dokka.links.DRI +import renderers.testPage +import utils.Span +import utils.match +import kotlin.test.Test + +class BasicTest : HtmlRenderingOnlyTestBase() { + @Test + fun `unresolved DRI link should render as text`() { + val page = testPage { + link("linkText", DRI("nonexistentPackage", "nonexistentClass")) + } + + HtmlRenderer(context).render(page) + renderedContent.match(Span("linkText")) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/BreadcrumbsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/BreadcrumbsTest.kt new file mode 100644 index 00000000..4bb0d41f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/BreadcrumbsTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jsoup.nodes.Element +import signatures.renderedContent +import utils.* +import kotlin.test.Test + +class BreadcrumbsTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + @Test + fun `should add breadcrumbs with current element`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/basic/TestClass.kt + |package testpackage + | + |class TestClass { + | fun foo() {} + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/testpackage/-test-class/foo.html").selectBreadcrumbs().match( + link("root"), + delimiter(), + link("testpackage"), + delimiter(), + link("TestClass"), + delimiter(), + current("foo"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `should mark only one element as current even if more elements have the same name`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/basic/TestClass.kt + |package testpackage + | + |class testname { + | val testname: String = "" + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/testpackage/testname/testname.html").selectBreadcrumbs().match( + link("root"), + delimiter(), + link("testpackage"), + delimiter(), + link("testname"), + delimiter(), + current("testname"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + private fun Element.selectBreadcrumbs() = this.select("div.breadcrumbs").single() + + private fun link(text: String): Tag = A(text) + private fun delimiter(): Tag = Span().withClasses("delimiter") + private fun current(text: String): Tag = Span(text).withClasses("current") +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CoverPageTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CoverPageTest.kt new file mode 100644 index 00000000..6b3ce2eb --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CoverPageTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import signatures.renderedContent +import utils.TestOutputWriterPlugin +import kotlin.test.Test +import kotlin.test.assertEquals + +class CoverPageTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(commonStdlibPath!!) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + } + } + + @Test + fun `names of nested inheritors`() { + val source = """ + |/src/main/kotlin/test/Test.kt + |package example + | + | sealed class Result{ + | class Success(): Result() + | class Failed(): Result() + | } + """ + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.renderedContent("root/example/-result/index.html") + val tableInheritors = content.select("div.table").single { it.previousElementSibling()?.text() == "Inheritors" && it.childrenSize() == 2 } + assertEquals(tableInheritors.getElementsContainingOwnText("Failed").singleOrNull()?.tagName(), "a") + assertEquals(tableInheritors.getElementsContainingOwnText("Success").singleOrNull()?.tagName(), "a") + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CustomFooterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CustomFooterTest.kt new file mode 100644 index 00000000..ff562c38 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CustomFooterTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.PluginConfigurationImpl +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jetbrains.dokka.base.templating.toJsonString +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import renderers.testPage +import utils.A +import utils.Div +import utils.Span +import utils.match +import kotlin.test.Test + +class CustomFooterTest : HtmlRenderingOnlyTestBase() { + @Test + fun `should include message from custom footer`() { + val page = testPage { } + HtmlRenderer(context).render(page) + renderedContent.match( + Span(A()), + Span(Div("Custom message")), + Span(Span("Generated by "), A(Span("dokka"), Span())) + ) + } + + override val configuration: DokkaConfigurationImpl + get() = super.configuration.copy( + pluginsConfiguration = listOf( + PluginConfigurationImpl( + DokkaBase::class.java.canonicalName, + DokkaConfiguration.SerializationFormat.JSON, + toJsonString(DokkaBaseConfiguration(footerMessage = """<div style="color: red">Custom message</div>""")) + ) + ) + ) + + override val renderedContent: Element + get() = files.contents.getValue("test-page.html").let { Jsoup.parse(it) }.select(".footer").single() +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/DivergentTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/DivergentTest.kt new file mode 100644 index 00000000..ccc43f12 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/DivergentTest.kt @@ -0,0 +1,316 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.pages.ContentDivergentGroup +import renderers.testPage +import utils.Br +import utils.match +import kotlin.test.Test +import kotlin.test.assertEquals + +class DivergentTest : HtmlRenderingOnlyTestBase() { + + @Test + fun simpleWrappingCase() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(js)) { + divergent { + text("a") + } + } + } + } + HtmlRenderer(context).render(page) + renderedContent.select("[data-togglable=DEFAULT/js]").single().match("a") + } + + @Test + fun noPlatformHintCase() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test"), implicitlySourceSetHinted = false) { + instance(setOf(DRI("test", "Test")), setOf(js)) { + divergent { + text("a") + } + } + } + } + HtmlRenderer(context).render(page) + renderedContent.match("a") + } + + @Test + fun divergentBetweenSourceSets() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(js)) { + divergent { + text("a") + } + } + instance(setOf(DRI("test", "Test")), setOf(jvm)) { + divergent { + text("b") + } + } + instance(setOf(DRI("test", "Test")), setOf(native)) { + divergent { + text("c") + } + } + } + } + + HtmlRenderer(context).render(page) + val content = renderedContent + content.select("[data-togglable=DEFAULT/js]").single().match("a") + content.select("[data-togglable=DEFAULT/jvm]").single().match("b") + content.select("[data-togglable=DEFAULT/native]").single().match("c") + } + + @Test + fun divergentInOneSourceSet() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(js)) { + divergent { + text("a") + } + } + instance(setOf(DRI("test", "Test2")), setOf(js)) { + divergent { + text("b") + } + } + instance(setOf(DRI("test", "Test3")), setOf(js)) { + divergent { + text("c") + } + } + } + } + + HtmlRenderer(context).render(page) + renderedContent.select("[data-togglable=DEFAULT/js]").single().match("abc") + } + + @Test + fun divergentInAndBetweenSourceSets() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(native)) { + divergent { + text("a") + } + } + instance(setOf(DRI("test", "Test")), setOf(js)) { + divergent { + text("b") + } + } + instance(setOf(DRI("test", "Test")), setOf(jvm)) { + divergent { + text("c") + } + } + instance(setOf(DRI("test", "Test2")), setOf(js)) { + divergent { + text("d") + } + } + instance(setOf(DRI("test", "Test3")), setOf(native)) { + divergent { + text("e") + } + } + } + } + + HtmlRenderer(context).render(page) + val content = renderedContent + val orderOfTabs = content.select(".platform-bookmarks-row").single().children().map { it.attr("data-toggle") } + + assertEquals(listOf("DEFAULT/js", "DEFAULT/jvm", "DEFAULT/native"), orderOfTabs) + + content.select("[data-togglable=DEFAULT/native]").single().match("ae") + content.select("[data-togglable=DEFAULT/js]").single().match("bd") + content.select("[data-togglable=DEFAULT/jvm]").single().match("c") + } + + @Test + fun divergentInAndBetweenSourceSetsWithGrouping() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(native)) { + divergent { + text("a") + } + after { + text("a+") + } + } + instance(setOf(DRI("test", "Test")), setOf(js)) { + divergent { + text("b") + } + after { + text("bd+") + } + } + instance(setOf(DRI("test", "Test")), setOf(jvm)) { + divergent { + text("c") + } + } + instance(setOf(DRI("test", "Test2")), setOf(js)) { + divergent { + text("d") + } + after { + text("bd+") + } + } + instance(setOf(DRI("test", "Test3")), setOf(native)) { + divergent { + text("e") + } + after { + text("e+") + } + } + } + } + + HtmlRenderer(context).render(page) + val content = renderedContent + content.select("[data-togglable=DEFAULT/native]").single().match("aa+", Br, "ee+") + content.select("[data-togglable=DEFAULT/js]").single().match("bdbd+") + content.select("[data-togglable=DEFAULT/jvm]").single().match("c") + } + + @Test + fun divergentSameBefore() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(native)) { + before { + text("ab-") + } + divergent { + text("a") + } + } + instance(setOf(DRI("test", "Test2")), setOf(native)) { + before { + text("ab-") + } + divergent { + text("b") + } + } + } + } + + HtmlRenderer(context).render(page) + renderedContent.select("[data-togglable=DEFAULT/native]").single().match("ab-ab") + } + + @Test + fun divergentSameAfter() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(native)) { + divergent { + text("a") + } + after { + text("ab+") + } + } + instance(setOf(DRI("test", "Test2")), setOf(native)) { + divergent { + text("b") + } + after { + text("ab+") + } + } + } + } + + HtmlRenderer(context).render(page) + renderedContent.select("[data-togglable=DEFAULT/native]").single().match("abab+") + } + + @Test + fun divergentGroupedByBeforeAndAfter() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(native)) { + before { + text("ab-") + } + divergent { + text("a") + } + after { + text("ab+") + } + } + instance(setOf(DRI("test", "Test2")), setOf(native)) { + before { + text("ab-") + } + divergent { + text("b") + } + after { + text("ab+") + } + } + } + } + + HtmlRenderer(context).render(page) + renderedContent.select("[data-togglable=DEFAULT/native]").single().match("ab-abab+") + } + + @Test + fun divergentDifferentBeforeAndAfter() { + val page = testPage { + divergentGroup(ContentDivergentGroup.GroupID("test")) { + instance(setOf(DRI("test", "Test")), setOf(native)) { + before { + text("a-") + } + divergent { + text("a") + } + after { + text("ab+") + } + } + instance(setOf(DRI("test", "Test2")), setOf(native)) { + before { + text("b-") + } + divergent { + text("b") + } + after { + text("ab+") + } + } + } + } + + HtmlRenderer(context).render(page) + renderedContent.select("[data-togglable=DEFAULT/native]").single().match("a-aab+", Br, "b-bab+") + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/FooterMessageTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/FooterMessageTest.kt new file mode 100644 index 00000000..149f970c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/FooterMessageTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.DokkaBaseConfiguration.Companion.defaultFooterMessage +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import renderers.testPage +import utils.A +import utils.Span +import utils.match +import kotlin.test.Test + +class FooterMessageTest : HtmlRenderingOnlyTestBase() { + @Test + fun `should include defaultFooter`() { + val page = testPage { } + HtmlRenderer(context).render(page) + renderedContent.match( + Span(A()), + Span(defaultFooterMessage), + Span(Span("Generated by "), A(Span("dokka"), Span())) + ) + } + + override val renderedContent: Element + get() = files.contents.getValue("test-page.html").let { Jsoup.parse(it) }.select(".footer").single() +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/FormattingUtilsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/FormattingUtilsTest.kt new file mode 100644 index 00000000..028ffa77 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/FormattingUtilsTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import kotlinx.html.body +import kotlinx.html.html +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.base.renderers.html.buildBreakableText +import kotlin.test.Test +import kotlin.test.assertEquals + +class FormattingUtilsTest { + @Test + fun `should build breakable text`(){ + val testedText = "kotlinx.collections.immutable" + val expectedHtml = """ + <html> + <body><span>kotlinx.</span><wbr></wbr><span>collections.</span><wbr></wbr><span>immutable</span></body> + </html> + """.trimIndent() + + val html = createHTML(prettyPrint = true).html { + body { + buildBreakableText(testedText) + } + } + + assertEquals(expectedHtml.trim(), html.trim()) + } + + @Test + fun `should build breakable text without empty spans`(){ + val testedText = "Package org.jetbrains.dokka.it.moduleC" + val expectedHtml = """ + <html> + <body><span><span>Package</span></span> <span>org.</span><wbr></wbr><span>jetbrains.</span><wbr></wbr><span>dokka.</span><wbr></wbr><span>it.</span><wbr></wbr><span>moduleC</span></body> + </html> + """.trimIndent() + + val html = createHTML(prettyPrint = true).html { + body { + buildBreakableText(testedText) + } + } + + assertEquals(expectedHtml.trim(), html.trim()) + } + + @Test + fun `should build breakable text for text with braces`(){ + val testedText = "[Common]kotlinx.collections.immutable" + val expectedHtml = """ + <html> + <body><span>[Common]kotlinx.</span><wbr></wbr><span>collections.</span><wbr></wbr><span>immutable</span></body> + </html> + """.trimIndent() + + val html = createHTML(prettyPrint = true).html { + body { + buildBreakableText(testedText) + } + } + + assertEquals(expectedHtml.trim(), html.trim()) + } + + @Test + fun `should build breakable text for camel case notation`(){ + val testedText = "DokkkkkkkaIsTheBest" + val expectedHtml = """ + <html> + <body><span>Dokkkkkkka</span><wbr></wbr><span>Is</span><wbr></wbr><span>The</span><wbr></wbr><span><span>Best</span></span></body> + </html> + """.trimIndent() + + val html = createHTML(prettyPrint = true).html { + body { + buildBreakableText(testedText) + } + } + + assertEquals(expectedHtml.trim(), html.trim()) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/GroupWrappingTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/GroupWrappingTest.kt new file mode 100644 index 00000000..cc9b763d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/GroupWrappingTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jetbrains.dokka.pages.TextStyle +import renderers.testPage +import utils.Div +import utils.P +import utils.match +import kotlin.test.Test + +class GroupWrappingTest : HtmlRenderingOnlyTestBase() { + + @Test + fun notWrapped() { + val page = testPage { + group { + text("a") + text("b") + } + text("c") + } + + HtmlRenderer(context).render(page) + + renderedContent.match("abc") + } + + @Test + fun paragraphWrapped() { + val page = testPage { + group(styles = setOf(TextStyle.Paragraph)) { + text("a") + text("b") + } + text("c") + } + + HtmlRenderer(context).render(page) + + renderedContent.match(P("ab"), "c") + } + + @Test + fun blockWrapped() { + val page = testPage { + group(styles = setOf(TextStyle.Block)) { + text("a") + text("b") + } + text("c") + } + + HtmlRenderer(context).render(page) + + renderedContent.match(Div("ab"), "c") + } + + @Test + fun nested() { + val page = testPage { + group(styles = setOf(TextStyle.Block)) { + text("a") + group(styles = setOf(TextStyle.Block)) { + group(styles = setOf(TextStyle.Block)) { + text("b") + text("c") + } + } + text("d") + } + } + + HtmlRenderer(context).render(page) + + renderedContent.match(Div("a", Div(Div("bc")), "d")) + } + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/HeaderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/HeaderTest.kt new file mode 100644 index 00000000..c19f965f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/HeaderTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.PluginConfigurationImpl +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.base.templating.toJsonString +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jsoup.Jsoup +import utils.TestOutputWriter +import utils.TestOutputWriterPlugin +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class HeaderTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + name = "jvm" + sourceRoots = listOf("src/jvm") + } + sourceSet { + name = "js" + sourceRoots = listOf("src/js") + } + } + } + + @Test + fun `should include homepage link if homepageLink is provided`() { + testRendering( + DokkaBaseConfiguration(homepageLink = "https://github.com/Kotlin/dokka/") + ) { _, _, writer -> + val renderedContent = navigationElement(writer) + + val sourceLinkElement = + assertNotNull(renderedContent.getElementById("homepage-link"), "Source link element not found") + val aElement = assertNotNull(sourceLinkElement.selectFirst("a")) + assertEquals("https://github.com/Kotlin/dokka/", aElement.attr("href")) + } + } + + @Test + fun `should not include homepage link by default`() { + testRendering(null) { _, _, writer -> + val renderedContent = navigationElement(writer) + assertNull(renderedContent.getElementById("homepage-link"), "Source link element found") + } + } + + private fun testRendering( + baseConfiguration: DokkaBaseConfiguration?, + block: (RootPageNode, DokkaContext, writer: TestOutputWriter) -> Unit + ) { + fun configuration(): DokkaConfigurationImpl { + baseConfiguration ?: return configuration + return configuration.copy( + pluginsConfiguration = listOf( + PluginConfigurationImpl( + DokkaBase::class.java.canonicalName, + DokkaConfiguration.SerializationFormat.JSON, + toJsonString(baseConfiguration) + ) + ) + ) + } + + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/jvm/Test.kt + |fun test() {} + |/src/js/Test.kt + |fun test() {} + """, + configuration(), + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { node, context -> + block(node, context, writerPlugin.writer) + } + } + } + + private fun navigationElement(writer: TestOutputWriter) = + writer + .contents + .getValue("index.html") + .let(Jsoup::parse) + .select(".navigation") + .single() + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/HtmlRenderingOnlyTestBase.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/HtmlRenderingOnlyTestBase.kt new file mode 100644 index 00000000..4e098371 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/HtmlRenderingOnlyTestBase.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.renderers.RootCreator +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.external.javadoc.JavadocExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProviderFactory +import org.jetbrains.dokka.testApi.context.MockContext +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import renderers.RenderingOnlyTestBase +import testApi.testRunner.defaultSourceSet +import utils.TestOutputWriter +import java.io.File + +abstract class HtmlRenderingOnlyTestBase : RenderingOnlyTestBase<Element>() { + + protected val js = defaultSourceSet.copy( + "JS", + defaultSourceSet.sourceSetID.copy(sourceSetName = "js"), + analysisPlatform = Platform.js, + sourceRoots = setOf(File("pl1")) + ) + protected val jvm = defaultSourceSet.copy( + "JVM", + defaultSourceSet.sourceSetID.copy(sourceSetName = "jvm"), + + analysisPlatform = Platform.jvm, + sourceRoots = setOf(File("pl1")) + ) + protected val native = defaultSourceSet.copy( + "NATIVE", + defaultSourceSet.sourceSetID.copy(sourceSetName = "native"), + analysisPlatform = Platform.native, + sourceRoots = setOf(File("pl1")) + ) + + val files = TestOutputWriter() + + open val configuration = DokkaConfigurationImpl( + sourceSets = listOf(js, jvm, native), + finalizeCoroutines = false + ) + + override val context = MockContext( + DokkaBase().outputWriter to { files }, + DokkaBase().locationProviderFactory to ::DokkaLocationProviderFactory, + DokkaBase().htmlPreprocessors to { RootCreator }, + DokkaBase().externalLocationProviderFactory to ::JavadocExternalLocationProviderFactory, + DokkaBase().externalLocationProviderFactory to ::DefaultExternalLocationProviderFactory, + testConfiguration = configuration + ) + + override val renderedContent: Element by lazy { + files.contents.getValue("test-page.html").let { Jsoup.parse(it) }.select("#content").single() + } + + protected fun linesAfterContentTag() = + files.contents.getValue("test-page.html").lines() + .dropWhile { !it.contains("""<div id="content">""") } + .joinToString(separator = "") { it.trim() } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/ListStylesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/ListStylesTest.kt new file mode 100644 index 00000000..f8afb54c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/ListStylesTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jetbrains.dokka.pages.ListStyle +import renderers.testPage +import utils.Dd +import utils.Dl +import utils.Dt +import utils.match +import kotlin.test.Test + + +class ListStylesTest : HtmlRenderingOnlyTestBase() { + + @Test + fun `description list render`() { + val page = testPage { + descriptionList { + item(styles = setOf(ListStyle.DescriptionTerm)) { + text("Description term #1") + } + item(styles = setOf(ListStyle.DescriptionTerm)) { + text("Description term #2") + } + item(styles = setOf(ListStyle.DescriptionDetails)) { + text("Description details describing terms #1 and #2") + } + } + } + + + HtmlRenderer(context).render(page) + renderedContent.match( + Dl( + Dt("Description term #1"), + Dt("Description term #2"), + Dd("Description details describing terms #1 and #2") + ) + ) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/NavigationIconTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/NavigationIconTest.kt new file mode 100644 index 00000000..d57f84df --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/NavigationIconTest.kt @@ -0,0 +1,292 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import utils.TestOutputWriterPlugin +import utils.navigationHtml +import utils.selectNavigationGrid +import kotlin.test.Test +import kotlin.test.assertEquals + +class NavigationIconTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + @Test + fun `should include all navigation icons`() { + val source = """ + |/src/main/kotlin/com/example/Empty.kt + |package com.example + | + |class Empty {} + """ + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val navIconAssets = writerPlugin.writer.contents + .filterKeys { it.startsWith("images/nav-icons") } + .keys.sorted() + + 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]) + assertEquals("images/nav-icons/annotation.svg", navIconAssets[3]) + assertEquals("images/nav-icons/class-kotlin.svg", navIconAssets[4]) + assertEquals("images/nav-icons/class.svg", navIconAssets[5]) + assertEquals("images/nav-icons/enum-kotlin.svg", navIconAssets[6]) + assertEquals("images/nav-icons/enum.svg", navIconAssets[7]) + assertEquals("images/nav-icons/exception-class.svg", navIconAssets[8]) + assertEquals("images/nav-icons/field-value.svg", navIconAssets[9]) + assertEquals("images/nav-icons/field-variable.svg", navIconAssets[10]) + assertEquals("images/nav-icons/function.svg", navIconAssets[11]) + 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]) + } + } + } + + @Test + fun `should add icon styles to kotlin class navigation item`() { + assertNavigationIcon( + source = kotlinSource("class Clazz {}"), + expectedIconClass = "class-kt", + expectedNavLinkText = "Clazz" + ) + } + + @Test + fun `should add icon styles to java class navigation item`() { + assertNavigationIcon( + source = javaSource( + className = "JavaClazz", + source = "public class JavaClazz {}" + ), + expectedIconClass = "class", + expectedNavLinkText = "JavaClazz" + ) + } + + @Test + fun `should add icon styles to kotlin abstract class navigation item`() { + assertNavigationIcon( + source = kotlinSource("abstract class AbstractClazz {}"), + expectedIconClass = "abstract-class-kt", + expectedNavLinkText = "AbstractClazz" + ) + } + + @Test + fun `should add icon styles to java abstract class navigation item`() { + assertNavigationIcon( + source = javaSource( + className = "AbstractJavaClazz", + source = "public abstract class AbstractJavaClazz {}" + ), + expectedIconClass = "abstract-class", + expectedNavLinkText = "AbstractJavaClazz" + ) + } + + @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( + source = kotlinSource("enum class KotlinEnum {}"), + expectedIconClass = "enum-class-kt", + expectedNavLinkText = "KotlinEnum" + ) + } + + @Test + fun `should add icon styles to java enum class navigation item`() { + assertNavigationIcon( + source = javaSource( + className = "JavaEnum", + source = "public enum JavaEnum {}" + ), + expectedIconClass = "enum-class", + expectedNavLinkText = "JavaEnum" + ) + } + + @Test + fun `should add icon styles to kotlin annotation navigation item`() { + assertNavigationIcon( + source = kotlinSource("annotation class KotlinAnnotation"), + expectedIconClass = "annotation-class-kt", + expectedNavLinkText = "KotlinAnnotation" + ) + } + + @Test + fun `should add icon styles to java annotation navigation item`() { + assertNavigationIcon( + source = javaSource( + className = "JavaAnnotation", + source = "public @interface JavaAnnotation {}" + ), + expectedIconClass = "annotation-class", + expectedNavLinkText = "JavaAnnotation" + ) + } + + + @Test + fun `should add icon styles to kotlin interface navigation item`() { + assertNavigationIcon( + source = kotlinSource("interface KotlinInterface"), + expectedIconClass = "interface-kt", + expectedNavLinkText = "KotlinInterface" + ) + } + + @Test + fun `should add icon styles to java interface navigation item`() { + assertNavigationIcon( + source = javaSource( + className = "JavaInterface", + source = "public interface JavaInterface {}" + ), + expectedIconClass = "interface", + expectedNavLinkText = "JavaInterface" + ) + } + + @Test + fun `should add icon styles to kotlin function navigation item`() { + assertNavigationIcon( + source = kotlinSource("fun ktFunction() {}"), + expectedIconClass = "function", + expectedNavLinkText = "ktFunction()" + ) + } + + @Test + fun `should add icon styles to kotlin exception class navigation item`() { + assertNavigationIcon( + source = kotlinSource("class KotlinException : Exception() {}"), + expectedIconClass = "exception-class", + expectedNavLinkText = "KotlinException" + ) + } + + @Test + fun `should add icon styles to kotlin object navigation item`() { + assertNavigationIcon( + source = kotlinSource("object KotlinObject {}"), + expectedIconClass = "object", + expectedNavLinkText = "KotlinObject" + ) + } + + @Test + fun `should add icon styles to kotlin val navigation item`() { + assertNavigationIcon( + source = kotlinSource("val value: String? = null"), + expectedIconClass = "val", + expectedNavLinkText = "value" + ) + } + + @Test + fun `should add icon styles to kotlin var navigation item`() { + assertNavigationIcon( + source = kotlinSource("var variable: String? = null"), + expectedIconClass = "var", + expectedNavLinkText = "variable" + ) + } + + private fun kotlinSource(source: String): String { + return """ + |/src/main/kotlin/com/example/Example.kt + |package com.example + | + |$source + """.trimIndent() + } + + private fun javaSource(className: String, source: String): String { + return """ + |/src/main/java/com/example/$className.java + |package com.example; + | + |$source + """.trimIndent() + } + + private fun assertNavigationIcon(source: String, expectedIconClass: String, expectedNavLinkText: String) { + val writerPlugin = TestOutputWriterPlugin() + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + val navigationGrid = content.selectNavigationGrid() + + val classNames = navigationGrid.child(0).classNames().toList() + assertEquals("nav-link-child", classNames[0]) + assertEquals("nav-icon", classNames[1]) + assertEquals(expectedIconClass, classNames[2]) + + val navLinkText = navigationGrid.child(1).text() + assertEquals(expectedNavLinkText, navLinkText) + } + } + } + + @Test + fun `should not generate nav link grids or icons for packages and modules`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/com/example/Example.kt + |package com.example + | + |class Example {} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + + assertEquals(3, content.size) + assertEquals("root-nav-submenu", content[0].id()) + assertEquals("root-nav-submenu-0", content[1].id()) + assertEquals("root-nav-submenu-0-0", content[2].id()) + + // there's 3 nav items, but only one icon + val navLinkGrids = content.select("span.nav-icon") + assertEquals(1, navLinkGrids.size) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/NavigationTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/NavigationTest.kt new file mode 100644 index 00000000..02074810 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/NavigationTest.kt @@ -0,0 +1,414 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.renderers.html.NavigationNodeIcon +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jsoup.nodes.Element +import utils.TestOutputWriterPlugin +import utils.navigationHtml +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class NavigationTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + @Test + fun `should sort alphabetically ignoring case`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/com/example/Sequences.kt + |package com.example + | + |fun <T> sequence(): Sequence<T> + | + |fun <T> Sequence(): Sequence<T> + | + |fun <T> Sequence<T>.any() {} + | + |interface Sequence<T> + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + assertEquals(6, content.size) + + // Navigation menu should be the following: + // - root + // - com.example + // - any() + // - Sequence interface + // - Sequence() + // - sequence() + + content[0].assertNavigationLink( + id = "root-nav-submenu", + text = "root", + address = "index.html", + ) + + content[1].assertNavigationLink( + id = "root-nav-submenu-0", + text = "com.example", + address = "root/com.example/index.html", + ) + + content[2].assertNavigationLink( + id = "root-nav-submenu-0-0", + text = "any()", + address = "root/com.example/any.html", + icon = NavigationNodeIcon.FUNCTION + ) + + content[3].assertNavigationLink( + id = "root-nav-submenu-0-1", + text = "Sequence", + address = "root/com.example/-sequence/index.html", + icon = NavigationNodeIcon.INTERFACE_KT + ) + + content[4].assertNavigationLink( + id = "root-nav-submenu-0-2", + text = "Sequence()", + address = "root/com.example/-sequence.html", + icon = NavigationNodeIcon.FUNCTION + ) + + content[5].assertNavigationLink( + id = "root-nav-submenu-0-3", + text = "sequence()", + address = "root/com.example/sequence.html", + icon = NavigationNodeIcon.FUNCTION + ) + } + } + } + + @Test + fun `should strike deprecated class link`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/com/example/SimpleDeprecatedClass.kt + |package com.example + | + |@Deprecated("reason") + |class SimpleDeprecatedClass {} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + assertEquals(3, content.size) + + // Navigation menu should be the following: + // - root + // - com.example + // - SimpleDeprecatedClass + + content[0].assertNavigationLink( + id = "root-nav-submenu", + text = "root", + address = "index.html", + ) + + content[1].assertNavigationLink( + id = "root-nav-submenu-0", + text = "com.example", + address = "root/com.example/index.html", + ) + + content[2].assertNavigationLink( + id = "root-nav-submenu-0-0", + text = "SimpleDeprecatedClass", + address = "root/com.example/-simple-deprecated-class/index.html", + icon = NavigationNodeIcon.CLASS_KT, + isStrikethrough = true + ) + } + } + } + + @Test + fun `should not strike pages where only one of N documentables is deprecated`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/com/example/File.kt + |package com.example + | + |/** + | * First + | */ + |@Deprecated("reason") + |fun functionWithCommonName() + | + |/** + | * Second + | */ + |fun functionWithCommonName() + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + assertEquals(3, content.size) + + // Navigation menu should be the following: + // - root + // - com.example + // - functionWithCommonName + + content[0].assertNavigationLink( + id = "root-nav-submenu", + text = "root", + address = "index.html", + ) + + content[1].assertNavigationLink( + id = "root-nav-submenu-0", + text = "com.example", + address = "root/com.example/index.html", + ) + + content[2].assertNavigationLink( + id = "root-nav-submenu-0-0", + text = "functionWithCommonName()", + address = "root/com.example/function-with-common-name.html", + icon = NavigationNodeIcon.FUNCTION, + isStrikethrough = false + ) + } + } + } + + @Test + fun `should have expandable classlikes`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/com/example/WithInner.kt + |package com.example + | + |class WithInner { + | // in-class functions should not be in navigation + | fun a() {} + | fun b() {} + | fun c() {} + | + | class InnerClass {} + | interface InnerInterface {} + | enum class InnerEnum {} + | object InnerObject {} + | annotation class InnerAnnotation {} + | companion object CompanionObject {} + |} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + assertEquals(9, content.size) + + // Navigation menu should be the following, sorted by name: + // - root + // - com.example + // - WithInner + // - CompanionObject + // - InnerAnnotation + // - InnerClass + // - InnerEnum + // - InnerInterface + // - InnerObject + + content[0].assertNavigationLink( + id = "root-nav-submenu", + text = "root", + address = "index.html", + ) + + content[1].assertNavigationLink( + id = "root-nav-submenu-0", + text = "com.example", + address = "root/com.example/index.html", + ) + + content[2].assertNavigationLink( + id = "root-nav-submenu-0-0", + text = "WithInner", + address = "root/com.example/-with-inner/index.html", + icon = NavigationNodeIcon.CLASS_KT + ) + + content[3].assertNavigationLink( + id = "root-nav-submenu-0-0-0", + text = "CompanionObject", + address = "root/com.example/-with-inner/-companion-object/index.html", + icon = NavigationNodeIcon.OBJECT + ) + + content[4].assertNavigationLink( + id = "root-nav-submenu-0-0-1", + text = "InnerAnnotation", + address = "root/com.example/-with-inner/-inner-annotation/index.html", + icon = NavigationNodeIcon.ANNOTATION_CLASS_KT + ) + + content[5].assertNavigationLink( + id = "root-nav-submenu-0-0-2", + text = "InnerClass", + address = "root/com.example/-with-inner/-inner-class/index.html", + icon = NavigationNodeIcon.CLASS_KT + ) + + content[6].assertNavigationLink( + id = "root-nav-submenu-0-0-3", + text = "InnerEnum", + address = "root/com.example/-with-inner/-inner-enum/index.html", + icon = NavigationNodeIcon.ENUM_CLASS_KT + ) + + content[7].assertNavigationLink( + id = "root-nav-submenu-0-0-4", + text = "InnerInterface", + address = "root/com.example/-with-inner/-inner-interface/index.html", + icon = NavigationNodeIcon.INTERFACE_KT + ) + + content[8].assertNavigationLink( + id = "root-nav-submenu-0-0-5", + text = "InnerObject", + address = "root/com.example/-with-inner/-inner-object/index.html", + icon = NavigationNodeIcon.OBJECT + ) + } + } + } + + @Test + fun `should be able to have deeply nested classlikes`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/com/example/DeeplyNested.kt + |package com.example + | + |class DeeplyNested { + | class FirstLevelClass { + | interface SecondLevelInterface { + | object ThirdLevelObject { + | annotation class FourthLevelAnnotation {} + | } + | } + | } + |} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + assertEquals(7, content.size) + + // Navigation menu should be the following + // - root + // - com.example + // - DeeplyNested + // - FirstLevelClass + // - SecondLevelInterface + // - ThirdLevelObject + // - FourthLevelAnnotation + + content[0].assertNavigationLink( + id = "root-nav-submenu", + text = "root", + address = "index.html", + ) + + content[1].assertNavigationLink( + id = "root-nav-submenu-0", + text = "com.example", + address = "root/com.example/index.html", + ) + + content[2].assertNavigationLink( + id = "root-nav-submenu-0-0", + text = "DeeplyNested", + address = "root/com.example/-deeply-nested/index.html", + icon = NavigationNodeIcon.CLASS_KT + ) + + content[3].assertNavigationLink( + id = "root-nav-submenu-0-0-0", + text = "FirstLevelClass", + address = "root/com.example/-deeply-nested/-first-level-class/index.html", + icon = NavigationNodeIcon.CLASS_KT + ) + + content[4].assertNavigationLink( + id = "root-nav-submenu-0-0-0-0", + text = "SecondLevelInterface", + address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/index.html", + icon = NavigationNodeIcon.INTERFACE_KT + ) + + content[5].assertNavigationLink( + id = "root-nav-submenu-0-0-0-0-0", + text = "ThirdLevelObject", + address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/" + + "-third-level-object/index.html", + icon = NavigationNodeIcon.OBJECT + ) + + content[6].assertNavigationLink( + id = "root-nav-submenu-0-0-0-0-0-0", + text = "FourthLevelAnnotation", + address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/" + + "-third-level-object/-fourth-level-annotation/index.html", + icon = NavigationNodeIcon.ANNOTATION_CLASS_KT + ) + } + } + } + + private fun Element.assertNavigationLink( + id: String, text: String, address: String, icon: NavigationNodeIcon? = null, isStrikethrough: Boolean = false + ) { + assertEquals(id, this.id()) + + val link = this.selectFirst("a") + assertNotNull(link) + assertEquals(text, link.text()) + assertEquals(address, link.attr("href")) + if (icon != null) { + val iconStyles = + this.selectFirst("div.overview span.nav-link-grid")?.child(0)?.classNames()?.toList() ?: emptyList() + assertEquals(3, iconStyles.size) + assertEquals("nav-link-child", iconStyles[0]) + assertEquals(icon.style(), "${iconStyles[1]} ${iconStyles[2]}") + } + if (isStrikethrough) { + val textInsideStrikethrough = link.selectFirst("strike")?.text() + assertEquals(text, textInsideStrikethrough) + } else { + assertNull(link.selectFirst("strike")) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SearchbarDataInstallerTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SearchbarDataInstallerTest.kt new file mode 100644 index 00000000..a5f5feb5 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SearchbarDataInstallerTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import utils.TestOutputWriterPlugin +import utils.pagesJson +import kotlin.test.Test +import kotlin.test.assertEquals + +class SearchbarDataInstallerTest: BaseAbstractTest() { + + @Test // see #2289 + fun `should display description of root declarations without a leading dot`() { + val configuration = dokkaConfiguration { + moduleName = "Dokka Module" + + sourceSets { + sourceSet { + sourceRoots = listOf("src/kotlin/Test.kt") + } + } + } + + val source = """ + |/src/kotlin/Test.kt + | + |class Test + | + """.trimIndent() + + val writerPlugin = TestOutputWriterPlugin() + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val searchRecords = writerPlugin.writer.pagesJson() + + assertEquals( + "Test", + searchRecords.find { record -> record.name == "class Test" }?.description ?: "" + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SourceSetDependentHintTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SourceSetDependentHintTest.kt new file mode 100644 index 00000000..e3c28984 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SourceSetDependentHintTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jetbrains.dokka.pages.TextStyle +import renderers.testPage +import testApi.testRunner.defaultSourceSet +import utils.Div +import utils.match +import java.io.File +import kotlin.test.Test + +class SourceSetDependentHintTest : HtmlRenderingOnlyTestBase() { + + private val pl1 = defaultSourceSet.copy( + "pl1", + defaultSourceSet.sourceSetID.copy(sourceSetName = "pl1"), + analysisPlatform = Platform.js, + sourceRoots = setOf(File("pl1")) + ) + private val pl2 = defaultSourceSet.copy( + "pl2", + defaultSourceSet.sourceSetID.copy(sourceSetName = "pl2"), + analysisPlatform = Platform.jvm, + sourceRoots = setOf(File("pl1")) + ) + private val pl3 = defaultSourceSet.copy( + "pl3", + defaultSourceSet.sourceSetID.copy(sourceSetName = "pl3"), + analysisPlatform = Platform.native, + sourceRoots = setOf(File("pl1")) + ) + + @Test + fun platformIndependentCase() { + val page = testPage { + sourceSetDependentHint(sourceSets = setOf(pl1, pl2, pl3), styles = setOf(TextStyle.Block)) { + text("a") + text("b") + text("c") + } + } + + HtmlRenderer(context).render(page) + renderedContent.match(Div(Div(Div("abc")))) + } + + @Test + fun completelyDivergentCase() { + val page = testPage { + sourceSetDependentHint(sourceSets = setOf(pl1, pl2, pl3), styles = setOf(TextStyle.Block)) { + text("a", sourceSets = setOf(pl1)) + text("b", sourceSets = setOf(pl2)) + text("c", sourceSets = setOf(pl3)) + } + } + + HtmlRenderer(context).render(page) + renderedContent.match(Div(Div(Div("a")), Div(Div("b")), Div(Div("c")))) + } + + @Test + fun overlappingCase() { + val page = testPage { + sourceSetDependentHint(sourceSets = setOf(pl1, pl2), styles = setOf(TextStyle.Block)) { + text("a", sourceSets = setOf(pl1)) + text("b", sourceSets = setOf(pl1, pl2)) + text("c", sourceSets = setOf(pl2)) + } + } + + HtmlRenderer(context).render(page) + renderedContent.match(Div(Div(Div("ab")), Div(Div("bc")))) + } + + @Test + fun caseThatCanBeSimplified() { + val page = testPage { + sourceSetDependentHint(sourceSets = setOf(pl1, pl2), styles = setOf(TextStyle.Block)) { + text("a", sourceSets = setOf(pl1, pl2)) + text("b", sourceSets = setOf(pl1)) + text("b", sourceSets = setOf(pl2)) + } + } + + HtmlRenderer(context).render(page) + renderedContent.match(Div(Div(Div("ab")))) + } + + @Test + fun caseWithGroupBreakingSimplification() { + val page = testPage { + sourceSetDependentHint(sourceSets = setOf(pl1, pl2), styles = setOf(TextStyle.Block)) { + group(styles = setOf(TextStyle.Block)) { + text("a", sourceSets = setOf(pl1, pl2)) + text("b", sourceSets = setOf(pl1)) + } + text("b", sourceSets = setOf(pl2)) + } + } + + HtmlRenderer(context).render(page) + renderedContent.match(Div(Div(Div(Div("ab"))), Div(Div(Div("a"), "b")))) + } + + @Test + fun caseWithGroupNotBreakingSimplification() { + val page = testPage { + sourceSetDependentHint(sourceSets = setOf(pl1, pl2)) { + group { + text("a", sourceSets = setOf(pl1, pl2)) + text("b", sourceSets = setOf(pl1)) + } + text("b", sourceSets = setOf(pl2)) + } + } + + HtmlRenderer(context).render(page) + renderedContent.match(Div(Div("ab"))) + } + + @Test + fun partiallyUnifiedCase() { + val page = testPage { + sourceSetDependentHint(sourceSets = setOf(pl1, pl2, pl3), styles = setOf(TextStyle.Block)) { + text("a", sourceSets = setOf(pl1)) + text("a", sourceSets = setOf(pl2)) + text("b", sourceSets = setOf(pl3)) + } + } + + HtmlRenderer(context).render(page) + renderedContent.match(Div(Div(Div("a")), Div(Div("b")))) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SourceSetFilterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SourceSetFilterTest.kt new file mode 100644 index 00000000..b461bfcd --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SourceSetFilterTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import signatures.renderedContent +import utils.TestOutputWriterPlugin +import kotlin.test.Test +import kotlin.test.assertEquals + +class SourceSetFilterTest : BaseAbstractTest() { + + @Test // see #3011 + fun `should separate multiple data-filterable attribute values with comma`() { + val configuration = dokkaConfiguration { + moduleName = "Dokka Module" + + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin/testing/Test.kt") + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf("src/jvmMain/kotlin/testing/Test.kt") + } + } + } + + val source = """ + |/src/commonMain/kotlin/testing/Test.kt + |package testing + | + |expect open class Test + | + |/src/jvmMain/kotlin/testing/Test.kt + |package testing + | + |actual open class Test + """.trimIndent() + + val writerPlugin = TestOutputWriterPlugin() + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val packagePage = writerPlugin.writer.renderedContent("-dokka -module/testing/index.html") + + val testClassRow = packagePage + .select("div[data-togglable=TYPE]") + .select("div[class=table-row]") + .single() + + assertEquals("Dokka Module/common,Dokka Module/jvm", testClassRow.attr("data-filterable-current")) + assertEquals("Dokka Module/common,Dokka Module/jvm", testClassRow.attr("data-filterable-set")) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/TabbedContentTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/TabbedContentTest.kt new file mode 100644 index 00000000..090127fd --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/TabbedContentTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jsoup.nodes.Element +import signatures.renderedContent +import utils.TestOutputWriterPlugin +import kotlin.test.Test +import kotlin.test.assertEquals + +class TabbedContentTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(commonStdlibPath!!) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + } + } + + private fun Element.getTabbedRow(type: String) = select(".table-row[data-togglable=$type]") + private fun Element.getTabbedTable(type: String) = select("div[data-togglable=$type] .table") + private fun Element.getMainContentDataType() = selectFirst(".main-content")?.attr("data-page-type") + + @Test + fun `should have correct tabbed content type`() { + val source = """ + |/src/main/kotlin/test/Test.kt + |package example + | + |val p = 0 + |fun foo() = 0 + | + | class A(val d: Int = 0) { + | class Success(): Result() + | class Failed(): Result() + | + | fun fn() = 0 + | } + | + | fun A.fn() = 0 + | fun A.fn2() = 0 + | fun A.fn3() = 0 + | val A.p = 0 + | val A.p2 = 0 + """ + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val classContent = writerPlugin.writer.renderedContent("root/example/-a/index.html") + assertEquals(1, classContent.getTabbedTable("CONSTRUCTOR").size) + assertEquals(1, classContent.getTabbedTable("PROPERTY").size) + assertEquals(1, classContent.getTabbedTable("CONSTRUCTOR").size) + assertEquals(1, classContent.getTabbedTable("FUNCTION").size) + assertEquals(1, classContent.getTabbedTable("TYPE").size) + assertEquals(3, classContent.getTabbedRow("EXTENSION_FUNCTION").size) + assertEquals(2, classContent.getTabbedRow("EXTENSION_PROPERTY").size) + assertEquals("classlike", classContent.getMainContentDataType()) + + val packagePage = writerPlugin.writer.renderedContent("root/example/index.html") + assertEquals(1, packagePage.getTabbedTable("TYPE").size) + assertEquals(1, packagePage.getTabbedTable("PROPERTY").size) + assertEquals(1, packagePage.getTabbedTable("FUNCTION").size) + assertEquals(3, packagePage.getTabbedRow("EXTENSION_FUNCTION").size) + assertEquals(2, packagePage.getTabbedRow("EXTENSION_PROPERTY").size) + assertEquals("package", packagePage.getMainContentDataType()) + } + } + } + + @Test + fun `should not have Types-tab where there are not types`() { + val source = """ + |/src/main/kotlin/test/Test.kt + |package example + | + |val p = 0 + |fun foo() = 0 + | + |/src/main/kotlin/test/PackageTwo.kt + |package example2 + | + |class A + """ + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val packagePage = writerPlugin.writer.renderedContent("root/example/index.html") + assertEquals(0, packagePage.select("*[data-togglable=TYPE]").size) + assertEquals(1, packagePage.getTabbedTable("PROPERTY").size) + assertEquals(1, packagePage.getTabbedTable("FUNCTION").size) + + val packagePage2 = writerPlugin.writer.renderedContent("root/example2/index.html") + assertEquals(2, packagePage2.select("*[data-togglable=TYPE]").size) + assertEquals(0, packagePage2.getTabbedTable("PROPERTY").size) + assertEquals(0, packagePage2.getTabbedTable("FUNCTION").size) + } + } + } + + @Test + fun `should have correct order of members and extensions`() { + val source = """ + |/src/main/kotlin/test/Test.kt + |package example + | + |val p = 0 + |fun foo() = 0 + | + |class A(val d: Int = 0) { + | fun fn() = 0 + | fun a() = 0 + | fun g() = 0 + |} + | + | fun A.fn() = 0 + """ + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val classContent = writerPlugin.writer.renderedContent("root/example/-a/index.html") + val funTable = classContent.select("div[data-togglable=FUNCTION] .table") + val orders = + funTable.select(".table-row").map { it.attr("data-togglable") } + assertEquals(listOf("", "", "EXTENSION_FUNCTION", ""), orders) + val names = + funTable.select(".main-subrow .inline-flex a").map { it.text() } + assertEquals(listOf("a", "fn", "fn", "g"), names) + } + } + } + + @Test + fun `should have expected order of content types within a members tab`() { + val source = """ + |/src/main/kotlin/test/Result.kt + |package example + | + |class Result(val d: Int = 0) { + | class Success(): Result() + | + | val isFailed = false + | fun reset() = 0 + | fun String.extension() = 0 + |} + """ + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val classContent = writerPlugin.writer.renderedContent("root/example/-result/index.html") + val tabSectionNames = classContent.select("div .tabs-section-body > div[data-togglable]") + .map { it.attr("data-togglable") } + + val expectedOrder = listOf("CONSTRUCTOR", "TYPE", "PROPERTY", "FUNCTION") + + assertEquals(expectedOrder.size, tabSectionNames.size) + expectedOrder.forEachIndexed { index, element -> + assertEquals(element, tabSectionNames[index]) + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/TextStylesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/TextStylesTest.kt new file mode 100644 index 00000000..0ca4e245 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/TextStylesTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jetbrains.dokka.pages.TextStyle +import org.jetbrains.dokka.pages.TokenStyle +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import renderers.testPage +import utils.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class TextStylesTest : HtmlRenderingOnlyTestBase() { + @Test + fun `should include bold`(){ + val page = testPage { + text("bold text", styles = setOf(TextStyle.Bold)) + } + HtmlRenderer(context).render(page) + renderedContent.match(B("bold text")) + } + + @Test + fun `should include italics`(){ + val page = testPage { + text("italics text", styles = setOf(TextStyle.Italic)) + } + HtmlRenderer(context).render(page) + renderedContent.match(I("italics text")) + } + + @Test + fun `should include strikethrought`(){ + val page = testPage { + text("strike text", styles = setOf(TextStyle.Strikethrough)) + } + HtmlRenderer(context).render(page) + renderedContent.match(STRIKE("strike text")) + } + + @Test + fun `should include token styles`(){ + val page = testPage { + text("keyword", styles = setOf(TokenStyle.Keyword)) + } + HtmlRenderer(context).render(page) + renderedContent.match(Span("keyword")) + val lastChild = renderedContent.children().last() ?: throw IllegalStateException("No element found") + assertEquals(lastChild.attr("class"), "token keyword") + } + + @Test + fun `should include multiple styles at one`(){ + val page = testPage { + text( + "styled text", + styles = setOf( + TextStyle.Strikethrough, + TextStyle.Bold, + TextStyle.Indented, + TextStyle.UnderCoverText, + TextStyle.BreakableAfter + ) + ) + } + HtmlRenderer(context).render(page) + renderedContent.match(STRIKE(B("styled text"))) + //Our dsl swallows nbsp so i manually check for it + files.contents.getValue("test-page.html").contains(" <strike><b>styled text</b></strike>") + } + + @Test + fun `should include blockquote`() { + val page = testPage { + group(styles = setOf(TextStyle.Quotation)) { + text("blockquote text") + } + } + HtmlRenderer(context).render(page) + renderedContent.match(BlockQuote("blockquote text")) + } + + @Test + fun `should include var`() { + val page = testPage { + group(styles = setOf(TextStyle.Var)) { + text("variable") + } + } + HtmlRenderer(context).render(page) + println(renderedContent) + renderedContent.match(Var("variable")) + } + + @Test + fun `should include underlined text`() { + val page = testPage { + group(styles = setOf(TextStyle.Underlined)) { + text("underlined text") + } + } + HtmlRenderer(context).render(page) + println(renderedContent) + renderedContent.match(U("underlined text")) + } + + override val renderedContent: Element + get() = files.contents.getValue("test-page.html").let { Jsoup.parse(it) }.select("#content").single() +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/resourceLinks/ResourceLinksTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/resourceLinks/ResourceLinksTest.kt new file mode 100644 index 00000000..c3302f70 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/resourceLinks/ResourceLinksTest.kt @@ -0,0 +1,301 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package resourceLinks + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.PluginConfigurationImpl +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.base.renderers.html.TEMPLATE_REPLACEMENT +import org.jetbrains.dokka.base.templating.toJsonString +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement +import org.jetbrains.dokka.transformers.pages.PageTransformer +import org.jsoup.Jsoup +import org.jsoup.nodes.TextNode +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import utils.TestOutputWriterPlugin +import utils.assertContains +import java.io.File +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ResourceLinksTest : BaseAbstractTest() { + class TestResourcesAppenderPlugin(val resources: List<String>) : DokkaPlugin() { + class TestResourcesAppender(val resources: List<String>) : PageTransformer { + override fun invoke(input: RootPageNode) = input.transformContentPagesTree { + it.modified( + embeddedResources = it.embeddedResources + resources + ) + } + } + + val appender by extending { + plugin<DokkaBase>().htmlPreprocessors with TestResourcesAppender(resources) + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = + PluginApiPreviewAcknowledgement + } + + @Test + fun resourceLinksTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/test/Test.kt") + } + } + } + val absoluteResources = listOf( + "https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css", + "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" + ) + val relativeResources = listOf( + "test/relativePath.js", + "test/relativePath.css" + ) + + val source = + """ + |/src/main/kotlin/test/Test.kt + |package example + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + testInline( + source, + configuration, + pluginOverrides = listOf(TestResourcesAppenderPlugin(absoluteResources + relativeResources), writerPlugin) + ) { + renderingStage = { _, _ -> + Jsoup + .parse(writerPlugin.writer.contents.getValue("root/example.html")) + .head() + .select("link, script") + .let { + absoluteResources.forEach { r -> + assertTrue(it.`is`("[href=$r], [src=$r]")) + } + relativeResources.forEach { r -> + assertTrue(it.`is`("[href=../$r] , [src=../$r]")) + } + } + } + } + } + + @ParameterizedTest + @ValueSource(booleans = [true, false]) + fun resourceCustomPreprocessorTest(isMultiModule: Boolean) { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/test/Test.kt") + } + } + delayTemplateSubstitution = isMultiModule + pluginsConfigurations = mutableListOf( + PluginConfigurationImpl( + DokkaBase::class.java.canonicalName, + DokkaConfiguration.SerializationFormat.JSON, + toJsonString( + DokkaBaseConfiguration( + customStyleSheets = listOf(File("test/customStyle.css")), + customAssets = listOf(File("test/customImage.svg")) + ) + ) + ) + ) + } + val source = + """ + |/src/main/kotlin/test/Test.kt + |package example + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + run { + if (isMultiModule) { + assertNull(writerPlugin.writer.contents["images/customImage.svg"]) + assertNull(writerPlugin.writer.contents["styles/customStyle.css"]) + } else { + assertNotNull(writerPlugin.writer.contents["images/customImage.svg"]) + assertNotNull(writerPlugin.writer.contents["styles/customStyle.css"]) + } + if (isMultiModule) { + Jsoup + .parse(writerPlugin.writer.contents.getValue("example.html")) + .head() + .select("link, script") + .let { + listOf("styles/customStyle.css").forEach { r -> + assertTrue(it.`is`("[href=$TEMPLATE_REPLACEMENT$r]")) + } + } + } else { + Jsoup + .parse(writerPlugin.writer.contents.getValue("root/example.html")) + .head() + .select("link, script") + .let { + listOf("styles/customStyle.css").forEach { r -> + assertTrue(it.`is`("[href=../$r], [src=../$r]")) + } + } + } + } + } + } + } + + @Test + fun resourceMultiModuleLinksTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/test/Test.kt") + } + } + delayTemplateSubstitution = false + } + val absoluteResources = listOf( + "https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css", + "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" + ) + val relativeResources = listOf( + "test/relativePath.js", + "test/relativePath.css" + ) + + val source = + """ + |/src/main/kotlin/test/Test.kt + |package example + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + testInline( + source, + configuration, + pluginOverrides = listOf(TestResourcesAppenderPlugin(absoluteResources + relativeResources), writerPlugin) + ) { + renderingStage = { _, _ -> + run { + assertNull(writerPlugin.writer.contents["scripts/relativePath.js"]) + assertNull(writerPlugin.writer.contents["styles/relativePath.js"]) + Jsoup + .parse(writerPlugin.writer.contents.getValue("root/example.html")) + .head() + .select("link, script") + .let { + absoluteResources.forEach { r -> + assertTrue(it.`is`("[href=$r], [src=$r]")) + } + relativeResources.forEach { r -> + assertTrue(it.`is`("[href=../$r] , [src=../$r]")) + } + } + } + } + } + } + + @Test // see #3040; plain text added to <head> can be rendered by engines inside <body> as well + fun `should not add unknown resources as text to the head or body section`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + + pluginsConfigurations = mutableListOf( + PluginConfigurationImpl( + DokkaBase::class.java.canonicalName, + DokkaConfiguration.SerializationFormat.JSON, + toJsonString( + DokkaBaseConfiguration( + customAssets = listOf(File("test/unknown-file.ext")) + ) + ) + ) + ) + } + + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package test + | + |class Test + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val testClassPage = writerPlugin.writer.contents + .getValue("root/test/-test/-test.html") + .let { Jsoup.parse(it) } + + val headChildNodes = testClassPage.head().childNodes() + assertTrue("<head> section should not contain non-blank text nodes") { + headChildNodes.all { it !is TextNode || it.isBlank } + } + + val bodyChildNodes = testClassPage.body().childNodes() + assertTrue("<body> section should not contain non-blank text nodes. Something leaked from head?") { + bodyChildNodes.all { it !is TextNode || it.isBlank } + } + } + } + } + + @Test + fun `should load script as defer if name ending in _deferred`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/test/Test.kt + |package test + | + |class Test + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val generatedFiles = writerPlugin.writer.contents + + assertContains(generatedFiles.keys, "scripts/symbol-parameters-wrapper_deferred.js") + + val scripts = generatedFiles.getValue("root/test/-test/-test.html").let { Jsoup.parse(it) }.select("script") + val deferredScriptSources = scripts.filter { element -> element.hasAttr("defer") }.map { it.attr("src") } + + // important to check symbol-parameters-wrapper_deferred specifically since it might break some features + assertContains(deferredScriptSources, "../../../scripts/symbol-parameters-wrapper_deferred.js") + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/AbstractRenderingTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/AbstractRenderingTest.kt new file mode 100644 index 00000000..4c4bbc4c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/AbstractRenderingTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package signatures + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.select.Elements +import utils.TestOutputWriterPlugin +import java.nio.file.Path +import java.nio.file.Paths + +abstract class AbstractRenderingTest : BaseAbstractTest() { + val testDataDir: Path = getTestDataDir("multiplatform/basicMultiplatformTest").toAbsolutePath() + + val configuration = dokkaConfiguration { + moduleName = "example" + sourceSets { + val common = sourceSet { + name = "common" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf(Paths.get("$testDataDir/commonMain/kotlin").toString()) + } + val jvmAndJsSecondCommonMain = sourceSet { + name = "jvmAndJsSecondCommonMain" + displayName = "jvmAndJsSecondCommonMain" + analysisPlatform = "common" + dependentSourceSets = setOf(common.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmAndJsSecondCommonMain/kotlin").toString()) + } + sourceSet { + name = "js" + displayName = "js" + analysisPlatform = "js" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jsMain/kotlin").toString()) + } + sourceSet { + name = "jvm" + displayName = "jvm" + analysisPlatform = "jvm" + dependentSourceSets = setOf(common.value.sourceSetID, jvmAndJsSecondCommonMain.value.sourceSetID) + sourceRoots = listOf(Paths.get("$testDataDir/jvmMain/kotlin").toString()) + } + } + } + + fun TestOutputWriterPlugin.renderedContent(path: String): Element = writer.contents.getValue(path) + .let { Jsoup.parse(it) }.select("#content").single() + + fun TestOutputWriterPlugin.renderedDivergentContent(path: String): Elements = + renderedContent(path).select("div.divergent-group") + + fun TestOutputWriterPlugin.renderedSourceDependentContent(path: String): Elements = + renderedContent(path).select("div.sourceset-dependent-content") + + val Element.brief: String + get() = children().select("p").text() + + val Element.rawBrief: String + get() = children().select("p").html() +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/DivergentSignatureTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/DivergentSignatureTest.kt new file mode 100644 index 00000000..509dd6e7 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/DivergentSignatureTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package signatures + +import utils.TestOutputWriterPlugin +import kotlin.test.Test +import kotlin.test.assertEquals + + +class DivergentSignatureTest : AbstractRenderingTest() { + + @Test + fun `group { common + jvm + js }`() { + + val writerPlugin = TestOutputWriterPlugin() + + testFromData( + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.renderedSourceDependentContent("example/example/-clock/get-time.html") + + assertEquals(3, content.count()) + val sourceSets = listOf("example/common", "example/js", "example/jvm") + sourceSets.forEach { + assertEquals("", content.select("[data-togglable=$it]").single().brief) + } + } + } + } + + @Test + fun `group { common + jvm }, group { js }`() { + + val writerPlugin = TestOutputWriterPlugin() + + testFromData( + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.renderedSourceDependentContent("example/example/-clock/get-times-in-millis.html") + + assertEquals(3, content.count()) + assertEquals("Time in minis", content.select("[data-togglable=example/common]").single().brief) + assertEquals("Time in minis", content.select("[data-togglable=example/jvm]").single().brief) + assertEquals("JS implementation of getTimeInMillis", content.select("[data-togglable=example/js]").single().brief) + } + } + } + + @Test + fun `group { js }, group { jvm }, group { js }`() { + + val writerPlugin = TestOutputWriterPlugin() + + testFromData( + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.renderedSourceDependentContent("example/example/-clock/get-year.html") + assertEquals(3, content.count()) + assertEquals("JVM custom kdoc", content.select("[data-togglable=example/jvm]").single().brief) + assertEquals("JS custom kdoc", content.select("[data-togglable=example/js]").single().brief) + assertEquals("", content.select("[data-togglable=example/common]").single().brief) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt new file mode 100644 index 00000000..13d1947f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package signatures + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.jdk +import utils.A +import utils.Span +import utils.TestOutputWriterPlugin +import utils.match +import kotlin.test.Ignore +import kotlin.test.Test + +class FunctionalTypeConstructorsSignatureTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(commonStdlibPath!!, jvmStdlibPath!!) + externalDocumentationLinks = listOf( + stdlibExternalDocumentationLink, + DokkaConfiguration.ExternalDocumentationLink.Companion.jdk(8) + ) + } + } + } + + private val jvmConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(jvmStdlibPath ?: throw IllegalStateException("JVM stdlib is not found")) + externalDocumentationLinks = listOf( + stdlibExternalDocumentationLink, + DokkaConfiguration.ExternalDocumentationLink.Companion.jdk(8) + ) + } + } + } + + fun source(signature: String) = + """ + |/src/main/kotlin/test/Test.kt + |package example + | + | $signature + """.trimIndent() + + @Test + fun `kotlin normal function`() { + val source = source("val nF: Function1<Int, String> = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", A("nF"), ": (", A("Int"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `kotlin syntactic sugar function`() { + val source = source("val nF: (Int) -> String = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", A("nF"), ": (", A("Int"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `kotlin syntactic sugar extension function`() { + val source = source("val nF: Boolean.(Int) -> String = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", A("nF"), ": ", A("Boolean"), ".(", A("Int"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `kotlin syntactic sugar function with param name`() { + val source = source("val nF: (param: Int) -> String = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", A("nF"), ": (param: ", A("Int"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `kotlin syntactic sugar function with param name of generic and functional type`() { + val source = source(""" + | @Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) + | @MustBeDocumented + | annotation class Fancy + | + | fun <T> f(): (param1: T, param2: @Fancy ()->Unit) -> String " + """.trimIndent()) + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, configuration, pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").lastSignature().match( + "fun <", A("T"), "> ", + A("f"), "(): (param1:", A("T"), + ", param2: ", Span("@", A("Fancy")), " () -> ", A("Unit"), + ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + @Ignore // Add coroutines on classpath and get proper import + @Test + fun `kotlin normal suspendable function`() { + val source = source("val nF: SuspendFunction1<Int, String> = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", A("nF"), ": suspend (", A("Int"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `kotlin syntactic sugar suspendable function`() { + val source = source("val nF: suspend (Int) -> String = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", A("nF"), ": suspend (", A("Int"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `kotlin syntactic sugar suspendable extension function`() { + val source = source("val nF: suspend Boolean.(Int) -> String = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", A("nF"), ": suspend ", A("Boolean"), ".(", A("Int"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `kotlin syntactic sugar suspendable function with param name`() { + val source = source("val nF: suspend (param: Int) -> String = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", A("nF"), ": suspend (param: ", A("Int"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `kotlin syntactic sugar suspendable fancy function with param name`() { + val source = + source("val nF: suspend (param1: suspend Boolean.(param2: List<Int>) -> Boolean) -> String = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "val ", + A("nF"), + ": suspend (param1: suspend", + A("Boolean"), + ".(param2: ", + A("List"), + "<", + A("Int"), + ">) -> ", + A("Boolean"), + ") -> ", + A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `java with java function`() { + val source = """ + |/src/main/kotlin/test/JavaClass.java + |package example + | + |public class JavaClass { + | public java.util.function.Function<Integer, String> javaFunction = null; + |} + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-java-class/index.html").lastSignature().match( + "open var ", A("javaFunction"), ": (", A("Integer"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `java with kotlin function`() { + val source = """ + |/src/main/kotlin/test/JavaClass.java + |package example + | + |public class JavaClass { + | public kotlin.jvm.functions.Function1<Integer, String> kotlinFunction = null; + |} + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + jvmConfiguration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-java-class/index.html").lastSignature().match( + "open var ", A("kotlinFunction"), ": (", A("Integer"), ") -> ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt new file mode 100644 index 00000000..b5e2a9c3 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt @@ -0,0 +1,461 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package signatures + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import utils.A +import utils.Span +import utils.TestOutputWriterPlugin +import utils.match +import utils.OnlyDescriptors +import kotlin.test.Test +import kotlin.test.assertEquals + +class InheritedAccessorsSignatureTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf( + commonStdlibPath ?: throw IllegalStateException("Common stdlib is not found"), + jvmStdlibPath ?: throw IllegalStateException("JVM stdlib is not found") + ) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + } + } + + @OnlyDescriptors("'var' expected but found: 'open var'") + @Test + fun `should collapse accessor functions inherited from java into the property`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | public int getA() { return a; } + | public void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/test/-b/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals( + 3, signatures.size, + "Expected 3 signatures: class signature, constructor and property" + ) + + val property = signatures[2] + property.match( + "var ", A("a"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + + writerPlugin.writer.renderedContent("root/test/-a/index.html").let { javaClassContent -> + val signatures = javaClassContent.signature().toList() + assertEquals( + 3, signatures.size, + "Expected 3 signatures: class signature, default constructor and property" + ) + + val property = signatures[2] + property.match( + "open var ", A("a"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + } + + @OnlyDescriptors("'var' expected but found: 'open var'") + @Test + fun `should render as val if inherited java property has no setter`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | public int getA() { return a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/test/-b/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals(3, signatures.size, "Expected 3 signatures: class signature, constructor and property") + + val property = signatures[2] + property.match( + "val ", A("a"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + + writerPlugin.writer.renderedContent("root/test/-a/index.html").let { javaClassContent -> + val signatures = javaClassContent.signature().toList() + assertEquals( + 3, + signatures.size, + "Expected 3 signatures: class signature, default constructor and property" + ) + + val property = signatures[2] + property.match( + "open val ", A("a"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + } + + @Test + fun `should keep inherited java setter as a regular function due to inaccessible property`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | public void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/test/-b/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals(3, signatures.size, "Expected 3 signatures: class signature, constructor and setter") + + val setterFunction = signatures[2] + setterFunction.match( + "open fun ", A("setA"), "(", Parameters( + Parameter("a: ", A("Int")) + ), ")", + ignoreSpanWithTokenStyle = true + ) + } + + writerPlugin.writer.renderedContent("root/test/-a/index.html").let { javaClassContent -> + val signatures = javaClassContent.signature().toList() + assertEquals( + 3, + signatures.size, + "Expected 3 signatures: class signature, default constructor and setter" + ) + + val setterFunction = signatures[2] + setterFunction.match( + "open fun ", A("setA"), "(", Parameters( + Parameter("a: ", A("Int")) + ), ")", + ignoreSpanWithTokenStyle = true + ) + } + } + } + } + + @OnlyDescriptors("'var' expected but found: 'open var'") + @Test + fun `should keep inherited java accessor lookalikes if underlying function is public`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | public int a = 1; + | public int getA() { return a; } + | public void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val signatures = writerPlugin.writer.renderedContent("root/test/-b/index.html").signature().toList() + assertEquals( + 5, signatures.size, + "Expected 5 signatures: class signature, constructor, property and two accessor lookalikes" + ) + + val getterLookalikeFunction = signatures[3] + getterLookalikeFunction.match( + "open fun ", A("getA"), "():", A("Int"), + ignoreSpanWithTokenStyle = true + ) + + val setterLookalikeFunction = signatures[4] + setterLookalikeFunction.match( + "open fun ", A("setA"), "(", Parameters( + Parameter("a: ", A("Int")) + ), ")", + ignoreSpanWithTokenStyle = true + ) + + val property = signatures[2] + property.match( + "var ", A("a"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `should keep kotlin property with no accessors when java inherits kotlin a var`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/JavaClass.java + |package test; + |public class JavaClass extends KotlinClass {} + | + |/src/test/KotlinClass.kt + |package test + |open class KotlinClass { + | var variable: String = "s" + |} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/test/-java-class/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals( + 3, + signatures.size, + "Expected to find 3 signatures: class, default constructor and property" + ) + + val property = signatures[2] + property.match( + "open var ", A("variable"), ": ", Span("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + } + + @Test + fun `kotlin property with compute get and set`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/JavaClass.java + |package test; + |public class JavaClass extends KotlinClass {} + | + |/src/test/KotlinClass.kt + |package test + |open class KotlinClass { + | var variable: String + | get() = "asd" + | set(value) {} + |} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/test/-kotlin-class/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals(3, signatures.size, "Expected to find 3 signatures: class, constructor and property") + + val property = signatures[2] + property.match( + "var ", A("variable"), ": ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + + // it's actually unclear how it should react in this situation. It should most likely not + // break the abstraction and display it as a simple variable just like can be seen from Kotlin, + // test added to control changes + writerPlugin.writer.renderedContent("root/test/-java-class/index.html").let { javaClassContent -> + val signatures = javaClassContent.signature().toList() + assertEquals( + 4, + signatures.size, + "Expected to find 4 signatures: class, default constructor and two accessors" + ) + + val getter = signatures[2] + getter.match( + "fun ", A("getVariable"), "(): ", Span("String"), + ignoreSpanWithTokenStyle = true + ) + + val setter = signatures[3] + setter.match( + "fun ", A("setVariable"), "(", Parameters( + Parameter("value: ", Span("String")) + ), ")", + ignoreSpanWithTokenStyle = true + ) + } + } + } + } + + @OnlyDescriptors("'var' expected but found: 'open var'") + @Test + fun `inherited property should inherit getter's visibility`() { + val configWithProtectedVisibility = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf( + commonStdlibPath ?: throw IllegalStateException("Common stdlib is not found"), + jvmStdlibPath ?: throw IllegalStateException("JVM stdlib is not found") + ) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PROTECTED + ) + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/JavaClass.java + |package test; + |public class JavaClass { + | private int protectedGetterAndProtectedSetter = 0; + | + | protected int getProtectedGetterAndProtectedSetter() { + | return protectedGetterAndProtectedSetter; + | } + | + | protected void setProtectedGetterAndProtectedSetter(int protectedGetterAndProtectedSetter) { + | this.protectedGetterAndProtectedSetter = protectedGetterAndProtectedSetter; + | } + |} + | + |/src/test/KotlinClass.kt + |package test + |open class KotlinClass : JavaClass() { } + """.trimIndent(), + configWithProtectedVisibility, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/test/-kotlin-class/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals(3, signatures.size, "Expected 3 signatures: class signature, constructor and property") + + val property = signatures[2] + property.match( + "protected var ", A("protectedGetterAndProtectedSetter"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + + writerPlugin.writer.renderedContent("root/test/-java-class/index.html").let { javaClassContent -> + val signatures = javaClassContent.signature().toList() + assertEquals( + 3, + signatures.size, + "Expected 3 signatures: class signature, default constructor and property" + ) + + val property = signatures[2] + property.match( + "protected open var ", A("protectedGetterAndProtectedSetter"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + } + + @OnlyDescriptors("'var' expected but found: 'open var'") + @Test + fun `should resolve protected java property as protected`() { + val configWithProtectedVisibility = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf( + commonStdlibPath ?: throw IllegalStateException("Common stdlib is not found"), + jvmStdlibPath ?: throw IllegalStateException("JVM stdlib is not found") + ) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PROTECTED + ) + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/JavaClass.java + |package test; + |public class JavaClass { + | protected int protectedProperty = 0; + |} + | + |/src/test/KotlinClass.kt + |package test + |open class KotlinClass : JavaClass() { } + """.trimIndent(), + configWithProtectedVisibility, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/test/-kotlin-class/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals(3, signatures.size, "Expected 2 signatures: class signature, constructor and property") + + val property = signatures[2] + property.match( + "protected var ", A("protectedProperty"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/ObviousTypeSkippingTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/ObviousTypeSkippingTest.kt new file mode 100644 index 00000000..71a0851b --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/ObviousTypeSkippingTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package signatures + +import matchers.content.assertNode +import matchers.content.hasExactText +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.firstMemberOfType +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.testApi.logger.TestLogger +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import kotlin.reflect.KClass + +class ObviousTypeSkippingTest : BaseAbstractTest( + logger = TestLogger(DokkaConsoleLogger(LoggingLevel.WARN)) +) { + + private fun source(signature: String) = + """ + |/src/test.kt + |package example + | + | $signature + """.trimIndent() + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + classpath = listOfNotNull(jvmStdlibPath) + } + } + } + + companion object TestDataSources { + @JvmStatic + fun `run tests for obvious types omitting`() = listOf( + forFunction("fun underTest(): Int = 5", "fun underTest(): Int"), + forFunction("fun underTest() = 5", "fun underTest(): Int"), + forFunction("fun underTest() {}", "fun underTest()"), + forFunction("fun underTest() = println(6)", "fun underTest()"), + forFunction("fun underTest(): Unit = println(6)", "fun underTest()"), + forFunction("fun underTest(): Unit? = if (true) println(6) else null", "fun underTest(): Unit?"), + forFunction("fun underTest() = if (true) println(6) else null", "fun underTest(): Unit?"), + forFunction("fun underTest(): Any = if (true) 7 else true", "fun underTest(): Any"), + forFunction("fun underTest() = if (true) 7 else true", "fun underTest(): Any"), + forFunction("fun underTest(): Any? = if (true) 7 else (null as String?)", "fun underTest(): Any?"), + forFunction("fun underTest() = if (true) 7 else (null as String?)", "fun underTest(): Any?"), + forFunction("fun underTest(arg: Int) {}", "fun underTest(arg: Int)"), + forFunction("fun underTest(arg: Unit) {}", "fun underTest(arg: Unit)"), + forFunction("fun <T: Iterable<Any>> underTest(arg: T) {}", "fun <T : Iterable<Any>> underTest(arg: T)"), + forFunction("fun <T: Iterable<Any?>> underTest(arg: T) {}", "fun <T : Iterable<Any?>> underTest(arg: T)"), + forFunction("fun <T> underTest(arg: T) {}", "fun <T> underTest(arg: T)"), + forFunction("fun <T: Any> underTest(arg: T) {}", "fun <T : Any> underTest(arg: T)"), + forFunction("fun <T: Any?> underTest(arg: T) {}", "fun <T> underTest(arg: T)"), + forProperty("val underTest: Int = 5", "val underTest: Int = 5"), + forProperty("val underTest = 5", "val underTest: Int = 5"), + forProperty("val underTest: Unit = println(5)", "val underTest: Unit"), + forProperty("val underTest = println(5)", "val underTest: Unit"), + forProperty("val underTest: Unit? = if (true) println(5) else null", "val underTest: Unit?"), + forProperty("val underTest = if (true) println(5) else null", "val underTest: Unit?"), + forProperty("val underTest: Any = if (true) println(5) else 5", "val underTest: Any"), + forProperty("val underTest = if (true) println(5) else 5", "val underTest: Any"), + forExtension("fun <T: Iterable<Any>> T.underTest() {}", "fun <T : Iterable<Any>> T.underTest()"), + forExtension("fun <T: Iterable<Any?>> T.underTest() {}", "fun <T : Iterable<Any?>> T.underTest()"), + forExtension("fun <T: Iterable<Any?>?> T.underTest() {}", "fun <T : Iterable<Any?>?> T.underTest()"), + forExtension("fun <T: Any> T.underTest() {}", "fun <T : Any> T.underTest()"), + forExtension("fun <T: Any?> T.underTest() {}", "fun <T> T.underTest()"), + forExtension("fun <T> T.underTest() {}", "fun <T> T.underTest()"), + forClass("class Testable<T: Any>", "class Testable<T : Any>"), + forClass("class Testable<T: Any?>", "class Testable<T>"), + forClass("class Testable<T: Any?>(t: T)", "class Testable<T>(t: T)"), + forClass("class Testable<T>", "class Testable<T>"), + forClass("class Testable(butWhy: Unit)", "class Testable(butWhy: Unit)"), + forMethod("class Testable { fun underTest(): Int = 5 }", "fun underTest(): Int"), + forMethod("class Testable { fun underTest() = 5 }", "fun underTest(): Int"), + forMethod("class Testable { fun underTest() {} }", "fun underTest()"), + forMethod("class Testable { fun underTest() = println(6) }", "fun underTest()"), + forMethod("class Testable { fun underTest(): Unit = println(6) }", "fun underTest()"), + forMethod( + "class Testable { fun underTest(): Unit? = if (true) println(6) else null }", + "fun underTest(): Unit?" + ), + forClassProperty("class Testable { val underTest: Unit = println(5) }", "val underTest: Unit"), + forClassProperty("class Testable { val underTest = println(5) }", "val underTest: Unit"), + forClassProperty( + "class Testable { val underTest: Unit? = if (true) println(5) else null }", + "val underTest: Unit?" + ), + forClassProperty( + "class Testable { val underTest = if (true) println(5) else null }", + "val underTest: Unit?" + ), + forClassProperty( + "class Testable { val underTest: Any = if (true) println(5) else 5 }", + "val underTest: Any" + ), + forClassProperty("class Testable { val underTest = if (true) println(5) else 5 }", "val underTest: Any"), + ) + } + + @ParameterizedTest(name = "{0}") + @MethodSource + fun `run tests for obvious types omitting`(testData: TestData) { + val (codeFragment, expectedSignature, placesToTest) = testData + testInline( + query = source(codeFragment), + configuration = configuration + ) { + pagesTransformationStage = { root -> + placesToTest.forEach { place -> + try { + when (place) { + is OnOwnPage -> + root.firstMemberOfType<ContentPage> { it.name == place.name }.content + .firstMemberOfType<ContentGroup> { it.dci.kind == ContentKind.Symbol } + .assertNode { hasExactText(expectedSignature) } + is OnParentPage -> + root.firstMemberOfType<ContentPage> { + place.pageType.isInstance(it) && (place.parentName.isNullOrBlank() || place.parentName == it.name) + } + .content + .firstMemberOfType<ContentGroup> { + it.dci.kind == place.section && (place.selfName.isNullOrBlank() || + it.dci.dri.toString().contains(place.selfName)) + } + .firstMemberOfType<ContentGroup> { it.dci.kind == ContentKind.Symbol } + .assertNode { hasExactText(expectedSignature) } + } + } catch (e: Throwable) { + logger.warn("$testData") // Because gradle has serious problem rendering custom test names + throw e + } + } + } + } + } + +} + +sealed class Place +data class OnOwnPage(val name: String) : Place() +data class OnParentPage( + val pageType: KClass<out ContentPage>, + val section: Kind, + val parentName: String? = null, + val selfName: String? = null +) : Place() + +data class TestData( + val codeFragment: String, + val expectedSignature: String, + val placesToTest: Iterable<Place> +) { + constructor(codeFragment: String, expectedSignature: String, vararg placesToTest: Place) + : this(codeFragment, expectedSignature, placesToTest.asIterable()) + + override fun toString() = "[code = \"$codeFragment\"]" +} + +private fun forFunction(codeFragment: String, expectedSignature: String, functionName: String = "underTest") = + TestData( + codeFragment, + expectedSignature, + OnParentPage(PackagePageNode::class, ContentKind.Functions), + OnOwnPage(functionName) + ) + +private fun forExtension(codeFragment: String, expectedSignature: String, functionName: String = "underTest") = + TestData( + codeFragment, + expectedSignature, + OnParentPage(PackagePageNode::class, ContentKind.Extensions), + OnOwnPage(functionName) + ) +private fun forMethod( + codeFragment: String, + expectedSignature: String, + functionName: String = "underTest", + className: String = "Testable" +) = + TestData( + codeFragment, + expectedSignature, + OnParentPage(ClasslikePageNode::class, ContentKind.Functions, className, functionName), + OnOwnPage(functionName) + ) + +private fun forProperty(codeFragment: String, expectedSignature: String) = + TestData(codeFragment, expectedSignature, OnParentPage(PackagePageNode::class, ContentKind.Properties)) + +private fun forClassProperty(codeFragment: String, expectedSignature: String, className: String = "Testable") = + TestData(codeFragment, expectedSignature, OnParentPage(ClasslikePageNode::class, ContentKind.Properties, className)) + +private fun forClass(codeFragment: String, expectedSignature: String, className: String = "Testable") = + TestData( + codeFragment, + expectedSignature, + OnParentPage(PackagePageNode::class, ContentKind.Classlikes), + OnOwnPage(className) + ) diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/RawHtmlRenderingTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/RawHtmlRenderingTest.kt new file mode 100644 index 00000000..c79d70fd --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/RawHtmlRenderingTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package signatures + +import org.jsoup.Jsoup +import utils.TestOutputWriterPlugin +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class RawHtmlRenderingTest: AbstractRenderingTest() { + @Test + fun `work with raw html with inline comment`() { + val writerPlugin = TestOutputWriterPlugin() + + testFromData( + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.renderedSourceDependentContent("example/example/-html-test/test.html") + assertEquals(1, content.count()) + assertEquals(content.select("[data-togglable=example/jvm]").single().rawBrief,"This is an example <!-- not visible --> of html") + + val indexContent = writerPlugin.writer.contents.getValue("example/example/-html-test/index.html") + .let { Jsoup.parse(it) } + assertTrue(indexContent.select("div.brief").any { it.html().contains("This is an example <!-- not visible --> of html")}) + } + } + } + + @Test + fun `work with raw html`() { + val writerPlugin = TestOutputWriterPlugin() + + testFromData( + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + //Module page + val content = writerPlugin.renderedContent("example/example/index.html").select("div.brief") + assertTrue(content.size > 0) + assertTrue(content.any { it.html().contains("<!-- this shouldn't be visible -->")}) + } + } + } + + @Test + fun `work with raw, visible html`() { + val writerPlugin = TestOutputWriterPlugin() + + testFromData( + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.renderedSourceDependentContent("example/example/-html-test/test-p.html") + assertEquals(1, content.count()) + assertEquals(content.select("[data-togglable=example/jvm]").single().rawBrief, "This is an <b> documentation </b>") + + val indexContent = writerPlugin.writer.contents.getValue("example/example/-html-test/index.html") + .let { Jsoup.parse(it) } + assertTrue(indexContent.select("div.brief").any { it.html().contains("This is an <b> documentation </b>")}) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/SignatureTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/SignatureTest.kt new file mode 100644 index 00000000..80a043fe --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/SignatureTest.kt @@ -0,0 +1,1035 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package signatures + +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.DefinitelyNonNullable +import org.jetbrains.dokka.model.dfs +import utils.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SignatureTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf( + commonStdlibPath ?: throw IllegalStateException("Common stdlib is not found"), + jvmStdlibPath ?: throw IllegalStateException("JVM stdlib is not found") + ) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + } + } + + private val mppConfiguration = dokkaConfiguration { + moduleName = "test" + sourceSets { + sourceSet { + name = "common" + sourceRoots = listOf("src/main/kotlin/common/Test.kt") + classpath = listOf(commonStdlibPath!!) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + sourceSet { + name = "jvm" + dependentSourceSets = setOf(DokkaSourceSetID("test", "common")) + sourceRoots = listOf("src/main/kotlin/jvm/Test.kt") + classpath = listOf( + commonStdlibPath ?: throw IllegalStateException("Common stdlib is not found"),) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + } + } + + fun source(signature: String) = + """ + |/src/main/kotlin/test/Test.kt + |package example + | + | $signature + """.trimIndent() + + @Test + fun `fun`() { + val source = source("fun simpleFun(): String = \"Celebrimbor\"") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "fun ", A("simpleFun"), "(): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `open fun`() { + val source = source("open fun simpleFun(): String = \"Celebrimbor\"") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "open fun ", A("simpleFun"), "(): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `open suspend fun`() { + val source = source("open suspend fun simpleFun(): String = \"Celebrimbor\"") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "open suspend fun ", A("simpleFun"), "(): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with params`() { + val source = source("fun simpleFun(a: Int, b: Boolean, c: Any): String = \"Celebrimbor\"") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "fun ", A("simpleFun"), "(", Parameters( + Parameter("a: ", A("Int"), ","), + Parameter("b: ", A("Boolean"), ","), + Parameter("c: ", A("Any")), + ), "): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with function param`() { + val source = source("fun simpleFun(a: (Int) -> String): String = \"Celebrimbor\"") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "fun ", A("simpleFun"), "(", Parameters( + Parameter("a: (", A("Int"), ") -> ", A("String")), + ),"): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with generic param`() { + val source = source("fun <T> simpleFun(): T = \"Celebrimbor\" as T") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "fun <", A("T"), "> ", A("simpleFun"), "(): ", + A("T"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with generic bounded param`() { + val source = source("fun <T : String> simpleFun(): T = \"Celebrimbor\" as T") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "fun <", A("T"), " : ", A("String"), "> ", A("simpleFun"), + "(): ", A("T"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with definitely non-nullable types`() { + val source = source("fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + documentablesTransformationStage = { + val fn = (it.dfs { it.name == "elvisLike" } as? DFunction).assertNotNull("Function elvisLike") + + assertTrue(fn.type is DefinitelyNonNullable) + assertTrue(fn.parameters[1].type is DefinitelyNonNullable) + } + renderingStage = { _, _ -> + val signature = writerPlugin.writer.renderedContent("root/example/elvis-like.html") + assertEquals(2, signature.select("a[href=\"https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html\"]").size) + signature.firstSignature().match( + "fun <", A("T"), "> ", A("elvisLike"), + "(", + Span( + Span("x: ", A("T"), ", "), + Span("y: ", A("T"), " & ", A("Any")) + ), + "): ", A("T"), " & ", A("Any"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with keywords, params and generic bound`() { + val source = source("inline suspend fun <T : String> simpleFun(a: Int, b: String): T = \"Celebrimbor\" as T") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "inline suspend fun <", A("T"), " : ", A("String"), "> ", A("simpleFun"), "(", Parameters( + Parameter("a: ", A("Int"), ","), + Parameter("b: ", A("String")), + ), "): ", A("T"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with vararg`() { + val source = source("fun simpleFun(vararg params: Int): Unit") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "fun ", A("simpleFun"), "(", Parameters( + Parameter("vararg params: ", A("Int")), + ), ")", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `class with no supertype`() { + val source = source("class SimpleClass") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-simple-class/index.html").firstSignature().match( + "class ", A("SimpleClass"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `class with generic supertype`() { + val source = source("class InheritingClassFromGenericType<T : Number, R : CharSequence> : Comparable<T>, Collection<R>") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-inheriting-class-from-generic-type/index.html").firstSignature().match( + "class ", A("InheritingClassFromGenericType"), " <", A("T"), " : ", A("Number"), ", ", A("R"), " : ", A("CharSequence"), + "> : ", A("Comparable"), "<", A("T"), "> , ", A("Collection"), "<", A("R"), ">", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `functional interface`() { + val source = source("fun interface KRunnable") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-k-runnable/index.html").firstSignature().match( + "fun interface ", A("KRunnable"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with annotation`() { + val source = """ + |/src/main/kotlin/test/Test.kt + |package example + | + | @MustBeDocumented() + | @Target(AnnotationTarget.FUNCTION) + | annotation class Marking + | + | @Marking() + | fun simpleFun(): String = "Celebrimbor" + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + Div( + Div("@", A("Marking")) + ), + "fun ", A("simpleFun"), + "(): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `property with annotation`() { + val source = """ + |/src/main/kotlin/test/Test.kt + |package example + | + | @MustBeDocumented() + | @Target(AnnotationTarget.FUNCTION) + | annotation class Marking + | + | @get:Marking() + | @set:Marking() + | var str: String + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/str.html").firstSignature().match( + Div( + Div("@get:", A("Marking")), + Div("@set:", A("Marking")) + ), + "var ", A("str"), + ": ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with two annotations`() { + val source = """ + |/src/main/kotlin/test/Test.kt + |package example + | + | @MustBeDocumented() + | @Target(AnnotationTarget.FUNCTION) + | annotation class Marking(val msg: String) + | + | @MustBeDocumented() + | @Target(AnnotationTarget.FUNCTION) + | annotation class Marking2(val int: Int) + | + | @Marking("Nenya") + | @Marking2(1) + | fun simpleFun(): String = "Celebrimbor" + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html") + .firstSignature() + .match( + Div( + Div("@", A("Marking"), "(", Span("msg = ", Span("\"Nenya\"")), Wbr, ")"), + Div("@", A("Marking2"), "(", Span("int = ", Span("1")), Wbr, ")") + ), + "fun ", A("simpleFun"), + "(): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `fun with annotation with array`() { + val source = """ + |/src/main/kotlin/test/Test.kt + |package example + | + | @MustBeDocumented() + | @Target(AnnotationTarget.FUNCTION) + | annotation class Marking(val msg: Array<String>) + | + | @Marking(["Nenya", "Vilya", "Narya"]) + | @Marking2(1) + | fun simpleFun(): String = "Celebrimbor" + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + Div( + Div( + "@", A("Marking"), "(", Span( + "msg = [", + Span(Span("\"Nenya\""), ", "), Wbr, + Span(Span("\"Vilya\""), ", "), Wbr, + Span(Span("\"Narya\"")), Wbr, "]" + ), Wbr, ")" + ) + ), + "fun ", A("simpleFun"), + "(): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `actual fun`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |expect fun simpleFun(): String + | + |/src/main/kotlin/jvm/Test.kt + |package example + | + |actual fun simpleFun(): String = "Celebrimbor" + | + """.trimMargin(), + mppConfiguration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val signatures = writerPlugin.writer.renderedContent("test/example/simple-fun.html").signature().toList() + + signatures[0].match( + "expect fun ", A("simpleFun"), + "(): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + signatures[1].match( + "actual fun ", A("simpleFun"), + "(): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `actual property with a default value`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |expect val prop: Int + | + |/src/main/kotlin/jvm/Test.kt + |package example + | + |actual val prop: Int = 2 + | + """.trimMargin(), + mppConfiguration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val signatures = writerPlugin.writer.renderedContent("test/example/prop.html").signature().toList() + + signatures[0].match( + "expect val ", A("prop"), + ": ", A("Int"), + ignoreSpanWithTokenStyle = true + ) + signatures[1].match( + "actual val ", A("prop"), + ": ", A("Int"), + " = 2", + ignoreSpanWithTokenStyle = true + ) + } + } + } + @Test + fun `actual typealias should have generic parameters and fully qualified name of the expansion type`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |expect class Array<T> + | + |/src/main/kotlin/jvm/Test.kt + |package example + | + |actual typealias Array<T> = kotlin.Array<T> + """.trimMargin(), + mppConfiguration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val signatures = writerPlugin.writer.renderedContent("test/example/-array/index.html").signature().toList() + + signatures[0].match( + "expect class ", A("Array"), "<", A("T"), ">", + ignoreSpanWithTokenStyle = true + ) + signatures[1].match( + "actual typealias ", A("Array"), "<", A("T"), "> = ", A("kotlin.Array"), "<", A("T"), ">", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `type with an actual typealias`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |expect class Foo + | + |/src/main/kotlin/jvm/Test.kt + |package example + | + |class Bar + |actual typealias Foo = Bar + | + """.trimMargin(), + mppConfiguration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val signatures = writerPlugin.writer.renderedContent("test/example/-foo/index.html").signature().toList() + + signatures[0].match( + "expect class ", A("Foo"), + ignoreSpanWithTokenStyle = true + ) + signatures[1].match( + "actual typealias ", A("Foo"), " = ", A("Bar"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `plain typealias of plain class`() { + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |typealias PlainTypealias = Int + | + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "typealias ", A("PlainTypealias"), " = ", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `plain typealias of plain class with annotation`() { + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |@MustBeDocumented + |@Target(AnnotationTarget.TYPEALIAS) + |annotation class SomeAnnotation + | + |@SomeAnnotation + |typealias PlainTypealias = Int + | + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + Div( + Div( + "@", A("SomeAnnotation") + ) + ), + "typealias ", A("PlainTypealias"), " = ", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `plain typealias of generic class`() { + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |typealias PlainTypealias = Comparable<Int> + | + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "typealias ", A("PlainTypealias"), " = ", A("Comparable"), + "<", A("Int"), ">", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `typealias with generics params`() { + + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |typealias GenericTypealias<T> = Comparable<T> + | + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "typealias ", A("GenericTypealias"), "<", A("T"), "> = ", A("Comparable"), + "<", A("T"), ">", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `typealias with generic params swapped`() { + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt + |package kotlinAsJavaPlugin + | + |typealias XD<B, A> = Map<A, B> + | + |class ABC { + | fun someFun(xd: XD<Int, String>) = 1 + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/kotlinAsJavaPlugin/-a-b-c/some-fun.html").firstSignature() + .match( + "fun ", A("someFun"), "(", Parameters( + Parameter("xd: ", A("XD"), "<", A("Int"), ", ", A("String"), ">"), + ), "):", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @OnlyDescriptors("Order of constructors is different in K2") + @Test + fun `generic constructor params`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |class GenericClass<T>(val x: Int) { + | constructor(x: T) : this(1) + | + | constructor(x: Int, y: String) : this(1) + | + | constructor(x: Int, y: List<T>) : this(1) + | + | constructor(x: Boolean, y: Int, z: String) : this(1) + | + | constructor(x: List<Comparable<Lazy<T>>>?) : this(1) + |} + | + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-generic-class/-generic-class.html").signature().zip( + listOf( + arrayOf( + "constructor(", + Parameters( + Parameter("x: ", A("T")) + ), + ")", + ), + arrayOf( + "constructor(", + Parameters( + Parameter("x: ", A("Int"), ", "), + Parameter("y: ", A("String")) + ), + ")", + ), + arrayOf( + "constructor(", + Parameters( + Parameter("x: ", A("Int"), ", "), + Parameter("y: ", A("List"), "<", A("T"), ">") + ), + ")", + ), + arrayOf( + "constructor(", + Parameters( + Parameter("x: ", A("Boolean"), ", "), + Parameter("y: ", A("Int"), ", "), + Parameter("z:", A("String")) + ), + ")", + ), + arrayOf( + "constructor(", + Parameters( + Parameter("x: ", A("List"), "<", A("Comparable"), "<", A("Lazy"), "<", A("T"), ">>>?") + ), + ")", + ), + arrayOf( + "constructor(", + Parameters( + Parameter("x: ", A("Int")) + ), + ")", + ), + ) + ).forEach { + it.first.match(*it.second, ignoreSpanWithTokenStyle = true) + } + } + } + } + + @Test + fun `constructor has its own custom signature keyword in Constructor tab`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |class PrimaryConstructorClass(x: String) { } + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val constructorTabFirstElement = + writerPlugin.writer.renderedContent("root/example/-primary-constructor-class/index.html") + .tab("CONSTRUCTOR") + .first() ?: throw NoSuchElementException("No Constructors tab found or it is empty") + + constructorTabFirstElement.firstSignature().match( + "constructor(", Parameters(Parameter("x: ", A("String"))), ")", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `primary constructor with properties check for all tokens`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |class PrimaryConstructorClass<T>(val x: Int, var s: String) { } + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-primary-constructor-class/index.html").firstSignature().match( + // In `<T>` expression, an empty `<span class="token keyword"></span>` is present for some reason + Span("class "), A("PrimaryConstructorClass"), Span("<"), Span(), A("T"), Span(">"), Span("("), Parameters( + Parameter(Span("val "), "x", Span(": "), A("Int"), Span(",")), + Parameter(Span("var "), "s", Span(": "), A("String")) + ), Span(")"), + ) + } + } + } + + @Test + fun `fun with default values`() { + val source = source("fun simpleFun(int: Int = 1, string: String = \"string\"): String = \"\"") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/simple-fun.html").firstSignature().match( + "fun", A("simpleFun"), "(", Parameters( + Parameter("int: ", A("Int"), " = 1,"), + Parameter("string: ", A("String"), " = \"string\"") + ), "): ", A("String"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `const val with default values`() { + val source = source("const val simpleVal = 1") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/index.html").firstSignature().match( + "const val ", A("simpleVal"), ": ", A("Int"), " = 1", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `should not expose enum constructor entry arguments`() { + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/EnumClass.kt + |package example + | + |enum class EnumClass(param: String = "Default") { + | EMPTY, + | WITH_ARG("arg") + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val enumEntrySignatures = writerPlugin.writer.renderedContent("root/example/-enum-class/index.html") + .select("div[data-togglable=ENTRY] .table") + .single() + .signature() + .select("div.block") + + enumEntrySignatures[0].match( + A("EMPTY"), + ignoreSpanWithTokenStyle = true + ) + + enumEntrySignatures[1].match( + A("WITH_ARG"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @OnlyDescriptors("'var' expected but found: 'open var'") + @Test + fun `java property without accessors should be var`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/test/JavaClass.java + |package test; + |public class JavaClass { + | public int property = 0; + |} + | + |/src/test/KotlinClass.kt + |package test + |open class KotlinClass : JavaClass() { } + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/test/-kotlin-class/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals(3, signatures.size, "Expected 2 signatures: class signature, constructor and property") + + val property = signatures[2] + property.match( + "var ", A("property"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + + writerPlugin.writer.renderedContent("root/test/-java-class/index.html").let { kotlinClassContent -> + val signatures = kotlinClassContent.signature().toList() + assertEquals( + 3, + signatures.size, + "Expected 3 signatures: class signature, default constructor and property" + ) + + val property = signatures[2] + property.match( + "open var ", A("property"), ":", A("Int"), + ignoreSpanWithTokenStyle = true + ) + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/VarianceSignatureTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/VarianceSignatureTest.kt new file mode 100644 index 00000000..0e8a8845 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/VarianceSignatureTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package signatures + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import utils.A +import utils.TestOutputWriterPlugin +import utils.match +import kotlin.test.Test + +class VarianceSignatureTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(commonStdlibPath!!) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + } + } + + fun source(signature: String) = + """ + |/src/main/kotlin/test/Test.kt + |package example + | + | $signature + """.trimIndent() + + @Test + fun `simple contravariance`() { + val source = source("class Generic<in T>") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-generic/index.html").firstSignature().match( + "class ", A("Generic"), "<in ", A("T"), ">", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `simple covariance`() { + val source = source("class Generic<out T>") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-generic/index.html").firstSignature().match( + "class ", A("Generic"), "<out ", A("T"), ">", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `simple invariance`() { + val source = source("class Generic<T>") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-generic/index.html").firstSignature().match( + "class ", A("Generic"), "<", A("T"), ">", + ignoreSpanWithTokenStyle = true + ) + } + } + } + + @Test + fun `covariance and bound`() { + val source = source("class Generic<out T : List<CharSequence>>") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-generic/index.html").firstSignature().match( + "class ", A("Generic"), "<out ", A("T"), ":", A("List"), "<", A("CharSequence"), ">>", + ignoreSpanWithTokenStyle = true + ) + } + } + } +} + diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt new file mode 100644 index 00000000..8f984485 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt @@ -0,0 +1,366 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package superFields + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.InheritedMember +import org.jetbrains.dokka.model.IsVar +import org.jetbrains.dokka.model.KotlinVisibility +import utils.OnlyDescriptors +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class DescriptorSuperPropertiesTest : BaseAbstractTest() { + + private val commonTestConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + } + } + } + + @Test + fun `kotlin inheriting java should append only getter`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | public int getA() { return a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + commonTestConfiguration + ) { + this.documentablesTransformationStage = { module -> + val kotlinProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + + val property = kotlinProperties.single { it.name == "a" } + val propertyInheritedFrom = property.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), propertyInheritedFrom) + + assertNull(property.setter) + assertNotNull(property.getter) + + val getterInheritedFrom = property.getter?.extra?.get(InheritedMember)?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), getterInheritedFrom) + + assertNull(property.extra[IsVar]) + } + } + } + + + @Test + fun `kotlin inheriting java should ignore setter lookalike for non accessible field`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | + | public void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + commonTestConfiguration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "B" } + + val property = testedClass.properties.firstOrNull { it.name == "a" } + assertNull(property, "Inherited property `a` should not be visible as it's not accessible") + + val setterLookalike = testedClass.functions.firstOrNull { it.name == "setA" } + assertNotNull(setterLookalike) { + "Expected setA to be a regular function because field `a` is neither var nor val from Kotlin's " + + "interop perspective, it's not accessible." + } + } + } + } + + + @Test + fun `kotlin inheriting java should append getter and setter`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | public int getA() { return a; } + | public void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + commonTestConfiguration + ) { + documentablesMergingStage = { module -> + val kotlinProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + val property = kotlinProperties.single { it.name == "a" } + property.extra[InheritedMember]?.inheritedFrom?.values?.single()?.run { + assertEquals( + DRI(packageName = "test", classNames = "A"), + this + ) + } + + val getter = property.getter + assertNotNull(getter) + assertEquals("getA", getter.name) + val getterInheritedFrom = getter.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), getterInheritedFrom) + + val setter = property.setter + assertNotNull(setter) + assertEquals("setA", setter.name) + val setterInheritedFrom = setter.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), setterInheritedFrom) + + assertNotNull(property.extra[IsVar]) + } + } + } + + @Test + @OnlyDescriptors("Incorrect test, see https://github.com/Kotlin/dokka/issues/3128") + fun `should have special getter and setter names for boolean property inherited from java`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private boolean bool = true; + | public boolean isBool() { return bool; } + | public void setBool(boolean bool) { this.bool = bool; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + commonTestConfiguration + ) { + documentablesMergingStage = { module -> + val kotlinProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + val boolProperty = kotlinProperties.single { it.name == "bool" } + + val getter = boolProperty.getter + assertNotNull(getter) + assertEquals("isBool", getter.name) + + val setter = boolProperty.setter + assertNotNull(setter) + assertEquals("setBool", setter.name) + + assertNotNull(boolProperty.extra[IsVar]) + } + } + } + + @OnlyDescriptors("Incorrect test, see https://github.com/Kotlin/dokka/issues/3128") + @Test + fun `kotlin inheriting java should not append anything since field is public api`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PROTECTED + ) + } + } + } + + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | protected int a = 1; + | public int getA() { return a; } + | public void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "B" } + val property = testedClass.properties.single { it.name == "a" } + + assertNull(property.getter) + assertNull(property.setter) + assertEquals(2, testedClass.functions.size) + + assertEquals("getA", testedClass.functions[0].name) + assertEquals("setA", testedClass.functions[1].name) + + val inheritedFrom = property.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + + assertNotNull(property.extra[IsVar]) + } + } + } + + @Test + fun `should inherit property visibility from getter`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PROTECTED + ) + } + } + } + + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | private int a = 1; + | protected int getA() { return a; } + | protected void setA(int a) { this.a = a; } + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "B" } + assertEquals(0, testedClass.functions.size) + + val property = testedClass.properties.single { it.name == "a" } + + assertNotNull(property.getter) + assertNotNull(property.setter) + + val propertyVisibility = property.visibility.values.single() + assertEquals(KotlinVisibility.Protected, propertyVisibility) + + val getterVisibility = property.getter?.visibility?.values?.single() + assertEquals(KotlinVisibility.Protected, getterVisibility) + + val setterVisibility = property.setter?.visibility?.values?.single() + assertEquals(KotlinVisibility.Protected, setterVisibility) + + val inheritedFrom = property.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + + assertNotNull(property.extra[IsVar]) + } + } + } + + @Test // checking for mapping between kotlin and java visibility + fun `should resolve inherited java protected field as protected`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PROTECTED + ) + } + } + } + + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | protected int protectedProperty = 0; + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "B" } + assertEquals(0, testedClass.functions.size) + + val property = testedClass.properties.single { it.name == "protectedProperty" } + + assertNull(property.getter) + assertNull(property.setter) + + val propertyVisibility = property.visibility.values.single() + assertEquals(KotlinVisibility.Protected, propertyVisibility) + + val inheritedFrom = property.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + + assertNotNull(property.extra[IsVar]) + } + } + } + + @Test + fun `should mark final property inherited from java as val`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | public final int a = 1; + |} + | + |/src/test/B.kt + |package test + |class B : A {} + """.trimIndent(), + commonTestConfiguration + ) { + documentablesMergingStage = { module -> + val kotlinProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + val property = kotlinProperties.single { it.name == "a" } + + assertNull(property.extra[IsVar]) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt new file mode 100644 index 00000000..38f263a6 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package superFields + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Annotations +import org.jetbrains.dokka.model.InheritedMember +import org.jetbrains.dokka.model.IsVar +import org.jetbrains.dokka.model.isJvmField +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class PsiSuperFieldsTest : BaseAbstractTest() { + + private val commonTestConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + } + } + } + + @Test + fun `java inheriting java`() { + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | public int a = 1; + |} + | + |/src/test/B.java + |package test; + |public class B extends A {} + """.trimIndent(), + commonTestConfiguration + ) { + documentablesMergingStage = { module -> + val inheritorProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + val property = inheritorProperties.single { it.name == "a" } + + val inheritedFrom = property.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + } + } + } + + @Test + fun `java inheriting kotlin common case`() { + testInline( + """ + |/src/test/A.kt + |package test + |open class A { + | var a: Int = 1 + | val b: Int = 2 + |} + | + |/src/test/B.java + |package test; + |public class B extends A {} + """.trimIndent(), + commonTestConfiguration + ) { + documentablesMergingStage = { module -> + val inheritorProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + inheritorProperties.single { it.name == "a" }.let { mutableProperty -> + assertNotNull(mutableProperty.getter) + assertNotNull(mutableProperty.setter) + + val inheritedFrom = mutableProperty.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + + assertNotNull(mutableProperty.extra[IsVar]) + } + + inheritorProperties.single { it.name == "b" }.let { immutableProperty -> + assertNotNull(immutableProperty.getter) + assertNull(immutableProperty.setter) + + val inheritedFrom = immutableProperty.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + + assertNull(immutableProperty.extra[IsVar]) + } + } + } + } + + @Test + fun `java inheriting kotlin with boolean property`() { + testInline( + """ + |/src/test/A.kt + |package test + |open class A { + | var isActive: Boolean = true + |} + | + |/src/test/B.java + |package test; + |public class B extends A {} + """.trimIndent(), + commonTestConfiguration + ) { + documentablesMergingStage = { module -> + val inheritorProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + val property = inheritorProperties.single { it.name == "isActive" } + + assertNotNull(property.getter) + assertEquals("isActive", property.getter?.name) + + assertNotNull(property.setter) + assertEquals("setActive", property.setter?.name) + + val inheritedFrom = property.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + + assertNotNull(property.extra[IsVar]) + } + } + } + + @Test + fun `java inheriting kotlin with @JvmField should not inherit accessors`() { + testInline( + """ + |/src/test/A.kt + |package test + |open class A { + | @kotlin.jvm.JvmField + | var a: Int = 1 + |} + | + |/src/test/B.java + |package test; + |public class B extends A {} + """.trimIndent(), + dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + name = "jvm" + classpath += jvmStdlibPath!! // needed for JvmField + } + } + } + ) { + documentablesMergingStage = { module -> + val inheritorProperties = module.packages.single().classlikes.single { it.name == "B" }.properties + val property = inheritorProperties.single { it.name == "a" } + + assertNull(property.getter) + assertNull(property.setter) + + val jvmFieldAnnotation = property.extra[Annotations]?.directAnnotations?.values?.single()?.find { + it.isJvmField() + } + assertNotNull(jvmFieldAnnotation) + + val inheritedFrom = property.extra[InheritedMember]?.inheritedFrom?.values?.single() + assertEquals(DRI(packageName = "test", classNames = "A"), inheritedFrom) + + assertNotNull(property.extra[IsVar]) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformerBuilders/PageTransformerBuilderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformerBuilders/PageTransformerBuilderTest.kt new file mode 100644 index 00000000..7ee67228 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformerBuilders/PageTransformerBuilderTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformerBuilders + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement +import org.jetbrains.dokka.transformers.pages.PageTransformer +import org.jetbrains.dokka.transformers.pages.pageMapper +import org.jetbrains.dokka.transformers.pages.pageScanner +import org.jetbrains.dokka.transformers.pages.pageStructureTransformer +import utils.assertNotNull +import kotlin.test.Test +import kotlin.test.assertEquals + +class PageTransformerBuilderTest : BaseAbstractTest() { + + class ProxyPlugin(transformer: PageTransformer) : DokkaPlugin() { + val pageTransformer by extending { CoreExtensions.pageTransformer with transformer } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = + PluginApiPreviewAcknowledgement + } + + @Test + fun scannerTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/transformerBuilder/Test.kt") + } + } + } + val list = mutableListOf<String>() + + var orig: PageNode? = null + + testInline( + """ + |/src/main/kotlin/transformerBuilder/Test.kt + |package transformerBuilder + | + |object Test { + | fun test2(str: String): Unit {println(str)} + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(ProxyPlugin(pageScanner { + list += name + })) + ) { + pagesGenerationStage = { + orig = it + } + pagesTransformationStage = { root -> + list.assertCount(4, "Page list: ") + orig?.let { root.assertTransform(it) } + } + } + } + + @Test + fun mapperTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/transformerBuilder/Test.kt") + } + } + } + + var orig: PageNode? = null + + testInline( + """ + |/src/main/kotlin/transformerBuilder/Test.kt + |package transformerBuilder + | + |object Test { + | fun test2(str: String): Unit {println(str)} + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(ProxyPlugin(pageMapper { + modified(name = name + "2") + })) + ) { + pagesGenerationStage = { + orig = it + } + pagesTransformationStage = { + it.let { root -> + root.name.assertEqual("root2", "Root name: ") + orig?.let { + root.assertTransform(it) { node -> node.modified(name = node.name + "2") } + } + } + } + } + } + + @Test + fun structureTransformerTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin/transformerBuilder/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/transformerBuilder/Test.kt + |package transformerBuilder + | + |object Test { + | fun test2(str: String): Unit {println(str)} + |} + """.trimMargin(), + configuration, + pluginOverrides = listOf(ProxyPlugin(pageStructureTransformer { + val ch = children.first() + modified( + children = listOf( + ch, + RendererSpecificResourcePage("test", emptyList(), RenderingStrategy.DoNothing) + ) + ) + })) + ) { + pagesTransformationStage = { root -> + root.children.assertCount(2, "Root children: ") + root.children.first().name.assertEqual("transformerBuilder") + root.children[1].name.assertEqual("test") + } + } + } + + @Test + fun `kotlin constructors tab should exist even though there is primary constructor only`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + testInline( + """ + |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt + |package kotlinAsJavaPlugin + | + |class Test(val xd: Int) + """.trimMargin(), + configuration + ) { + pagesGenerationStage = { root -> + val content = root.children + .flatMap { it.children<ContentPage>() } + .map { it.content }.single().children + .filterIsInstance<ContentGroup>() + .single { it.dci.kind == ContentKind.Main }.children + + val contentWithConstructorsHeader = content.find { tabContent -> tabContent.dfs { it is ContentText && (it as? ContentText)?.text == "Constructors"} != null } + + contentWithConstructorsHeader.assertNotNull("contentWithConstructorsHeader") + + contentWithConstructorsHeader?.dfs { it.dci.kind == ContentKind.Constructors && it is ContentGroup } + .assertNotNull("constructor group") + } + } + } + + private fun <T> Collection<T>.assertCount(n: Int, prefix: String = "") = + assertEquals(n, count(), "${prefix}Expected $n, got ${count()}") + + private fun <T> T.assertEqual(expected: T, prefix: String = "") = + assertEquals(expected, this, "${prefix}Expected $expected, got $this") + + private fun PageNode.assertTransform(expected: PageNode, block: (PageNode) -> PageNode = { it }): Unit = this.let { + it.name.assertEqual(block(expected).name) + it.children.zip(expected.children).forEach { (g, e) -> + g.name.assertEqual(block(e).name) + g.assertTransform(e, block) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/AbstractContextModuleAndPackageDocumentationReaderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/AbstractContextModuleAndPackageDocumentationReaderTest.kt new file mode 100644 index 00000000..8ce9360f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/AbstractContextModuleAndPackageDocumentationReaderTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.model.withDescendants +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path + +abstract class AbstractContextModuleAndPackageDocumentationReaderTest { + @TempDir + protected lateinit var temporaryDirectory: Path + + + companion object { + val SourceSetDependent<DocumentationNode>.texts: List<String> + get() = values.flatMap { it.withDescendants() } + .flatMap { it.children } + .flatMap { it.children } + .mapNotNull { it as? Text } + .map { it.body } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/CommentsToContentConverterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/CommentsToContentConverterTest.kt new file mode 100644 index 00000000..1387c0e0 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/CommentsToContentConverterTest.kt @@ -0,0 +1,484 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import matchers.content.* +import org.jetbrains.dokka.base.transformers.pages.comments.DocTagToContentConverter +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.pages.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class CommentsToContentConverterTest { + private val converter = DocTagToContentConverter() + + private fun executeTest( + docTag: DocTag, + match: ContentMatcherBuilder<ContentComposite>.() -> Unit, + ) { + val dci = DCI( + setOf( + DRI("kotlin", "Any") + ), + ContentKind.Comment + ) + converter.buildContent( + Li( + listOf( + docTag + ) + ), + dci, + emptySet() + ).single().assertNode(match) + } + + @Test + fun `simple text`() { + val docTag = P(listOf(Text("This is simple test of string Next line"))) + executeTest(docTag) { + group { +"This is simple test of string Next line" } + } + } + + @Test + fun `simple text with new line`() { + val docTag = P( + listOf( + Text("This is simple test of string"), + Br, + Text("Next line") + ) + ) + executeTest(docTag) { + group { + +"This is simple test of string" + node<ContentBreakLine>() + +"Next line" + } + } + } + + @Test + fun `paragraphs`() { + val docTag = P( + listOf( + P(listOf(Text("Paragraph number one"))), + P(listOf(Text("Paragraph"), Br, Text("number two"))) + ) + ) + executeTest(docTag) { + group { + group { +"Paragraph number one" } + group { + +"Paragraph" + node<ContentBreakLine>() + +"number two" + } + } + } + } + + @Test + fun `unordered list with empty lines`() { + val docTag = Ul( + listOf( + Li(listOf(P(listOf(Text("list item 1 continue 1"))))), + Li(listOf(P(listOf(Text("list item 2"), Br, Text("continue 2"))))) + ) + ) + executeTest(docTag) { + node<ContentList> { + group { + +"list item 1 continue 1" + } + group { + +"list item 2" + node<ContentBreakLine>() + +"continue 2" + } + } + } + } + + @Test + fun `nested list`() { + val docTag = P( + listOf( + Ul( + listOf( + Li(listOf(P(listOf(Text("Outer first Outer next line"))))), + Li(listOf(P(listOf(Text("Outer second"))))), + Ul( + listOf( + Li(listOf(P(listOf(Text("Middle first Middle next line"))))), + Li(listOf(P(listOf(Text("Middle second"))))), + Ul( + listOf( + Li(listOf(P(listOf(Text("Inner first Inner next line"))))) + ) + ), + Li(listOf(P(listOf(Text("Middle third"))))) + ) + ), + Li(listOf(P(listOf(Text("Outer third"))))) + ) + ), + P(listOf(Text("New paragraph"))) + ) + ) + executeTest(docTag) { + group { + node<ContentList> { + group { +"Outer first Outer next line" } + group { +"Outer second" } + node<ContentList> { + group { +"Middle first Middle next line" } + group { +"Middle second" } + node<ContentList> { + group { +"Inner first Inner next line" } + } + group { +"Middle third" } + } + group { +"Outer third" } + } + group { +"New paragraph" } + } + } + } + + @Test + fun `header and paragraphs`() { + val docTag = P( + listOf( + H1(listOf(Text("Header 1"))), + P(listOf(Text("Following text"))), + P(listOf(Text("New paragraph"))) + ) + ) + executeTest(docTag) { + group { + header(1) { +"Header 1" } + group { +"Following text" } + group { +"New paragraph" } + } + } + } + + @Test + fun `header levels`() { + val docTag = P( + listOf( + H1(listOf(Text("Header 1"))), + P(listOf(Text("Text 1"))), + H2(listOf(Text("Header 2"))), + P(listOf(Text("Text 2"))), + H3(listOf(Text("Header 3"))), + P(listOf(Text("Text 3"))), + H4(listOf(Text("Header 4"))), + P(listOf(Text("Text 4"))), + H5(listOf(Text("Header 5"))), + P(listOf(Text("Text 5"))), + H6(listOf(Text("Header 6"))), + P(listOf(Text("Text 6"))) + ) + ) + executeTest(docTag) { + group { + header(1) { +"Header 1" } + group { +"Text 1" } + header(2) { +"Header 2" } + group { +"Text 2" } + header(3) { +"Header 3" } + group { +"Text 3" } + header(4) { +"Header 4" } + group { +"Text 4" } + header(5) { +"Header 5" } + group { +"Text 5" } + header(6) { +"Header 6" } + group { +"Text 6" } + } + } + } + + @Test + fun `block quotes`() { + val docTag = P( + listOf( + BlockQuote( + listOf( + P( + listOf( + Text("Blockquotes are very handy in email to emulate reply text. This line is part of the same quote.") + ) + ) + ) + ), + P(listOf(Text("Quote break."))), + BlockQuote( + listOf( + P(listOf(Text("Quote"))) + ) + ) + ) + ) + executeTest(docTag) { + group { + group { + group { + +"Blockquotes are very handy in email to emulate reply text. This line is part of the same quote." + } + } + group { +"Quote break." } + group { + group { + +"Quote" + } + } + } + } + } + + @Test + fun `nested block quotes`() { + val docTag = P( + listOf( + BlockQuote( + listOf( + P(listOf(Text("text 1 text 2"))), + BlockQuote( + listOf( + P(listOf(Text("text 3 text 4"))) + ) + ), + P(listOf(Text("text 5"))) + ) + ), + P(listOf(Text("Quote break."))), + BlockQuote( + listOf( + P(listOf(Text("Quote"))) + ) + ) + ) + ) + executeTest(docTag) { + group { + group { + group { +"text 1 text 2" } + group { + group { +"text 3 text 4" } + } + group { +"text 5" } + } + group { +"Quote break." } + group { + group { +"Quote" } + } + } + } + } + + @Test + fun `multiline code`() { + val docTag = P( + listOf( + CodeBlock( + listOf( + Text("val x: Int = 0"), Br, + Text("val y: String = \"Text\""), Br, Br, + Text(" val z: Boolean = true"), Br, + Text("for(i in 0..10) {"), Br, + Text(" println(i)"), Br, + Text("}") + ), + mapOf("lang" to "kotlin") + ), + P(listOf(Text("Sample text"))) + ) + ) + executeTest(docTag) { + group { + node<ContentCodeBlock> { + +"val x: Int = 0" + node<ContentBreakLine>() + +"val y: String = \"Text\"" + node<ContentBreakLine>() + node<ContentBreakLine>() + +" val z: Boolean = true" + node<ContentBreakLine>() + +"for(i in 0..10) {" + node<ContentBreakLine>() + +" println(i)" + node<ContentBreakLine>() + +"}" + } + group { +"Sample text" } + } + } + } + + @Test + fun `inline link`() { + val docTag = P( + listOf( + A( + listOf(Text("I'm an inline-style link")), + mapOf("href" to "https://www.google.com") + ) + ) + ) + executeTest(docTag) { + group { + link { + +"I'm an inline-style link" + check { + assertEquals( + (this as? ContentResolvedLink)?.address ?: error("Link should be resolved"), + "https://www.google.com" + ) + } + } + } + } + } + + + @Test + fun `ordered list`() { + val docTag = + Ol( + listOf( + Li( + listOf( + P(listOf(Text("test1"))), + P(listOf(Text("test2"))), + ) + ), + Li( + listOf( + P(listOf(Text("test3"))), + P(listOf(Text("test4"))), + ) + ) + ) + ) + executeTest(docTag) { + node<ContentList> { + group { + +"test1" + +"test2" + } + group { + +"test3" + +"test4" + } + } + } + } + + @Test + fun `nested ordered list`() { + val docTag = P( + listOf( + Ol( + listOf( + Li(listOf(P(listOf(Text("Outer first Outer next line"))))), + Li(listOf(P(listOf(Text("Outer second"))))), + Ol( + listOf( + Li(listOf(P(listOf(Text("Middle first Middle next line"))))), + Li(listOf(P(listOf(Text("Middle second"))))), + Ol( + listOf( + Li(listOf(P(listOf(Text("Inner first Inner next line"))))) + ), + mapOf("start" to "1") + ), + Li(listOf(P(listOf(Text("Middle third"))))) + ), + mapOf("start" to "1") + ), + Li(listOf(P(listOf(Text("Outer third"))))) + ), + mapOf("start" to "1") + ), + P(listOf(Text("New paragraph"))) + ) + ) + executeTest(docTag) { + group { + node<ContentList> { + group { +"Outer first Outer next line" } + group { +"Outer second" } + node<ContentList> { + group { +"Middle first Middle next line" } + group { +"Middle second" } + node<ContentList> { + +"Inner first Inner next line" + } + group { +"Middle third" } + } + group { +"Outer third" } + } + group { + +"New paragraph" + } + } + } + } + + @Test + fun `description list`() { + val docTag = + Dl( + listOf( + Dt( + listOf( + Text("description list can have...") + ) + ), + Dt( + listOf( + Text("... two consecutive description terms") + ) + ), + Dd( + listOf( + Text("and usually has some sort of a description, like this one") + ) + ) + ) + ) + + executeTest(docTag) { + composite<ContentList> { + check { + assertTrue(style.contains(ListStyle.DescriptionList), "Expected DL style") + } + group { + check { + assertTrue(style.contains(ListStyle.DescriptionTerm), "Expected DT style") + } + +"description list can have..." + } + group { + check { + assertTrue(style.contains(ListStyle.DescriptionTerm), "Expected DT style") + } + +"... two consecutive description terms" + } + group { + check { + assertTrue(style.contains(ListStyle.DescriptionDetails), "Expected DD style") + } + +"and usually has some sort of a description, like this one" + } + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ContextModuleAndPackageDocumentationReaderTest1.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ContextModuleAndPackageDocumentationReaderTest1.kt new file mode 100644 index 00000000..dfb3eff1 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ContextModuleAndPackageDocumentationReaderTest1.kt @@ -0,0 +1,187 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.testApi.logger.TestLogger +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import testApi.testRunner.TestDokkaConfigurationBuilder +import testApi.testRunner.dModule +import testApi.testRunner.dPackage +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ContextModuleAndPackageDocumentationReaderTest1 : AbstractContextModuleAndPackageDocumentationReaderTest() { + + + private val includeSourceSetA by lazy { temporaryDirectory.resolve("includeA.md").toFile() } + private val includeSourceSetB by lazy { temporaryDirectory.resolve("includeB.md").toFile() } + + @BeforeTest + fun materializeIncludes() { + includeSourceSetA.writeText( + """ + # Module moduleA + This is moduleA + + # Package sample.a + This is package sample.a\r\n + + # Package noise.b + This will just add some noise + """.trimIndent().replace("\n", "\r\n") + ) + + includeSourceSetB.writeText( + """ + # Module moduleB + This is moduleB + + # Package sample.b + This is package sample.b + + # Package noise.b + This will just add some more noise + """.trimIndent() + ) + } + + private val configurationBuilder = TestDokkaConfigurationBuilder().apply { + moduleName = "moduleA" + } + + private val sourceSetA by configurationBuilder.sourceSet { + name = "sourceSetA" + includes = listOf(includeSourceSetA.canonicalPath) + } + + + private val sourceSetB by configurationBuilder.sourceSet { + name = "sourceSetB" + includes = listOf(includeSourceSetB.canonicalPath) + } + + + private val sourceSetB2 by configurationBuilder.sourceSet { + name = "sourceSetB2" + includes = emptyList() + } + + + private val context by lazy { + DokkaContext.create( + configuration = configurationBuilder.build(), + logger = TestLogger(DokkaConsoleLogger(LoggingLevel.DEBUG)), + pluginOverrides = emptyList() + ) + } + + private val reader by lazy { context.plugin<InternalKotlinAnalysisPlugin>().querySingle { moduleAndPackageDocumentationReader } } + + @Test + fun `assert moduleA with sourceSetA`() { + val documentation = reader.read(dModule(name = "moduleA", sourceSets = setOf(sourceSetA))) + assertEquals( + 1, documentation.keys.size, + "Expected moduleA only containing documentation in a single source set" + ) + assertEquals( + "sourceSetA", documentation.keys.single().sourceSetID.sourceSetName, + "Expected moduleA documentation coming from sourceSetA" + ) + + assertEquals( + "This is moduleA", documentation.texts.single(), + "Expected moduleA documentation being present" + ) + } + + @Test + fun `assert moduleA with no source sets`() { + val documentation = reader.read(dModule("moduleA")) + assertEquals( + emptyMap(), documentation, + "Expected no documentation received for module not declaring a matching sourceSet" + ) + } + + @Test + fun `assert moduleA with unknown source set`() { + assertFailsWith<IllegalStateException>( + "Expected no documentation received for module with unknown sourceSet" + ) { + reader.read( + dModule("moduleA", sourceSets = setOf(configurationBuilder.unattachedSourceSet { name = "unknown" })) + ) + } + } + + @Test + fun `assert moduleA with all sourceSets`() { + val documentation = reader.read(dModule("moduleA", sourceSets = setOf(sourceSetA, sourceSetB, sourceSetB2))) + assertEquals(1, documentation.entries.size, "Expected only one entry from sourceSetA") + assertEquals(sourceSetA, documentation.keys.single(), "Expected only one entry from sourceSetA") + assertEquals("This is moduleA", documentation.texts.single()) + } + + @Test + fun `assert moduleB with sourceSetB and sourceSetB2`() { + val documentation = reader.read(dModule("moduleB", sourceSets = setOf(sourceSetB, sourceSetB2))) + assertEquals(1, documentation.keys.size, "Expected only one entry from sourceSetB") + assertEquals(sourceSetB, documentation.keys.single(), "Expected only one entry from sourceSetB") + assertEquals("This is moduleB", documentation.texts.single()) + } + + @Test + fun `assert sample_A in sourceSetA`() { + val documentation = reader.read(dPackage(DRI("sample.a"), sourceSets = setOf(sourceSetA))) + assertEquals(1, documentation.keys.size, "Expected only one entry from sourceSetA") + assertEquals(sourceSetA, documentation.keys.single(), "Expected only one entry from sourceSetA") + assertEquals("This is package sample.a\\r\\n", documentation.texts.single()) + } + + @Test + fun `assert sample_a_sub in sourceSetA`() { + val documentation = reader.read(dPackage(DRI("sample.a.sub"), sourceSets = setOf(sourceSetA))) + assertEquals( + emptyMap<DokkaSourceSet, DocumentationNode>(), documentation, + "Expected no documentation found for different package" + ) + } + + @Test + fun `assert sample_a in sourceSetB`() { + val documentation = reader.read(dPackage(DRI("sample.a"), sourceSets = setOf(sourceSetB))) + assertEquals( + emptyMap<DokkaSourceSet, DocumentationNode>(), documentation, + "Expected no documentation found for different sourceSet" + ) + } + + @Test + fun `assert sample_b in sourceSetB`() { + val documentation = reader.read(dPackage(DRI("sample.b"), sourceSets = setOf(sourceSetB))) + assertEquals(1, documentation.keys.size, "Expected only one entry from sourceSetB") + assertEquals(sourceSetB, documentation.keys.single(), "Expected only one entry from sourceSetB") + assertEquals("This is package sample.b", documentation.texts.single()) + } + + @Test + fun `assert sample_b in sourceSetB and sourceSetB2`() { + val documentation = reader.read(dPackage(DRI("sample.b"), sourceSets = setOf(sourceSetB, sourceSetB2))) + assertEquals(1, documentation.keys.size, "Expected only one entry from sourceSetB") + assertEquals(sourceSetB, documentation.keys.single(), "Expected only one entry from sourceSetB") + assertEquals("This is package sample.b", documentation.texts.single()) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ContextModuleAndPackageDocumentationReaderTest3.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ContextModuleAndPackageDocumentationReaderTest3.kt new file mode 100644 index 00000000..ebd5b7eb --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ContextModuleAndPackageDocumentationReaderTest3.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import testApi.testRunner.TestDokkaConfigurationBuilder +import testApi.testRunner.dPackage +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContextModuleAndPackageDocumentationReaderTest3 : AbstractContextModuleAndPackageDocumentationReaderTest() { + + private val include by lazy { temporaryDirectory.resolve("include.md").toFile() } + + @BeforeTest + fun materializeInclude() { + include.writeText( + """ + # Package + This is the root package + + # Package [root] + This is also the root package + """.trimIndent() + ) + } + + private val configurationBuilder = TestDokkaConfigurationBuilder() + + private val sourceSet by configurationBuilder.sourceSet { + includes = listOf(include.canonicalPath) + } + + private val context by lazy { + DokkaContext.create( + configuration = configurationBuilder.build(), + logger = DokkaConsoleLogger(LoggingLevel.DEBUG), + pluginOverrides = emptyList() + ) + } + + private val reader by lazy { context.plugin<InternalKotlinAnalysisPlugin>().querySingle { moduleAndPackageDocumentationReader } } + + + @Test + fun `root package is matched by empty string and the root keyword`() { + val documentation = reader.read(dPackage(DRI(""), sourceSets = setOf(sourceSet))) + assertEquals( + listOf("This is the root package", "This is also the root package"), documentation.texts + ) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/DivisionSwitchTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/DivisionSwitchTest.kt new file mode 100644 index 00000000..fec5fc47 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/DivisionSwitchTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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.dfs +import org.jetbrains.dokka.pages.ClasslikePageNode +import org.jetbrains.dokka.pages.ContentHeader +import org.jetbrains.dokka.pages.ContentNode +import org.jetbrains.dokka.pages.ContentText +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class DivisionSwitchTest : BaseAbstractTest() { + + private val query = """ + |/src/source0.kt + package package0 + /** + * Documentation for ClassA + */ + class ClassA { + val A: String = "A" + fun a() {} + fun b() {} + } + + /src/source1.kt + package package0 + /** + * Documentation for ClassB + */ + class ClassB : ClassA() { + val B: String = "B" + fun d() {} + fun e() {} + } + """.trimMargin() + + private fun configuration(switchOn: Boolean) = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + suppressObviousFunctions = false + pluginsConfigurations.add( + PluginConfigurationImpl( + DokkaBase::class.qualifiedName!!, + DokkaConfiguration.SerializationFormat.JSON, + """{ "separateInheritedMembers": $switchOn }""", + ) + ) + } + + private fun testClassB(switchOn: Boolean, operation: (ClasslikePageNode) -> Unit) { + testInline( + query, + configuration(switchOn), + cleanupOutput = true + ) { + pagesTransformationStage = { root -> + val classB = root.dfs { it.name == "ClassB" } as? ClasslikePageNode + assertNotNull(classB, "Tested class not found!") + operation(classB) + } + } + } + + 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 not split inherited and regular methods`() { + testClassB(false) { classB -> + val functions = classB.findSectionWithName("Functions") + assertNotNull(functions, "Functions not found!") + assertEquals(7, functions.children.size, "Incorrect number of functions found") + } + } + + @Test + fun `should not split inherited and regular properties`() { + testClassB(false) { classB -> + val properties = classB.findSectionWithName("Properties") + assertNotNull(properties, "Properties not found!") + assertEquals(2, properties.children.size, "Incorrect number of properties found") + } + } + + @Test + fun `should split inherited and regular methods`() { + testClassB(true) { classB -> + val functions = classB.findSectionWithName("Functions") + val inheritedFunctions = classB.findSectionWithName("Inherited functions") + assertNotNull(functions, "Functions not found!") + assertEquals(2, functions.children.size, "Incorrect number of functions found") + assertNotNull(inheritedFunctions, "Inherited functions not found!") + assertEquals(5, inheritedFunctions.children.size, "Incorrect number of inherited functions found") + } + } + + @Test + fun `should split inherited and regular properties`() { + testClassB(true) { classB -> + val properties = classB.findSectionWithName("Properties") + assertNotNull(properties, "Properties not found!") + assertEquals(1, properties.children.size, "Incorrect number of properties found") + val inheritedProperties = classB.findSectionWithName("Inherited properties") + assertNotNull(inheritedProperties, "Inherited properties not found!") + assertEquals(1, inheritedProperties.children.size, "Incorrect number of inherited properties found") + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/InheritedEntriesDocumentableFilterTransfromerTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/InheritedEntriesDocumentableFilterTransfromerTest.kt new file mode 100644 index 00000000..c07dd5b8 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/InheritedEntriesDocumentableFilterTransfromerTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DEnum +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class InheritedEntriesDocumentableFilterTransformerTest : BaseAbstractTest() { + val suppressingInheritedConfiguration = dokkaConfiguration { + suppressInheritedMembers = true + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src") + } + } + } + + val nonSuppressingInheritedConfiguration = dokkaConfiguration { + suppressObviousFunctions = false + suppressInheritedMembers = false + sourceSets { + sourceSet { + sourceRoots = listOf("src") + } + } + } + + + @Test + fun `should suppress toString, equals and hashcode but keep custom ones`() { + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + data class Suppressed(val x: String) { + override fun toString(): String { + return "custom" + } + } + """.trimIndent(), + suppressingInheritedConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals(listOf("toString", "copy", "component1").sorted(), functions.map { it.name }.sorted()) + } + } + } + + @Test + fun `should suppress toString, equals and hashcode`() { + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + data class Suppressed(val x: String) + """.trimIndent(), + suppressingInheritedConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals(listOf("copy", "component1").sorted(), functions.map { it.name }.sorted()) + } + } + } + + @Test + fun `should also suppress properites`(){ + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + open class Parent { + val parentValue = "String" + } + + class Child : Parent { + + } + """.trimIndent(), + suppressingInheritedConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val properties = modules.flatMap { it.packages }.flatMap { it.classlikes }.first { it.name == "Child" }.properties + assertEquals(0, properties.size) + } + } + } + + @Test + fun `should not suppress properites if config says so`(){ + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + open class Parent { + val parentValue = "String" + } + + class Child : Parent { + + } + """.trimIndent(), + nonSuppressingInheritedConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val properties = modules.flatMap { it.packages }.flatMap { it.classlikes }.first { it.name == "Child" }.properties + assertEquals(listOf("parentValue"), properties.map { it.name }) + } + } + } + + @Test + fun `should work with enum entries`(){ + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + enum class Suppressed { + ENTRY_SUPPRESSED + } + """.trimIndent(), + suppressingInheritedConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val entry = (modules.flatMap { it.packages }.flatMap { it.classlikes }.first { it.name == "Suppressed" } as DEnum).entries.first() + assertEquals(emptyList(), entry.properties) + assertEquals(emptyList(), entry.functions) + assertEquals(emptyList(), entry.classlikes) + } + } + } + + @Test + fun `should work with enum entries when not suppressing`(){ + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + enum class Suppressed { + ENTRY_SUPPRESSED; + class A + } + """.trimIndent(), + nonSuppressingInheritedConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val entry = (modules.flatMap { it.packages }.flatMap { it.classlikes }.first { it.name == "Suppressed" } as DEnum).entries.first() + assertEquals(listOf("name", "ordinal"), entry.properties.map { it.name }) + assertTrue(entry.functions.map { it.name }.containsAll(listOf("compareTo", "equals", "hashCode", "toString"))) + assertEquals(emptyList(), entry.classlikes) + } + } + } +} + diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/InvalidContentModuleAndPackageDocumentationReaderTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/InvalidContentModuleAndPackageDocumentationReaderTest.kt new file mode 100644 index 00000000..ca7536d4 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/InvalidContentModuleAndPackageDocumentationReaderTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import testApi.testRunner.TestDokkaConfigurationBuilder +import testApi.testRunner.dModule +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + + +class InvalidContentModuleAndPackageDocumentationReaderTest : AbstractContextModuleAndPackageDocumentationReaderTest() { + + private val includeA by lazy { temporaryDirectory.resolve("includeA.md").toFile() } + private val includeB by lazy { temporaryDirectory.resolve("includeB.md").toFile() } + + @BeforeTest + fun materializeInclude() { + includeA.writeText( + """ + Invalid random stuff + + # Module moduleA + Simple stuff + """.trimIndent() + ) + includeB.writeText( + """ + # Module moduleB + ### + """.trimIndent() + ) + } + + private val configurationBuilderA = TestDokkaConfigurationBuilder().apply { + moduleName = "moduleA" + } + private val configurationBuilderB = TestDokkaConfigurationBuilder().apply { + moduleName = "moduleB" + } + + private val sourceSetA by configurationBuilderA.sourceSet { + includes = listOf(includeA.canonicalPath) + } + + private val sourceSetB by configurationBuilderB.sourceSet { + includes = listOf(includeB.canonicalPath) + } + + private val contextA by lazy { + DokkaContext.create( + configuration = configurationBuilderA.build(), + logger = DokkaConsoleLogger(LoggingLevel.DEBUG), + pluginOverrides = emptyList() + ) + } + private val contextB by lazy { + DokkaContext.create( + configuration = configurationBuilderB.build(), + logger = DokkaConsoleLogger(LoggingLevel.DEBUG), + pluginOverrides = emptyList() + ) + } + + private val readerA by lazy { contextA.plugin<InternalKotlinAnalysisPlugin>().querySingle { moduleAndPackageDocumentationReader } } + private val readerB by lazy { contextB.plugin<InternalKotlinAnalysisPlugin>().querySingle { moduleAndPackageDocumentationReader } } + + + @Test + fun `parsing should fail with a message when documentation is in not proper format`() { + val exception = + runCatching { readerA.read(dModule(name = "moduleA", sourceSets = setOf(sourceSetA))) }.exceptionOrNull() + assertEquals( + "Unexpected classifier: \"Invalid\", expected either \"Module\" or \"Package\". \n" + + "For more information consult the specification: https://kotlinlang.org/docs/dokka-module-and-package-docs.html", + exception?.message + ) + } + + @Test + fun `parsing should fail with a message where it encountered error and why`() { + val exception = + runCatching { readerB.read(dModule(name = "moduleB", sourceSets = setOf(sourceSetB))) }.exceptionOrNull()?.message!! + + //I don't want to assert whole message since it contains a path to a temporary folder + assertTrue(exception.contains("Wrong AST Tree. Header does not contain expected content in ")) + assertTrue(exception.contains("includeB.md")) + assertTrue(exception.contains("element starts from offset 0 and ends 3: ###")) + } +} + diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt new file mode 100644 index 00000000..18e42e47 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt @@ -0,0 +1,386 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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 utils.assertNotNull +import utils.findSectionWithName +import kotlin.test.Test +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.add( + PluginConfigurationImpl( + DokkaBase::class.qualifiedName!!, + DokkaConfiguration.SerializationFormat.JSON, + """{ "mergeImplicitExpectActualDeclarations": $switchOn }""", + ) + ) + } + + private fun ContentNode.findTabWithType(type: TabbedContentType): ContentNode? = dfs { node -> + node.children.filterIsInstance<ContentGroup>().any { gr -> + gr.extra[TabbedContentTypeExtra]?.value == type + } + } + + @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 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( + """ + |/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") + props.children.singleOrNull().assertNotNull("prop1") + + val functions = classPage.findSectionWithName("Functions").assertNotNull("Functions") + 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<ContentDivergentGroup>().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.content.findTabWithType(BasicTabbedContentType.ENTRY).assertNotNull("Entries") + 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.single { it.sourceSets.size == 2 && it.dci.dri.singleOrNull()?.callable?.name == "method1" } + .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" + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerFunctionalTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerFunctionalTest.kt new file mode 100644 index 00000000..54f0120a --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerFunctionalTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.junit.jupiter.api.io.TempDir +import transformers.AbstractContextModuleAndPackageDocumentationReaderTest.Companion.texts +import utils.OnlyDescriptorsMPP +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals + +class ModuleAndPackageDocumentationTransformerFunctionalTest : BaseAbstractTest() { + + @OnlyDescriptorsMPP("#3238") + @Test + fun `multiplatform project`(@TempDir tempDir: Path) { + val include = tempDir.resolve("include.md").toFile() + include.writeText( + """ + # Module moduleA + This is moduleA + + # Package + This is the root package + + # Package [root] + This is also the root package + + # Package common + This is the common package + + # Package jvm + This is the jvm package + + # Package js + This is the js package + """.trimIndent() + ) + val configuration = dokkaConfiguration { + moduleName = "moduleA" + sourceSets { + sourceSet { + name = "commonMain" + displayName = "common" + analysisPlatform = "common" + sourceRoots = listOf("src/commonMain/kotlin") + includes = listOf(include.canonicalPath) + } + sourceSet { + name = "jsMain" + displayName = "js" + analysisPlatform = "js" + sourceRoots = listOf("src/jsMain/kotlin") + dependentSourceSets = setOf(DokkaSourceSetID("moduleA", "commonMain")) + includes = listOf(include.canonicalPath) + } + sourceSet { + name = "jvmMain" + displayName = "jvm" + analysisPlatform = "jvm" + sourceRoots = listOf("src/jvmMain/kotlin") + dependentSourceSets = setOf(DokkaSourceSetID("moduleA", "commonMain")) + includes = listOf(include.canonicalPath) + } + } + } + + testInline( + """ + /src/commonMain/kotlin/common/CommonApi.kt + package common + val commonApi = "common" + + /src/jsMain/kotlin/js/JsApi.kt + package js + val jsApi = "js" + + /src/jvmMain/kotlin/jvm/JvmApi.kt + package jvm + val jvmApi = "jvm" + + /src/commonMain/kotlin/CommonRoot.kt + val commonRoot = "commonRoot" + + /src/jsMain/kotlin/JsRoot.kt + val jsRoot = "jsRoot" + + /src/jvmMain/kotlin/JvmRoot.kt + val jvmRoot = "jvmRoot" + """.trimIndent(), + configuration + ) { + this.documentablesMergingStage = { module -> + val packageNames = module.packages.map { it.dri.packageName ?: "NULL" } + assertEquals( + listOf("", "common", "js", "jvm").sorted(), packageNames.sorted(), + "Expected all packages to be present" + ) + + /* Assert module documentation */ + assertEquals(3, module.documentation.keys.size, "Expected all three source sets") + assertEquals("This is moduleA", module.documentation.texts.distinct().joinToString()) + + /* Assert root package */ + val rootPackage = module.packages.single { it.dri.packageName == "" } + assertEquals(3, rootPackage.documentation.keys.size, "Expected all three source sets") + assertEquals( + listOf("This is the root package", "This is also the root package"), + rootPackage.documentation.texts.distinct() + ) + + /* Assert common package */ + val commonPackage = module.packages.single { it.dri.packageName == "common" } + assertEquals(3, commonPackage.documentation.keys.size, "Expected all three source sets") + assertEquals("This is the common package", commonPackage.documentation.texts.distinct().joinToString()) + + /* Assert js package */ + val jsPackage = module.packages.single { it.dri.packageName == "js" } + assertEquals( + "This is the js package", + jsPackage.documentation.texts.joinToString() + ) + + /* Assert the jvm package */ + val jvmPackage = module.packages.single { it.dri.packageName == "jvm" } + assertEquals( + "This is the jvm package", + jvmPackage.documentation.texts.joinToString() + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerUnitTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerUnitTest.kt new file mode 100644 index 00000000..a54b6c68 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerUnitTest.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.internal.ModuleAndPackageDocumentationReader +import org.jetbrains.dokka.analysis.markdown.jb.MARKDOWN_ELEMENT_FILE_NAME +import org.jetbrains.dokka.base.transformers.documentables.ModuleAndPackageDocumentationTransformer +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.DPackage +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.CustomDocTag +import org.jetbrains.dokka.model.doc.Description +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.Text +import testApi.testRunner.dPackage +import testApi.testRunner.sourceSet +import kotlin.test.Test +import kotlin.test.assertEquals + + +class ModuleAndPackageDocumentationTransformerUnitTest { + + @Test + fun `empty list of modules`() { + val transformer = ModuleAndPackageDocumentationTransformer( + object : ModuleAndPackageDocumentationReader { + override fun read(module: DModule): SourceSetDependent<DocumentationNode> = throw NotImplementedError() + override fun read(pkg: DPackage): SourceSetDependent<DocumentationNode> = throw NotImplementedError() + override fun read(module: DokkaConfiguration.DokkaModuleDescription): DocumentationNode = throw NotImplementedError() + } + ) + + assertEquals( + emptyList<DModule>(), transformer(emptyList()), + ) + } + + @Test + fun `single module documentation`() { + val transformer = ModuleAndPackageDocumentationTransformer( + object : ModuleAndPackageDocumentationReader { + override fun read(pkg: DPackage): SourceSetDependent<DocumentationNode> = throw NotImplementedError() + override fun read(module: DModule): SourceSetDependent<DocumentationNode> { + return module.sourceSets.associateWith { sourceSet -> + documentationNode("doc" + sourceSet.displayName) + } + } + override fun read(module: DokkaConfiguration.DokkaModuleDescription): DocumentationNode = throw NotImplementedError() + } + ) + + val result = transformer( + listOf( + DModule( + "ModuleName", + documentation = emptyMap(), + packages = emptyList(), + sourceSets = setOf( + sourceSet("A"), + sourceSet("B") + ) + ) + ) + ) + + assertEquals( + DModule( + "ModuleName", + documentation = mapOf( + sourceSet("A") to documentationNode("docA"), + sourceSet("B") to documentationNode("docB") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B")), + packages = emptyList() + ), + result.single() + ) + + } + + @Test + fun `merges with already existing module documentation`() { + val transformer = ModuleAndPackageDocumentationTransformer( + object : ModuleAndPackageDocumentationReader { + override fun read(pkg: DPackage): SourceSetDependent<DocumentationNode> = throw NotImplementedError() + override fun read(module: DModule): SourceSetDependent<DocumentationNode> { + /* Only add documentation for first source set */ + return module.sourceSets.take(1).associateWith { sourceSet -> + documentationNode("doc" + sourceSet.displayName) + } + } + override fun read(module: DokkaConfiguration.DokkaModuleDescription): DocumentationNode = throw NotImplementedError() + } + ) + + val result = transformer( + listOf( + DModule( + "MyModule", + documentation = mapOf( + sourceSet("A") to documentationNode("pre-existing:A"), + sourceSet("B") to documentationNode("pre-existing:B") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B")), + packages = emptyList() + ) + ) + ) + + assertEquals( + DModule( + "MyModule", + documentation = mapOf( + /* Expect previous documentation and newly attached one */ + sourceSet("A") to documentationNode("pre-existing:A", "docA"), + /* Only first source set will get documentation attached */ + sourceSet("B") to documentationNode("pre-existing:B") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B")), + packages = emptyList() + ), + result.single() + ) + } + + @Test + fun `package documentation`() { + val transformer = ModuleAndPackageDocumentationTransformer( + object : ModuleAndPackageDocumentationReader { + override fun read(module: DModule): SourceSetDependent<DocumentationNode> = emptyMap() + override fun read(pkg: DPackage): SourceSetDependent<DocumentationNode> { + /* Only attach documentation to packages with 'attach' */ + if ("attach" !in pkg.dri.packageName.orEmpty()) return emptyMap() + /* Only attach documentation to two source sets */ + return pkg.sourceSets.take(2).associateWith { sourceSet -> + documentationNode("doc:${sourceSet.displayName}:${pkg.dri.packageName}") + } + } + override fun read(module: DokkaConfiguration.DokkaModuleDescription): DocumentationNode = throw NotImplementedError() + } + ) + + val result = transformer( + listOf( + DModule( + "MyModule", + documentation = emptyMap(), + sourceSets = emptySet(), + packages = listOf( + dPackage( + dri = DRI("com.sample"), + documentation = mapOf( + sourceSet("A") to documentationNode("pre-existing:A:com.sample") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B"), sourceSet("C")), + ), + dPackage( + dri = DRI("com.attach"), + documentation = mapOf( + sourceSet("A") to documentationNode("pre-existing:A:com.attach") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B"), sourceSet("C")) + ), + dPackage( + dri = DRI("com.attach.sub"), + documentation = mapOf( + sourceSet("A") to documentationNode("pre-existing:A:com.attach.sub"), + sourceSet("B") to documentationNode("pre-existing:B:com.attach.sub"), + sourceSet("C") to documentationNode("pre-existing:C:com.attach.sub") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B"), sourceSet("C")), + ) + ) + ) + ) + ) + + result.single().packages.forEach { pkg -> + assertEquals( + setOf(sourceSet("A"), sourceSet("B"), sourceSet("C")), pkg.sourceSets, + "Expected source sets A, B, C for package ${pkg.dri.packageName}" + ) + } + + val comSample = result.single().packages.single { it.dri.packageName == "com.sample" } + assertEquals( + mapOf(sourceSet("A") to documentationNode("pre-existing:A:com.sample")), + comSample.documentation, + "Expected no documentation added to package 'com.sample' because of wrong package" + ) + + val comAttach = result.single().packages.single { it.dri.packageName == "com.attach" } + assertEquals( + mapOf( + sourceSet("A") to documentationNode("pre-existing:A:com.attach", "doc:A:com.attach"), + sourceSet("B") to documentationNode("doc:B:com.attach") + ), + comAttach.documentation, + "Expected documentation added to source sets A and B" + ) + + assertEquals( + DModule( + "MyModule", + documentation = emptyMap(), + sourceSets = emptySet(), + packages = listOf( + dPackage( + dri = DRI("com.sample"), + documentation = mapOf( + /* No documentation added, since in wrong package */ + sourceSet("A") to documentationNode("pre-existing:A:com.sample") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B"), sourceSet("C")), + + ), + dPackage( + dri = DRI("com.attach"), + documentation = mapOf( + /* Documentation added */ + sourceSet("A") to documentationNode("pre-existing:A:com.attach", "doc:A:com.attach"), + sourceSet("B") to documentationNode("doc:B:com.attach") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B"), sourceSet("C")), + ), + dPackage( + dri = DRI("com.attach.sub"), + documentation = mapOf( + /* Documentation added */ + sourceSet("A") to documentationNode( + "pre-existing:A:com.attach.sub", + "doc:A:com.attach.sub" + ), + /* Documentation added */ + sourceSet("B") to documentationNode( + "pre-existing:B:com.attach.sub", + "doc:B:com.attach.sub" + ), + /* No documentation added, since in wrong source set */ + sourceSet("C") to documentationNode("pre-existing:C:com.attach.sub") + ), + sourceSets = setOf(sourceSet("A"), sourceSet("B"), sourceSet("C")), + ) + ) + ), result.single() + ) + } + + + private fun documentationNode(vararg texts: String): DocumentationNode { + return DocumentationNode( + texts.toList() + .map { Description(CustomDocTag(listOf(Text(it)), name = MARKDOWN_ELEMENT_FILE_NAME)) }) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ObviousAndInheritedFunctionsDocumentableFilterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ObviousAndInheritedFunctionsDocumentableFilterTest.kt new file mode 100644 index 00000000..d035948f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ObviousAndInheritedFunctionsDocumentableFilterTest.kt @@ -0,0 +1,229 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import testApi.testRunner.dokkaConfiguration +import kotlin.test.assertEquals + +class ObviousAndInheritedFunctionsDocumentableFilterTest : BaseAbstractTest() { + companion object { + @JvmStatic + fun suppressingObviousConfiguration() = listOf(dokkaConfiguration { + suppressInheritedMembers = false + suppressObviousFunctions = true + sourceSets { + sourceSet { + sourceRoots = listOf("src") + } + } + }) + + @JvmStatic + fun nonSuppressingObviousConfiguration() = listOf(dokkaConfiguration { + suppressObviousFunctions = false + suppressInheritedMembers = false + sourceSets { + sourceSet { + sourceRoots = listOf("src") + } + } + }) + + @JvmStatic + fun suppressingInheritedConfiguration() = listOf(dokkaConfiguration { + suppressInheritedMembers = true + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src") + } + } + }) + + @JvmStatic + fun nonSuppressingInheritedConfiguration() = listOf(dokkaConfiguration { + suppressObviousFunctions = false + suppressInheritedMembers = false + sourceSets { + sourceSet { + sourceRoots = listOf("src") + } + } + }) + } + + + @ParameterizedTest + @MethodSource(value = ["suppressingObviousConfiguration"]) + fun `should suppress toString, equals and hashcode`(suppressingConfiguration: DokkaConfigurationImpl) { + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + data class Suppressed(val x: String) + """.trimIndent(), + suppressingConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals(0, functions.size) + } + } + } + + @ParameterizedTest + @MethodSource(value = ["suppressingObviousConfiguration", "suppressingInheritedConfiguration"]) + fun `should suppress toString, equals and hashcode for interface`(suppressingConfiguration: DokkaConfigurationImpl) { + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + interface Suppressed + """.trimIndent(), + suppressingConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals(0, functions.size) + } + } + } + + @ParameterizedTest + @MethodSource(value = ["suppressingObviousConfiguration", "suppressingInheritedConfiguration"]) + fun `should suppress toString, equals and hashcode in Java`(suppressingConfiguration: DokkaConfigurationImpl) { + testInline( + """ + /src/suppressed/Suppressed.java + package suppressed; + public class Suppressed { + } + """.trimIndent(), + suppressingConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals(0, functions.size) + } + } + } + + @ParameterizedTest + @MethodSource(value = ["suppressingObviousConfiguration"]) + fun `should suppress toString, equals and hashcode but keep custom ones`(suppressingConfiguration: DokkaConfigurationImpl) { + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + data class Suppressed(val x: String) { + override fun toString(): String { + return "custom" + } + } + """.trimIndent(), + suppressingConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals(listOf("toString"), functions.map { it.name }) + } + } + } + + @ParameterizedTest + @MethodSource(value = ["suppressingObviousConfiguration", "suppressingInheritedConfiguration"]) + fun `should suppress toString, equals and hashcode but keep custom ones in Java`(suppressingConfiguration: DokkaConfigurationImpl) { + testInline( + """ + /src/suppressed/Suppressed.java + package suppressed; + public class Suppressed { + @Override + public String toString() { + return ""; + } + } + """.trimIndent(), + suppressingConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals(listOf("toString"), functions.map { it.name }) + } + } + } + + @ParameterizedTest + @MethodSource(value = ["nonSuppressingObviousConfiguration", "nonSuppressingInheritedConfiguration"]) + fun `should not suppress toString, equals and hashcode if custom config is provided`(nonSuppressingConfiguration: DokkaConfigurationImpl) { + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + data class Suppressed(val x: String) + """.trimIndent(), + nonSuppressingConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals( + listOf("copy", "equals", "toString", "component1", "hashCode").sorted(), + functions.map { it.name }.sorted() + ) + } + } + } + + @ParameterizedTest + @MethodSource(value = ["nonSuppressingObviousConfiguration", "nonSuppressingInheritedConfiguration"]) + fun `not should suppress toString, equals and hashcode for interface if custom config is provided`(nonSuppressingConfiguration: DokkaConfigurationImpl) { + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + interface Suppressed + """.trimIndent(), + nonSuppressingConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + assertEquals(listOf("equals", "hashCode", "toString").sorted(), functions.map { it.name }.sorted()) + } + } + } + + @ParameterizedTest + @MethodSource(value = ["nonSuppressingObviousConfiguration", "nonSuppressingInheritedConfiguration"]) + fun `should not suppress toString, equals and hashcode if custom config is provided in Java`(nonSuppressingConfiguration: DokkaConfigurationImpl) { + testInline( + """ + /src/suppressed/Suppressed.java + package suppressed; + public class Suppressed { + } + """.trimIndent(), + nonSuppressingConfiguration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val functions = modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + //I would normally just assert names but this would make it JDK dependent, so this is better + assertEquals( + 5, + setOf( + "equals", + "hashCode", + "toString", + "notify", + "notifyAll" + ).intersect(functions.map { it.name }).size + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ReportUndocumentedTransformerTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ReportUndocumentedTransformerTest.kt new file mode 100644 index 00000000..c824e690 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/ReportUndocumentedTransformerTest.kt @@ -0,0 +1,927 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaDefaults +import org.jetbrains.dokka.PackageOptionsImpl +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + + +class ReportUndocumentedTransformerTest : BaseAbstractTest() { + @Test + fun `undocumented class gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |class X + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport(Regex("init")) + assertSingleUndocumentedReport(Regex("""sample/X/""")) + } + } + } + + @Test + fun `undocumented non-public class does not get reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |internal class X + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + @Test + fun `undocumented function gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |/** Documented */ + |class X { + | fun x() + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("X")) + assertSingleUndocumentedReport(Regex("X/x")) + } + } + } + + @Test + fun `undocumented property gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |/** Documented */ + |class X { + | val x: Int = 0 + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("X")) + assertSingleUndocumentedReport(Regex("X/x")) + } + } + } + + @Test + fun `undocumented primary constructor does not get reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |/** Documented */ + |class X(private val x: Int) { + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + @Test + fun `data class component functions do not get reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |/** Documented */ + |data class X(val x: Int) { + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport(Regex("component")) + assertNumberOfUndocumentedReports(1) + } + } + } + + @Ignore + @Test + fun `undocumented secondary constructor gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |/** Documented */ + |class X { + | constructor(unit: Unit) : this() + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("X")) + assertSingleUndocumentedReport(Regex("X.*init.*Unit")) + } + } + } + + @Test + fun `undocumented inherited function does not get reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |/** Documented */ + |open class A { + | fun a() = Unit + |} + | + |/** Documented */ + |class B : A() + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport(Regex("B")) + assertSingleUndocumentedReport(Regex("A.*a")) + } + } + } + + @Test + fun `undocumented inherited property does not get reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |/** Documented */ + |open class A { + | val a = Unit + |} + | + |/** Documented */ + |class B : A() + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport(Regex("B")) + assertSingleUndocumentedReport(Regex("A.*a")) + } + } + } + + @Test + fun `overridden function does not get reported when super is documented`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + |import kotlin.Exception + | + |/** Documented */ + |open class A { + | /** Documented */ + | fun a() = Unit + |} + | + |/** Documented */ + |class B : A() { + | override fun a() = throw Exception() + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + @Test + fun `overridden property does not get reported when super is documented`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + |import kotlin.Exception + | + |/** Documented */ + |open class A { + | /** Documented */ + | open val a = 0 + |} + | + |/** Documented */ + |class B : A() { + | override val a = 1 + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + @Test + fun `report disabled by source set`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = false + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |class X + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + @Test + fun `report enabled by package configuration`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + perPackageOptions += packageOptions( + matchingRegex = "sample.*", + reportUndocumented = true, + ) + reportUndocumented = false + sourceRoots = listOf("src/main/kotlin/Test.kt") + } + } + } + + testInline( + """ + |/src/main/kotlin/Test.kt + |package sample + | + |class X + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("X")) + } + } + } + + @Test + fun `report enabled by more specific package configuration`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + perPackageOptions += packageOptions( + matchingRegex = "sample.*", + reportUndocumented = false, + ) + perPackageOptions += packageOptions( + matchingRegex = "sample.enabled.*", + reportUndocumented = true, + ) + reportUndocumented = false + sourceRoots = listOf("src/main/kotlin/") + } + } + } + + testInline( + """ + |/src/main/kotlin/sample/disabled/Disabled.kt + |package sample.disabled + |class Disabled + | + |/src/main/kotlin/sample/enabled/Enabled.kt + |package sample.enabled + |class Enabled + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("Enabled")) + assertNumberOfUndocumentedReports(1) + } + } + } + + @Test + fun `report disabled by more specific package configuration`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + perPackageOptions += packageOptions( + matchingRegex = "sample.*", + reportUndocumented = true, + ) + perPackageOptions += packageOptions( + matchingRegex = "sample.disabled.*", + reportUndocumented = false, + ) + reportUndocumented = true + sourceRoots = listOf("src/main/kotlin/") + } + } + } + + testInline( + """ + |/src/main/kotlin/sample/disabled/Disabled.kt + |package sample.disabled + |class Disabled + | + |/src/main/kotlin/sample/enabled/Enabled.kt + |package sample.enabled + |class Enabled + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("Enabled")) + assertNumberOfUndocumentedReports(1) + } + } + } + + @Test + fun `multiplatform undocumented class gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + val commonMain by sourceSet { + reportUndocumented = true + analysisPlatform = Platform.common.toString() + name = "commonMain" + displayName = "commonMain" + sourceRoots = listOf("src/commonMain/kotlin") + } + + sourceSet { + reportUndocumented = true + analysisPlatform = Platform.jvm.toString() + name = "jvmMain" + displayName = "jvmMain" + sourceRoots = listOf("src/jvmMain/kotlin") + dependentSourceSets = setOf(commonMain.sourceSetID) + } + } + } + + testInline( + """ + |/src/commonMain/kotlin/sample/Common.kt + |package sample + |expect class X + | + |/src/jvmMain/kotlin/sample/JvmMain.kt + |package sample + |actual class X + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNumberOfUndocumentedReports(2, Regex("X")) + assertSingleUndocumentedReport(Regex("X.*jvmMain")) + assertSingleUndocumentedReport(Regex("X.*commonMain")) + } + } + } + + @Test + fun `multiplatform undocumented class does not get reported if expect is documented`() { + val configuration = dokkaConfiguration { + sourceSets { + val commonMain by sourceSet { + reportUndocumented = true + analysisPlatform = Platform.common.toString() + name = "commonMain" + displayName = "commonMain" + sourceRoots = listOf("src/commonMain/kotlin") + } + + sourceSet { + reportUndocumented = true + analysisPlatform = Platform.jvm.toString() + name = "jvmMain" + displayName = "jvmMain" + sourceRoots = listOf("src/jvmMain/kotlin") + dependentSourceSets = setOf(commonMain.sourceSetID) + } + } + } + + testInline( + """ + |/src/commonMain/kotlin/sample/Common.kt + |package sample + |/** Documented */ + |expect class X + | + |/src/jvmMain/kotlin/sample/JvmMain.kt + |package sample + |actual class X + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNumberOfUndocumentedReports(0) + } + } + } + + @Test + fun `multiplatform undocumented function gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + val commonMain by sourceSet { + reportUndocumented = true + analysisPlatform = Platform.common.toString() + name = "commonMain" + displayName = "commonMain" + sourceRoots = listOf("src/commonMain/kotlin") + } + + sourceSet { + reportUndocumented = true + analysisPlatform = Platform.jvm.toString() + name = "jvmMain" + displayName = "jvmMain" + sourceRoots = listOf("src/jvmMain/kotlin") + dependentSourceSets = setOf(commonMain.sourceSetID) + } + + sourceSet { + reportUndocumented = true + analysisPlatform = Platform.native.toString() + name = "macosMain" + displayName = "macosMain" + sourceRoots = listOf("src/macosMain/kotlin") + dependentSourceSets = setOf(commonMain.sourceSetID) + } + } + } + + testInline( + """ + |/src/commonMain/kotlin/sample/Common.kt + |package sample + |expect fun x() + | + |/src/macosMain/kotlin/sample/MacosMain.kt + |package sample + |/** Documented */ + |actual fun x() = Unit + | + |/src/jvmMain/kotlin/sample/JvmMain.kt + |package sample + |actual fun x() = Unit + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNumberOfUndocumentedReports(2) + assertSingleUndocumentedReport(Regex("x.*commonMain")) + assertSingleUndocumentedReport(Regex("x.*jvmMain")) + } + } + } + + @Test + fun `java undocumented class gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Test.java + |package sample + |public class Test { } + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport(Regex("init")) + assertSingleUndocumentedReport(Regex("""Test""")) + assertNumberOfUndocumentedReports(1) + } + } + } + + @Test + fun `java undocumented non-public class does not get reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Test.java + |package sample + |class Test { } + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + @Test + fun `java undocumented constructor does not get reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Test.java + |package sample + |/** Documented */ + |public class Test { + | public Test() { + | } + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + @Test + fun `java undocumented method gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/X.java + |package sample + |/** Documented */ + |public class X { + | public void x { } + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("X")) + assertSingleUndocumentedReport(Regex("X.*x")) + assertNumberOfUndocumentedReports(1) + } + } + } + + @Test + fun `java undocumented property gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/X.java + |package sample + |/** Documented */ + |public class X { + | public int x = 0; + |} + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("X")) + assertSingleUndocumentedReport(Regex("X.*x")) + assertNumberOfUndocumentedReports(1) + } + } + } + + @Test + fun `java undocumented inherited method gets reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Super.java + |package sample + |/** Documented */ + |public class Super { + | public void x() {} + |} + | + |/src/main/java/sample/X.java + |package sample + |/** Documented */ + |public class X extends Super { + | public void x() {} + |} + | + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertSingleUndocumentedReport(Regex("X")) + assertSingleUndocumentedReport(Regex("X.*x")) + assertSingleUndocumentedReport(Regex("Super.*x")) + assertNumberOfUndocumentedReports(2) + } + } + } + + @Test + fun `java documented inherited method does not get reported`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Super.java + |package sample + |/** Documented */ + |public class Super { + | /** Documented */ + | public void x() {} + |} + | + |/src/main/java/sample/X.java + |package sample + |/** Documented */ + |public class X extends Super { + | + |} + | + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + @Test + fun `java overridden function does not get reported when super is documented`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + reportUndocumented = true + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Super.java + |package sample + |/** Documented */ + |public class Super { + | /** Documented */ + | public void x() {} + |} + | + |/src/main/java/sample/X.java + |package sample + |/** Documented */ + |public class X extends Super { + | @Override + | public void x() {} + |} + | + """.trimMargin(), + configuration + ) { + pagesTransformationStage = { + assertNoUndocumentedReport() + } + } + } + + private fun assertNumberOfUndocumentedReports(expectedReports: Int, regex: Regex = Regex(".")) { + val reports = logger.warnMessages + .filter { it.startsWith("Undocumented:") } + val matchingReports = reports + .filter { it.contains(regex) } + + assertEquals( + expectedReports, matchingReports.size, + "Expected $expectedReports report of documented code ($regex).\n" + + "Found matching reports: $matchingReports\n" + + "Found reports: $reports" + ) + } + + private fun assertSingleUndocumentedReport(regex: Regex) { + assertNumberOfUndocumentedReports(1, regex) + } + + private fun assertNoUndocumentedReport(regex: Regex) { + assertNumberOfUndocumentedReports(0, regex) + } + + private fun assertNoUndocumentedReport() { + assertNoUndocumentedReport(Regex(".")) + } + + private fun packageOptions( + matchingRegex: String, + reportUndocumented: Boolean?, + includeNonPublic: Boolean = true, + skipDeprecated: Boolean = false, + suppress: Boolean = false, + documentedVisibilities: Set<DokkaConfiguration.Visibility> = DokkaDefaults.documentedVisibilities + ) = PackageOptionsImpl( + matchingRegex = matchingRegex, + reportUndocumented = reportUndocumented, + includeNonPublic = includeNonPublic, + documentedVisibilities = documentedVisibilities, + skipDeprecated = skipDeprecated, + suppress = suppress + ) +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SourceLinkTransformerTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SourceLinkTransformerTest.kt new file mode 100644 index 00000000..87424120 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SourceLinkTransformerTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.SourceLinkDefinitionImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jsoup.nodes.Element +import signatures.renderedContent +import utils.TestOutputWriterPlugin +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class SourceLinkTransformerTest : BaseAbstractTest() { + + private fun Element.getSourceLink() = select(".symbol .floating-right") + .select("a[href]") + .attr("href") + + @Test + fun `source link should lead to name`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + sourceLinks = listOf( + SourceLinkDefinitionImpl( + localDirectory = "src/main/kotlin", + remoteUrl = URL("https://github.com/user/repo/tree/master/src/main/kotlin"), + remoteLineSuffix = "#L" + ) + ) + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/basic/Deprecated.kt + |package testpackage + | + |/** + |* Marks the annotated declaration as deprecated. ... + |*/ + |@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS) + |@MustBeDocumented + |public annotation class Deprecated( + | val message: String, + | val replaceWith: ReplaceWith = ReplaceWith(""), + | val level: DeprecationLevel = DeprecationLevel.WARNING + |) + """.trimMargin(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val page = writerPlugin.writer.renderedContent("root/testpackage/-deprecated/index.html") + val sourceLink = page.getSourceLink() + + assertEquals( + "https://github.com/user/repo/tree/master/src/main/kotlin/basic/Deprecated.kt#L8", + sourceLink + ) + } + } + } + + @Test + fun `source link should be for actual typealias`() { + val mppConfiguration = dokkaConfiguration { + moduleName = "test" + sourceSets { + sourceSet { + name = "common" + sourceRoots = listOf("src/main/kotlin/common/Test.kt") + classpath = listOf(commonStdlibPath!!) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + } + sourceSet { + name = "jvm" + dependentSourceSets = setOf(DokkaSourceSetID("test", "common")) + sourceRoots = listOf("src/main/kotlin/jvm/Test.kt") + classpath = listOf(commonStdlibPath!!) + externalDocumentationLinks = listOf(stdlibExternalDocumentationLink) + sourceLinks = listOf( + SourceLinkDefinitionImpl( + localDirectory = "src/main/kotlin", + remoteUrl = URL("https://github.com/user/repo/tree/master/src/main/kotlin"), + remoteLineSuffix = "#L" + ) + ) + } + } + } + + val writerPlugin = TestOutputWriterPlugin() + + testInline( + """ + |/src/main/kotlin/common/Test.kt + |package example + | + |expect class Foo + | + |/src/main/kotlin/jvm/Test.kt + |package example + | + |class Bar + |actual typealias Foo = Bar + | + """.trimMargin(), + mppConfiguration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val page = writerPlugin.writer.renderedContent("test/example/-foo/index.html") + val sourceLink = page.getSourceLink() + + assertEquals( + "https://github.com/user/repo/tree/master/src/main/kotlin/jvm/Test.kt#L4", + sourceLink + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SuppressTagFilterTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SuppressTagFilterTest.kt new file mode 100644 index 00000000..5392a028 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SuppressTagFilterTest.kt @@ -0,0 +1,211 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.WithCompanion +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class SuppressTagFilterTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + } + } + } + + @Test + fun `should filter classes with suppress tag`() { + testInline( + """ + |/src/suppressed/NotSuppressed.kt + |/** + | * sample docs + |*/ + |class NotSuppressed + |/src/suppressed/Suppressed.kt + |/** + | * sample docs + | * @suppress + |*/ + |class Suppressed + """.trimIndent(), configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + assertEquals( + "NotSuppressed", + modules.flatMap { it.packages }.flatMap { it.classlikes }.singleOrNull()?.name + ) + } + } + } + + @Test + fun `should filter functions with suppress tag`() { + testInline( + """ + |/src/suppressed/Suppressed.kt + |class Suppressed { + | /** + | * sample docs + | * @suppress + | */ + | fun suppressedFun(){ } + |} + """.trimIndent(), configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + assertNull(modules.flatMap { it.packages }.flatMap { it.classlikes }.flatMap { it.functions } + .firstOrNull { it.name == "suppressedFun" }) + } + } + } + + @Test + fun `should filter top level functions`() { + testInline( + """ + |/src/suppressed/Suppressed.kt + |/** + | * sample docs + | * @suppress + | */ + |fun suppressedFun(){ } + | + |/** + | * Sample + | */ + |fun notSuppressedFun() { } + """.trimIndent(), configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + assertNull(modules.flatMap { it.packages }.flatMap { it.functions } + .firstOrNull { it.name == "suppressedFun" }) + } + } + } + + @Test + fun `should filter setter`() { + testInline( + """ + |/src/suppressed/Suppressed.kt + |var property: Int + |/** @suppress */ + |private set + """.trimIndent(), configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val prop = modules.flatMap { it.packages }.flatMap { it.properties } + .find { it.name == "property" } + assertNotNull(prop) + assertNotNull(prop.getter) + assertNull(prop.setter) + } + } + } + + @Test + fun `should filter top level type aliases`() { + testInline( + """ + |/src/suppressed/suppressed.kt + |/** + | * sample docs + | * @suppress + | */ + |typealias suppressedTypeAlias = String + | + |/** + | * Sample + | */ + |typealias notSuppressedTypeAlias = String + """.trimIndent(), configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + assertNull(modules.flatMap { it.packages }.flatMap { it.typealiases } + .firstOrNull { it.name == "suppressedTypeAlias" }) + assertNotNull(modules.flatMap { it.packages }.flatMap { it.typealiases } + .firstOrNull { it.name == "notSuppressedTypeAlias" }) + } + } + } + + @Test + fun `should filter companion object`() { + testInline( + """ + |/src/suppressed/Suppressed.kt + |class Suppressed { + |/** + | * @suppress + | */ + |companion object { + | val x = 1 + |} + |} + """.trimIndent(), configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + assertNull((modules.flatMap { it.packages }.flatMap { it.classlikes } + .firstOrNull { it.name == "Suppressed" } as? WithCompanion)?.companion) + } + } + } + + @Test + fun `should suppress inner classlike`() { + testInline( + """ + |/src/suppressed/Testing.kt + |class Testing { + | /** + | * Sample + | * @suppress + | */ + | inner class Suppressed { + | val x = 1 + | } + |} + """.trimIndent(), configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val testingClass = modules.flatMap { it.packages }.flatMap { it.classlikes }.single() + assertNull(testingClass.classlikes.firstOrNull()) + } + } + } + + @Test + fun `should suppress enum entry`() { + testInline( + """ + |/src/suppressed/Testing.kt + |enum class Testing { + | /** + | * Sample + | * @suppress + | */ + | SUPPRESSED, + | + | /** + | * Not suppressed + | */ + | NOT_SUPPRESSED + |} + """.trimIndent(), configuration + ) { + preMergeDocumentablesTransformationStage = { modules -> + val testingClass = modules.flatMap { it.packages }.flatMap { it.classlikes }.single() as DEnum + assertEquals(listOf("NOT_SUPPRESSED"), testingClass.entries.map { it.name }) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SuppressedByConfigurationDocumentableFilterTransformerTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SuppressedByConfigurationDocumentableFilterTransformerTest.kt new file mode 100644 index 00000000..f946a885 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/SuppressedByConfigurationDocumentableFilterTransformerTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.DokkaDefaults +import org.jetbrains.dokka.PackageOptionsImpl +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import kotlin.test.Test +import kotlin.test.assertEquals + +class SuppressedByConfigurationDocumentableFilterTransformerTest : BaseAbstractTest() { + + @Test + fun `class filtered by package options`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + perPackageOptions = listOf( + packageOptions(matchingRegex = "suppressed.*", suppress = true), + packageOptions(matchingRegex = "default.*", suppress = false) + ) + } + } + } + + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + class Suppressed + + /src/default/Default.kt + package default + class Default.kt + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + assertEquals(1, module.children.size, "Expected just a single package in module") + assertEquals(1, module.packages.size, "Expected just a single package in module") + + val pkg = module.packages.single() + assertEquals("default", pkg.dri.packageName, "Expected 'default' package in module") + assertEquals(1, pkg.children.size, "Expected just a single child in 'default' package") + assertEquals(1, pkg.classlikes.size, "Expected just a single child in 'default' package") + + val classlike = pkg.classlikes.single() + assertEquals(DRI("default", "Default"), classlike.dri, "Expected 'Default' class in 'default' package") + } + } + } + + @Test + fun `class filtered by more specific package options`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + perPackageOptions = listOf( + packageOptions(matchingRegex = "parent.some.*", suppress = false), + packageOptions(matchingRegex = "parent.some.suppressed.*", suppress = true), + + packageOptions(matchingRegex = "parent.other.*", suppress = true), + packageOptions(matchingRegex = "parent.other.default.*", suppress = false) + ) + } + } + } + + testInline( + """ + /src/parent/some/Some.kt + package parent.some + class Some + + /src/parent/some/suppressed/Suppressed.kt + package parent.some.suppressed + class Suppressed + + /src/parent/other/Other.kt + package parent.other + class Other + + /src/parent/other/default/Default.kt + package parent.other.default + class Default + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + assertEquals(2, module.packages.size, "Expected two packages in module") + assertEquals( + listOf(DRI("parent.some"), DRI("parent.other.default")).sortedBy { it.packageName }, + module.packages.map { it.dri }.sortedBy { it.packageName }, + "Expected 'parent.some' and 'parent.other.default' packages to be not suppressed" + ) + } + } + } + + @Test + fun `class filtered by parent file path`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + suppressedFiles = listOf("src/suppressed") + } + } + } + + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + class Suppressed + + /src/default/Default.kt + package default + class Default.kt + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + assertEquals(1, module.children.size, "Expected just a single package in module") + assertEquals(1, module.packages.size, "Expected just a single package in module") + + val pkg = module.packages.single() + assertEquals("default", pkg.dri.packageName, "Expected 'default' package in module") + assertEquals(1, pkg.children.size, "Expected just a single child in 'default' package") + assertEquals(1, pkg.classlikes.size, "Expected just a single child in 'default' package") + + val classlike = pkg.classlikes.single() + assertEquals(DRI("default", "Default"), classlike.dri, "Expected 'Default' class in 'default' package") + } + } + } + + @Test + fun `class filtered by exact file path`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + suppressedFiles = listOf("src/suppressed/Suppressed.kt") + } + } + } + + testInline( + """ + /src/suppressed/Suppressed.kt + package suppressed + class Suppressed + + /src/default/Default.kt + package default + class Default.kt + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + assertEquals(1, module.children.size, "Expected just a single package in module") + assertEquals(1, module.packages.size, "Expected just a single package in module") + + val pkg = module.packages.single() + assertEquals("default", pkg.dri.packageName, "Expected 'default' package in module") + assertEquals(1, pkg.children.size, "Expected just a single child in 'default' package") + assertEquals(1, pkg.classlikes.size, "Expected just a single child in 'default' package") + + val classlike = pkg.classlikes.single() + assertEquals(DRI("default", "Default"), classlike.dri, "Expected 'Default' class in 'default' package") + } + } + } + + private fun packageOptions( + matchingRegex: String, + suppress: Boolean + ) = PackageOptionsImpl( + matchingRegex = matchingRegex, + suppress = suppress, + includeNonPublic = true, + documentedVisibilities = DokkaDefaults.documentedVisibilities, + reportUndocumented = false, + skipDeprecated = false + ) + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/transformers/isExceptionTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/isExceptionTest.kt new file mode 100644 index 00000000..a387c60d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/transformers/isExceptionTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package transformers + +import org.jetbrains.dokka.base.transformers.documentables.isException +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DTypeAlias +import utils.AbstractModelTest +import kotlin.test.Test + +class IsExceptionKotlinTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "classes") { + @Test + fun `isException should work for kotlin exception`(){ + inlineModelTest( + """ + |class ExampleException(): Exception()""" + ) { + with((this / "classes" / "ExampleException").cast<DClass>()) { + name equals "ExampleException" + isException equals true + } + } + } + + @Test + fun `isException should work for java exceptions`(){ + inlineModelTest( + """ + |class ExampleException(): java.lang.Exception()""" + ) { + with((this / "classes" / "ExampleException").cast<DClass>()) { + name equals "ExampleException" + isException equals true + } + } + } + + @Test + fun `isException should work for RuntimeException`(){ + inlineModelTest( + """ + |class ExampleException(reason: String): RuntimeException(reason)""" + ) { + with((this / "classes" / "ExampleException").cast<DClass>()) { + name equals "ExampleException" + isException equals true + } + } + } + + @Test + fun `isException should work if exception is typealiased`(){ + inlineModelTest( + """ + |typealias ExampleException = java.lang.Exception""" + ) { + with((this / "classes" / "ExampleException").cast<DTypeAlias>()) { + name equals "ExampleException" + isException equals true + } + } + } + + @Test + fun `isException should work if exception is extending a typaliased class`(){ + inlineModelTest( + """ + |class ExampleException(): Exception() + |typealias ExampleExceptionAlias = ExampleException""" + ) { + with((this / "classes" / "ExampleExceptionAlias").cast<DTypeAlias>()) { + name equals "ExampleExceptionAlias" + isException equals true + } + } + } + + @Test + fun `isException should return false for a basic class`(){ + inlineModelTest( + """ + |class NotAnException(): Serializable""" + ) { + with((this / "classes" / "NotAnException").cast<DClass>()) { + name equals "NotAnException" + isException equals false + } + } + } + + @Test + fun `isException should return false for a typealias`(){ + inlineModelTest( + """ + |typealias NotAnException = Serializable""" + ) { + with((this / "classes" / "NotAnException").cast<DTypeAlias>()) { + name equals "NotAnException" + isException equals false + } + } + } +} + +class IsExceptionJavaTest: AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { + @Test + fun `isException should work for java exceptions`(){ + inlineModelTest( + """ + |public class ExampleException extends java.lang.Exception { }""" + ) { + with((this / "java" / "ExampleException").cast<DClass>()) { + name equals "ExampleException" + isException equals true + } + } + } + + @Test + fun `isException should work for RuntimeException`(){ + inlineModelTest( + """ + |public class ExampleException extends java.lang.RuntimeException""" + ) { + with((this / "java" / "ExampleException").cast<DClass>()) { + name equals "ExampleException" + isException equals true + } + } + } + + @Test + fun `isException should return false for a basic class`(){ + inlineModelTest( + """ + |public class NotAnException extends Serializable""" + ) { + with((this / "java" / "NotAnException").cast<DClass>()) { + name equals "NotAnException" + isException equals false + } + } + } +} + diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/AccessorMethodNamingTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/AccessorMethodNamingTest.kt new file mode 100644 index 00000000..ff36337a --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/AccessorMethodNamingTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DProperty +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * https://kotlinlang.org/docs/java-to-kotlin-interop.html#properties + * https://kotlinlang.org/docs/java-interop.html#getters-and-setters + */ +class AccessorMethodNamingTest : BaseAbstractTest() { + + @Test + fun `standard property`() { + testAccessors("data class TestCase(var standardString: String, var standardBoolean: Boolean)") { + doTest("standardString", "getStandardString", "setStandardString") + doTest("standardBoolean", "getStandardBoolean", "setStandardBoolean") + } + } + + @Test + fun `properties that start with the word 'is' use the special is rules`() { + testAccessors("data class TestCase(var isFoo: String, var isBar: Boolean)") { + doTest("isFoo", "isFoo", "setFoo") + doTest("isBar", "isBar", "setBar") + } + } + + @Test + fun `properties that start with a word that starts with 'is' use get and set`() { + testAccessors("data class TestCase(var issuesFetched: Int, var issuesWereDisplayed: Boolean)") { + doTest("issuesFetched", "getIssuesFetched", "setIssuesFetched") + doTest("issuesWereDisplayed", "getIssuesWereDisplayed", "setIssuesWereDisplayed") + } + } + + @Test + fun `properties that start with the word 'is' followed by underscore use the special is rules`() { + testAccessors("data class TestCase(var is_foo: String, var is_bar: Boolean)") { + doTest("is_foo", "is_foo", "set_foo") + doTest("is_bar", "is_bar", "set_bar") + } + } + + @Test + fun `properties that start with the word 'is' followed by a number use the special is rules`() { + testAccessors("data class TestCase(var is1of: String, var is2of: Boolean)") { + doTest("is1of", "is1of", "set1of") + doTest("is2of", "is2of", "set2of") + } + } + + @Test + fun `sanity check short names`() { + testAccessors( + """ + data class TestCase( + var i: Boolean, + var `is`: Boolean, + var isz: Boolean, + var isA: Int, + var isB: Boolean, + ) + """.trimIndent() + ) { + doTest("i", "getI", "setI") + doTest("is", "getIs", "setIs") + doTest("isz", "getIsz", "setIsz") + doTest("isA", "isA", "setA") + doTest("isB", "isB", "setB") + } + } + + private fun testAccessors(code: String, block: PropertyTestCase.() -> Unit) { + val configuration = dokkaConfiguration { + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + testInline(""" + /src/main/kotlin/sample/TestCase.kt + package sample + + $code + """.trimIndent(), + configuration) { + documentablesMergingStage = { module -> + val properties = module.packages.single().classlikes.first().properties + PropertyTestCase(properties).apply { + block() + finish() + } + } + } + } + + private class PropertyTestCase(private val properties: List<DProperty>) { + private var testsDone: Int = 0 + + fun doTest(kotlinName: String, getter: String? = null, setter: String? = null) { + properties.first { it.name == kotlinName }.let { + assertEquals(getter, it.getter?.name) + assertEquals(setter, it.setter?.name) + } + testsDone += 1 + } + + fun finish() { + assertTrue(testsDone > 0, "No tests in TestCase") + assertEquals(testsDone, properties.size) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/Bug1341.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/Bug1341.kt new file mode 100644 index 00000000..6a7bfc97 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/Bug1341.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import kotlin.test.Test +import kotlin.test.assertEquals + +class Bug1341 : BaseAbstractTest() { + @Test + fun `reproduce bug #1341`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + analysisPlatform = "jvm" + } + } + } + + testInline( + """ + /src/com/sample/OtherClass.kt + package com.sample + class OtherClass internal constructor() { + internal annotation class CustomAnnotation + } + + /src/com/sample/ClassUsingAnnotation.java + package com.sample + public class ClassUsingAnnotation { + @OtherClass.CustomAnnotation + public int doSomething() { + return 1; + } + } + """.trimIndent(), + configuration + ) { + this.documentablesMergingStage = { module -> + assertEquals(DRI("com.sample"), module.packages.single().dri) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt new file mode 100644 index 00000000..6812f0b4 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt @@ -0,0 +1,1107 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.modifiers +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.* +import utils.text +import kotlin.test.* +import utils.OnlyDescriptors + +class DefaultDescriptorToDocumentableTranslatorTest : BaseAbstractTest() { + val configuration = dokkaConfiguration { + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + classpath = listOf(commonStdlibPath!!, jvmStdlibPath!!) + } + } + } + + @Suppress("DEPRECATION") // for includeNonPublic + val javaConfiguration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + includeNonPublic = true + } + } + } + + @Test + fun `data class kdocs over generated methods`() { + testInline( + """ + |/src/main/kotlin/sample/XD.kt + |package sample + |/** + | * But the fat Hobbit, he knows. Eyes always watching. + | */ + |data class XD(val xd: String) { + | /** + | * But the fat Hobbit, he knows. Eyes always watching. + | */ + | fun custom(): String = "" + | + | /** + | * Memory is not what the heart desires. That is only a mirror. + | */ + | override fun equals(other: Any?): Boolean = true + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + assertEquals("", module.documentationOf("XD", "copy")) + assertEquals( + "Memory is not what the heart desires. That is only a mirror.", + module.documentationOf( + "XD", + "equals" + ) + ) + assertEquals("", module.documentationOf("XD", "hashCode")) + assertEquals("", module.documentationOf("XD", "toString")) + assertEquals("But the fat Hobbit, he knows. Eyes always watching.", module.documentationOf("XD", "custom")) + } + } + } + + @Test + fun `simple class kdocs`() { + testInline( + """ + |/src/main/kotlin/sample/XD.kt + |package sample + |/** + | * But the fat Hobbit, he knows. Eyes always watching. + | */ + |class XD(val xd: String) { + | /** + | * But the fat Hobbit, he knows. Eyes always watching. + | */ + | fun custom(): String = "" + | + | /** + | * Memory is not what the heart desires. That is only a mirror. + | */ + | override fun equals(other: Any?): Boolean = true + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + assertEquals("But the fat Hobbit, he knows. Eyes always watching.", module.documentationOf("XD", "custom")) + assertEquals( + "Memory is not what the heart desires. That is only a mirror.", + module.documentationOf( + "XD", + "equals" + ) + ) + } + } + } + + @Test + fun `kdocs with code block`() { + testInline( + """ + |/src/main/kotlin/sample/TestForCodeInDocs.kt + |package sample + |/** + | * Utility for building a String that represents an XML document. + | * The XmlBlob object is immutable and the passed values are copied where it makes sense. + | * + | * Note the XML Declaration is not output as part of the XmlBlob + | * + | * + | * val soapAttrs = attrs("soap-env" to "http://www.w3.org/2001/12/soap-envelope", + | * "soap-env:encodingStyle" to "http://www.w3.org/2001/12/soap-encoding") + | * val soapXml = node("soap-env:Envelope", soapAttrs, + | * node("soap-env:Body", attrs("xmlns:m" to "http://example"), + | * node("m:GetExample", + | * node("m:GetExampleName", "BasePair") + | * ) + | * ) + | * ) + | * + | * + | */ + |class TestForCodeInDocs { + |} + """.trimIndent(), configuration + ) { + documentablesMergingStage = { module -> + val description = module.descriptionOf("TestForCodeInDocs") + val expected = listOf( + P( + children = listOf(Text("Utility for building a String that represents an XML document. The XmlBlob object is immutable and the passed values are copied where it makes sense.")) + ), + P( + children = listOf(Text("Note the XML Declaration is not output as part of the XmlBlob")) + ), + CodeBlock( + children = listOf( + Text( + """val soapAttrs = attrs("soap-env" to "http://www.w3.org/2001/12/soap-envelope", + "soap-env:encodingStyle" to "http://www.w3.org/2001/12/soap-encoding") +val soapXml = node("soap-env:Envelope", soapAttrs, + node("soap-env:Body", attrs("xmlns:m" to "http://example"), + node("m:GetExample", + node("m:GetExampleName", "BasePair") + ) + ) +)""" + ) + ) + ) + ) + assertEquals(expected, description?.root?.children) + } + } + } + + private fun runTestSuitesAgainstGivenClasses(classlikes: List<DClasslike>, testSuites: List<List<TestSuite>>) { + classlikes.zip(testSuites).forEach { (classlike, testSuites) -> + testSuites.forEach { testSuite -> + when (testSuite) { + is TestSuite.PropertyDoesntExist -> assertEquals( + null, + classlike.properties.firstOrNull { it.name == testSuite.propertyName }, + "Test for class ${classlike.name} failed" + ) + is TestSuite.PropertyExists -> classlike.properties.single { it.name == testSuite.propertyName } + .run { + assertEquals( + testSuite.modifier, + modifier.values.single(), + "Test for class ${classlike.name} with property $name failed" + ) + assertEquals( + testSuite.visibility, + visibility.values.single(), + "Test for class ${classlike.name} with property $name failed" + ) + assertEquals( + testSuite.additionalModifiers, + extra[AdditionalModifiers]?.content?.values?.single() ?: emptySet<ExtraModifiers>(), + "Test for class ${classlike.name} with property $name failed" + ) + } + is TestSuite.FunctionDoesntExist -> assertEquals( + null, + classlike.functions.firstOrNull { it.name == testSuite.propertyName }, + "Test for class ${classlike.name} failed" + ) + is TestSuite.FunctionExists -> classlike.functions.single { it.name == testSuite.propertyName } + .run { + assertEquals( + testSuite.modifier, + modifier.values.single(), + "Test for class ${classlike.name} with function $name failed" + ) + assertEquals( + testSuite.visibility, + visibility.values.single(), + "Test for class ${classlike.name} with function $name failed" + ) + assertEquals( + testSuite.additionalModifiers, + extra[AdditionalModifiers]?.content?.values?.single() ?: emptySet<ExtraModifiers>(), + "Test for class ${classlike.name} with function $name failed" + ) + } + } + } + } + } + + @Test + fun `derived properties with non-public code included`() { + + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PRIVATE, + DokkaConfiguration.Visibility.PROTECTED, + DokkaConfiguration.Visibility.INTERNAL, + ) + } + } + } + + testInline( + """ + |/src/main/kotlin/sample/XD.kt + |package sample + | + |open class A { + | private val privateProperty: Int = 1 + | protected val protectedProperty: Int = 2 + | internal val internalProperty: Int = 3 + | val publicProperty: Int = 4 + | open val propertyToOverride: Int = 5 + | + | private fun privateFun(): Int = 6 + | protected fun protectedFun(): Int = 7 + | internal fun internalFun(): Int = 8 + | fun publicFun(): Int = 9 + | open fun funToOverride(): Int = 10 + |} + | + |open class B : A() { + | override val propertyToOverride: Int = 11 + | + | override fun funToOverride(): Int = 12 + |} + |class C : B() + """.trimIndent(), + configuration + ) { + + documentablesMergingStage = { module -> + val classes = module.packages.single().classlikes.sortedBy { it.name } + + val testSuites: List<List<TestSuite>> = listOf( + listOf( + TestSuite.PropertyExists( + "privateProperty", + KotlinModifier.Final, + KotlinVisibility.Private, + emptySet() + ), + TestSuite.PropertyExists( + "protectedProperty", + KotlinModifier.Final, + KotlinVisibility.Protected, + emptySet() + ), + TestSuite.PropertyExists( + "internalProperty", + KotlinModifier.Final, + KotlinVisibility.Internal, + emptySet() + ), + TestSuite.PropertyExists( + "publicProperty", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.PropertyExists( + "propertyToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionExists( + "privateFun", + KotlinModifier.Final, + KotlinVisibility.Private, + emptySet() + ), + TestSuite.FunctionExists( + "protectedFun", + KotlinModifier.Final, + KotlinVisibility.Protected, + emptySet() + ), + TestSuite.FunctionExists( + "internalFun", + KotlinModifier.Final, + KotlinVisibility.Internal, + emptySet() + ), + TestSuite.FunctionExists( + "publicFun", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionExists( + "funToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + emptySet() + ) + ), + listOf( + TestSuite.PropertyExists( + "privateProperty", + KotlinModifier.Final, + KotlinVisibility.Private, + emptySet() + ), + TestSuite.PropertyExists( + "protectedProperty", + KotlinModifier.Final, + KotlinVisibility.Protected, + emptySet() + ), + TestSuite.PropertyExists( + "internalProperty", + KotlinModifier.Final, + KotlinVisibility.Internal, + emptySet() + ), + TestSuite.PropertyExists( + "publicProperty", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.PropertyExists( + "propertyToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ), + TestSuite.FunctionExists( + "privateFun", + KotlinModifier.Final, + KotlinVisibility.Private, + emptySet() + ), + TestSuite.FunctionExists( + "protectedFun", + KotlinModifier.Final, + KotlinVisibility.Protected, + emptySet() + ), + TestSuite.FunctionExists( + "internalFun", + KotlinModifier.Final, + KotlinVisibility.Internal, + emptySet() + ), + TestSuite.FunctionExists( + "publicFun", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionExists( + "funToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ) + ), + listOf( + TestSuite.PropertyExists( + "privateProperty", + KotlinModifier.Final, + KotlinVisibility.Private, + emptySet() + ), + TestSuite.PropertyExists( + "protectedProperty", + KotlinModifier.Final, + KotlinVisibility.Protected, + emptySet() + ), + TestSuite.PropertyExists( + "internalProperty", + KotlinModifier.Final, + KotlinVisibility.Internal, + emptySet() + ), + TestSuite.PropertyExists( + "publicProperty", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.PropertyExists( + "propertyToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ), + TestSuite.FunctionExists( + "privateFun", + KotlinModifier.Final, + KotlinVisibility.Private, + emptySet() + ), + TestSuite.FunctionExists( + "protectedFun", + KotlinModifier.Final, + KotlinVisibility.Protected, + emptySet() + ), + TestSuite.FunctionExists( + "internalFun", + KotlinModifier.Final, + KotlinVisibility.Internal, + emptySet() + ), + TestSuite.FunctionExists( + "publicFun", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionExists( + "funToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ) + ) + ) + + runTestSuitesAgainstGivenClasses(classes, testSuites) + } + } + } + + + @Test + fun `derived properties with only public code`() { + + @Suppress("DEPRECATION") // for includeNonPublic + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + includeNonPublic = false + } + } + } + + testInline( + """ + |/src/main/kotlin/sample/XD.kt + |package sample + | + |open class A { + | private val privateProperty: Int = 1 + | protected val protectedProperty: Int = 2 + | internal val internalProperty: Int = 3 + | val publicProperty: Int = 4 + | open val propertyToOverride: Int = 5 + | open val propertyToOverrideButCloseMeanwhile: Int = 6 + | + | private fun privateFun(): Int = 7 + | protected fun protectedFun(): Int = 8 + | internal fun internalFun(): Int = 9 + | fun publicFun(): Int = 10 + | open fun funToOverride(): Int = 11 + | open fun funToOverrideButCloseMeanwhile(): Int = 12 + |} + | + |open class B : A() { + | override val propertyToOverride: Int = 13 + | final override val propertyToOverrideButCloseMeanwhile: Int = 14 + | + | override fun funToOverride(): Int = 15 + | final override fun funToOverrideButCloseMeanwhile(): Int = 16 + |} + |class C : B() + """.trimIndent(), + configuration + ) { + + documentablesMergingStage = { module -> + val classes = module.packages.single().classlikes.sortedBy { it.name } + + val testSuites: List<List<TestSuite>> = listOf( + listOf( + TestSuite.PropertyDoesntExist("privateProperty"), + TestSuite.PropertyDoesntExist("protectedProperty"), + TestSuite.PropertyDoesntExist("internalProperty"), + TestSuite.PropertyExists( + "publicProperty", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.PropertyExists( + "propertyToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.PropertyExists( + "propertyToOverrideButCloseMeanwhile", + KotlinModifier.Open, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionDoesntExist("privateFun"), + TestSuite.FunctionDoesntExist("protectedFun"), + TestSuite.FunctionDoesntExist("internalFun"), + TestSuite.FunctionExists( + "publicFun", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionExists( + "funToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionExists( + "funToOverrideButCloseMeanwhile", + KotlinModifier.Open, + KotlinVisibility.Public, + emptySet() + ) + ), + listOf( + TestSuite.PropertyDoesntExist("privateProperty"), + TestSuite.PropertyDoesntExist("protectedProperty"), + TestSuite.PropertyDoesntExist("internalProperty"), + TestSuite.PropertyExists( + "publicProperty", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.PropertyExists( + "propertyToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ), + TestSuite.PropertyExists( + "propertyToOverrideButCloseMeanwhile", + KotlinModifier.Final, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ), + TestSuite.FunctionDoesntExist("privateFun"), + TestSuite.FunctionDoesntExist("protectedFun"), + TestSuite.FunctionDoesntExist("internalFun"), + TestSuite.FunctionExists( + "publicFun", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionExists( + "funToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ), + TestSuite.FunctionExists( + "funToOverrideButCloseMeanwhile", + KotlinModifier.Final, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ) + ), + listOf( + TestSuite.PropertyDoesntExist("privateProperty"), + TestSuite.PropertyDoesntExist("protectedProperty"), + TestSuite.PropertyDoesntExist("internalProperty"), + TestSuite.PropertyExists( + "publicProperty", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.PropertyExists( + "propertyToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ), + TestSuite.PropertyExists( + "propertyToOverrideButCloseMeanwhile", + KotlinModifier.Final, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ), + TestSuite.FunctionDoesntExist("privateFun"), + TestSuite.FunctionDoesntExist("protectedFun"), + TestSuite.FunctionDoesntExist("internalFun"), + TestSuite.FunctionExists( + "publicFun", + KotlinModifier.Final, + KotlinVisibility.Public, + emptySet() + ), + TestSuite.FunctionExists( + "funToOverride", + KotlinModifier.Open, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ), + TestSuite.FunctionExists( + "funToOverrideButCloseMeanwhile", + KotlinModifier.Final, + KotlinVisibility.Public, + setOf(ExtraModifiers.KotlinOnlyModifiers.Override) + ) + ) + ) + + runTestSuitesAgainstGivenClasses(classes, testSuites) + } + } + } + + @Ignore // The compiler throws away annotations on unresolved types upstream + @Test + fun `Can annotate unresolved type`() { + testInline( + """ + |/src/main/java/sample/FooLibrary.kt + |package sample; + |@MustBeDocumented + |@Target(AnnotationTarget.TYPE) + |annotation class Hello() + |fun bar(): @Hello() TypeThatDoesntResolve + """.trimMargin(), + javaConfiguration + ) { + documentablesMergingStage = { module -> + val type = module.packages.single().functions.single().type as GenericTypeConstructor + assertEquals( + Annotations.Annotation(DRI("sample", "Hello"), emptyMap()), + type.extra[Annotations]?.directAnnotations?.values?.single()?.single() + ) + } + } + } + + /** + * Kotlin Int becomes java int. Java int cannot be annotated in source, but Kotlin Int can be. + * This is paired with KotlinAsJavaPluginTest.`Java primitive annotations work`() + */ + @Test + fun `Java primitive annotations work`() { + testInline( + """ + |/src/main/java/sample/FooLibrary.kt + |package sample; + |@MustBeDocumented + |@Target(AnnotationTarget.TYPE) + |annotation class Hello() + |fun bar(): @Hello() Int + """.trimMargin(), + javaConfiguration + ) { + documentablesMergingStage = { module -> + val type = module.packages.single().functions.single().type as GenericTypeConstructor + assertEquals( + Annotations.Annotation(DRI("sample", "Hello"), emptyMap()), + type.extra[Annotations]?.directAnnotations?.values?.single()?.single() + ) + assertEquals("kotlin/Int///PointingToDeclaration/", type.dri.toString()) + } + } + } + + @Test + fun `should preserve regular functions that look like accessors, but are not accessors`() { + testInline( + """ + |/src/main/kotlin/A.kt + |package test + |class A { + | private var v: Int = 0 + | + | // not accessors because declared separately, just functions + | fun setV(new: Int) { v = new } + | fun getV(): Int = v + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testClass = module.packages.single().classlikes.single { it.name == "A" } + val setterLookalike = testClass.functions.firstOrNull { it.name == "setV" } + assertNotNull(setterLookalike) { + "Expected regular function not found, wrongly categorized as setter?" + } + + val getterLookalike = testClass.functions.firstOrNull { it.name == "getV" } + assertNotNull(getterLookalike) { + "Expected regular function not found, wrongly categorized as getter?" + } + } + } + } + + @Test + fun `should correctly add IsVar extra for properties`() { + testInline( + """ + |/src/main/kotlin/A.kt + |package test + |class A { + | public var mutable: Int = 0 + | public val immutable: Int = 0 + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testClass = module.packages.single().classlikes.single { it.name == "A" } + assertEquals(2, testClass.properties.size) + + val mutable = testClass.properties[0] + assertEquals("mutable", mutable.name) + assertNotNull(mutable.extra[IsVar]) + + val immutable = testClass.properties[1] + assertEquals("immutable", immutable.name) + assertNull(immutable.extra[IsVar]) + } + } + } + + @Test + fun `should correctly parse multiple see tags with static function and property links`() { + testInline( + """ + |/src/main/kotlin/com/example/package/CollectionExtensions.kt + |package com.example.util + | + |object CollectionExtensions { + | val property = "Hi" + | + | fun emptyList() {} + | fun emptyMap() {} + | fun emptySet() {} + |} + | + |/src/main/kotlin/com/example/foo.kt + |package com.example + | + |import com.example.util.CollectionExtensions.emptyMap + |import com.example.util.CollectionExtensions.emptyList + |import com.example.util.CollectionExtensions.emptySet + |import com.example.util.CollectionExtensions.property + | + |/** + | * @see [List] stdlib list + | * @see [Map] stdlib map + | * @see [emptyMap] static emptyMap + | * @see [emptyList] static emptyList + | * @see [emptySet] static emptySet + | * @see [property] static property + | */ + |fun foo() {} + """.trimIndent(), + configuration + ) { + fun assertSeeTag(tag: TagWrapper, expectedName: String, expectedDescription: String) { + assertTrue(tag is See) + assertEquals(expectedName, tag.name) + val description = tag.children.joinToString { it.text().trim() } + assertEquals(expectedDescription, description) + } + + documentablesMergingStage = { module -> + val testFunction = module.packages.find { it.name == "com.example" } + ?.functions + ?.single { it.name == "foo" } + assertNotNull(testFunction) + + val documentationTags = testFunction.documentation.values.single().children + assertEquals(7, documentationTags.size) + + val descriptionTag = documentationTags[0] + assertTrue(descriptionTag is Description, "Expected first tag to be empty description") + assertTrue(descriptionTag.children.isEmpty(), "Expected first tag to be empty description") + + assertSeeTag( + tag = documentationTags[1], + expectedName = "kotlin.collections.List", + expectedDescription = "stdlib list" + ) + assertSeeTag( + tag = documentationTags[2], + expectedName = "kotlin.collections.Map", + expectedDescription = "stdlib map" + ) + assertSeeTag( + tag = documentationTags[3], + expectedName = "com.example.util.CollectionExtensions.emptyMap", + expectedDescription = "static emptyMap" + ) + assertSeeTag( + tag = documentationTags[4], + expectedName = "com.example.util.CollectionExtensions.emptyList", + expectedDescription = "static emptyList" + ) + assertSeeTag( + tag = documentationTags[5], + expectedName = "com.example.util.CollectionExtensions.emptySet", + expectedDescription = "static emptySet" + ) + assertSeeTag( + tag = documentationTags[6], + expectedName = "com.example.util.CollectionExtensions.property", + expectedDescription = "static property" + ) + } + } + } + + @Test + fun `should have documentation for synthetic Enum values functions`() { + testInline( + """ + |/src/main/kotlin/test/KotlinEnum.kt + |package test + | + |enum class KotlinEnum { + | FOO, BAR; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val kotlinEnum = module.packages.find { it.name == "test" } + ?.classlikes + ?.single { it.name == "KotlinEnum" } + assertNotNull(kotlinEnum) + val valuesFunction = kotlinEnum.functions.single { it.name == "values" } + + val expectedValuesType = GenericTypeConstructor( + dri = DRI( + packageName = "kotlin", + classNames = "Array" + ), + projections = listOf( + Invariance( + GenericTypeConstructor( + dri = DRI( + packageName = "test", + classNames = "KotlinEnum" + ), + projections = emptyList() + ) + ) + ) + ) + assertEquals(expectedValuesType, valuesFunction.type) + + val expectedDocumentation = DocumentationNode(listOf( + Description( + CustomDocTag( + children = listOf( + P(listOf( + Text( + "Returns an array containing the constants of this enum type, in the order " + + "they're declared." + ), + )), + P(listOf( + Text("This method may be used to iterate over the constants.") + )) + ), + name = "MARKDOWN_FILE" + ) + ) + )) + assertEquals(expectedDocumentation, valuesFunction.documentation.values.single()) + } + } + } + + @Test + fun `should have documentation for synthetic Enum entries property`() { + testInline( + """ + |/src/main/kotlin/test/KotlinEnum.kt + |package test + | + |enum class KotlinEnum { + | FOO, BAR; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val kotlinEnum = module.packages.find { it.name == "test" } + ?.classlikes + ?.single { it.name == "KotlinEnum" } + + assertNotNull(kotlinEnum) + + val entriesProperty = kotlinEnum.properties.single { it.name == "entries" } + val expectedEntriesType = GenericTypeConstructor( + dri = DRI( + packageName = "kotlin.enums", + classNames = "EnumEntries" + ), + projections = listOf( + Invariance( + GenericTypeConstructor( + dri = DRI( + packageName = "test", + classNames = "KotlinEnum" + ), + projections = emptyList() + ) + ) + ) + ) + assertEquals(expectedEntriesType, entriesProperty.type) + + val expectedDocumentation = DocumentationNode(listOf( + Description( + CustomDocTag( + children = listOf( + P(listOf( + Text( + "Returns a representation of an immutable list of all enum entries, " + + "in the order they're declared." + ), + )), + P(listOf( + Text("This method may be used to iterate over the enum entries.") + )) + ), + name = "MARKDOWN_FILE" + ) + ) + )) + assertEquals(expectedDocumentation, entriesProperty.documentation.values.single()) + } + } + } + + @OnlyDescriptors("Fix kdoc link") // TODO + @Test + fun `should have documentation for synthetic Enum valueOf functions`() { + testInline( + """ + |/src/main/kotlin/test/KotlinEnum.kt + |package test + | + |enum class KotlinEnum { + | FOO, BAR; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val kotlinEnum = module.packages.find { it.name == "test" } + ?.classlikes + ?.single { it.name == "KotlinEnum" } + assertNotNull(kotlinEnum) + + val expectedValueOfType = GenericTypeConstructor( + dri = DRI( + packageName = "test", + classNames = "KotlinEnum" + ), + projections = emptyList() + ) + + val expectedDocumentation = DocumentationNode(listOf( + Description( + CustomDocTag( + children = listOf( + P(listOf( + Text( + "Returns the enum constant of this type with the specified name. " + + "The string must match exactly an identifier used to declare an enum " + + "constant in this type. (Extraneous whitespace characters are not permitted.)" + ) + )) + ), + name = "MARKDOWN_FILE" + ) + ), + Throws( + root = CustomDocTag( + children = listOf( + P(listOf( + Text("if this enum type has no constant with the specified name") + )) + ), + name = "MARKDOWN_FILE" + ), + name = "kotlin.IllegalArgumentException", + exceptionAddress = DRI( + packageName = "kotlin", + classNames = "IllegalArgumentException", + target = PointingToDeclaration + ), + ) + )) + + val valueOfFunction = kotlinEnum.functions.single { it.name == "valueOf" } + assertEquals(expectedDocumentation, valueOfFunction.documentation.values.single()) + assertEquals(expectedValueOfType, valueOfFunction.type) + + val valueOfParamDRI = (valueOfFunction.parameters.single().type as GenericTypeConstructor).dri + assertEquals(DRI(packageName = "kotlin", classNames = "String"), valueOfParamDRI) + } + } + } + + @Test + fun `should add data modifier to data objects`() { + testInline( + """ + |/src/main/kotlin/test/KotlinDataObject.kt + |package test + | + |data object KotlinDataObject {} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val pckg = module.packages.single { it.name == "test" } + + val dataObject = pckg.classlikes.single { it.name == "KotlinDataObject" } + assertTrue(dataObject is DObject) + + val modifiers = dataObject.modifiers().values.flatten() + assertEquals(1, modifiers.size) + assertEquals(ExtraModifiers.KotlinOnlyModifiers.Data, modifiers[0]) + } + } + } +} + +private sealed class TestSuite { + abstract val propertyName: String + + data class PropertyDoesntExist( + override val propertyName: String + ) : TestSuite() + + + data class PropertyExists( + override val propertyName: String, + val modifier: KotlinModifier, + val visibility: KotlinVisibility, + val additionalModifiers: Set<ExtraModifiers.KotlinOnlyModifiers> + ) : TestSuite() + + data class FunctionDoesntExist( + override val propertyName: String, + ) : TestSuite() + + data class FunctionExists( + override val propertyName: String, + val modifier: KotlinModifier, + val visibility: KotlinVisibility, + val additionalModifiers: Set<ExtraModifiers.KotlinOnlyModifiers> + ) : TestSuite() +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt new file mode 100644 index 00000000..7e9bff1e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt @@ -0,0 +1,1027 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.Visibility +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.* +import kotlin.test.* + +class DefaultPsiToDocumentableTranslatorTest : BaseAbstractTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + } + } + } + + @Test + fun `method overriding two documented classes picks closest class documentation`() { + testInline( + """ + |/src/main/java/sample/BaseClass1.java + |package sample; + |public class BaseClass1 { + | /** B1 */ + | public void x() { } + |} + | + |/src/main/java/sample/BaseClass2.java + |package sample; + |public class BaseClass2 extends BaseClass1 { + | /** B2 */ + | public void x() { } + |} + | + |/src/main/java/sample/X.java + |package sample; + |public class X extends BaseClass2 { + | public void x() { } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val documentationOfFunctionX = module.documentationOf("X", "x") + assertTrue( + "B2" in documentationOfFunctionX, + "Expected nearest super method documentation to be parsed as documentation. " + + "Documentation: $documentationOfFunctionX" + ) + } + } + } + + @Test + fun `method overriding class and interface picks class documentation`() { + testInline( + """ + |/src/main/java/sample/BaseClass1.java + |package sample; + |public class BaseClass1 { + | /** B1 */ + | public void x() { } + |} + | + |/src/main/java/sample/Interface1.java + |package sample; + |public interface Interface1 { + | /** I1 */ + | public void x() {} + |} + | + |/src/main/java/sample/X.java + |package sample; + |public class X extends BaseClass1 implements Interface1 { + | public void x() { } + |} + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val documentationOfFunctionX = module.documentationOf("X", "x") + assertTrue( + "B1" in documentationOfFunctionX, + "Expected documentation of superclass being prioritized over interface " + + "Documentation: $documentationOfFunctionX" + ) + } + } + } + + @Test + fun `method overriding two classes picks closest documented class documentation`() { + testInline( + """ + |/src/main/java/sample/BaseClass1.java + |package sample; + |public class BaseClass1 { + | /** B1 */ + | public void x() { } + |} + | + |/src/main/java/sample/BaseClass2.java + |package sample; + |public class BaseClass2 extends BaseClass1 { + | public void x() {} + |} + | + |/src/main/java/sample/X.java + |package sample; + |public class X extends BaseClass2 { + | public void x() { } + |} + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val documentationOfFunctionX = module.documentationOf("X", "x") + assertTrue( + "B1" in documentationOfFunctionX, + "Expected Documentation \"B1\", found: \"$documentationOfFunctionX\"" + ) + } + } + } + + @Test + fun `java package-info package description`() { + testInline( + """ + |/src/main/java/sample/BaseClass1.java + |package sample; + |public class BaseClass1 { + | /** B1 */ + | void x() { } + |} + | + |/src/main/java/sample/BaseClass2.java + |package sample; + |public class BaseClass2 extends BaseClass1 { + | void x() {} + |} + | + |/src/main/java/sample/X.java + |package sample; + |public class X extends BaseClass2 { + | void x() { } + |} + | + |/src/main/java/sample/package-info.java + |/** + | * Here comes description from package-info + | */ + |package sample; + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + val documentationOfPackage = module.packages.single().documentation.values.single().children.single() + .firstMemberOfType<Text>().body + assertEquals( + "Here comes description from package-info", documentationOfPackage + ) + } + } + } + + @Test + fun `java package-info package annotations`() { + testInline( + """ + |/src/main/java/sample/PackageAnnotation.java + |package sample; + |@java.lang.annotation.Target(java.lang.annotation.ElementType.PACKAGE) + |public @interface PackageAnnotation { + |} + | + |/src/main/java/sample/package-info.java + |@PackageAnnotation + |package sample; + """.trimMargin(), + configuration + ) { + documentablesMergingStage = { module -> + assertEquals( + Annotations.Annotation(DRI("sample", "PackageAnnotation"), emptyMap()), + module.packages.single().extra[Annotations]?.directAnnotations?.values?.single()?.single() + ) + } + } + } + + @Test + fun `should add default value to constant properties`() { + testInline( + """ + |/src/main/java/test/JavaConstants.java + |package test; + | + |public class JavaConstants { + | public static final byte BYTE = 1; + | public static final short SHORT = 2; + | public static final int INT = 3; + | public static final long LONG = 4L; + | public static final float FLOAT = 5.0f; + | public static final double DOUBLE = 6.0d; + | public static final String STRING = "Seven"; + | public static final char CHAR = 'E'; + | public static final boolean BOOLEAN = true; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "JavaConstants" } + + val constants = testedClass.properties + assertEquals(9, constants.size) + + val constantsByName = constants.associateBy { it.name } + fun getConstantExpression(name: String): Expression? { + return constantsByName.getValue(name).extra[DefaultValue]?.expression?.values?.first() + } + + assertEquals(IntegerConstant(1), getConstantExpression("BYTE")) + assertEquals(IntegerConstant(2), getConstantExpression("SHORT")) + assertEquals(IntegerConstant(3), getConstantExpression("INT")) + assertEquals(IntegerConstant(4), getConstantExpression("LONG")) + assertEquals(FloatConstant(5.0f), getConstantExpression("FLOAT")) + assertEquals(DoubleConstant(6.0), getConstantExpression("DOUBLE")) + assertEquals(StringConstant("Seven"), getConstantExpression("STRING")) + assertEquals(StringConstant("E"), getConstantExpression("CHAR")) + assertEquals(BooleanConstant(true), getConstantExpression("BOOLEAN")) + } + } + } + + @Test + fun `should resolve static imports used as annotation param values as literal values`() { + testInline( + """ + |/src/main/java/test/JavaClassUsingAnnotation.java + |package test; + | + |import static test.JavaConstants.STRING; + |import static test.JavaConstants.INTEGER; + |import static test.JavaConstants.LONG; + |import static test.JavaConstants.BOOLEAN; + |import static test.JavaConstants.DOUBLE; + |import static test.JavaConstants.FLOAT; + |import static test.JavaConstants.BYTE; + |import static test.JavaConstants.SHORT; + |import static test.JavaConstants.CHAR; + | + |@JavaAnnotation( + | byteValue = BYTE, shortValue = SHORT, intValue = INTEGER, longValue = LONG, booleanValue = BOOLEAN, + | doubleValue = DOUBLE, floatValue = FLOAT, stringValue = STRING, charValue = CHAR + |) + |public class JavaClassUsingAnnotation { + |} + | + |/src/main/java/test/JavaAnnotation.java + |package test; + |@Documented + |public @interface JavaAnnotation { + | byte byteValue(); + | short shortValue(); + | int intValue(); + | long longValue(); + | boolean booleanValue(); + | double doubleValue(); + | float floatValue(); + | String stringValue(); + | char charValue(); + |} + | + |/src/main/java/test/JavaConstants.java + |package test; + |public class JavaConstants { + | public static final byte BYTE = 3; + | public static final short SHORT = 4; + | public static final int INTEGER = 5; + | public static final long LONG = 6L; + | public static final boolean BOOLEAN = true; + | public static final double DOUBLE = 7.0d; + | public static final float FLOAT = 8.0f; + | public static final String STRING = "STRING_CONSTANT_VALUE"; + | public static final char CHAR = 'c'; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "JavaClassUsingAnnotation" } + + val annotation = (testedClass as DClass).extra[Annotations]?.directAnnotations?.values?.single()?.single() + assertNotNull(annotation) + + assertEquals("JavaAnnotation", annotation.dri.classNames) + + assertEquals(IntValue(3), annotation.params["byteValue"]) + assertEquals(IntValue(4), annotation.params["shortValue"]) + assertEquals(IntValue(5), annotation.params["intValue"]) + assertEquals(LongValue(6), annotation.params["longValue"]) + assertEquals(BooleanValue(true), annotation.params["booleanValue"]) + assertEquals(DoubleValue(7.0), annotation.params["doubleValue"]) + assertEquals(FloatValue(8.0f), annotation.params["floatValue"]) + assertEquals(StringValue("STRING_CONSTANT_VALUE"), annotation.params["stringValue"]) + assertEquals(StringValue("c"), annotation.params["charValue"]) + } + } + } + + // TODO [beresnev] fix +// class OnlyPsiPlugin : DokkaPlugin() { +// private val kotlinAnalysisPlugin by lazy { plugin<Kotlin>() } +// +// @Suppress("unused") +// val psiOverrideDescriptorTranslator by extending { +// (plugin<JavaAnalysisPlugin>().psiToDocumentableTranslator +// override kotlinAnalysisPlugin.descriptorToDocumentableTranslator) +// } +// +// @OptIn(DokkaPluginApiPreview::class) +// override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = +// PluginApiPreviewAcknowledgement +// } +// +// // for Kotlin classes from DefaultPsiToDocumentableTranslator +// @Test +// fun `should resolve ultralight class`() { +// val configurationWithNoJVM = dokkaConfiguration { +// sourceSets { +// sourceSet { +// sourceRoots = listOf("src/main/java") +// } +// } +// } +// +// testInline( +// """ +// |/src/main/java/example/Test.kt +// |package example +// | +// |open class KotlinSubClass { +// | fun kotlinSubclassFunction(bar: String): String { +// | return "KotlinSubClass" +// | } +// |} +// | +// |/src/main/java/example/JavaLeafClass.java +// |package example; +// | +// |public class JavaLeafClass extends KotlinSubClass { +// | public String javaLeafClassFunction(String baz) { +// | return "JavaLeafClass"; +// | } +// |} +// """.trimMargin(), +// configurationWithNoJVM, +// pluginOverrides = listOf(OnlyPsiPlugin()) // suppress a descriptor translator because of psi and descriptor translators work in parallel +// ) { +// documentablesMergingStage = { module -> +// val kotlinSubclassFunction = +// module.packages.single().classlikes.find { it.name == "JavaLeafClass" }?.functions?.find { it.name == "kotlinSubclassFunction" } +// .assertNotNull("kotlinSubclassFunction ") +// +// assertEquals( +// "String", +// (kotlinSubclassFunction.type as? TypeConstructor)?.dri?.classNames +// ) +// assertEquals( +// "String", +// (kotlinSubclassFunction.parameters.firstOrNull()?.type as? TypeConstructor)?.dri?.classNames +// ) +// } +// } +// } + + @Test + fun `should preserve regular functions that are named like getters, but are not getters`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private int a = 1; + | public String getA() { return "s"; } // wrong return type + | public int getA(String param) { return 123; } // shouldn't have params + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testClass = module.packages.single().classlikes.single { it.name == "A" } + + val getterLookalikes = testClass.functions.filter { it.name == "getA" } + assertEquals(2, getterLookalikes.size, "Not all expected regular functions found, wrongly categorized as getters?") + } + } + } + + @Test + fun `should ignore additional non-accessor setters`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private int a = 1; + | + | public int getA() { return a; } + | + | public void setA(long a) { } + | public void setA(Number a) {} + | + | // the qualifying setter is intentionally in the middle + | // to rule out the order making a difference + | public void setA(int a) { } + | + | public void setA(String a) {} + | public void setA() {} + | + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testClass = module.packages.single().classlikes.single { it.name == "A" } + + val property = testClass.properties.single { it.name == "a" } + assertNotNull(property.getter) + + val setter = property.setter + assertNotNull(setter) + assertEquals(1, setter.parameters.size) + assertEquals(PrimitiveJavaType("int"), setter.parameters[0].type) + + val regularSetterFunctions = testClass.functions.filter { it.name == "setA" } + assertEquals(4, regularSetterFunctions.size) + } + } + } + + @Test + fun `should not qualify methods with subtype parameters as type accessors`() { + testInline( + """ + |/src/main/java/test/Shape.java + |package test; + |public class Shape { } + | + |/src/main/java/test/Triangle.java + |package test; + |public class Triangle extends Shape { } + | + |/src/main/java/test/Square.java + |package test; + |public class Square extends Shape { } + | + |/src/main/java/test/Test.java + |package test; + |public class Test { + | private Shape foo = 1; + | + | public Shape getFoo() { return new Square(); } + | + | public void setFoo(Square foo) { } + | public void setFoo(Triangle foo) { } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testClass = module.packages.single().classlikes.single { it.name == "Test" } + + val field = testClass.properties.singleOrNull { it.name == "foo" } + assertNotNull(field) { + "Expected the foo property to exist because the field is private with a public getter" + } + assertNull(field.setter) + + val setterMethodsWithSubtypeParams = testClass.functions.filter { it.name == "setFoo" } + assertEquals( + 2, + setterMethodsWithSubtypeParams.size, + "Expected the setter methods to not qualify as accessors because of subtype parameters" + ) + } + } + } + + @Test + fun `should preserve private fields without getters even if they have qualifying setters`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private int a = 1; + | + | public void setA(int a) { } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val tetClass = module.packages.single().classlikes.single { it.name == "A" } + + val property = tetClass.properties.firstOrNull { it.name == "a" } + assertNull(property, "Expected the property to stay private because there are no getters") + + val regularSetterFunction = tetClass.functions.firstOrNull { it.name == "setA" } + assertNotNull(regularSetterFunction) { + "The qualifying setter function should stay a regular function because the field is inaccessible" + } + } + } + } + + @Test + fun `should not mark a multi-param setter overload as an accessor`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private int field = 1; + | + | public void setField(int a, int b) { } + | public int getField() { return a; } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testClass = module.packages.single().classlikes.single { it.name == "A" } as DClass + + val property = testClass.properties.single { it.name == "field" } + assertEquals("getField", property.getter?.name) + assertNull(property.setter) + + + // the setField function should not qualify to be an accessor due to the second param + assertEquals(1, testClass.functions.size) + assertEquals("setField", testClass.functions[0].name) + } + } + } + + @Test + fun `should not associate accessors with field because field is public api`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + documentedVisibilities = setOf( + DokkaConfiguration.Visibility.PUBLIC, + DokkaConfiguration.Visibility.PROTECTED + ) + } + } + } + + testInline( + """ + |/src/test/A.java + |package test; + |public class A { + | protected int a = 1; + | public int getA() { return a; } + | public void setA(int a) { this.a = a; } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "A" } + + val property = testedClass.properties.single { it.name == "a" } + assertEquals(JavaVisibility.Protected, property.visibility.values.single()) + assertNull(property.getter) + assertNull(property.setter) + + assertEquals(2, testedClass.functions.size) + + assertEquals("getA", testedClass.functions[0].name) + assertEquals("setA", testedClass.functions[1].name) + } + } + } + + @Test + fun `should add IsVar extra for field with getter and setter`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private int a = 1; + | public int getA() { return a; } + | public void setA(int a) { this.a = a; } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "A" } + + val property = testedClass.properties.single { it.name == "a" } + assertNotNull(property.extra[IsVar]) + } + } + } + + @Test + fun `should not add IsVar extra if field does not have a setter`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private int a = 1; + | public int getA() { return a; } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "A" } + + val property = testedClass.properties.single { it.name == "a" } + assertNull(property.extra[IsVar]) + } + } + } + + @Test + fun `should add IsVar for non-final java field without any accessors`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | public int a = 1; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "A" } + + val property = testedClass.properties.single { it.name == "a" } + assertNotNull(property.extra[IsVar]) + } + } + } + + @Test + fun `should not add IsVar for final java field`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | public final int a = 2; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.packages.single().classlikes.single { it.name == "A" } + + val publicFinal = testedClass.properties.single { it.name == "a" } + assertNull(publicFinal.extra[IsVar]) + } + } + } + + @Test // see https://github.com/Kotlin/dokka/issues/2646 + fun `should resolve PsiImmediateClassType as class reference`() { + testInline( + """ + |/src/main/java/test/JavaEnum.java + |package test; + |public enum JavaEnum { + | FOO, BAR + |} + | + |/src/main/java/test/ContainingEnumType.java + |package test; + |public class ContainingEnumType { + | + | public JavaEnum returningEnumType() { + | return null; + | } + | + | public JavaEnum[] returningEnumTypeArray() { + | return null; + | } + | + | public void acceptingEnumType(JavaEnum javaEnum) {} + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val expectedType = GenericTypeConstructor( + dri = DRI(packageName = "test", classNames = "JavaEnum", target = PointingToDeclaration), + projections = emptyList() + ) + val expectedArrayType = GenericTypeConstructor( + dri = DRI("kotlin", "Array", target = PointingToDeclaration), + projections = listOf(expectedType) + ) + + val classWithEnumUsage = module.packages.single().classlikes.single { it.name == "ContainingEnumType" } + + val returningEnum = classWithEnumUsage.functions.single { it.name == "returningEnumType" } + assertEquals(expectedType, returningEnum.type) + + val acceptingEnum = classWithEnumUsage.functions.single { it.name == "acceptingEnumType" } + assertEquals(1, acceptingEnum.parameters.size) + assertEquals(expectedType, acceptingEnum.parameters[0].type) + + val returningArray = classWithEnumUsage.functions.single { it.name == "returningEnumTypeArray" } + assertEquals(expectedArrayType, returningArray.type) + } + } + } + + @Test + fun `should have documentation for synthetic Enum values functions`() { + testInline( + """ + |/src/main/java/test/JavaEnum.java + |package test + | + |public enum JavaEnum { + | FOO, BAR; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val kotlinEnum = module.packages.find { it.name == "test" } + ?.classlikes + ?.single { it.name == "JavaEnum" } + assertNotNull(kotlinEnum) + + val valuesFunction = kotlinEnum.functions.single { it.name == "values" } + + val expectedDocumentation = DocumentationNode(listOf( + Description( + CustomDocTag( + children = listOf( + P(listOf( + Text( + "Returns an array containing the constants of this enum type, " + + "in the order they're declared. This method may be used to " + + "iterate over the constants." + ), + )) + ), + name = "MARKDOWN_FILE" + ) + ), + Return( + CustomDocTag( + children = listOf( + P(listOf( + Text("an array containing the constants of this enum type, in the order they're declared") + )) + ), + name = "MARKDOWN_FILE" + ) + ) + )) + assertEquals(expectedDocumentation, valuesFunction.documentation.values.single()) + + val expectedValuesType = GenericTypeConstructor( + dri = DRI( + packageName = "kotlin", + classNames = "Array" + ), + projections = listOf( + GenericTypeConstructor( + dri = DRI( + packageName = "test", + classNames = "JavaEnum" + ), + projections = emptyList() + ) + ) + ) + assertEquals(expectedValuesType, valuesFunction.type) + } + } + } + + @Test + fun `should have documentation for synthetic Enum valueOf functions`() { + testInline( + """ + |/src/main/java/test/JavaEnum.java + |package test + | + |public enum JavaEnum { + | FOO, BAR; + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val javaEnum = module.packages.find { it.name == "test" } + ?.classlikes + ?.single { it.name == "JavaEnum" } + assertNotNull(javaEnum) + + val valueOfFunction = javaEnum.functions.single { it.name == "valueOf" } + + val expectedDocumentation = DocumentationNode(listOf( + Description( + CustomDocTag( + children = listOf( + P(listOf( + Text( + "Returns the enum constant of this type with the " + + "specified name. The string must match exactly an identifier used " + + "to declare an enum constant in this type. (Extraneous whitespace " + + "characters are not permitted.)" + ) + )) + ), + name = "MARKDOWN_FILE" + ) + ), + Return( + root = CustomDocTag( + children = listOf( + P(listOf( + Text("the enum constant with the specified name") + )) + ), + name = "MARKDOWN_FILE" + ) + ), + Throws( + name = "java.lang.IllegalArgumentException", + exceptionAddress = DRI( + packageName = "java.lang", + classNames = "IllegalArgumentException", + target = PointingToDeclaration + ), + root = CustomDocTag( + children = listOf( + P(listOf( + Text("if this enum type has no constant with the specified name") + )) + ), + name = "MARKDOWN_FILE" + ) + ), + )) + assertEquals(expectedDocumentation, valueOfFunction.documentation.values.single()) + + val expectedValueOfType = GenericTypeConstructor( + dri = DRI( + packageName = "test", + classNames = "JavaEnum" + ), + projections = emptyList() + ) + assertEquals(expectedValueOfType, valueOfFunction.type) + + val valueOfParamDRI = (valueOfFunction.parameters.single().type as GenericTypeConstructor).dri + assertEquals(DRI(packageName = "java.lang", classNames = "String"), valueOfParamDRI) + } + } + } + + @Test + fun `should have public default constructor in public class`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.findClasslike(packageName = "test", "A") as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 default constructor") + assertTrue( + testedClass.constructors.first().parameters.isEmpty(), + "Expect default constructor doesn't have params" + ) + assertEquals(JavaVisibility.Public, testedClass.constructors.first().visibility()) + } + } + } + + @Test + fun `should have package-private default constructor in package-private class`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + documentedVisibilities = setOf(Visibility.PUBLIC, Visibility.PACKAGE) + } + } + } + + testInline( + """ + |/src/main/java/test/A.java + |package test; + |class A { + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.findClasslike(packageName = "test", "A") as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 default constructor") + assertEquals(JavaVisibility.Default, testedClass.constructors.first().visibility()) + } + } + } + + @Test + fun `should have private default constructor in private nested class`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + documentedVisibilities = setOf(Visibility.PUBLIC, Visibility.PRIVATE) + } + } + } + + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private static class PrivateNested{} + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val parentClass = module.findClasslike(packageName = "test", "A") as DClass + val testedClass = parentClass.classlikes.single { it.name == "PrivateNested" } as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 default constructor") + assertEquals(JavaVisibility.Private, testedClass.constructors.first().visibility()) + } + } + } + + @Test + fun `should not have a default public constructor because have explicit private`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + documentedVisibilities = setOf(Visibility.PUBLIC, Visibility.PRIVATE) + } + } + } + + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private A(){} + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.findClasslike(packageName = "test", "A") as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 declared constructor") + assertEquals(JavaVisibility.Private, testedClass.constructors.first().visibility()) + } + } + } + + @Test + fun `default constructor should get the package name`() { + testInline( + """ + |/src/main/java/org/test/A.java + |package org.test; + |public class A { + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.findClasslike(packageName = "org.test", "A") as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 default constructor") + + val constructorDRI = testedClass.constructors.first().dri + assertEquals("org.test", constructorDRI.packageName) + assertEquals("A", constructorDRI.classNames) + } + } + } +} + +private fun DFunction.visibility() = visibility.values.first() diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/ExternalDocumentablesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/ExternalDocumentablesTest.kt new file mode 100644 index 00000000..1879c538 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/ExternalDocumentablesTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.analysis.kotlin.internal.ExternalDocumentablesProvider +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DInterface +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.cast +import kotlin.test.Test +import kotlin.test.assertEquals + +class ExternalDocumentablesTest : BaseAbstractTest() { + @Test + fun `external documentable from java stdlib`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + analysisPlatform = "jvm" + classpath += jvmStdlibPath!! + } + } + } + + testInline( + """ + /src/com/sample/MyList.kt + package com.sample + class MyList: ArrayList<Int>() + """.trimIndent(), + configuration + ) { + lateinit var provider: ExternalDocumentablesProvider + pluginsSetupStage = { + provider = it.plugin<InternalKotlinAnalysisPlugin>().querySingle { externalDocumentablesProvider } + } + documentablesTransformationStage = { mod -> + val entry = mod.packages.single().classlikes.single().cast<DClass>().supertypes.entries.single() + val res = provider.findClasslike( + entry.value.single().typeConstructor.dri, + entry.key) + assertEquals("ArrayList", res?.name) + assertEquals("java.util/ArrayList///PointingToDeclaration/", res?.dri?.toString()) + + val supertypes = res?.cast<DClass>()?.supertypes?.values?.single() + ?.map { it.typeConstructor.dri.classNames } + assertEquals( + listOf("AbstractList", "RandomAccess", "Cloneable", "Serializable", "MutableList"), + supertypes + ) + } + } + } + + @Test + fun `external documentable from dependency`() { + val coroutinesPath = + ClassLoader.getSystemResource("kotlinx/coroutines/Job.class") + ?.file + ?.replace("file:", "") + ?.replaceAfter(".jar", "") + + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + analysisPlatform = "jvm" + classpath += listOf(jvmStdlibPath!!, coroutinesPath!!) + } + } + } + + testInline( + """ + /src/com/sample/MyJob.kt + package com.sample + import kotlinx.coroutines.Job + abstract class MyJob: Job + """.trimIndent(), + configuration + ) { + lateinit var provider: ExternalDocumentablesProvider + pluginsSetupStage = { + provider = it.plugin<InternalKotlinAnalysisPlugin>().querySingle { externalDocumentablesProvider } + } + documentablesTransformationStage = { mod -> + val entry = mod.packages.single().classlikes.single().cast<DClass>().supertypes.entries.single() + val res = provider.findClasslike( + entry.value.single().typeConstructor.dri, + entry.key) + assertEquals("Job", res?.name) + assertEquals("kotlinx.coroutines/Job///PointingToDeclaration/", res?.dri?.toString()) + + val supertypes = res?.cast<DInterface>()?.supertypes?.values?.single() + ?.map { it.typeConstructor.dri.classNames } + assertEquals( + listOf("CoroutineContext.Element"), + supertypes + ) + } + } + } + + @Test + fun `external documentable for nested class`() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src") + analysisPlatform = "jvm" + classpath += jvmStdlibPath!! + } + } + } + + testInline( + """ + /src/com/sample/MyList.kt + package com.sample + abstract class MyEntry: Map.Entry<Int, String> + """.trimIndent(), + configuration + ) { + lateinit var provider: ExternalDocumentablesProvider + pluginsSetupStage = { + provider = it.plugin<InternalKotlinAnalysisPlugin>().querySingle { externalDocumentablesProvider } + } + documentablesTransformationStage = { mod -> + val entry = mod.packages.single().classlikes.single().cast<DClass>().supertypes.entries.single() + val res = provider.findClasslike( + entry.value.single().typeConstructor.dri, + entry.key) + assertEquals("Entry", res?.name) + assertEquals("kotlin.collections/Map.Entry///PointingToDeclaration/", res?.dri?.toString()) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritDocsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritDocsTest.kt new file mode 100644 index 00000000..6413866f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritDocsTest.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.doc.CustomDocTag +import org.jetbrains.dokka.model.doc.Description +import org.jetbrains.dokka.model.doc.P +import org.jetbrains.dokka.model.doc.Text +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +class JavadocInheritDocsTest : BaseAbstractTest() { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + } + } + } + + @Test + fun `work when whole description is inherited`() { + testInline( + """ + |/src/main/java/sample/Superclass.java + |package sample; + |/** + |* Superclass docs + |*/ + |public class Superclass { } + | + |/src/main/java/sample/Subclass.java + |package sample; + |/** + |* {@inheritDoc} + |*/ + |public class Subclass extends Superclass { } + """.trimIndent(), configuration + ) { + documentablesMergingStage = { module -> + val subclass = module.findClasslike("sample", "Subclass") + val descriptionGot = subclass.documentation.values.first().children.first() + val expectedDescription = Description( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("Superclass docs")) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expectedDescription, descriptionGot) + } + } + } + + @Test + fun `work when inherited part is inside description`() { + testInline( + """ + |/src/main/java/sample/Superclass.java + |package sample; + |/** + |* Superclass docs + |*/ + |public class Superclass { } + | + |/src/main/java/sample/Subclass.java + |package sample; + |/** + |* Subclass docs. {@inheritDoc} End of subclass docs + |*/ + |public class Subclass extends Superclass { } + """.trimIndent(), configuration + ) { + documentablesMergingStage = { module -> + val subclass = module.findClasslike("sample", "Subclass") + val descriptionGot = subclass.documentation.values.first().children.first() + val expectedDescription = Description( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("Subclass docs. Superclass docs End of subclass docs")) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expectedDescription, descriptionGot) + } + } + } + + @Test + fun `work when inherited part is empty`() { + testInline( + """ + |/src/main/java/sample/Superclass.java + |package sample; + |public class Superclass { } + | + |/src/main/java/sample/Subclass.java + |package sample; + |/** + |* Subclass docs. {@inheritDoc} End of subclass docs + |*/ + |public class Subclass extends Superclass { } + """.trimIndent(), configuration + ) { + documentablesMergingStage = { module -> + val subclass = module.findClasslike("sample", "Subclass") + val descriptionGot = subclass.documentation.values.first().children.first() + val expectedDescription = Description( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("Subclass docs. End of subclass docs")) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expectedDescription, descriptionGot) + } + } + } + + @Test + @Ignore // This should be enabled when we have proper tag inheritance in javadoc parser + fun `work when inherited part is empty in supertype but present in its supertype`() { + testInline( + """ + |/src/main/java/sample/SuperSuperclass.java + |package sample; + |/** + |* Super super docs + |*/ + |public class SuperSuperclass { } + |/src/main/java/sample/Superclass.java + |package sample; + |public class Superclass extends SuperSuperClass { } + | + |/src/main/java/sample/Subclass.java + |package sample; + |/** + |* Subclass docs. {@inheritDoc} End of subclass docs + |*/ + |public class Subclass extends Superclass { } + """.trimIndent(), configuration + ) { + documentablesMergingStage = { module -> + val subclass = module.findClasslike("sample", "Subclass") + val descriptionGot = subclass.documentation.values.first().children.first() + val expectedDescription = Description( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("Subclass docs. Super super docs End of subclass docs")) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expectedDescription, descriptionGot) + } + } + } + + @Test + //Original javadoc doesn't treat interfaces as valid candidates for inherit doc + fun `work with interfaces`() { + testInline( + """ + |/src/main/java/sample/SuperInterface.java + |package sample; + |/** + |* Super super docs + |*/ + |public interface SuperInterface { } + | + |/src/main/java/sample/Subclass.java + |package sample; + |/** + |* Subclass docs. {@inheritDoc} End of subclass docs + |*/ + |public interface Subclass extends SuperInterface { } + """.trimIndent(), configuration + ) { + documentablesMergingStage = { module -> + val subclass = module.findClasslike("sample", "Subclass") + val descriptionGot = subclass.documentation.values.first().children.first() + val expectedDescription = Description( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("Subclass docs. End of subclass docs")) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expectedDescription, descriptionGot) + } + } + } + + + @Test + fun `work with multiple supertypes`() { + testInline( + """ + |/src/main/java/sample/SuperInterface.java + |package sample; + |/** + |* Super interface docs + |*/ + |public interface SuperInterface { } + |/src/main/java/sample/Superclass.java + |package sample; + |/** + |* Super class docs + |*/ + |public class Superclass { } + | + |/src/main/java/sample/Subclass.java + |package sample; + |/** + |* Subclass docs. {@inheritDoc} End of subclass docs + |*/ + |public class Subclass extends Superclass implements SuperInterface { } + """.trimIndent(), configuration + ) { + documentablesMergingStage = { module -> + val subclass = module.findClasslike("sample", "Subclass") + val descriptionGot = subclass.documentation.values.first().children.first() + val expectedDescription = Description( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("Subclass docs. Super class docs End of subclass docs")) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expectedDescription, descriptionGot) + } + } + } + + @Test + fun `work with methods`() { + testInline( + """ + |/src/main/java/sample/Superclass.java + |package sample; + |public class Superclass { + |/** + |* Sample super method + |* + |* @return super string + |* @throws RuntimeException super throws + |* @see java.lang.String super see + |* @deprecated super deprecated + |*/ + |public String test() { + | return ""; + |} + |} + |/src/main/java/sample/Subclass.java + |package sample; + |public class Subclass extends Superclass { + | /** + | * Sample sub method. {@inheritDoc} + | */ + | @Override + | public String test() { + | return super.test(); + | } + |} + """.trimIndent(), configuration + ) { + documentablesMergingStage = { module -> + val function = module.findFunction("sample", "Subclass", "test") + val descriptionGot = function.documentation.values.first().children.first() + val expectedDescription = Description( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("Sample sub method. Sample super method")) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expectedDescription, descriptionGot) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt new file mode 100644 index 00000000..a20db4ca --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt @@ -0,0 +1,252 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.doc.* +import kotlin.test.Test +import kotlin.test.assertEquals +import org.jetbrains.dokka.model.doc.Deprecated as DokkaDeprecatedTag +import org.jetbrains.dokka.model.doc.Throws as DokkaThrowsTag + +class JavadocInheritedDocTagsTest : BaseAbstractTest() { + @Suppress("DEPRECATION") // for includeNonPublic + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + includeNonPublic = true + } + } + } + + private fun performTagsTest(test: (DModule) -> Unit) { + testInline( + """ + |/src/main/java/sample/Superclass.java + |package sample; + |/** + |* @author super author + |*/ + |class Superclass { + | /** + | * Sample super method + | * + | * @return super string + | * @throws RuntimeException super throws + | * @see java.lang.String super see + | * @deprecated super deprecated + | */ + | public String test(){ + | return ""; + | } + | + | /** + | * + | * @param xd String superclass + | * @param asd Integer superclass + | */ + | public void test2(String xd, Integer asd){ + | } + |} + |/src/main/java/sample/Subclass.java + |package sample; + |/** + |* @author Ja, {@inheritDoc} + |*/ + |class Subclass extends Superclass { + |/** + | * Sample sub method. {@inheritDoc} + | * + | * @return "sample string". {@inheritDoc} + | * @throws RuntimeException because i can, {@inheritDoc} + | * @throws IllegalStateException this should be it {@inheritDoc} + | * @see java.lang.String string, {@inheritDoc} + | * @deprecated do not use, {@inheritDoc} + | */ + | @Override + | public String test() { + | return super.test(); + | } + | + | /** + | * + | * @param asd2 integer subclass, {@inheritDoc} + | * @param xd2 string subclass, {@inheritDoc} + | */ + | public void test2(String xd2, Integer asd2){ + | } + |} + """.trimIndent(), configuration + ) { + documentablesMergingStage = test + } + } + + @Test + fun `work with return`() { + performTagsTest { module -> + val function = module.findFunction("sample", "Subclass", "test") + val renderedTag = function.documentation.values.first().children.filterIsInstance<Return>().first() + val expectedTag = Return( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("\"sample string\". super string")) + ) + ), + name = "MARKDOWN_FILE" + ) + ) + + assertEquals(expectedTag, renderedTag) + } + } + + @Test + fun `work with throws`() { + performTagsTest { module -> + val function = module.findFunction("sample", "Subclass", "test") + val renderedTag = + function.documentation.values.first().children.first { it is DokkaThrowsTag && it.name == "java.lang.RuntimeException" } + val expectedTag = DokkaThrowsTag( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("because i can, super throws")) + ) + ), + name = "MARKDOWN_FILE" + ), + "java.lang.RuntimeException", + DRI("java.lang", "RuntimeException", target = PointingToDeclaration) + ) + + assertEquals(expectedTag, renderedTag) + } + } + + @Test + fun `work with throws when exceptions are different`() { + performTagsTest { module -> + val function = module.findFunction("sample", "Subclass", "test") + val renderedTag = + function.documentation.values.first().children.first { it is DokkaThrowsTag && it.name == "java.lang.IllegalStateException" } + val expectedTag = DokkaThrowsTag( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("this should be it")) + ) + ), + name = "MARKDOWN_FILE" + ), + "java.lang.IllegalStateException", + DRI("java.lang", "IllegalStateException", target = PointingToDeclaration) + ) + + assertEquals(expectedTag, renderedTag) + } + } + + @Test + fun `work with deprecated`() { + performTagsTest { module -> + val function = module.findFunction("sample", "Subclass", "test") + val renderedTag = function.documentation.values.first().children.filterIsInstance<DokkaDeprecatedTag>().first() + val expectedTag = DokkaDeprecatedTag( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("do not use, Sample super method")) + ) + ), + name = "MARKDOWN_FILE" + ), + ) + + assertEquals(expectedTag, renderedTag) + } + } + + @Test + fun `work with see also`() { + performTagsTest { module -> + val function = module.findFunction("sample", "Subclass", "test") + val renderedTag = function.documentation.values.first().children.filterIsInstance<See>().first() + val expectedTag = See( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("string,")) + ) + ), + name = "MARKDOWN_FILE" + ), + "java.lang.String", + DRI("java.lang", "String") + ) + + assertEquals(expectedTag, renderedTag) + } + } + + @Test + fun `work with author`() { + performTagsTest { module -> + val classlike = module.findClasslike("sample", "Subclass") + val renderedTag = classlike.documentation.values.first().children.filterIsInstance<Author>().first() + val expectedTag = Author( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("Ja, super author")) + ) + ), + name = "MARKDOWN_FILE" + ), + ) + + assertEquals(expectedTag, renderedTag) + } + } + + @Test + fun `work with params`() { + performTagsTest { module -> + val function = module.findFunction("sample", "Subclass", "test2") + val (asdTag, xdTag) = function.documentation.values.first().children.filterIsInstance<Param>() + .sortedBy { it.name } + + val expectedAsdTag = Param( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("integer subclass, Integer superclass")) + ) + ), + name = "MARKDOWN_FILE" + ), + "asd2" + ) + val expectedXdTag = Param( + CustomDocTag( + children = listOf( + P( + children = listOf(Text("string subclass, String superclass")) + ) + ), + name = "MARKDOWN_FILE" + ), + "xd2" + ) + assertEquals(expectedAsdTag, asdTag) + assertEquals(expectedXdTag, xdTag) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocParserTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocParserTest.kt new file mode 100644 index 00000000..3f83f462 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocParserTest.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.childrenOfType +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.firstChildOfType +import org.jetbrains.dokka.model.firstMemberOfType +import utils.text +import kotlin.test.Test +import kotlin.test.assertEquals + +class JavadocParserTest : BaseAbstractTest() { + + private fun performJavadocTest(testOperation: (DModule) -> Unit) { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Date2.java + |/** + | * The class <code>Date</code> represents a specific instant + | * in time, with millisecond precision. + | * <p> + | * Prior to JDK 1.1, the class <code>Date</code> had two additional + | * functions. It allowed the interpretation of dates as year, month, day, hour, + | * minute, and second values. It also allowed the formatting and parsing + | * of date strings. Unfortunately, the API for these functions was not + | * amenable to internationalization. As of JDK 1.1, the + | * <code>Calendar</code> class should be used to convert between dates and time + | * fields and the <code>DateFormat</code> class should be used to format and + | * parse date strings. + | * The corresponding methods in <code>Date</code> are deprecated. + | * <p> + | * Although the <code>Date</code> class is intended to reflect + | * coordinated universal time (UTC), it may not do so exactly, + | * depending on the host environment of the Java Virtual Machine. + | * Nearly all modern operating systems assume that 1 day = + | * 24 × 60 × 60 = 86400 seconds + | * in all cases. In UTC, however, about once every year or two there + | * is an extra second, called a "leap second." The leap + | * second is always added as the last second of the day, and always + | * on December 31 or June 30. For example, the last minute of the + | * year 1995 was 61 seconds long, thanks to an added leap second. + | * Most computer clocks are not accurate enough to be able to reflect + | * the leap-second distinction. + | * <p> + | * Some computer standards are defined in terms of Greenwich mean + | * time (GMT), which is equivalent to universal time (UT). GMT is + | * the "civil" name for the standard; UT is the + | * "scientific" name for the same standard. The + | * distinction between UTC and UT is that UTC is based on an atomic + | * clock and UT is based on astronomical observations, which for all + | * practical purposes is an invisibly fine hair to split. Because the + | * earth's rotation is not uniform (it slows down and speeds up + | * in complicated ways), UT does not always flow uniformly. Leap + | * seconds are introduced as needed into UTC so as to keep UTC within + | * 0.9 seconds of UT1, which is a version of UT with certain + | * corrections applied. There are other time and date systems as + | * well; for example, the time scale used by the satellite-based + | * global positioning system (GPS) is synchronized to UTC but is + | * <i>not</i> adjusted for leap seconds. An interesting source of + | * further information is the U.S. Naval Observatory, particularly + | * the Directorate of Time at: + | * <blockquote><pre> + | * <a href=http://tycho.usno.navy.mil>http://tycho.usno.navy.mil</a> + | * </pre></blockquote> + | * <p> + | * and their definitions of "Systems of Time" at: + | * <blockquote><pre> + | * <a href=http://tycho.usno.navy.mil/systime.html>http://tycho.usno.navy.mil/systime.html</a> + | * </pre></blockquote> + | * <p> + | * In all methods of class <code>Date</code> that accept or return + | * year, month, date, hours, minutes, and seconds values, the + | * following representations are used: + | * <ul> + | * <li>A year <i>y</i> is represented by the integer + | * <i>y</i> <code>- 1900</code>. + | * <li>A month is represented by an integer from 0 to 11; 0 is January, + | * 1 is February, and so forth; thus 11 is December. + | * <li>A date (day of month) is represented by an integer from 1 to 31 + | * in the usual manner. + | * <li>An hour is represented by an integer from 0 to 23. Thus, the hour + | * from midnight to 1 a.m. is hour 0, and the hour from noon to 1 + | * p.m. is hour 12. + | * <li>A minute is represented by an integer from 0 to 59 in the usual manner. + | * <li>A second is represented by an integer from 0 to 61; the values 60 and + | * 61 occur only for leap seconds and even then only in Java + | * implementations that actually track leap seconds correctly. Because + | * of the manner in which leap seconds are currently introduced, it is + | * extremely unlikely that two leap seconds will occur in the same + | * minute, but this specification follows the date and time conventions + | * for ISO C. + | * </ul> + | * <pre class="prettyprint"> + | * <androidx.fragment.app.FragmentContainerView + | * xmlns:android="http://schemas.android.com/apk/res/android" + | * xmlns:app="http://schemas.android.com/apk/res-auto" + | * android:id="@+id/fragment_container_view" + | * android:layout_width="match_parent" + | * android:layout_height="match_parent"> + | * </androidx.fragment.app.FragmentContainerView> + | * </pre> + | * <p> + | * In all cases, arguments given to methods for these purposes need + | * not fall within the indicated ranges; for example, a date may be + | * specified as January 32 and is interpreted as meaning February 1. + | * + | * <pre class="prettyprint"> + | * class MyFragment extends Fragment { + | * public MyFragment() { + | * super(R.layout.fragment_main); + | * } + | * } + | * </pre> + | + | * @author James Gosling + | * @author Arthur van Hoff + | * @author Alan Liu + | * @see java.text.DateFormat + | * @see java.util.Calendar + | * @since JDK1.0 + | * @apiSince 1 + | */ + |public class Date2 implements java.io.Serializable, java.lang.Cloneable, java.lang.Comparable<java.util.Date> { + | void x() { } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = testOperation + } + } + + @Test + fun `correctly parsed list`() { + performJavadocTest { module -> + val dateDescription = module.descriptionOf("Date2")!! + assertEquals(6, dateDescription.firstChildOfType<Ul>().children.filterIsInstance<Li>().size) + } + } + + @Test + fun `correctly parsed author tags`() { + performJavadocTest { module -> + val authors = module.findClasslike().documentation.values.single().childrenOfType<Author>() + assertEquals(3, authors.size) + assertEquals("James Gosling", authors[0].firstMemberOfType<Text>().text()) + assertEquals("Arthur van Hoff", authors[1].firstMemberOfType<Text>().text()) + assertEquals("Alan Liu", authors[2].firstMemberOfType<Text>().text()) + } + } + + @Test + fun `correctly parsed see tags`() { + performJavadocTest { module -> + val sees = module.findClasslike().documentation.values.single().childrenOfType<See>() + assertEquals(2, sees.size) + assertEquals(DRI("java.text", "DateFormat"), sees[0].address) + assertEquals("java.text.DateFormat", sees[0].name) + assertEquals(DRI("java.util", "Calendar"), sees[1].address) + assertEquals("java.util.Calendar", sees[1].name) + } + } + + @Test + fun `correctly parsed code block`(){ + performJavadocTest { module -> + val dateDescription = module.descriptionOf("Date2")!! + val preTagContent = dateDescription.firstChildOfType<Pre>().firstChildOfType<Text>() + val expectedText = """<androidx.fragment.app.FragmentContainerView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/fragment_container_view" + android:layout_width="match_parent" + android:layout_height="match_parent"> +</androidx.fragment.app.FragmentContainerView>""".trimIndent() + assertEquals(expectedText.trim(), preTagContent.body.trim()) + } + } + + @Test + fun `correctly parsed code block with curly braces (which PSI has problem with)`() { + performJavadocTest { module -> + val dateDescription = module.descriptionOf("Date2")!! + val preTagContent = dateDescription.childrenOfType<Pre>()[1].firstChildOfType<Text>() + val expectedText = """class MyFragment extends Fragment { + public MyFragment() { + super(R.layout.fragment_main); + } +}""".trimIndent() + assertEquals(expectedText.trim(), preTagContent.body.trim()) + } + } + +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/translators/utils.kt b/dokka-subprojects/plugin-base/src/test/kotlin/translators/utils.kt new file mode 100644 index 00000000..b1979f60 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/translators/utils.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package translators + +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.Description +import org.jetbrains.dokka.model.doc.Text + +fun DModule.documentationOf(className: String, functionName: String? = null): String = + descriptionOf(className, functionName) + ?.firstMemberOfType<Text>() + ?.body.orEmpty() + +fun DModule.descriptionOf(className: String, functionName: String? = null): Description? { + val classlike = packages.single() + .classlikes.single { it.name == className } + val target: Documentable = + if (functionName != null) classlike.functions.single { it.name == functionName } else classlike + return target.documentation.values.singleOrNull() + ?.firstChildOfTypeOrNull<Description>() +} + +fun DModule.findPackage(packageName: String? = null) = + packageName?.let { packages.firstOrNull { pkg -> pkg.packageName == packageName } + ?: throw NoSuchElementException("No packageName with name $packageName") } ?: packages.single() + +fun DModule.findClasslike(packageName: String? = null, className: String? = null): DClasslike { + val pkg = findPackage(packageName) + return className?.let { + pkg.classlikes.firstOrNull { cls -> cls.name == className } + ?: throw NoSuchElementException("No classlike with name $className") + } ?: pkg.classlikes.single() +} + +fun DModule.findFunction(packageName: String? = null, className: String, functionName: String? = null): DFunction { + val classlike = findClasslike(packageName, className) + return functionName?.let { + classlike.functions.firstOrNull { fn -> fn.name == functionName } + ?: throw NoSuchElementException("No classlike with name $functionName") + } ?: classlike.functions.single() +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/utils/HtmlUtils.kt b/dokka-subprojects/plugin-base/src/test/kotlin/utils/HtmlUtils.kt new file mode 100644 index 00000000..cb700a94 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/utils/HtmlUtils.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package utils + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.jetbrains.dokka.base.renderers.html.SearchRecord +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.select.Elements + +internal fun TestOutputWriter.navigationHtml(): Element = contents.getValue("navigation.html").let { Jsoup.parse(it) } + +internal fun TestOutputWriter.pagesJson(): List<SearchRecord> = jacksonObjectMapper().readValue(contents.getValue("scripts/pages.json")) + +internal fun Elements.selectNavigationGrid(): Element { + return this.select("div.overview").select("span.nav-link-grid").single() +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/utils/ModelUtils.kt b/dokka-subprojects/plugin-base/src/test/kotlin/utils/ModelUtils.kt new file mode 100644 index 00000000..fc7e9a2c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/utils/ModelUtils.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package utils + +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaPlugin + +abstract class AbstractModelTest(val path: String? = null, val pkg: String) : ModelDSL(), AssertDSL { + + fun inlineModelTest( + query: String, + platform: String = "jvm", + prependPackage: Boolean = true, + cleanupOutput: Boolean = true, + pluginsOverrides: List<DokkaPlugin> = emptyList(), + configuration: DokkaConfigurationImpl? = null, + block: DModule.() -> Unit + ) { + val testConfiguration = configuration ?: dokkaConfiguration { + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + analysisPlatform = platform + classpath += jvmStdlibPath!! + } + } + } + val prepend = path.let { p -> p?.let { "|$it\n" } ?: "" } + if (prependPackage) "|package $pkg" else "" + + testInline( + query = ("$prepend\n$query").trim().trimIndent(), + configuration = testConfiguration, + cleanupOutput = cleanupOutput, + pluginOverrides = pluginsOverrides + ) { + documentablesTransformationStage = block + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/utils/TagsAnnotations.kt b/dokka-subprojects/plugin-base/src/test/kotlin/utils/TagsAnnotations.kt new file mode 100644 index 00000000..a81b1dae --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/utils/TagsAnnotations.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package utils + +import org.junit.jupiter.api.Tag + + +/** + * Run a test only for descriptors, not symbols. + * + * In theory, these tests can be fixed + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Retention( + AnnotationRetention.RUNTIME +) +@Tag("onlyDescriptors") +annotation class OnlyDescriptors(val reason: String = "") + +/** + * Run a test only for descriptors, not symbols. + * + * These tests cannot be fixed until Analysis API does not support MPP + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Retention( + AnnotationRetention.RUNTIME +) +@Tag("onlyDescriptorsMPP") +annotation class OnlyDescriptorsMPP(val reason: String = "") diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt b/dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt new file mode 100644 index 00000000..39ac4b23 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package utils + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.P +import kotlin.collections.orEmpty +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.asserter +import kotlin.test.fail + +@DslMarker +annotation class TestDSL + +@TestDSL +abstract class ModelDSL : BaseAbstractTest() { + operator fun Documentable?.div(name: String): Documentable? = + this?.children?.find { it.name == name } + + inline fun <reified T : Documentable> Documentable?.cast(): T = + (this as? T).assertNotNull() +} + +@TestDSL +interface AssertDSL { + infix fun Any?.equals(other: Any?) = assertEquals(other, this) + infix fun Collection<Any>?.allEquals(other: Any?) = + this?.onEach { it equals other } ?: run { fail("Collection is empty") } + infix fun <T> Collection<T>?.exists(e: T) { + assertTrue(this.orEmpty().isNotEmpty(), "Collection cannot be null or empty") + assertTrue(this!!.any{it == e}, "Collection doesn't contain $e") + } + + infix fun <T> Collection<T>?.counts(n: Int) = this.orEmpty().assertCount(n) + + infix fun <T> T?.notNull(name: String): T = this.assertNotNull(name) + + fun <T> Collection<T>.assertCount(n: Int, prefix: String = "") = + assertEquals(n, count(), "${prefix}Expected $n, got ${count()}") +} + +/* + * TODO replace with kotlin.test.assertContains after migrating to Kotlin 1.5+ + */ +internal fun <T> assertContains(iterable: Iterable<T>, element: T, ) { + asserter.assertTrue( + { "Expected the collection to contain the element.\nCollection <$iterable>, element <$element>." }, + iterable.contains(element) + ) +} + +inline fun <reified T : Any> Any?.assertIsInstance(name: String): T = + this.let { it as? T } ?: throw AssertionError("$name should not be null") + +fun TagWrapper.text(): String = when (val t = this) { + is NamedTagWrapper -> "${t.name}: [${t.root.text()}]" + else -> t.root.text() +} + +fun DocTag.text(): String = when (val t = this) { + is Text -> t.body + is Code -> t.children.joinToString("\n") { it.text() } + is P -> t.children.joinToString("") { it.text() } + "\n" + else -> t.children.joinToString("") { it.text() } +} + +fun <T : Documentable> T?.comments(): String = docs().map { it.text() } + .joinToString(separator = "\n") { it } + +fun <T> T?.assertNotNull(name: String = ""): T = this ?: throw AssertionError("$name should not be null") + +fun <T : Documentable> T?.docs() = this?.documentation.orEmpty().values.flatMap { it.children } + +val DClass.supers + get() = supertypes.flatMap { it.component2() } + +val Bound.name: String? + get() = when (this) { + is Nullable -> inner.name + is DefinitelyNonNullable -> inner.name + is TypeParameter -> name + is PrimitiveJavaType -> name + is TypeConstructor -> dri.classNames + is JavaObject -> "Object" + is Void -> "void" + is Dynamic -> "dynamic" + is UnresolvedBound -> "<ERROR CLASS>" + is TypeAliased -> typeAlias.name + } diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/utils/contentUtils.kt b/dokka-subprojects/plugin-base/src/test/kotlin/utils/contentUtils.kt new file mode 100644 index 00000000..3ca0bd2d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/utils/contentUtils.kt @@ -0,0 +1,355 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package utils + +import matchers.content.* +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.pages.* +import kotlin.test.assertEquals + +//TODO: Try to unify those functions after update to 1.4 +fun ContentMatcherBuilder<*>.functionSignature( + annotations: Map<String, Set<String>>, + visibility: String, + modifier: String, + keywords: Set<String>, + name: String, + returnType: String? = null, + vararg params: Pair<String, ParamAttributes> +) = + platformHinted { + bareSignature(annotations, visibility, modifier, keywords, name, returnType, *params) + } + +fun ContentMatcherBuilder<*>.bareSignature( + annotations: Map<String, Set<String>>, + visibility: String, + modifier: String, + keywords: Set<String>, + name: String, + returnType: String? = null, + vararg params: Pair<String, ParamAttributes> +) = group { + annotations.entries.forEach { + group { + unwrapAnnotation(it) + } + } + if (visibility.isNotBlank()) +"$visibility " + if (modifier.isNotBlank()) +"$modifier " + +("${keywords.joinToString("") { "$it " }}fun ") + link { +name } + +"(" + if (params.isNotEmpty()) { + group { + params.forEachIndexed { id, (n, t) -> + group { + t.annotations.forEach { + unwrapAnnotation(it) + } + t.keywords.forEach { + +it + } + + +"$n: " + group { link { +(t.type) } } + if (id != params.lastIndex) + +", " + } + } + } + } + +")" + if (returnType != null) { + +(": ") + group { + link { + +(returnType) + } + } + } +} + +fun ContentMatcherBuilder<*>.classSignature( + annotations: Map<String, Set<String>>, + visibility: String, + modifier: String, + keywords: Set<String>, + name: String, + vararg params: Pair<String, ParamAttributes>, + parent: String? = null +) = group { + annotations.entries.forEach { + group { + unwrapAnnotation(it) + } + } + if (visibility.isNotBlank()) +"$visibility " + if (modifier.isNotBlank()) +"$modifier " + +("${keywords.joinToString("") { "$it " }}class ") + link { +name } + if (params.isNotEmpty()) { + +"(" + group { + params.forEachIndexed { id, (n, t) -> + group { + t.annotations.forEach { + unwrapAnnotation(it) + } + t.keywords.forEach { + +it + } + + +"$n: " + group { link { +(t.type) } } + if (id != params.lastIndex) + +", " + } + } + } + +")" + } + if (parent != null) { + +(" : ") + link { + +(parent) + } + } +} + +fun ContentMatcherBuilder<*>.functionSignatureWithReceiver( + annotations: Map<String, Set<String>>, + visibility: String?, + modifier: String?, + keywords: Set<String>, + receiver: String, + name: String, + returnType: String? = null, + vararg params: Pair<String, ParamAttributes> +) = + platformHinted { + bareSignatureWithReceiver(annotations, visibility, modifier, keywords, receiver, name, returnType, *params) + } + +fun ContentMatcherBuilder<*>.bareSignatureWithReceiver( + annotations: Map<String, Set<String>>, + visibility: String?, + modifier: String?, + keywords: Set<String>, + receiver: String, + name: String, + returnType: String? = null, + vararg params: Pair<String, ParamAttributes> +) = group { // TODO: remove it when double wrapping for signatures will be resolved + annotations.entries.forEach { + group { + unwrapAnnotation(it) + } + } + if (visibility != null && visibility.isNotBlank()) +"$visibility " + if (modifier != null && modifier.isNotBlank()) +"$modifier " + +("${keywords.joinToString("") { "$it " }}fun ") + group { + link { +receiver } + } + +"." + link { +name } + +"(" + if (params.isNotEmpty()) { + group { + params.forEachIndexed { id, (n, t) -> + group { + t.annotations.forEach { + unwrapAnnotation(it) + } + t.keywords.forEach { + +it + } + + +"$n: " + group { link { +(t.type) } } + if (id != params.lastIndex) + +", " + } + } + } + } + +")" + if (returnType != null) { + +(": ") + group { + link { + +(returnType) + } + } + } +} + +fun ContentMatcherBuilder<*>.propertySignature( + annotations: Map<String, Set<String>>, + visibility: String, + modifier: String, + keywords: Set<String>, + preposition: String, + name: String, + type: String? = null, + value: String? = null +) { + group { + header { +"Package-level declarations" } + skipAllNotMatching() + } + tabbedGroup { + group { + skipAllNotMatching() + tab(BasicTabbedContentType.PROPERTY) { + header{ + "Properties" } + table { + group { + link { +name } + divergentGroup { + divergentInstance { + divergent { + group { + group { + annotations.entries.forEach { + group { + unwrapAnnotation(it) + } + } + if (visibility.isNotBlank()) +"$visibility " + if (modifier.isNotBlank()) +"$modifier " + +("${keywords.joinToString("") { "$it " }}$preposition ") + link { +name } + if (type != null) { + +(": ") + group { + link { + +(type) + } + } + } + if (value != null) { + +(" = $value") + } + } + } + } + } + } + } + } + } + } + } +} + + +fun ContentMatcherBuilder<*>.typealiasSignature(name: String, expressionTarget: String) { + group { + header { +"Package-level declarations" } + skipAllNotMatching() + } + group { + group { + tab(BasicTabbedContentType.TYPE) { + header{ + "Types" } + table { + group { + link { +name } + divergentGroup { + divergentInstance { + group { + group { + group { + group { + +"typealias " + group { + group { + link { +name } + } + skipAllNotMatching() + } + +" = " + group { + link { +expressionTarget } + } + } + } + } + } + } + skipAllNotMatching() + } + } + skipAllNotMatching() + } + skipAllNotMatching() + } + } + } +} + +fun ContentMatcherBuilder<*>.pWrapped(text: String) = + group {// TODO: remove it when double wrapping for descriptions will be resolved + group { +text } + } + +fun ContentMatcherBuilder<*>.unnamedTag(tag: String, content: ContentMatcherBuilder<ContentGroup>.() -> Unit) = + group { + header(4) { +tag } + content() + } + +fun ContentMatcherBuilder<*>.comment(content: ContentMatcherBuilder<ContentGroup>.() -> Unit) = + group { + group { + content() + } + } + +fun ContentMatcherBuilder<*>.unwrapAnnotation(elem: Map.Entry<String, Set<String>>) { + group { + +"@" + link { +elem.key } + if(elem.value.isNotEmpty()) { + +"(" + elem.value.forEach { + group { + +("$it = ") + skipAllNotMatching() + } + } + +")" + } + } +} +inline fun<reified T> PageNode.contentPage(name: String, block: T.() -> Unit) { + (dfs { it.name == name } as? T).assertNotNull("The page `$name` is not found").block() +} + +fun ClasslikePageNode.assertHasFunctions(vararg expectedFunctionName: String) { + val functions = this.findSectionWithName("Functions").assertNotNull("Functions") + val functionsName = functions.children.map { (it.dfs { it is ContentText } as ContentText).text } + assertEquals(expectedFunctionName.toList(), functionsName) +} + +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() +} + +data class ParamAttributes( + val annotations: Map<String, Set<String>>, + val keywords: Set<String>, + val type: String +) + +fun RootPageNode.findTestType(packageName: String, name: String) = + children.single { it.name == packageName }.children.single { it.name == name } as ContentPage diff --git a/dokka-subprojects/plugin-base/src/test/resources/content/samples/samples.kt b/dokka-subprojects/plugin-base/src/test/resources/content/samples/samples.kt new file mode 100644 index 00000000..4c9f38dc --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/content/samples/samples.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package test + +fun sampleForClassDescription() { + print("Hello") +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include1.md b/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include1.md new file mode 100644 index 00000000..09882ec1 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include1.md @@ -0,0 +1,14 @@ +# Module example + +This is JVM documentation for module example + +# Package example + +This is JVM documentation for package example + +## Example + +```kotlin +\@SqlTable(People::class) +class Person(val name: String, uuid: UUID = UUID.randomUUID()) : Entity(uuid) +``` diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include11.md b/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include11.md new file mode 100644 index 00000000..fa27b23d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include11.md @@ -0,0 +1,3 @@ +# Module example + +This is second JVM documentation for module example diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include2.md b/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include2.md new file mode 100644 index 00000000..1574003d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include2.md @@ -0,0 +1,7 @@ +# Module example + +This is JS documentation for module example + +# Package greeteer + +This is JS documentation for package greeteer
\ No newline at end of file diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/kotlin/JsClass.kt b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/kotlin/JsClass.kt new file mode 100644 index 00000000..62a961c1 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/kotlin/JsClass.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package p2 + +class JsClass { + + /** + * @sample samples.SamplesJs.exampleUsage + */ + fun printWithExclamation(msg: String) = println("$msg!") +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/resources/Samples.kt b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/resources/Samples.kt new file mode 100644 index 00000000..5dc791d7 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/resources/Samples.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package samples + +import p2.JsClass + +class SamplesJs { + + fun exampleUsage() { + JsClass().printWithExclamation("Hi, Js") + } +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/kotlin/JvmClass.kt b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/kotlin/JvmClass.kt new file mode 100644 index 00000000..0014cdb7 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/kotlin/JvmClass.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package p2 + +class JvmClass { + + /** + * @sample samples.SamplesJvm.exampleUsage + */ + fun printWithExclamation(msg: String) = println("$msg!") +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/resources/Samples.kt b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/resources/Samples.kt new file mode 100644 index 00000000..f32538cc --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/resources/Samples.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package samples + +import p2.JvmClass + +class SamplesJvm { + + fun exampleUsage() { + JvmClass().printWithExclamation("Hi, Jvm") + } +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/sources/jsMain/kotlin/JsClass.kt b/dokka-subprojects/plugin-base/src/test/resources/linkable/sources/jsMain/kotlin/JsClass.kt new file mode 100644 index 00000000..180d9519 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/sources/jsMain/kotlin/JsClass.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package p1 + +class JsClass diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/sources/jvmMain/kotlin/JvmClass.kt b/dokka-subprojects/plugin-base/src/test/resources/linkable/sources/jvmMain/kotlin/JvmClass.kt new file mode 100644 index 00000000..3adf9520 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/sources/jvmMain/kotlin/JvmClass.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package p1 + +class JvmClass diff --git a/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/JavaEnum.java b/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/JavaEnum.java new file mode 100644 index 00000000..016365a7 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/JavaEnum.java @@ -0,0 +1,5 @@ +package linking.source; + +public enum JavaEnum { + ON_DECEIT, ON_DESTROY; +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/JavaLinker.java b/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/JavaLinker.java new file mode 100644 index 00000000..ac416530 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/JavaLinker.java @@ -0,0 +1,8 @@ +package linking.source; + +/** + * Reference link {@link linking.source.KotlinEnum} should resolve <p> + * sjuff sjuff {@link linking.source.KotlinEnum#ON_CREATE} should resolve <p> + * sjujj sjujj {@link linking.source.JavaEnum#ON_DECEIT} should resolve + */ +public class JavaLinker {} diff --git a/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/KotlinEnum.kt b/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/KotlinEnum.kt new file mode 100644 index 00000000..2d5498c1 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/KotlinEnum.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package linking.source + +enum class KotlinEnum { + ON_CREATE, ON_CATASTROPHE +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/KotlinLinker.kt b/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/KotlinLinker.kt new file mode 100644 index 00000000..ca6e233d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/KotlinLinker.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package linking.source + +/** + * Reference link [KotlinEnum] should resolve <p> + * stuff stuff [KotlinEnum.ON_CREATE] should resolve <p> + * stuff stuff [JavaEnum.ON_DECEIT] should resolve + */ +class KotlinLinker {} diff --git a/dokka-subprojects/plugin-base/src/test/resources/locationProvider/jdk8-package-list b/dokka-subprojects/plugin-base/src/test/resources/locationProvider/jdk8-package-list new file mode 100644 index 00000000..351c1868 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/locationProvider/jdk8-package-list @@ -0,0 +1,217 @@ +java.applet +java.awt +java.awt.color +java.awt.datatransfer +java.awt.dnd +java.awt.event +java.awt.font +java.awt.geom +java.awt.im +java.awt.im.spi +java.awt.image +java.awt.image.renderable +java.awt.print +java.beans +java.beans.beancontext +java.io +java.lang +java.lang.annotation +java.lang.instrument +java.lang.invoke +java.lang.management +java.lang.ref +java.lang.reflect +java.math +java.net +java.nio +java.nio.channels +java.nio.channels.spi +java.nio.charset +java.nio.charset.spi +java.nio.file +java.nio.file.attribute +java.nio.file.spi +java.rmi +java.rmi.activation +java.rmi.dgc +java.rmi.registry +java.rmi.server +java.security +java.security.acl +java.security.cert +java.security.interfaces +java.security.spec +java.sql +java.text +java.text.spi +java.time +java.time.chrono +java.time.format +java.time.temporal +java.time.zone +java.util +java.util.concurrent +java.util.concurrent.atomic +java.util.concurrent.locks +java.util.function +java.util.jar +java.util.logging +java.util.prefs +java.util.regex +java.util.spi +java.util.stream +java.util.zip +javax.accessibility +javax.activation +javax.activity +javax.annotation +javax.annotation.processing +javax.crypto +javax.crypto.interfaces +javax.crypto.spec +javax.imageio +javax.imageio.event +javax.imageio.metadata +javax.imageio.plugins.bmp +javax.imageio.plugins.jpeg +javax.imageio.spi +javax.imageio.stream +javax.jws +javax.jws.soap +javax.lang.model +javax.lang.model.element +javax.lang.model.type +javax.lang.model.util +javax.management +javax.management.loading +javax.management.modelmbean +javax.management.monitor +javax.management.openmbean +javax.management.relation +javax.management.remote +javax.management.remote.rmi +javax.management.timer +javax.naming +javax.naming.directory +javax.naming.event +javax.naming.ldap +javax.naming.spi +javax.net +javax.net.ssl +javax.print +javax.print.attribute +javax.print.attribute.standard +javax.print.event +javax.rmi +javax.rmi.CORBA +javax.rmi.ssl +javax.script +javax.security.auth +javax.security.auth.callback +javax.security.auth.kerberos +javax.security.auth.login +javax.security.auth.spi +javax.security.auth.x500 +javax.security.cert +javax.security.sasl +javax.sound.midi +javax.sound.midi.spi +javax.sound.sampled +javax.sound.sampled.spi +javax.sql +javax.sql.rowset +javax.sql.rowset.serial +javax.sql.rowset.spi +javax.swing +javax.swing.border +javax.swing.colorchooser +javax.swing.event +javax.swing.filechooser +javax.swing.plaf +javax.swing.plaf.basic +javax.swing.plaf.metal +javax.swing.plaf.multi +javax.swing.plaf.nimbus +javax.swing.plaf.synth +javax.swing.table +javax.swing.text +javax.swing.text.html +javax.swing.text.html.parser +javax.swing.text.rtf +javax.swing.tree +javax.swing.undo +javax.tools +javax.transaction +javax.transaction.xa +javax.xml +javax.xml.bind +javax.xml.bind.annotation +javax.xml.bind.annotation.adapters +javax.xml.bind.attachment +javax.xml.bind.helpers +javax.xml.bind.util +javax.xml.crypto +javax.xml.crypto.dom +javax.xml.crypto.dsig +javax.xml.crypto.dsig.dom +javax.xml.crypto.dsig.keyinfo +javax.xml.crypto.dsig.spec +javax.xml.datatype +javax.xml.namespace +javax.xml.parsers +javax.xml.soap +javax.xml.stream +javax.xml.stream.events +javax.xml.stream.util +javax.xml.transform +javax.xml.transform.dom +javax.xml.transform.sax +javax.xml.transform.stax +javax.xml.transform.stream +javax.xml.validation +javax.xml.ws +javax.xml.ws.handler +javax.xml.ws.handler.soap +javax.xml.ws.http +javax.xml.ws.soap +javax.xml.ws.spi +javax.xml.ws.spi.http +javax.xml.ws.wsaddressing +javax.xml.xpath +org.ietf.jgss +org.omg.CORBA +org.omg.CORBA.DynAnyPackage +org.omg.CORBA.ORBPackage +org.omg.CORBA.TypeCodePackage +org.omg.CORBA.portable +org.omg.CORBA_2_3 +org.omg.CORBA_2_3.portable +org.omg.CosNaming +org.omg.CosNaming.NamingContextExtPackage +org.omg.CosNaming.NamingContextPackage +org.omg.Dynamic +org.omg.DynamicAny +org.omg.DynamicAny.DynAnyFactoryPackage +org.omg.DynamicAny.DynAnyPackage +org.omg.IOP +org.omg.IOP.CodecFactoryPackage +org.omg.IOP.CodecPackage +org.omg.Messaging +org.omg.PortableInterceptor +org.omg.PortableInterceptor.ORBInitInfoPackage +org.omg.PortableServer +org.omg.PortableServer.CurrentPackage +org.omg.PortableServer.POAManagerPackage +org.omg.PortableServer.POAPackage +org.omg.PortableServer.ServantLocatorPackage +org.omg.PortableServer.portable +org.omg.SendingContext +org.omg.stub.java.rmi +org.w3c.dom +org.w3c.dom.bootstrap +org.w3c.dom.events +org.w3c.dom.ls +org.w3c.dom.views +org.xml.sax +org.xml.sax.ext +org.xml.sax.helpers diff --git a/dokka-subprojects/plugin-base/src/test/resources/locationProvider/multi-module-package-list b/dokka-subprojects/plugin-base/src/test/resources/locationProvider/multi-module-package-list new file mode 100644 index 00000000..03f33d9a --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/locationProvider/multi-module-package-list @@ -0,0 +1,8 @@ +$dokka.format:html-v1 +$dokka.linkExtension:html +$dokka.location:/NoPackageClass///PointingToDeclaration/moduleB/[root]/-no-package-class/index.html +module:moduleA +foo +bar +module:moduleB +baz diff --git a/dokka-subprojects/plugin-base/src/test/resources/locationProvider/old-package-list b/dokka-subprojects/plugin-base/src/test/resources/locationProvider/old-package-list new file mode 100644 index 00000000..d05b4535 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/locationProvider/old-package-list @@ -0,0 +1,9 @@ +$dokka.format:kotlin-website-html +$dokka.linkExtension:html +$dokka.location:kotlin.text.StringBuilderkotlin.relocated.text/-string-builder/index.html +$dokka.location:kotlin$minus(java.math.BigDecimal, java.math.BigDecimal)kotlin/java.math.-big-decimal/minus.html +$dokka.location:kotlin.Int$MAX_VALUE()kotlin/-int/max-value.html +kotlin +kotlin.text +kotlin.reflect + diff --git a/dokka-subprojects/plugin-base/src/test/resources/locationProvider/stdlib-package-list b/dokka-subprojects/plugin-base/src/test/resources/locationProvider/stdlib-package-list new file mode 100644 index 00000000..298321aa --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/locationProvider/stdlib-package-list @@ -0,0 +1,67 @@ +$dokka.format:html-v1 +$dokka.linkExtension:html +$dokka.location://arrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/kotlin-stdlib/[JS root]/array-with-fun.html +$dokka.location://booleanArray/#kotlin.Int#kotlin.Any/PointingToDeclaration/kotlin-stdlib/[JS root]/boolean-array.html +$dokka.location://booleanArrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,kotlin.Boolean]/PointingToDeclaration/kotlin-stdlib/[JS root]/boolean-array-with-fun.html +$dokka.location://charArray/#kotlin.Int#kotlin.Any/PointingToDeclaration/kotlin-stdlib/[JS root]/char-array.html +$dokka.location://charArrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,kotlin.Char]/PointingToDeclaration/kotlin-stdlib/[JS root]/char-array-with-fun.html +$dokka.location://fillArrayFun/#kotlin.Array[TypeParam(bounds=[kotlin.Any?])]#kotlin.Function1[kotlin.Int,TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/kotlin-stdlib/[JS root]/fill-array-fun.html +$dokka.location://longArray/#kotlin.Int#kotlin.Any/PointingToDeclaration/kotlin-stdlib/[JS root]/long-array.html +$dokka.location://longArrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,kotlin.Long]/PointingToDeclaration/kotlin-stdlib/[JS root]/long-array-with-fun.html +$dokka.location://newArray/#kotlin.Int#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/kotlin-stdlib/[JS root]/new-array.html +$dokka.location://untypedCharArrayWithFun/#kotlin.Int#kotlin.Function1[kotlin.Int,kotlin.Char]/PointingToDeclaration/kotlin-stdlib/[JS root]/untyped-char-array-with-fun.html +kotlin +kotlin.annotation +kotlin.browser +kotlin.collections +kotlin.comparisons +kotlin.concurrent +kotlin.contracts +kotlin.coroutines +kotlin.coroutines.cancellation +kotlin.coroutines.intrinsics +kotlin.dom +kotlin.experimental +kotlin.io +kotlin.js +kotlin.jvm +kotlin.math +kotlin.native +kotlin.native.concurrent +kotlin.native.ref +kotlin.properties +kotlin.random +kotlin.ranges +kotlin.reflect +kotlin.reflect.full +kotlin.reflect.jvm +kotlin.sequences +kotlin.streams +kotlin.system +kotlin.text +kotlin.time +kotlinx.browser +kotlinx.cinterop +kotlinx.cinterop.internal +kotlinx.dom +kotlinx.wasm.jsinterop +org.khronos.webgl +org.w3c.css.masking +org.w3c.dom +org.w3c.dom.clipboard +org.w3c.dom.css +org.w3c.dom.encryptedmedia +org.w3c.dom.events +org.w3c.dom.mediacapture +org.w3c.dom.mediasource +org.w3c.dom.parsing +org.w3c.dom.pointerevents +org.w3c.dom.svg +org.w3c.dom.url +org.w3c.fetch +org.w3c.files +org.w3c.notifications +org.w3c.performance +org.w3c.workers +org.w3c.xhr + diff --git a/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/commonMain/kotlin/Clock.kt b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/commonMain/kotlin/Clock.kt new file mode 100644 index 00000000..5459b2d7 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/commonMain/kotlin/Clock.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package example + +/** + * Documentation for expected class Clock + * in common module + */ +expect open class Clock() { + fun getTime(): String + /** + * Time in minis + */ + fun getTimesInMillis(): String + fun getYear(): String +} + diff --git a/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/commonMain/kotlin/House.kt b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/commonMain/kotlin/House.kt new file mode 100644 index 00000000..e79e2904 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/commonMain/kotlin/House.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package example + +class House(val street: String, val number: Int) { + + /** + * The owner of the house + */ + var owner: String = "" + + /** + * The owner of the house + */ + val differentOwner: String = "" + + fun addFloor() {} + + class Basement { + val pickles : List<Any> = mutableListOf() + } + + companion object { + val DEFAULT = House("",0) + } +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jsMain/kotlin/Clock.kt b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jsMain/kotlin/Clock.kt new file mode 100644 index 00000000..aad5787d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jsMain/kotlin/Clock.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package example + +import greeteer.Greeter +import kotlin.js.Date + +/** + * Documentation for actual class Clock in JS + */ +actual open class Clock { + actual fun getTime() = Date.now().toString() + fun onlyJsFunction(): Int = 42 + + /** + * JS implementation of getTimeInMillis + */ + actual fun getTimesInMillis(): String = Date.now().toString() + + /** + * JS custom kdoc + */ + actual fun getYear(): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} + +fun main() { + Greeter().greet().also { println(it) } +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmAndJsSecondCommonMain/kotlin/Greeter.kt b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmAndJsSecondCommonMain/kotlin/Greeter.kt new file mode 100644 index 00000000..e9e15d0b --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmAndJsSecondCommonMain/kotlin/Greeter.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package greeteer + +import example.Clock + +class Greeter { + /** + * Some docs for the [greet] function + */ + fun greet() = Clock().let{ "Hello there! THe time is ${it.getTime()}" } +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/Clock.kt b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/Clock.kt new file mode 100644 index 00000000..f38ff749 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/Clock.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package example + +import greeteer.Greeter + +/** + * Documentation for actual class Clock in JVM + */ +actual open class Clock { + actual fun getTime(): String = System.currentTimeMillis().toString() + + /** + * Time in minis + */ + actual fun getTimesInMillis(): String = System.currentTimeMillis().toString() + + /** + * Documentation for onlyJVMFunction on... + * wait for it... + * ...JVM! + */ + fun onlyJVMFunction(): Double = 2.5 + + open fun getDayOfTheWeek(): String { + TODO("not implemented") + } + + /** + * JVM custom kdoc + */ + actual fun getYear(): String { + TODO("not implemented") + } +} + +fun clockList() = listOf(Clock()) + +fun main() { + Greeter().greet().also { println(it) } +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/ClockDays.kt b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/ClockDays.kt new file mode 100644 index 00000000..e96acf1c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/ClockDays.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package example + +/** + * frgergergrthe + * */ +enum class ClockDays { + /** + * dfsdfsdfds + * */ + FIRST, + SECOND, // test2 + THIRD, // test3 + FOURTH, // test4 + FIFTH // test5 +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/HtmlTest.kt b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/HtmlTest.kt new file mode 100644 index 00000000..2fa3622b --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/HtmlTest.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package example + +/** + * <!-- this shouldn't be visible --> + */ +class HtmlTest { + /** + * This is an example <!-- not visible --> of html + */ + fun test(){ + + } + + /** + * This is an <b> documentation </b> + */ + fun testP(){ + + } +} diff --git a/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/ParticularClock.kt b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/ParticularClock.kt new file mode 100644 index 00000000..3b7a403d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/ParticularClock.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package example + +import greeteer.Greeter + +class ParticularClock(private val clockDay: ClockDays) : Clock() { + + /** + * Rings bell [times] + */ + fun ringBell(times: Int) {} + + /** + * Uses provider [greeter] + */ + fun useGreeter(greeter: Greeter) { + + } + + /** + * Day of the week + */ + override fun getDayOfTheWeek() = clockDay.name +} + +/** + * A sample extension function + * When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ + * @usesMathJax + */ +fun Clock.extensionFun() { + +} |