diff options
Diffstat (limited to 'dokka-subprojects/plugin-base/src/test/kotlin/signatures')
8 files changed, 2330 insertions, 0 deletions
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 + ) + } + } + } +} + |