diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt index 9cfa2bfef9..a43a8890f7 100644 --- a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt @@ -232,7 +232,6 @@ internal class DokkaPsiParser( JavaClassKindTypes.INTERFACE ) }).toSourceSetDependent() - val modifiers = getModifier().toSourceSetDependent() val implementedInterfacesExtra = ImplementedInterfaces(ancestry.allImplementedInterfaces().toSourceSetDependent()) @@ -326,6 +325,7 @@ internal class DokkaPsiParser( companion = null, generics = mapTypeParameters(dri), supertypes = ancestors, + modifier = getModifier().toSourceSetDependent(), sourceSets = setOf(sourceSetData), isExpectActual = false, extra = PropertyContainer.withAll( @@ -349,7 +349,7 @@ internal class DokkaPsiParser( supertypes = ancestors, documentation = documentation, expectPresentInSet = null, - modifier = modifiers, + modifier = getModifier().toSourceSetDependent(), sourceSets = setOf(sourceSetData), isExpectActual = false, extra = PropertyContainer.withAll( @@ -598,10 +598,17 @@ internal class DokkaPsiParser( else -> getBound(type) } - private fun PsiModifierListOwner.getModifier() = when { - hasModifier(JvmModifier.ABSTRACT) -> JavaModifier.Abstract - hasModifier(JvmModifier.FINAL) -> JavaModifier.Final - else -> JavaModifier.Empty + private fun PsiModifierListOwner.getModifier(): JavaModifier { + val isInterface = this is PsiClass && this.isInterface + + return if (isInterface) { + // Java interface can't have modality modifiers except for "sealed", which is not supported yet in Dokka + JavaModifier.Empty + } else when { + hasModifier(JvmModifier.ABSTRACT) -> JavaModifier.Abstract + hasModifier(JvmModifier.FINAL) -> JavaModifier.Final + else -> JavaModifier.Empty + } } private fun PsiTypeParameterListOwner.mapTypeParameters(dri: DRI): List { diff --git a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/documentable/KotlinModalityTest.kt b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/documentable/KotlinModalityTest.kt new file mode 100644 index 0000000000..0d05b304fe --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/documentable/KotlinModalityTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.documentable + +import org.jetbrains.dokka.analysis.test.api.kotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.parse +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DInterface +import org.jetbrains.dokka.model.KotlinModifier +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class KotlinModalityTest { + + @Test + fun `should parse kotlin sealed class`() { + val project = kotlinJvmTestProject { + ktFile("Sealed.kt") { + +"sealed class Sealed" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Sealed", classlike.name) + assertTrue(classlike is DClass) + assertEquals(KotlinModifier.Sealed, classlike.modifier.values.single()) + } + + @Test + fun `should parse kotlin abstract class`() { + val project = kotlinJvmTestProject { + ktFile("Abstract.kt") { + +"abstract class Abstract" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Abstract", classlike.name) + assertTrue(classlike is DClass) + assertEquals(KotlinModifier.Abstract, classlike.modifier.values.single()) + } + + @Test + fun `should parse kotlin open class`() { + val project = kotlinJvmTestProject { + ktFile("Open.kt") { + +"open class Open" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Open", classlike.name) + assertTrue(classlike is DClass) + assertEquals(KotlinModifier.Open, classlike.modifier.values.single()) + } + + @Test + fun `should parse kotlin class`() { + val project = kotlinJvmTestProject { + ktFile("Class.kt") { + +"class Class" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Class", classlike.name) + assertTrue(classlike is DClass) + assertEquals(KotlinModifier.Final, classlike.modifier.values.single()) + } + + @Test + fun `should parse kotlin sealed interface`() { + val project = kotlinJvmTestProject { + ktFile("Sealed.kt") { + +"sealed interface Sealed" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Sealed", classlike.name) + assertTrue(classlike is DInterface) + assertEquals(KotlinModifier.Sealed, classlike.modifier.values.single()) + } + + @Test + fun `should parse ordinary kotlin interface`() { + val project = kotlinJvmTestProject { + ktFile("Interface.kt") { + +"interface Interface" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Interface", classlike.name) + assertTrue(classlike is DInterface) + assertEquals(KotlinModifier.Empty, classlike.modifier.values.single()) + } + + @Test + fun `should parse abstract kotlin interface`() { + val project = kotlinJvmTestProject { + ktFile("Interface.kt") { + +"abstract interface Interface" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Interface", classlike.name) + assertTrue(classlike is DInterface) + assertEquals(KotlinModifier.Empty, classlike.modifier.values.single()) + } + +} diff --git a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/JavaModalityTest.kt b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/JavaModalityTest.kt new file mode 100644 index 0000000000..a883be0891 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/JavaModalityTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.jvm.java + +import org.jetbrains.dokka.analysis.test.api.javaTestProject +import org.jetbrains.dokka.analysis.test.api.parse +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DInterface +import org.jetbrains.dokka.model.JavaModifier +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JavaModalityTest { + + @Test + fun `should parse java class`() { + val project = javaTestProject { + javaFile("Class.java") { + +"class Class {}" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Class", classlike.name) + assertTrue(classlike is DClass) + assertEquals(JavaModifier.Empty, classlike.modifier.values.single()) + } + + @Test + fun `should parse java abstract class`() { + val project = javaTestProject { + javaFile("Abstract.java") { + +"abstract class Abstract {}" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Abstract", classlike.name) + assertTrue(classlike is DClass) + assertEquals(JavaModifier.Abstract, classlike.modifier.values.single()) + } + + @Test + fun `should parse java final class`() { + val project = javaTestProject { + javaFile("Final.java") { + +"final class Final {}" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Final", classlike.name) + assertTrue(classlike is DClass) + assertEquals(JavaModifier.Final, classlike.modifier.values.single()) + } + + @Test + fun `should parse java interface`() { + val project = javaTestProject { + javaFile("Interface.java") { + +"interface Interface {}" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Interface", classlike.name) + assertTrue(classlike is DInterface) + assertEquals(JavaModifier.Empty, classlike.modifier.values.single()) + } + + @Test + fun `should parse abstract java interface`() { + val project = javaTestProject { + javaFile("Interface.java") { + +"abstract interface Interface {}" + } + } + + val module = project.parse() + val pkg = module.packages.single() + val classlike = pkg.classlikes.single() + + assertEquals("Interface", classlike.name) + assertTrue(classlike is DInterface) + assertEquals(JavaModifier.Empty, classlike.modifier.values.single()) + } + +} diff --git a/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultDescriptorToDocumentableTranslator.kt b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultDescriptorToDocumentableTranslator.kt index 1eb1165136..1cf44d2a5d 100644 --- a/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultDescriptorToDocumentableTranslator.kt +++ b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultDescriptorToDocumentableTranslator.kt @@ -243,6 +243,7 @@ private class DokkaDescriptorVisitor( documentation = info.docs, generics = generics.await(), companion = descriptor.companion(driWithPlatform), + modifier = descriptor.modifier().toSourceSetDependent(), sourceSets = setOf(sourceSet), isExpectActual = (isExpect || isActual), extra = PropertyContainer.withAll( @@ -1111,12 +1112,24 @@ private class DokkaDescriptorVisitor( objectDescriptor(it, dri) } - private fun MemberDescriptor.modifier() = when (modality) { - Modality.FINAL -> KotlinModifier.Final - Modality.SEALED -> KotlinModifier.Sealed - Modality.OPEN -> KotlinModifier.Open - Modality.ABSTRACT -> KotlinModifier.Abstract - else -> KotlinModifier.Empty + private fun MemberDescriptor.modifier(): KotlinModifier { + val isInterface = this is ClassDescriptor && this.kind == ClassKind.INTERFACE + return if (isInterface) { + // only two modalities are possible for interfaces: + // - `SEALED` - when it's declared as `sealed interface` + // - `ABSTRACT` - when it's declared as `interface` or `abstract interface` (`abstract` is redundant but possible here) + when (modality) { + Modality.SEALED -> KotlinModifier.Sealed + else -> KotlinModifier.Empty + } + } else { + when (modality) { + Modality.FINAL -> KotlinModifier.Final + Modality.SEALED -> KotlinModifier.Sealed + Modality.OPEN -> KotlinModifier.Open + Modality.ABSTRACT -> KotlinModifier.Abstract + } + } } private fun MemberDescriptor.createSources(): SourceSetDependent = diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt index b763434a96..1ac87d233b 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt @@ -301,6 +301,7 @@ internal class DokkaSymbolVisitor( generics = generics, documentation = documentation, companion = companionObject, + modifier = namedClassOrObjectSymbol.getDokkaModality().toSourceSetDependent(), sourceSets = setOf(sourceSet), isExpectActual = (isExpect || isActual), extra = PropertyContainer.withAll( @@ -518,7 +519,7 @@ internal class DokkaSymbolVisitor( setter = propertySymbol.setter?.let { visitPropertyAccessor(it, propertySymbol, dri) }, visibility = propertySymbol.visibility.toDokkaVisibility().toSourceSetDependent(), documentation = getDocumentation(propertySymbol)?.toSourceSetDependent() ?: emptyMap(), // TODO - modifier = propertySymbol.modality.toDokkaModifier().toSourceSetDependent(), + modifier = propertySymbol.getDokkaModality().toSourceSetDependent(), type = toBoundFrom(propertySymbol.returnType), expectPresentInSet = sourceSet.takeIf { isExpect }, sourceSets = setOf(sourceSet), @@ -567,7 +568,7 @@ internal class DokkaSymbolVisitor( setter = null, visibility = javaFieldSymbol.getDokkaVisibility().toSourceSetDependent(), documentation = getDocumentation(javaFieldSymbol)?.toSourceSetDependent() ?: emptyMap(), // TODO - modifier = javaFieldSymbol.modality.toDokkaModifier().toSourceSetDependent(), + modifier = javaFieldSymbol.getDokkaModality().toSourceSetDependent(), type = toBoundFrom(javaFieldSymbol.returnType), expectPresentInSet = sourceSet.takeIf { isExpect }, sourceSets = setOf(sourceSet), @@ -633,7 +634,7 @@ internal class DokkaSymbolVisitor( visibility = propertyAccessorSymbol.visibility.toDokkaVisibility().toSourceSetDependent(), generics = generics, documentation = getDocumentation(propertyAccessorSymbol)?.toSourceSetDependent() ?: emptyMap(), - modifier = propertyAccessorSymbol.modality.toDokkaModifier().toSourceSetDependent(), + modifier = propertyAccessorSymbol.getDokkaModality().toSourceSetDependent(), type = toBoundFrom(propertyAccessorSymbol.returnType), sourceSets = setOf(sourceSet), isExpectActual = (isExpect || isActual), @@ -886,7 +887,25 @@ internal class DokkaSymbolVisitor( // ----------- Translators of modifiers ---------------------------------------------------------------------------- - private fun KtSymbolWithModality.getDokkaModality() = modality.toDokkaModifier() + private fun KtSymbolWithModality.getDokkaModality(): KotlinModifier { + val isInterface = this is KtClassOrObjectSymbol && classKind == KtClassKind.INTERFACE + return if (isInterface) { + // only two modalities are possible for interfaces: + // - `SEALED` - when it's declared as `sealed interface` + // - `ABSTRACT` - when it's declared as `interface` or `abstract interface` (`abstract` is redundant but possible here) + when (modality) { + Modality.SEALED -> KotlinModifier.Sealed + else -> KotlinModifier.Empty + } + } else { + when (modality) { + Modality.FINAL -> KotlinModifier.Final + Modality.SEALED -> KotlinModifier.Sealed + Modality.OPEN -> KotlinModifier.Open + Modality.ABSTRACT -> KotlinModifier.Abstract + } + } + } private fun KtSymbolWithVisibility.getDokkaVisibility() = visibility.toDokkaVisibility() private fun KtValueParameterSymbol.additionalExtras() = listOfNotNull( ExtraModifiers.KotlinOnlyModifiers.NoInline.takeIf { isNoinline }, @@ -933,14 +952,6 @@ internal class DokkaSymbolVisitor( ExtraModifiers.KotlinOnlyModifiers.Fun.takeIf { isFun }, ).toSet().takeUnless { it.isEmpty() } - private fun Modality.toDokkaModifier() = when (this) { - Modality.FINAL -> KotlinModifier.Final - Modality.SEALED -> KotlinModifier.Sealed - Modality.OPEN -> KotlinModifier.Open - Modality.ABSTRACT -> KotlinModifier.Abstract - } - - private fun org.jetbrains.kotlin.descriptors.Visibility.toDokkaVisibility(): Visibility = when (this) { Visibilities.Public -> KotlinVisibility.Public Visibilities.Protected -> KotlinVisibility.Protected diff --git a/dokka-subprojects/core/api/dokka-core.api b/dokka-subprojects/core/api/dokka-core.api index c04d3f2f53..d286915f4a 100644 --- a/dokka-subprojects/core/api/dokka-core.api +++ b/dokka-subprojects/core/api/dokka-core.api @@ -1097,16 +1097,17 @@ public final class org/jetbrains/dokka/model/DFunction : org/jetbrains/dokka/mod public fun withNewExtras (Lorg/jetbrains/dokka/model/properties/PropertyContainer;)Lorg/jetbrains/dokka/model/DFunction; } -public final class org/jetbrains/dokka/model/DInterface : org/jetbrains/dokka/model/DClasslike, org/jetbrains/dokka/model/WithCompanion, org/jetbrains/dokka/model/WithGenerics, org/jetbrains/dokka/model/WithSupertypes, org/jetbrains/dokka/model/properties/WithExtraProperties { - public fun (Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;Ljava/util/Map;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;Lorg/jetbrains/dokka/model/DObject;Ljava/util/List;Ljava/util/Map;Ljava/util/Set;ZLorg/jetbrains/dokka/model/properties/PropertyContainer;)V - public synthetic fun (Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;Ljava/util/Map;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;Lorg/jetbrains/dokka/model/DObject;Ljava/util/List;Ljava/util/Map;Ljava/util/Set;ZLorg/jetbrains/dokka/model/properties/PropertyContainer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class org/jetbrains/dokka/model/DInterface : org/jetbrains/dokka/model/DClasslike, org/jetbrains/dokka/model/WithAbstraction, org/jetbrains/dokka/model/WithCompanion, org/jetbrains/dokka/model/WithGenerics, org/jetbrains/dokka/model/WithSupertypes, org/jetbrains/dokka/model/properties/WithExtraProperties { + public fun (Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;Ljava/util/Map;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;Lorg/jetbrains/dokka/model/DObject;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Ljava/util/Set;ZLorg/jetbrains/dokka/model/properties/PropertyContainer;)V + public synthetic fun (Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;Ljava/util/Map;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;Lorg/jetbrains/dokka/model/DObject;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Ljava/util/Set;ZLorg/jetbrains/dokka/model/properties/PropertyContainer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lorg/jetbrains/dokka/links/DRI; public final fun component10 ()Lorg/jetbrains/dokka/model/DObject; public final fun component11 ()Ljava/util/List; public final fun component12 ()Ljava/util/Map; - public final fun component13 ()Ljava/util/Set; - public final fun component14 ()Z - public final fun component15 ()Lorg/jetbrains/dokka/model/properties/PropertyContainer; + public final fun component13 ()Ljava/util/Map; + public final fun component14 ()Ljava/util/Set; + public final fun component15 ()Z + public final fun component16 ()Lorg/jetbrains/dokka/model/properties/PropertyContainer; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/util/Map; public final fun component4 ()Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet; @@ -1115,8 +1116,8 @@ public final class org/jetbrains/dokka/model/DInterface : org/jetbrains/dokka/mo public final fun component7 ()Ljava/util/List; public final fun component8 ()Ljava/util/List; public final fun component9 ()Ljava/util/Map; - public final fun copy (Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;Ljava/util/Map;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;Lorg/jetbrains/dokka/model/DObject;Ljava/util/List;Ljava/util/Map;Ljava/util/Set;ZLorg/jetbrains/dokka/model/properties/PropertyContainer;)Lorg/jetbrains/dokka/model/DInterface; - public static synthetic fun copy$default (Lorg/jetbrains/dokka/model/DInterface;Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;Ljava/util/Map;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;Lorg/jetbrains/dokka/model/DObject;Ljava/util/List;Ljava/util/Map;Ljava/util/Set;ZLorg/jetbrains/dokka/model/properties/PropertyContainer;ILjava/lang/Object;)Lorg/jetbrains/dokka/model/DInterface; + public final fun copy (Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;Ljava/util/Map;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;Lorg/jetbrains/dokka/model/DObject;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Ljava/util/Set;ZLorg/jetbrains/dokka/model/properties/PropertyContainer;)Lorg/jetbrains/dokka/model/DInterface; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/model/DInterface;Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;Ljava/util/Map;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;Lorg/jetbrains/dokka/model/DObject;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Ljava/util/Set;ZLorg/jetbrains/dokka/model/properties/PropertyContainer;ILjava/lang/Object;)Lorg/jetbrains/dokka/model/DInterface; public fun equals (Ljava/lang/Object;)Z public fun getChildren ()Ljava/util/List; public fun getClasslikes ()Ljava/util/List; @@ -1127,6 +1128,7 @@ public final class org/jetbrains/dokka/model/DInterface : org/jetbrains/dokka/mo public fun getExtra ()Lorg/jetbrains/dokka/model/properties/PropertyContainer; public fun getFunctions ()Ljava/util/List; public fun getGenerics ()Ljava/util/List; + public fun getModifier ()Ljava/util/Map; public fun getName ()Ljava/lang/String; public fun getProperties ()Ljava/util/List; public fun getSourceSets ()Ljava/util/Set; diff --git a/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/model/Documentable.kt b/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/model/Documentable.kt index c6109f47bf..d206d05076 100644 --- a/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/model/Documentable.kt +++ b/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/model/Documentable.kt @@ -244,10 +244,11 @@ public data class DInterface( override val companion: DObject?, override val generics: List, override val supertypes: SourceSetDependent>, + override val modifier: SourceSetDependent, override val sourceSets: Set, override val isExpectActual: Boolean, override val extra: PropertyContainer = PropertyContainer.empty() -) : DClasslike(), WithCompanion, WithGenerics, WithSupertypes, WithExtraProperties { +) : DClasslike(), WithAbstraction, WithCompanion, WithGenerics, WithSupertypes, WithExtraProperties { override val children: List get() = (functions + properties + classlikes) diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureProvider.kt index fda04f82c3..984a1873a5 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureProvider.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureProvider.kt @@ -36,7 +36,7 @@ public class KotlinSignatureProvider( private val contentBuilder = PageContentBuilder(ctcc, this, logger) private val ignoredVisibilities = setOf(JavaVisibility.Public, KotlinVisibility.Public) - private val ignoredModifiers = setOf(JavaModifier.Final, KotlinModifier.Final) + private val ignoredModifiers = setOf(JavaModifier.Final, KotlinModifier.Final, KotlinModifier.Empty) private val ignoredExtraModifiers = setOf( ExtraModifiers.KotlinOnlyModifiers.TailRec, ExtraModifiers.KotlinOnlyModifiers.External @@ -57,6 +57,33 @@ public class KotlinSignatureProvider( ) } + private fun Documentable.isDataClass(sourceSet: DokkaSourceSet): Boolean { + return (this as? DClass) + ?.extra?.get(AdditionalModifiers) + ?.content?.get(sourceSet) + ?.contains(ExtraModifiers.KotlinOnlyModifiers.Data) == true + } + + private fun PageContentBuilder.DocumentableContentBuilder.modifier( + documentable: T, + sourceSet: DokkaSourceSet + ) where T : Documentable, T : WithAbstraction { + val modifier = documentable.modifier[sourceSet] ?: return + + val kotlinModifier = if (modifier == JavaModifier.Empty) { + // java `interface` -> kotlin `interface` + // java `class` -> kotlin `open class` + when (documentable) { + is DInterface -> KotlinModifier.Empty + else -> KotlinModifier.Open + } + } else modifier + + if (kotlinModifier in ignoredModifiers) return + + keyword("${kotlinModifier.name} ") + } + private fun PageContentBuilder.DocumentableContentBuilder.processExtraModifiers(t: T) where T : Documentable, T : WithExtraProperties { sourceSetDependentText( @@ -132,18 +159,8 @@ public class KotlinSignatureProvider( annotationsBlock(c) c.visibility[sourceSet]?.takeIf { it !in ignoredVisibilities && it.name.isNotBlank() }?.name?.let { keyword("$it ") } if (c.isExpectActual) keyword(if (sourceSet == c.expectPresentInSet) "expect " else "actual ") - if (c is DClass) { - val modifier = - if (c.modifier[sourceSet] !in ignoredModifiers) { - when { - c.extra[AdditionalModifiers]?.content?.get(sourceSet)?.contains(ExtraModifiers.KotlinOnlyModifiers.Data) == true -> "" - c.modifier[sourceSet] is JavaModifier.Empty -> "${KotlinModifier.Open.name} " - else -> c.modifier[sourceSet]?.name?.let { "$it " } - } - } else { - null - } - modifier?.takeIf { it.isNotEmpty() }?.let { keyword(it) } + if (c is WithAbstraction && !c.isDataClass(sourceSet)) { + modifier(c, sourceSet) } when (c) { is DClass -> { @@ -245,9 +262,7 @@ public class KotlinSignatureProvider( annotationsBlock(p) p.visibility[sourceSet].takeIf { it !in ignoredVisibilities }?.name?.let { keyword("$it ") } if (p.isExpectActual) keyword(if (sourceSet == p.expectPresentInSet) "expect " else "actual ") - p.modifier[sourceSet].takeIf { it !in ignoredModifiers }?.let { - if (it is JavaModifier.Empty) KotlinModifier.Open else it - }?.name?.let { keyword("$it ") } + modifier(p, sourceSet) p.modifiers()[sourceSet]?.toSignatureString()?.takeIf { it.isNotEmpty() }?.let { keyword(it) } if (p.isMutable()) keyword("var ") else keyword("val ") list(p.generics, prefix = "<", suffix = "> ", @@ -300,9 +315,7 @@ public class KotlinSignatureProvider( if (f.isConstructor) { keyword("constructor") } else { - f.modifier[sourceSet]?.takeIf { it !in ignoredModifiers }?.let { - if (it is JavaModifier.Empty) KotlinModifier.Open else it - }?.name?.let { keyword("$it ") } + modifier(f, sourceSet) f.modifiers()[sourceSet]?.toSignatureString()?.takeIf { it.isNotEmpty() }?.let { keyword(it) } keyword("fun ") list( diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/SignatureTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/SignatureTest.kt index 115c90b140..52705eacd2 100644 --- a/dokka-subprojects/plugin-base/src/test/kotlin/signatures/SignatureTest.kt +++ b/dokka-subprojects/plugin-base/src/test/kotlin/signatures/SignatureTest.kt @@ -5,11 +5,13 @@ package signatures import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl 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.jsoup.nodes.Element import utils.* import kotlin.test.Test import kotlin.test.assertEquals @@ -356,6 +358,201 @@ class SignatureTest : BaseAbstractTest() { } } + @Test + fun `kotlin sealed class should render sealed`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |sealed class Class + """.trimMargin(), + ) { + renderedContent("root/example/-class/index.html").firstSignature().matchIgnoringSpans( + "sealed class", A("Class"), + ) + } + + @Test + fun `kotlin abstract class should render abstract`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |abstract class Class + """.trimMargin() + ) { + renderedContent("root/example/-class/index.html").firstSignature().matchIgnoringSpans( + "abstract class", A("Class"), + ) + } + + @Test + fun `kotlin open class should render open`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |open class Class + """.trimMargin() + ) { + renderedContent("root/example/-class/index.html").firstSignature().matchIgnoringSpans( + "open class", A("Class"), + ) + } + + @Test + fun `kotlin final class should render just class`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |final class Class + """.trimMargin() + ) { + renderedContent("root/example/-class/index.html").firstSignature().matchIgnoringSpans( + "class ", A("Class"), + ) + } + + @Test + fun `kotlin sealed interface should render sealed`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |sealed interface Interface + """.trimMargin() + ) { + renderedContent("root/example/-interface/index.html").firstSignature().matchIgnoringSpans( + "sealed interface", A("Interface"), + ) + } + + @Test + fun `kotlin interface should render just interface`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |interface Interface + """.trimMargin() + ) { + renderedContent("root/example/-interface/index.html").firstSignature().matchIgnoringSpans( + "interface", A("Interface"), + ) + } + + @Test + fun `kotlin abstract interface should render just interface`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |abstract interface Interface + """.trimMargin() + ) { + renderedContent("root/example/-interface/index.html").firstSignature().matchIgnoringSpans( + "interface", A("Interface"), + ) + } + + @Test + fun `kotlin enum should render just enum`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |enum class EnumClass { T } + """.trimMargin() + ) { + renderedContent("root/example/-enum-class/index.html").firstSignature().matchIgnoringSpans( + "enum", A("EnumClass"), ":", A("Enum"), "<", A("EnumClass"), ">" + ) + } + + @Test + fun `kotlin object should render just object`() = testRender( + """ + |/src/main/kotlin/common/Test.kt + |package example + |object Obj + """.trimMargin() + ) { + renderedContent("root/example/-obj/index.html").firstSignature().matchIgnoringSpans( + "object", A("Obj"), + ) + } + + @Test + fun `java class should render open`() = testRender( + """ + |/src/example/Class.java + |package example; + |public class Class {} + """.trimMargin() + ) { + renderedContent("root/example/-class/index.html").firstSignature().matchIgnoringSpans( + "open class", A("Class"), + ) + } + + @Test + fun `java final class should render just class`() = testRender( + """ + |/src/example/Class.java + |package example; + |public final class Class {} + """.trimMargin() + ) { + renderedContent("root/example/-class/index.html").firstSignature().matchIgnoringSpans( + "class", A("Class"), + ) + } + + @Test + fun `java abstract class should render abstract`() = testRender( + """ + |/src/example/Class.java + |package example; + |public abstract class Class {} + """.trimMargin() + ) { + renderedContent("root/example/-class/index.html").firstSignature().matchIgnoringSpans( + "abstract class", A("Class"), + ) + } + + @Test + fun `java interface should render just interface`() = testRender( + """ + |/src/example/Interface.java + |package example; + |public interface Interface {} + """.trimMargin() + ) { + renderedContent("root/example/-interface/index.html").firstSignature().matchIgnoringSpans( + "interface ", A("Interface"), + ) + } + + @Test + fun `java abstract interface should render just interface`() = testRender( + """ + |/src/example/Interface.java + |package example; + |public abstract interface Interface {} + """.trimMargin() + ) { + renderedContent("root/example/-interface/index.html").firstSignature().matchIgnoringSpans( + "interface", A("Interface"), + ) + } + + @Test + fun `java enum should render just enum`() = testRender( + """ + |/src/example/EnumClass.java + |package example; + |public enum EnumClass { T; } + """.trimMargin() + ) { + renderedContent("root/example/-enum-class/index.html").firstSignature().matchIgnoringSpans( + "enum", A("EnumClass"), + ) + } + @Test fun `constructor property on class page`() { val source = source("data class DataClass(val arg: String)") @@ -1132,4 +1329,19 @@ class SignatureTest : BaseAbstractTest() { } } } + + private fun testRender( + query: String, + configuration: DokkaConfigurationImpl = this.configuration, + block: TestOutputWriter.() -> Unit + ) { + val writerPlugin = TestOutputWriterPlugin() + testInline(query, configuration, pluginOverrides = listOf(writerPlugin)) { + renderingStage = { _, _ -> writerPlugin.writer.block() } + } + } + + private fun Element.matchIgnoringSpans(vararg matchers: Any) { + return match(*matchers, ignoreSpanWithTokenStyle = true) + } }