aboutsummaryrefslogtreecommitdiff
path: root/dokka-subprojects/plugin-base/src/test
diff options
context:
space:
mode:
authorIgnat Beresnev <ignat.beresnev@jetbrains.com>2023-11-10 11:46:54 +0100
committerGitHub <noreply@github.com>2023-11-10 11:46:54 +0100
commit8e5c63d035ef44a269b8c43430f43f5c8eebfb63 (patch)
tree1b915207b2b9f61951ddbf0ff2e687efd053d555 /dokka-subprojects/plugin-base/src/test
parenta44efd4ba0c2e4ab921ff75e0f53fc9335aa79db (diff)
downloaddokka-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')
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/basic/AbortGracefullyOnMissingDocumentablesTest.kt22
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/basic/DRITest.kt351
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/basic/DokkaBasicTests.kt46
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/basic/FailOnWarningTest.kt128
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/basic/LoggerTest.kt48
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/ContentInDescriptionTest.kt142
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/HighlightingTest.kt83
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/ContentForAnnotationsTest.kt351
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/FileLevelJvmNameTest.kt115
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/JavaDeprecatedTest.kt144
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/KotlinDeprecatedTest.kt401
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/annotations/SinceKotlinTest.kt350
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/exceptions/ContentForExceptions.kt439
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/functions/ContentForBriefTest.kt388
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/functions/ContentForConstructors.kt53
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/inheritors/ContentForInheritorsTest.kt499
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/params/ContentForParamsTest.kt1529
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/properties/ContentForClassWithParamsAndPropertiesTest.kt272
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/receiver/ContentForReceiverTest.kt61
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/samples/ContentForSamplesTest.kt207
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt866
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/signatures/ConstructorsSignaturesTest.kt469
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/signatures/ContentForSignaturesTest.kt515
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/content/typealiases/TypealiasTest.kt83
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/enums/JavaEnumsTest.kt75
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/enums/KotlinEnumsTest.kt471
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/expect/AbstractExpectTest.kt109
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectGenerator.kt17
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectTest.kt28
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/expect/ExpectUtils.kt32
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/expectActuals/ExpectActualsTest.kt179
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/filter/DeprecationFilterTest.kt264
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/filter/EmptyPackagesFilterTest.kt70
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/filter/JavaFileFilterTest.kt40
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/filter/JavaVisibilityFilterTest.kt308
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/filter/KotlinArrayDocumentableReplacerTest.kt211
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/filter/VisibilityFilterTest.kt755
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/issues/IssuesTest.kt59
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/linkableContent/LinkableContentTest.kt418
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/linking/EnumValuesLinkingTest.kt142
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/AndroidExternalLocationProviderTest.kt109
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/DefaultExternalLocationProviderTest.kt78
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/Dokka010ExternalLocationProviderTest.kt123
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/DokkaLocationProviderTest.kt126
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/JavadocExternalLocationProviderTest.kt85
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/locationProvider/MultiModuleLinkingTest.kt74
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/markdown/KDocTest.kt51
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt240
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/markdown/ParserTest.kt1633
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/ClassesTest.kt594
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/CommentTest.kt338
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/ExtensionsTest.kt159
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/FunctionsTest.kt403
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/InheritorsTest.kt428
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/JavaTest.kt491
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt365
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/ObjectTest.kt43
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/PackagesTest.kt123
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/PropertyTest.kt277
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/JavaAnnotationsForParametersTest.kt181
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt195
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/model/annotations/KotlinAnnotationsForParametersTest.kt105
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/multiplatform/BasicMultiplatformTest.kt58
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/packageList/PackageListTest.kt69
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/pageMerger/PageNodeMergerTest.kt465
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/parsers/JavadocParserTest.kt618
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/BasicTest.kt24
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/BreadcrumbsTest.kt88
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CoverPageTest.kt51
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CustomFooterTest.kt48
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/DivergentTest.kt316
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/FooterMessageTest.kt31
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/FormattingUtilsTest.kt86
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/GroupWrappingTest.kt82
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/HeaderTest.kt102
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/HtmlRenderingOnlyTestBase.kt68
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/ListStylesTest.kt45
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/NavigationIconTest.kt292
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/NavigationTest.kt414
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SearchbarDataInstallerTest.kt50
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SourceSetDependentHintTest.kt139
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/SourceSetFilterTest.kt68
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/TabbedContentTest.kt188
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/TextStylesTest.kt113
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/resourceLinks/ResourceLinksTest.kt301
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/signatures/AbstractRenderingTest.kt65
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/signatures/DivergentSignatureTest.kt73
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt312
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt461
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/signatures/ObviousTypeSkippingTest.kt206
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/signatures/RawHtmlRenderingTest.kt70
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/signatures/SignatureTest.kt1035
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/signatures/VarianceSignatureTest.kt108
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt366
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt177
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformerBuilders/PageTransformerBuilderTest.kt195
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/AbstractContextModuleAndPackageDocumentationReaderTest.kt27
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/CommentsToContentConverterTest.kt484
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/ContextModuleAndPackageDocumentationReaderTest1.kt187
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/ContextModuleAndPackageDocumentationReaderTest3.kt61
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/DivisionSwitchTest.kt126
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/InheritedEntriesDocumentableFilterTransfromerTest.kt162
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/InvalidContentModuleAndPackageDocumentationReaderTest.kt100
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt386
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerFunctionalTest.kt137
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerUnitTest.kt260
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/ObviousAndInheritedFunctionsDocumentableFilterTest.kt229
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/ReportUndocumentedTransformerTest.kt927
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/SourceLinkTransformerTest.kt131
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/SuppressTagFilterTest.kt211
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/SuppressedByConfigurationDocumentableFilterTransformerTest.kt193
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/transformers/isExceptionTest.kt147
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/AccessorMethodNamingTest.kt123
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/Bug1341.kt48
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt1107
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt1027
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/ExternalDocumentablesTest.kt144
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritDocsTest.kt312
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt252
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocParserTest.kt208
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/translators/utils.kt43
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/utils/HtmlUtils.kt20
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/utils/ModelUtils.kt43
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/utils/TagsAnnotations.kt42
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt94
-rw-r--r--dokka-subprojects/plugin-base/src/test/kotlin/utils/contentUtils.kt355
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/content/samples/samples.kt9
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include1.md14
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include11.md3
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/includes/include2.md7
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/kotlin/JsClass.kt13
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/resources/Samples.kt14
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/kotlin/JvmClass.kt13
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/resources/Samples.kt14
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/sources/jsMain/kotlin/JsClass.kt7
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linkable/sources/jvmMain/kotlin/JvmClass.kt7
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/JavaEnum.java5
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/JavaLinker.java8
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/KotlinEnum.kt9
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/linking/jvmMain/kotlin/linking/source/KotlinLinker.kt12
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/locationProvider/jdk8-package-list217
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/locationProvider/multi-module-package-list8
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/locationProvider/old-package-list9
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/locationProvider/stdlib-package-list67
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/commonMain/kotlin/Clock.kt19
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/commonMain/kotlin/House.kt28
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jsMain/kotlin/Clock.kt32
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmAndJsSecondCommonMain/kotlin/Greeter.kt14
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/Clock.kt43
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/ClockDays.kt19
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/HtmlTest.kt24
-rw-r--r--dokka-subprojects/plugin-base/src/test/resources/multiplatform/basicMultiplatformTest/jvmMain/kotlin/example/ParticularClock.kt36
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.&nbsp;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.&nbsp;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.&nbsp;this.
+ | */
+ |public class Parent {
+ |}
+ |
+ |/src/main/kotlin/sample/ParentKt.kt
+ |package sample;
+ |/**
+ | * Hello World! Docs with period issue, e.g.&nbsp;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.&nbsp;"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("&nbsp;<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&nbsp;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&nbsp;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&nbsp;day&nbsp;=
+ | * 24&nbsp;&times;&nbsp;60&nbsp;&times;&nbsp;60&nbsp;= 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>&nbsp;<code>-&nbsp;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">
+ | * &lt;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"&gt;
+ | * &lt;/androidx.fragment.app.FragmentContainerView&gt;
+ | * </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() {
+
+}