diff --git a/core/src/main/kotlin/model/Documentable.kt b/core/src/main/kotlin/model/Documentable.kt index f3b88d8ee1..62268fefc0 100644 --- a/core/src/main/kotlin/model/Documentable.kt +++ b/core/src/main/kotlin/model/Documentable.kt @@ -340,12 +340,13 @@ data class DTypeParameter( constructor( dri: DRI, name: String, + presentableName: String?, documentation: SourceSetDependent, expectPresentInSet: DokkaSourceSet?, bounds: List, sourceSets: Set, extra: PropertyContainer = PropertyContainer.empty() - ) : this(Invariance(TypeParameter(dri, name)), documentation, expectPresentInSet, bounds, sourceSets, extra) + ) : this(Invariance(TypeParameter(dri, name, presentableName)), documentation, expectPresentInSet, bounds, sourceSets, extra) override val dri: DRI by variantTypeParameter.inner::dri override val name: String by variantTypeParameter.inner::name @@ -376,13 +377,28 @@ data class DTypeAlias( sealed class Projection sealed class Bound : Projection() -data class TypeParameter(val dri: DRI, val name: String) : Bound() +data class TypeParameter(val dri: DRI, val name: String, val presentableName: String? = null) : Bound() object Star : Projection() -data class TypeConstructor( - val dri: DRI, - val projections: List, - val modifier: FunctionModifiers = FunctionModifiers.NONE -) : Bound() + +sealed class TypeConstructor : Bound() { + abstract val dri: DRI + abstract val projections: List + abstract val presentableName: String? +} + +data class GenericTypeConstructor( + override val dri: DRI, + override val projections: List, + override val presentableName: String? = null +) : TypeConstructor() + +data class FunctionalTypeConstructor( + override val dri: DRI, + override val projections: List, + val isExtensionFunction: Boolean = false, + val isSuspendable: Boolean = false, + override val presentableName: String? = null +) : TypeConstructor() data class Nullable(val inner: Bound) : Bound() @@ -406,14 +422,10 @@ object JavaObject : Bound() object Dynamic : Bound() data class UnresolvedBound(val name: String) : Bound() -enum class FunctionModifiers { - NONE, FUNCTION, EXTENSION -} - fun Variance.withDri(dri: DRI) = when(this) { - is Contravariance -> Contravariance(TypeParameter(dri, inner.name)) - is Covariance -> Covariance(TypeParameter(dri, inner.name)) - is Invariance -> Invariance(TypeParameter(dri, inner.name)) + is Contravariance -> Contravariance(TypeParameter(dri, inner.name, inner.presentableName)) + is Covariance -> Covariance(TypeParameter(dri, inner.name, inner.presentableName)) + is Invariance -> Invariance(TypeParameter(dri, inner.name, inner.presentableName)) } private fun String.shorten(maxLength: Int) = lineSequence().first().let { diff --git a/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt b/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt index 0a22cecea1..d701de04b2 100644 --- a/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt +++ b/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt @@ -323,14 +323,15 @@ class KotlinSignatureProvider(ctcc: CommentsToContentConverter, logger: DokkaLog when (p) { is TypeParameter -> link(p.name, p.dri) - is TypeConstructor -> if (p.function) + is FunctionalTypeConstructor -> +funType(mainDRI.single(), mainSourcesetData, p) - else + + is GenericTypeConstructor -> group(styles = emptySet()) { val linkText = if (showFullyQualifiedName && p.dri.packageName != null) { "${p.dri.packageName}.${p.dri.classNames.orEmpty()}" } else p.dri.classNames.orEmpty() - + if (p.presentableName != null) text(p.presentableName + ": ") link(linkText, p.dri) list(p.projections, prefix = "<", suffix = ">") { signatureForProjection(it, showFullyQualifiedName) @@ -357,14 +358,18 @@ class KotlinSignatureProvider(ctcc: CommentsToContentConverter, logger: DokkaLog is UnresolvedBound -> text(p.name) } - private fun funType(dri: DRI, sourceSets: Set, type: TypeConstructor) = + private fun funType(dri: DRI, sourceSets: Set, type: FunctionalTypeConstructor) = contentBuilder.contentFor(dri, sourceSets, ContentKind.Main) { - if (type.extension) { + + if (type.presentableName != null) text(type.presentableName + ": ") + if (type.isSuspendable) text("suspend ") + + if (type.isExtensionFunction) { signatureForProjection(type.projections.first()) text(".") } - val args = if (type.extension) + val args = if (type.isExtensionFunction) type.projections.drop(1) else type.projections @@ -379,16 +384,11 @@ class KotlinSignatureProvider(ctcc: CommentsToContentConverter, logger: DokkaLog } } -private fun PrimitiveJavaType.translateToKotlin() = TypeConstructor( +private fun PrimitiveJavaType.translateToKotlin() = GenericTypeConstructor( dri = dri, - projections = emptyList() + projections = emptyList(), + presentableName = null ) private val DTypeParameter.nontrivialBounds: List get() = bounds.filterNot { it is Nullable && it.inner.driOrNull == DriOfAny } - -val TypeConstructor.function - get() = modifier == FunctionModifiers.FUNCTION || modifier == FunctionModifiers.EXTENSION - -val TypeConstructor.extension - get() = modifier == FunctionModifiers.EXTENSION diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt index f58b2b366f..8525586ace 100644 --- a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -17,8 +17,11 @@ import org.jetbrains.dokka.model.properties.PropertyContainer import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.builtins.functions.FunctionClassDescriptor +import org.jetbrains.kotlin.builtins.isBuiltinExtensionFunctionalType import org.jetbrains.kotlin.builtins.isExtensionFunctionType import org.jetbrains.kotlin.builtins.isFunctionType +import org.jetbrains.kotlin.builtins.isSuspendFunctionTypeOrSubtype import org.jetbrains.kotlin.codegen.isJvmStaticInObjectOrClassOrInterface import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.ClassKind @@ -586,7 +589,7 @@ private class DokkaDescriptorVisitor( private fun ClassDescriptor.resolveClassDescriptionData(): ClassInfo { fun toTypeConstructor(kt: KotlinType) = - TypeConstructor( + GenericTypeConstructor( DRI.from(kt.constructor.declarationDescriptor as DeclarationDescriptor), kt.arguments.map { it.toProjection() }) @@ -621,7 +624,7 @@ private class DokkaDescriptorVisitor( private fun TypeParameterDescriptor.toVariantTypeParameter() = DTypeParameter( variantTypeParameter( - TypeParameter(DRI.from(this), name.identifier) + TypeParameter(DRI.from(this), name.identifier, annotations.getPresentableName()) ), resolveDescriptorData(), null, @@ -630,6 +633,10 @@ private class DokkaDescriptorVisitor( extra = PropertyContainer.withAll(additionalExtras().toSourceSetDependent().toAdditionalModifiers()) ) + private fun org.jetbrains.kotlin.descriptors.annotations.Annotations.getPresentableName(): String? = + map { it.toAnnotation() }.singleOrNull { it.dri.classNames == "ParameterName" }?.params?.get("name") + .safeAs()?.value?.drop(1)?.dropLast(1) // Dropping enclosing doublequotes because we don't want to have it in our custom signature serializer + private fun KotlinType.toBound(): Bound = when (this) { is DynamicType -> Dynamic is AbbreviatedType -> TypeAliased( @@ -639,14 +646,20 @@ private class DokkaDescriptorVisitor( else -> when (val ctor = constructor.declarationDescriptor) { is TypeParameterDescriptor -> TypeParameter( dri = DRI.from(ctor), - name = ctor.name.asString() + name = ctor.name.asString(), + presentableName = annotations.getPresentableName() + ) + is FunctionClassDescriptor -> FunctionalTypeConstructor( + DRI.from(ctor), + arguments.map { it.toProjection() }, + isExtensionFunction = isExtensionFunctionType || isBuiltinExtensionFunctionalType, + isSuspendable = isSuspendFunctionTypeOrSubtype, + presentableName = annotations.getPresentableName() ) - else -> TypeConstructor( + else -> GenericTypeConstructor( DRI.from(ctor!!), // TODO: remove '!!' arguments.map { it.toProjection() }, - if (isExtensionFunctionType) FunctionModifiers.EXTENSION - else if (isFunctionType) FunctionModifiers.FUNCTION - else FunctionModifiers.NONE + annotations.getPresentableName() ) }.let { if (isMarkedNullable) Nullable(it) else it diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index c413f5c8a8..d67bd9f570 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -22,9 +22,11 @@ import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation +import org.jetbrains.kotlin.builtins.functions.FunctionClassDescriptor import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot import org.jetbrains.kotlin.descriptors.Visibilities +import org.jetbrains.kotlin.idea.caches.resolve.util.getJavaClassDescriptor import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName import org.jetbrains.kotlin.load.java.JvmAbi import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName @@ -139,7 +141,7 @@ class DefaultPsiToDocumentableTranslator( psiClass.isInterface -> DRI.from(psiClass) to JavaClassKindTypes.INTERFACE else -> DRI.from(psiClass) to JavaClassKindTypes.CLASS } - TypeConstructor( + GenericTypeConstructor( dri, psi.parameters.map(::getProjection) ) to javaClassKind @@ -370,11 +372,19 @@ class DefaultPsiToDocumentableTranslator( dri = DRI.from(resolved), name = resolved.name.orEmpty() ) - else -> - TypeConstructor(DRI.from(resolved), type.parameters.map { getProjection(it) }) + Regex("kotlin\\.jvm\\.functions\\.Function.*").matches(resolved.qualifiedName ?: "") || + Regex("java\\.util\\.function\\.Function.*").matches( + resolved.qualifiedName ?: "" + ) -> FunctionalTypeConstructor( + DRI.from(resolved), + type.parameters.map { getProjection(it) } + ) + else -> GenericTypeConstructor( + DRI.from(resolved), + type.parameters.map { getProjection(it) }) } } - is PsiArrayType -> TypeConstructor( + is PsiArrayType -> GenericTypeConstructor( DRI("kotlin", "Array"), listOf(getProjection(type.componentType)) ) @@ -411,6 +421,7 @@ class DefaultPsiToDocumentableTranslator( DTypeParameter( dri.copy(target = dri.target.nextTarget()), type.name.orEmpty(), + null, javadocParser.parseDocumentation(type).toSourceSetDependent(), null, mapBounds(type.bounds), diff --git a/plugins/base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt b/plugins/base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt new file mode 100644 index 0000000000..bac3ced2d6 --- /dev/null +++ b/plugins/base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt @@ -0,0 +1,260 @@ +package signatures + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.jdk +import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import utils.A +import utils.Span +import utils.TestOutputWriterPlugin +import utils.match + +class FunctionalTypeConstructorsSignatureTest : AbstractCoreTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + classpath = listOf(commonStdlibPath!!) + 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 = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example.html").firstSignature().match( + "val ", A("nF"), ": (", A("Int"), ") -> ", A("String"), Span() + ) + } + } + } + + @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.html").firstSignature().match( + "val ", A("nF"), ": (", A("Int"), ") -> ", A("String"), Span() + ) + } + } + } + + @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.html").firstSignature().match( + "val ", A("nF"), ": ", A("Boolean"), ".(", A("Int"), ") -> ", A("String"), Span() + ) + } + } + } + + @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.html").firstSignature().match( + "val ", A("nF"), ": (param: ", A("Int"), ") -> ", A("String"), Span() + ) + } + } + } + + @Disabled // Add coroutines on classpath and get proper import + @Test + fun `kotlin normal suspendable function`() { + val source = source("val nF: SuspendFunction1 = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example.html").firstSignature().match( + "val ", A("nF"), ": suspend (", A("Int"), ") -> ", A("String"), Span() + ) + } + } + } + + @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.html").firstSignature().match( + "val ", A("nF"), ": suspend (", A("Int"), ") -> ", A("String"), Span() + ) + } + } + } + + @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.html").firstSignature().match( + "val ", A("nF"), ": suspend ", A("Boolean"), ".(", A("Int"), ") -> ", A("String"), Span() + ) + } + } + } + + @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.html").firstSignature().match( + "val ", A("nF"), ": suspend (param: ", A("Int"), ") -> ", A("String"), Span() + ) + } + } + } + + @Test + fun `kotlin syntactic sugar suspendable fancy function with param name`() { + val source = + source("val nF: suspend (param1: suspend Boolean.(param2: List) -> Boolean) -> String = { _ -> \"\" }") + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example.html").firstSignature().match( + "val ", + A("nF"), + ": suspend (param1: suspend", + A("Boolean"), + ".(param2: ", + A("List"), + "<", + A("Int"), + ">) -> ", + A("Boolean"), + ") -> ", + A("String"), + Span() + ) + } + } + } + + @Test + fun `java with java function`() { + val source = """ + |/src/main/kotlin/test/JavaClass.java + |package example + | + |public class JavaClass { + | public java.util.function.Function javaFunction = null; + |} + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-java-class/index.html").signature().last().match( + "open val ", A("javaFunction"), ": (", A("Integer"), ") -> ", A("String"), Span() + ) + } + } + } + + @Test + fun `java with kotlin function`() { + val source = """ + |/src/main/kotlin/test/JavaClass.java + |package example + | + |public class JavaClass { + | public kotlin.jvm.functions.Function1 kotlinFunction = null; + |} + """.trimIndent() + val writerPlugin = TestOutputWriterPlugin() + + testInline( + source, + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + writerPlugin.writer.renderedContent("root/example/-java-class/index.html").signature().last().match( + "open val ", A("kotlinFunction"), ": (", A("Integer"), ") -> ", A("String"), Span() + ) + } + } + } +} diff --git a/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt index 5a9f085528..5112ae0bcf 100644 --- a/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt +++ b/plugins/kotlin-as-java/src/main/kotlin/converters/KotlinToJavaConverter.kt @@ -188,7 +188,11 @@ private fun Projection.asJava(): Projection = when(this) { private fun Bound.asJava(): Bound = when(this) { is TypeParameter -> copy(dri.possiblyAsJava()) - is TypeConstructor -> copy( + is GenericTypeConstructor -> copy( + dri = dri.possiblyAsJava(), + projections = projections.map { it.asJava() } + ) + is FunctionalTypeConstructor -> copy( dri = dri.possiblyAsJava(), projections = projections.map { it.asJava() } ) @@ -229,7 +233,7 @@ internal fun DObject.asJava(): DObject = copy( visibility = sourceSets.map { it to JavaVisibility.Public }.toMap(), - type = TypeConstructor(dri, emptyList()), + type = GenericTypeConstructor(dri, emptyList()), setter = null, getter = null, sourceSets = sourceSets, @@ -276,7 +280,10 @@ internal fun String.getAsPrimitive(): JvmPrimitiveType? = org.jetbrains.kotlin.b private fun DRI.partialFqName() = packageName?.let { "$it." } + classNames private fun DRI.possiblyAsJava() = this.partialFqName().mapToJava()?.toDRI(this) ?: this -private fun TypeConstructor.possiblyAsJava() = copy(dri = this.dri.possiblyAsJava()) +private fun TypeConstructor.possiblyAsJava() = when(this) { + is GenericTypeConstructor -> copy(dri = this.dri.possiblyAsJava()) + is FunctionalTypeConstructor -> copy(dri = this.dri.possiblyAsJava()) +} private fun String.mapToJava(): ClassId? = JavaToKotlinClassMap.mapKotlinToJava(FqName(this).toUnsafe())