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 org.junit.jupiter.api.Test import utils.* import kotlin.test.assertEquals 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!!) 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 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 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 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") assert(fn.type is DefinitelyNonNullable) assert(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 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 : Comparable, Collection") 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) | | @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 `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.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 | """.trimMargin(), configuration, pluginOverrides = listOf(writerPlugin) ) { renderingStage = { _, _ -> writerPlugin.writer.renderedContent("root/example.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 = Comparable | """.trimMargin(), configuration, pluginOverrides = listOf(writerPlugin) ) { renderingStage = { _, _ -> writerPlugin.writer.renderedContent("root/example.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 = Map | |class ABC { | fun someFun(xd: XD) = 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 ) } } } @Test fun `generic constructor params`() { val writerPlugin = TestOutputWriterPlugin() testInline( """ |/src/main/kotlin/common/Test.kt |package example | |class GenericClass(val x: Int) { | constructor(x: T) : this(1) | | constructor(x: Int, y: String) : this(1) | | constructor(x: Int, y: List) : this(1) | | constructor(x: Boolean, y: Int, z: String) : this(1) | | constructor(x: List>>?) : 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("Constructors") .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(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 `` expression, an empty `` 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.table[data-togglable=Entries]") .single() .signature() .select("div.block") enumEntrySignatures[0].match( A("EMPTY"), ignoreSpanWithTokenStyle = true ) enumEntrySignatures[1].match( A("WITH_ARG"), ignoreSpanWithTokenStyle = true ) } } } @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 ) } } } } }