From 8e5c63d035ef44a269b8c43430f43f5c8eebfb63 Mon Sep 17 00:00:00 2001 From: Ignat Beresnev Date: Fri, 10 Nov 2023 11:46:54 +0100 Subject: 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 --- .../kotlin/translators/AccessorMethodNamingTest.kt | 123 +++ .../src/test/kotlin/translators/Bug1341.kt | 48 + ...efaultDescriptorToDocumentableTranslatorTest.kt | 1107 ++++++++++++++++++++ .../DefaultPsiToDocumentableTranslatorTest.kt | 1027 ++++++++++++++++++ .../translators/ExternalDocumentablesTest.kt | 144 +++ .../kotlin/translators/JavadocInheritDocsTest.kt | 312 ++++++ .../translators/JavadocInheritedDocTagsTest.kt | 252 +++++ .../test/kotlin/translators/JavadocParserTest.kt | 208 ++++ .../src/test/kotlin/translators/utils.kt | 43 + 9 files changed, 3264 insertions(+) create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/AccessorMethodNamingTest.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/Bug1341.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/ExternalDocumentablesTest.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritDocsTest.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/JavadocParserTest.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/translators/utils.kt (limited to 'dokka-subprojects/plugin-base/src/test/kotlin/translators') 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) { + 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, testSuites: List>) { + 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(), + "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(), + "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> = 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> = 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 + ) : 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 + ) : 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().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; +