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 b8dabe42b6..9cfa2bfef9 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 @@ -236,6 +236,18 @@ internal class DokkaPsiParser( val implementedInterfacesExtra = ImplementedInterfaces(ancestry.allImplementedInterfaces().toSourceSetDependent()) + // used only for class and enum + val innerModifierExtra = when { + // top level java classes - no `inner` + psi.containingClass == null -> null + // java `static class` = kotlin `class` + psi.hasModifier(JvmModifier.STATIC) -> null + // java `class` = kotlin `inner class` + else -> setOf( + ExtraModifiers.KotlinOnlyModifiers.Inner + ).toSourceSetDependent().toAdditionalModifiers() + } + when { isAnnotationType -> DAnnotation( @@ -295,6 +307,7 @@ internal class DokkaPsiParser( isExpectActual = false, extra = PropertyContainer.withAll( implementedInterfacesExtra, + innerModifierExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations() ) @@ -341,6 +354,7 @@ internal class DokkaPsiParser( isExpectActual = false, extra = PropertyContainer.withAll( implementedInterfacesExtra, + innerModifierExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() .toAnnotations(), ancestry.exceptionInSupertypesOrNull() diff --git a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/InnerModifierJavaAnalysisTest.kt b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/InnerModifierJavaAnalysisTest.kt new file mode 100644 index 0000000000..a4561b7b4d --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/InnerModifierJavaAnalysisTest.kt @@ -0,0 +1,293 @@ +/* + * 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.* +import org.jetbrains.dokka.model.properties.WithExtraProperties +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class InnerModifierJavaAnalysisTest { + + @Test + fun `top level java declarations should not have kotlin inner modifier`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaClass.java") { + +"public class JavaClass { }" + } + javaFile(pathFromSrc = "JavaEnum.java") { + +"public enum JavaEnum { S; }" + } + javaFile(pathFromSrc = "JavaInterface.java") { + +"public interface JavaInterface { }" + } + javaFile(pathFromSrc = "JavaAnnotation.java") { + +"public @interface JavaAnnotation { }" + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + + assertEquals(4, pkg.classlikes.size) + + pkg.classlikes.forEach { + @Suppress("UNCHECKED_CAST") + assertTrue((it as WithExtraProperties).kotlinOnlyModifiers().isEmpty()) + } + } + + @Test + fun `java declarations nested inside interfaces should not have kotlin inner modifier`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaInterface.java") { + +""" + public interface JavaInterface { + public class InnerClass { } + public static class NestedClass { } + public enum InnerEnum {S} + public static enum NestedEnum {S} + public interface InnerInterface { } + public static interface NestedInterface { } + public @interface InnerAnnotation { } + public static @interface NestedAnnotation { } + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val javaInterface = pkg.classlikes.single() + + assertTrue(javaInterface is DInterface) + assertEquals(8, javaInterface.classlikes.size) + + javaInterface.classlikes.forEach { + @Suppress("UNCHECKED_CAST") + assertTrue((it as WithExtraProperties).kotlinOnlyModifiers().isEmpty()) + } + } + + @Test + fun `java declarations nested inside annotation interface should not have kotlin inner modifier`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaAnnotation.java") { + +""" + public @interface JavaAnnotation { + public class InnerClass { } + public static class NestedClass { } + public enum InnerEnum {S} + public static enum NestedEnum {S} + public interface InnerInterface { } + public static interface NestedInterface { } + public @interface InnerAnnotation { } + public static @interface NestedAnnotation { } + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val javaAnnotation = pkg.classlikes.single() + + assertTrue(javaAnnotation is DAnnotation) + assertEquals(8, javaAnnotation.classlikes.size) + + javaAnnotation.classlikes.forEach { + @Suppress("UNCHECKED_CAST") + assertTrue((it as WithExtraProperties).kotlinOnlyModifiers().isEmpty()) + } + } + + // java classes tests + + @Test + fun `java class nested inside class should have kotlin inner modifiers`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaClass.java") { + +""" + public class JavaClass { + public class NestedClass { } + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val javaClass = pkg.classlikes.single() + + assertTrue(javaClass is DClass) + assertEquals("JavaClass", javaClass.name) + + val nestedClass = javaClass.classlikes.single() + + assertTrue(nestedClass is DClass) + assertEquals("NestedClass", nestedClass.name) + + assertEquals( + setOf(ExtraModifiers.KotlinOnlyModifiers.Inner), + nestedClass.kotlinOnlyModifiers().values.single() + ) + } + + @Test + fun `static java class nested inside class should not have kotlin inner modifiers`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaClass.java") { + +""" + public class JavaClass { + public static class NestedClass { } + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val javaClass = pkg.classlikes.single() + + assertTrue(javaClass is DClass) + assertEquals("JavaClass", javaClass.name) + + val nestedClass = javaClass.classlikes.single() + + assertTrue(nestedClass is DClass) + assertEquals("NestedClass", nestedClass.name) + + assertTrue(nestedClass.kotlinOnlyModifiers().isEmpty()) + } + + @Test + fun `java non-classes nested inside class should not have kotlin inner modifier`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaClass.java") { + +""" + public class JavaClass { + public enum InnerEnum {S} + public static enum NestedEnum {S} + public interface InnerInterface { } + public static interface NestedInterface { } + public @interface InnerAnnotation { } + public static @interface NestedAnnotation { } + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val javaClass = pkg.classlikes.single() + + assertTrue(javaClass is DClass) + assertEquals(6, javaClass.classlikes.size) + + javaClass.classlikes.forEach { + @Suppress("UNCHECKED_CAST") + assertTrue((it as WithExtraProperties).kotlinOnlyModifiers().isEmpty()) + } + } + + // java enums tests + + @Test + fun `static java class nested inside enum should not have kotlin inner modifiers`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaEnum.java") { + +""" + public enum JavaEnum { S; + public static class NestedClass { } + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val javaEnum = pkg.classlikes.single() + + assertTrue(javaEnum is DEnum) + assertEquals("JavaEnum", javaEnum.name) + + val nestedClass = javaEnum.classlikes.single() + + assertTrue(nestedClass is DClass) + assertEquals("NestedClass", nestedClass.name) + + assertTrue(nestedClass.kotlinOnlyModifiers().isEmpty()) + } + + @Test + fun `java class nested inside enum should have kotlin inner modifiers`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaEnum.java") { + +""" + public enum JavaEnum { S; + public class NestedClass { } + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val javaEnum = pkg.classlikes.single() + + assertTrue(javaEnum is DEnum) + assertEquals("JavaEnum", javaEnum.name) + + val nestedClass = javaEnum.classlikes.single() + + assertTrue(nestedClass is DClass) + assertEquals("NestedClass", nestedClass.name) + + assertEquals( + setOf(ExtraModifiers.KotlinOnlyModifiers.Inner), + nestedClass.kotlinOnlyModifiers().values.single() + ) + } + + @Test + fun `java non-classes nested inside enum should not have kotlin inner modifier`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "JavaEnum.java") { + +""" + public enum JavaEnum { S; + public enum InnerEnum {S} + public static enum NestedEnum {S} + public interface InnerInterface { } + public static interface NestedInterface { } + public @interface InnerAnnotation { } + public static @interface NestedAnnotation { } + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val javaEnum = pkg.classlikes.single() + + assertTrue(javaEnum is DEnum) + assertEquals(6, javaEnum.classlikes.size) + + javaEnum.classlikes.forEach { + @Suppress("UNCHECKED_CAST") + assertTrue((it as WithExtraProperties).kotlinOnlyModifiers().isEmpty()) + } + } + + // copied from org.jetbrains.dokka.base.signatures.KotlinSignatureUtils + private fun WithExtraProperties.kotlinOnlyModifiers(): SourceSetDependent> { + return extra[AdditionalModifiers]?.content?.entries?.associate { + it.key to it.value.filterIsInstance().toSet() + } ?: emptyMap() + } +} 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 4d5611891f..04b2cb2068 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 @@ -9,6 +9,7 @@ import com.intellij.psi.PsiNamedElement import com.intellij.psi.util.PsiLiteralUtil.* import kotlinx.coroutines.* import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.Platform import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin import org.jetbrains.dokka.analysis.java.parsers.JavadocParser import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin @@ -98,11 +99,13 @@ internal class DefaultDescriptorToDocumentableTranslator( .mapNotNull { analysisContext.resolveSession.getPackageFragment(it) } .toList() - val javadocParser = JavadocParser( - docCommentParsers = context.plugin().query { docCommentParsers }, - docCommentFinder = context.plugin().docCommentFinder - ) - + val javadocParser = + if (sourceSet.analysisPlatform == Platform.jvm) + JavadocParser( + docCommentParsers = context.plugin().query { docCommentParsers }, + docCommentFinder = context.plugin().docCommentFinder + ) + else null return DokkaDescriptorVisitor(sourceSet, kdocFinder, kotlinAnalysis[sourceSet], context.logger, javadocParser).run { packageFragments.parallelMap { @@ -128,10 +131,13 @@ internal class DefaultDescriptorToDocumentableTranslator( override fun translateClassDescriptor(descriptor: ClassDescriptor, sourceSet: DokkaSourceSet): DClasslike { val driInfo = DRI.from(descriptor.parents.first()).withEmptyInfo() - val javadocParser = JavadocParser( - docCommentParsers = context.plugin().query { docCommentParsers }, - docCommentFinder = context.plugin().docCommentFinder - ) + val javadocParser = + if (sourceSet.analysisPlatform == Platform.jvm) + JavadocParser( + docCommentParsers = context.plugin().query { docCommentParsers }, + docCommentFinder = context.plugin().docCommentFinder + ) + else null return newSingleThreadContext("Generating documentable model of classlike").use { coroutineContext -> // see https://github.com/Kotlin/dokka/issues/3151 runBlocking(coroutineContext) { @@ -149,12 +155,15 @@ internal data class DRIWithPlatformInfo( internal fun DRI.withEmptyInfo() = DRIWithPlatformInfo(this, emptyMap()) +/** + * @param javadocParser can be null for non JVM platform + */ private class DokkaDescriptorVisitor( private val sourceSet: DokkaSourceSet, private val kDocFinder: KDocFinder, private val analysisContext: AnalysisContext, private val logger: DokkaLogger, - private val javadocParser: JavadocParser + private val javadocParser: JavadocParser? = null ) { private val syntheticDocProvider = SyntheticDescriptorDocumentationProvider(kDocFinder, sourceSet) @@ -1080,16 +1089,16 @@ private class DokkaDescriptorVisitor( else it } ) - } ?: getJavaDocs())?.takeIf { it.children.isNotEmpty() } + } ?: javadocParser?.getJavaDocs(this))?.takeIf { it.children.isNotEmpty() } } - private fun DeclarationDescriptor.getJavaDocs(): DocumentationNode? { - val overriddenDescriptors = (this as? CallableDescriptor)?.overriddenDescriptors ?: emptyList() - val allDescriptors = overriddenDescriptors + listOf(this) + private fun JavadocParser.getJavaDocs(declarationDescriptor: DeclarationDescriptor): DocumentationNode? { + val overriddenDescriptors = (declarationDescriptor as? CallableDescriptor)?.overriddenDescriptors ?: emptyList() + val allDescriptors = overriddenDescriptors + listOf(declarationDescriptor) return allDescriptors .mapNotNull { it.findPsi() as? PsiNamedElement } .firstOrNull() - ?.let { javadocParser.parseDocumentation(it) } + ?.let { parseDocumentation(it) } } private suspend fun ClassDescriptor.companion(dri: DRIWithPlatformInfo): DObject? = companionObjectDescriptor?.let { diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt index e1c9f4799f..4bcee7e8f7 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt @@ -5,6 +5,9 @@ package org.jetbrains.dokka.analysis.kotlin.symbols.services import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.java.parsers.JavadocParser import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin import org.jetbrains.dokka.analysis.kotlin.symbols.translators.DokkaSymbolVisitor import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getClassIdFromDRI @@ -16,6 +19,7 @@ import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.querySingle import org.jetbrains.kotlin.analysis.api.analyze import org.jetbrains.kotlin.analysis.api.symbols.KtNamedClassOrObjectSymbol +import org.jetbrains.dokka.plugability.query import org.jetbrains.dokka.analysis.kotlin.documentable.ExternalDocumentableProvider internal class SymbolExternalDocumentablesProvider(val context: DokkaContext) : ExternalDocumentableProvider { @@ -26,7 +30,14 @@ internal class SymbolExternalDocumentablesProvider(val context: DokkaContext) : return analyze(kotlinAnalysis.getModule(sourceSet)) { val symbol = getClassOrObjectSymbolByClassId(classId) as? KtNamedClassOrObjectSymbol?: return@analyze null - val translator = DokkaSymbolVisitor(sourceSet, sourceSet.displayName, kotlinAnalysis, logger = context.logger) + val javadocParser = + if (sourceSet.analysisPlatform == Platform.jvm) + JavadocParser( + docCommentParsers = context.plugin().query { docCommentParsers }, + docCommentFinder = context.plugin().docCommentFinder + ) + else null + val translator = DokkaSymbolVisitor(sourceSet, sourceSet.displayName, kotlinAnalysis, logger = context.logger, javadocParser) val parentDRI = symbol.getContainingSymbol()?.let { getDRIFromSymbol(it) } ?: /* top level */ DRI(dri.packageName) with(translator) { 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 298d0182ca..9a17f831ab 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 @@ -8,6 +8,7 @@ package org.jetbrains.dokka.analysis.kotlin.symbols.translators import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.* import com.intellij.psi.util.PsiLiteralUtil import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.Platform import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin import org.jetbrains.dokka.analysis.java.parsers.JavadocParser import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.getGeneratedKDocDocumentationFrom @@ -65,7 +66,7 @@ internal class DefaultSymbolToDocumentableTranslator(context: DokkaContext) : As moduleName = context.configuration.moduleName, analysisContext = analysisContext, logger = context.logger, - javadocParser = javadocParser + javadocParser = if(sourceSet.analysisPlatform == Platform.jvm) javadocParser else null ).visitModule() } } @@ -79,6 +80,8 @@ internal fun T.wrapWithVariance(variance: org.jetbrains.kotlin.types /** * Maps [KtSymbol] to Documentable model [Documentable] + * + * @param javadocParser can be null for non JVM platform */ internal class DokkaSymbolVisitor( private val sourceSet: DokkaConfiguration.DokkaSourceSet, diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt index 9b646f241c..c8a18788f7 100644 --- a/dokka-subprojects/plugin-base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt @@ -4,6 +4,7 @@ package model +import org.jetbrains.dokka.Platform import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.links.PointingToDeclaration @@ -26,6 +27,48 @@ class MultiLanguageInheritanceTest : BaseAbstractTest() { } } + @Test + fun `should not try to find javadoc for non JVM source set`() { + // Bug #3209 is actual for Dokka K1 + // Technical note: `KtPropertyAccessor`, i.e. ``, is not KtCallableDeclaration so `findKDoc` returns null + // Meanwhile, `getJavaDocs()` for KtPropertyAccessor tries to unexpectedly parse the KDoc documentation of property, i.e. `withHintType` + + val nonJvmConfiguration = dokkaConfiguration { + suppressObviousFunctions = false + sourceSets { + sourceSet { + analysisPlatform = Platform.common.key + sourceRoots = listOf("src/main/kotlin") + } + } + } + + testInline( + """ + |/src/main/kotlin/sample/Parent.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |interface Parent { + | /** + | * Sample description from parent + | */ + | val withHintType: String + |} + | + |/src/main/kotlin/sample/Child.kt + |package sample + |public class Child : Parent { + | override val withHintType: String + | get() = "" + |} + """.trimIndent(), + nonJvmConfiguration + ) { } + } + @Test fun `from java to kotlin`() { testInline(