diff --git a/gradle.properties b/gradle.properties index 346b5a8008..565ff4e0af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ dokka_publication_channels=bintray-kotlin-dev&space-dokka-dev dokka_integration_test_parallelism=2 # Versions kotlin_version=1.5.0 -coroutines_version=1.4.3 +coroutines_version=1.5.0 kotlinx_html_version=0.7.3 kotlin_plugin_version=202-1.5.0-release-755-release-IJ8194.7 idea_version=202.7660.26 diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index e0fc312adb..bfb83e6e36 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -115,7 +115,8 @@ public class org/jetbrains/dokka/base/parsers/MarkdownParser : org/jetbrains/dok public final class org/jetbrains/dokka/base/parsers/MarkdownParser$Companion { public final fun fqName (Lorg/jetbrains/dokka/links/DRI;)Ljava/lang/String; - public final fun parseFromKDocTag (Lorg/jetbrains/kotlin/kdoc/psi/impl/KDocTag;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)Lorg/jetbrains/dokka/model/doc/DocumentationNode; + public final fun parseFromKDocTag (Lorg/jetbrains/kotlin/kdoc/psi/impl/KDocTag;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Z)Lorg/jetbrains/dokka/model/doc/DocumentationNode; + public static synthetic fun parseFromKDocTag$default (Lorg/jetbrains/dokka/base/parsers/MarkdownParser$Companion;Lorg/jetbrains/kotlin/kdoc/psi/impl/KDocTag;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ZILjava/lang/Object;)Lorg/jetbrains/dokka/model/doc/DocumentationNode; } public abstract class org/jetbrains/dokka/base/parsers/Parser { @@ -1349,7 +1350,7 @@ public final class org/jetbrains/dokka/base/translators/psi/DefaultPsiToDocument } public final class org/jetbrains/dokka/base/translators/psi/DefaultPsiToDocumentableTranslator$DokkaPsiParser { - public fun (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Lorg/jetbrains/dokka/utilities/DokkaLogger;)V + public fun (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Lorg/jetbrains/dokka/analysis/DokkaResolutionFacade;Lorg/jetbrains/dokka/utilities/DokkaLogger;)V public final fun parsePackage (Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -1359,7 +1360,7 @@ public abstract interface class org/jetbrains/dokka/base/translators/psi/parsers public final class org/jetbrains/dokka/base/translators/psi/parsers/JavadocParser : org/jetbrains/dokka/base/translators/psi/parsers/JavaDocumentationParser { public static final field Companion Lorg/jetbrains/dokka/base/translators/psi/parsers/JavadocParser$Companion; - public fun (Lorg/jetbrains/dokka/utilities/DokkaLogger;)V + public fun (Lorg/jetbrains/dokka/utilities/DokkaLogger;Lorg/jetbrains/dokka/analysis/DokkaResolutionFacade;)V public fun parseDocumentation (Lcom/intellij/psi/PsiNamedElement;)Lorg/jetbrains/dokka/model/doc/DocumentationNode; } diff --git a/plugins/base/build.gradle.kts b/plugins/base/build.gradle.kts index 1281ac3f01..c2210258f0 100644 --- a/plugins/base/build.gradle.kts +++ b/plugins/base/build.gradle.kts @@ -1,8 +1,5 @@ import org.jetbrains.registerDokkaArtifactPublication -plugins { - id("com.jfrog.bintray") -} dependencies { val coroutines_version: String by project diff --git a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt index 80a9e508f3..34dceeea95 100644 --- a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt +++ b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt @@ -504,6 +504,7 @@ open class MarkdownParser( kDocTag: KDocTag?, externalDri: (String) -> DRI?, kdocLocation: String?, + parseWithChildren: Boolean = true ): DocumentationNode { return if (kDocTag == null) { DocumentationNode(emptyList()) @@ -517,7 +518,7 @@ open class MarkdownParser( } val allTags = - listOf(kDocTag) + if (kDocTag.canHaveParent()) getAllKDocTags(findParent(kDocTag)) else emptyList() + listOf(kDocTag) + if (kDocTag.canHaveParent() && parseWithChildren) getAllKDocTags(findParent(kDocTag)) else emptyList() DocumentationNode( allTags.map { when (it.knownTag) { diff --git a/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt b/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt index 4eac9d3f45..6446a77576 100644 --- a/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt +++ b/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt @@ -123,7 +123,7 @@ class KotlinSignatureProvider(ctcc: CommentsToContentConverter, logger: DokkaLog text( if (c.modifier[sourceSet] !in ignoredModifiers) when { - c.extra[AdditionalModifiers]?.content?.contains(ExtraModifiers.KotlinOnlyModifiers.Data) == true -> "" + 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 " } ?: "" } diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt index a8fabc9573..f80cb6df64 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt @@ -159,6 +159,6 @@ internal class ReportUndocumentedTransformer : DocumentableTransformer { val packageName = documentable.dri.packageName ?: return null return dokkaSourceSet.perPackageOptions .filter { packageOptions -> Regex(packageOptions.matchingRegex).matches(packageName) } - .maxBy { packageOptions -> packageOptions.matchingRegex.length } + .maxByOrNull { packageOptions -> packageOptions.matchingRegex.length } } } diff --git a/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt index a391b534fa..cadd3de059 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt @@ -13,7 +13,7 @@ class DefaultSamplesTransformer(context: DokkaContext) : SamplesTransformer(cont override fun processBody(psiElement: PsiElement): String { val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() val lines = text.split("\n") - val indent = lines.filter(String::isNotBlank).map { it.takeWhile(Char::isWhitespace).count() }.min() ?: 0 + val indent = lines.filter(String::isNotBlank).map { it.takeWhile(Char::isWhitespace).count() }.minOrNull() ?: 0 return lines.joinToString("\n") { it.drop(indent) } } diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt index 92ffd9b67b..8bb8b527cb 100644 --- a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -1,7 +1,9 @@ package org.jetbrains.dokka.base.translators.descriptors +import com.intellij.psi.PsiNamedElement import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet import org.jetbrains.dokka.analysis.DescriptorDocumentableSource import org.jetbrains.dokka.analysis.DokkaResolutionFacade @@ -10,6 +12,7 @@ import org.jetbrains.dokka.analysis.from import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.parsers.MarkdownParser import org.jetbrains.dokka.base.translators.isDirectlyAnException +import org.jetbrains.dokka.base.translators.psi.parsers.JavadocParser import org.jetbrains.dokka.base.translators.unquotedValue import org.jetbrains.dokka.links.* import org.jetbrains.dokka.links.Callable @@ -38,6 +41,7 @@ import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor import org.jetbrains.kotlin.idea.core.getDirectlyOverriddenDeclarations import org.jetbrains.kotlin.idea.kdoc.findKDoc import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi import org.jetbrains.kotlin.load.kotlin.toSourceElement import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.* @@ -58,6 +62,7 @@ import org.jetbrains.kotlin.types.* import org.jetbrains.kotlin.types.typeUtil.immediateSupertypes import org.jetbrains.kotlin.types.typeUtil.isAnyOrNullableAny import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult import org.jetbrains.kotlin.utils.addToStdlib.safeAs import java.nio.file.Paths import org.jetbrains.kotlin.resolve.constants.AnnotationValue as ConstantsAnnotationValue @@ -110,6 +115,8 @@ private class DokkaDescriptorVisitor( private val resolutionFacade: DokkaResolutionFacade, private val logger: DokkaLogger ) { + private val javadocParser = JavadocParser(logger, resolutionFacade) + private fun Collection.filterDescriptorsInSourceSet() = filter { it.toSourceElement.containingFile.toString().let { path -> path.isNotBlank() && sourceSet.sourceRoots.any { root -> @@ -848,7 +855,7 @@ private class DokkaDescriptorVisitor( org.jetbrains.kotlin.types.Variance.OUT_VARIANCE -> Covariance(this) } - private fun DeclarationDescriptor.getDocumentation() = findKDoc().let { + private fun DeclarationDescriptor.getDocumentation() = (findKDoc()?.let { MarkdownParser.parseFromKDocTag( kDocTag = it, externalDri = { link: String -> @@ -871,7 +878,12 @@ private class DokkaDescriptorVisitor( else it } ) - }.takeIf { it.children.isNotEmpty() } + } ?: getJavaDocs())?.takeIf { it.children.isNotEmpty() } + + private fun DeclarationDescriptor.getJavaDocs() = (this as? CallableDescriptor) + ?.overriddenDescriptors + ?.mapNotNull { it.findPsi() as? PsiNamedElement } + ?.firstNotNullResult { javadocParser.parseDocumentation(it) } private suspend fun ClassDescriptor.companion(dri: DRIWithPlatformInfo): DObject? = companionObjectDescriptor?.let { objectDescriptor(it, dri) diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index f728c8a7af..be7b826b97 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -12,6 +12,7 @@ import com.intellij.psi.impl.source.PsiImmediateClassType import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.DokkaResolutionFacade import org.jetbrains.dokka.analysis.KotlinAnalysis import org.jetbrains.dokka.analysis.PsiDocumentableSource import org.jetbrains.dokka.analysis.from @@ -46,6 +47,7 @@ import org.jetbrains.kotlin.descriptors.Visibilities import org.jetbrains.kotlin.descriptors.java.JavaVisibilities import org.jetbrains.kotlin.idea.caches.resolve.util.getJavaClassDescriptor import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade import org.jetbrains.kotlin.load.java.JvmAbi import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName @@ -69,7 +71,7 @@ class DefaultPsiToDocumentableTranslator( sourceSet.sourceRoots.any { root -> file.startsWith(root) } - val (environment, _) = kotlinAnalysis[sourceSet] + val (environment, facade) = kotlinAnalysis[sourceSet] val sourceRoots = environment.configuration.get(CLIConfigurationKeys.CONTENT_ROOTS) ?.filterIsInstance() @@ -88,6 +90,7 @@ class DefaultPsiToDocumentableTranslator( val docParser = DokkaPsiParser( sourceSet, + facade, context.logger ) @@ -106,10 +109,11 @@ class DefaultPsiToDocumentableTranslator( class DokkaPsiParser( private val sourceSetData: DokkaSourceSet, + facade: DokkaResolutionFacade, private val logger: DokkaLogger ) { - private val javadocParser: JavaDocumentationParser = JavadocParser(logger) + private val javadocParser: JavaDocumentationParser = JavadocParser(logger, facade) private val cachedBounds = hashMapOf() @@ -207,6 +211,7 @@ class DefaultPsiToDocumentableTranslator( } parseSupertypes(superTypes) val (regularFunctions, accessors) = splitFunctionsAndAccessors() + val overriden = regularFunctions.flatMap { it.findSuperMethods().toList() } val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent() val allFunctions = async { regularFunctions.parallelMapNotNull { @@ -214,8 +219,7 @@ class DefaultPsiToDocumentableTranslator( it, parentDRI = dri ) else null - } + - superMethods.parallelMap { parseFunction(it.first, inheritedFrom = it.second) } + } + superMethods.filter { it.first !in overriden }.parallelMap { parseFunction(it.first, inheritedFrom = it.second) } } val source = PsiDocumentableSource(this).toSourceSetDependent() val classlikes = async { innerClasses.asIterable().parallelMap { parseClasslike(it, dri) } } diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt index 21c2c72a14..67d0a7185d 100644 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt @@ -7,6 +7,7 @@ import com.intellij.psi.javadoc.PsiDocComment import com.intellij.psi.javadoc.PsiDocTag import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName +import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull internal data class CommentResolutionContext( @@ -21,21 +22,34 @@ internal class InheritDocResolver( ) { internal fun resolveFromContext(context: CommentResolutionContext) = when (context.tag) { - JavadocTag.THROWS, JavadocTag.EXCEPTION -> context.name?.let { name -> resolveThrowsTag(context.tag, context.comment, name) } - JavadocTag.PARAM -> context.parameterIndex?.let { paramIndex -> resolveParamTag(context.comment, paramIndex) } + JavadocTag.THROWS, JavadocTag.EXCEPTION -> context.name?.let { name -> + resolveThrowsTag( + context.tag, + context.comment, + name + ) + } + JavadocTag.PARAM -> context.parameterIndex?.let { paramIndex -> + resolveParamTag( + context.comment, + paramIndex + ) + } JavadocTag.DEPRECATED -> resolveGenericTag(context.comment, JavadocTag.DESCRIPTION) JavadocTag.SEE -> emptyList() else -> context.tag?.let { tag -> resolveGenericTag(context.comment, tag) } } - private fun resolveGenericTag(currentElement: PsiDocComment, tag: JavadocTag): List = + private fun resolveGenericTag(currentElement: PsiDocComment, tag: JavadocTag) = when (val owner = currentElement.owner) { is PsiClass -> lowestClassWithTag(owner, tag) is PsiMethod -> lowestMethodWithTag(owner, tag) else -> null }?.tagsByName(tag)?.flatMap { - when (it) { - is PsiDocTag -> it.contentElementsWithSiblingIfNeeded() + when { + it is PsiDocumentationContent && it.psiElement is PsiDocTag -> + it.psiElement.contentElementsWithSiblingIfNeeded() + .map { content -> PsiDocumentationContent(content, it.tag) } else -> listOf(it) } }.orEmpty() @@ -49,58 +63,70 @@ internal class InheritDocResolver( tag: JavadocTag, currentElement: PsiDocComment, exceptionFqName: String - ): List = - (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, tag) } + ): List { + val closestDocs = (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, tag) } .orEmpty().firstOrNull { findClosestDocComment(it, logger)?.hasTagWithExceptionOfType(tag, exceptionFqName) == true - }?.docComment?.tagsByName(tag)?.flatMap { + } + + return when (closestDocs?.language?.id) { + "kotlin" -> closestDocs.toKdocComment()?.tagsByName(tag, exceptionFqName).orEmpty() + else -> closestDocs?.docComment?.tagsByName(tag)?.flatMap { when (it) { is PsiDocTag -> it.contentElementsWithSiblingIfNeeded() else -> listOf(it) } - }?.withoutReferenceLink().orEmpty() + }?.withoutReferenceLink().orEmpty().map { PsiDocumentationContent(it, tag) } + } + } private fun resolveParamTag( currentElement: PsiDocComment, parameterIndex: Int, - ): List = + ): List = (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, JavadocTag.PARAM) } .orEmpty().flatMap { if (parameterIndex >= it.parameterList.parametersCount || parameterIndex < 0) emptyList() else { val closestTag = findClosestDocComment(it, logger) val hasTag = closestTag?.hasTag(JavadocTag.PARAM) - if (hasTag != true) emptyList() - else { - val parameterName = it.parameterList.parameters[parameterIndex].name - closestTag.tagsByName(JavadocTag.PARAM) - .filterIsInstance().map { it.contentElementsWithSiblingIfNeeded() }.firstOrNull { - it.firstOrNull()?.text == parameterName - }.orEmpty() + when { + hasTag != true -> emptyList() + closestTag is JavaDocComment -> resolveJavaParamTag(closestTag, parameterIndex, it) + .withoutReferenceLink().map { PsiDocumentationContent(it, JavadocTag.PARAM) } + closestTag is KotlinDocComment -> resolveKdocTag(closestTag, parameterIndex) + else -> emptyList() } } - }.withoutReferenceLink() + } + + private fun resolveJavaParamTag(comment: JavaDocComment, parameterIndex: Int, method: PsiMethod) = + comment.comment.tagsByName(JavadocTag.PARAM) + .filterIsInstance().map { it.contentElementsWithSiblingIfNeeded() }.firstOrNull { + it.firstOrNull()?.text == method.parameterList.parameters[parameterIndex].name + }.orEmpty() + + private fun resolveKdocTag(comment: KotlinDocComment, parameterIndex: Int): List = + listOf(comment.tagsByName(JavadocTag.PARAM)[parameterIndex]) //if we are in psi class javadoc only inherits docs from classes and not from interfaces - private fun lowestClassWithTag(baseClass: PsiClass, javadocTag: JavadocTag): PsiDocComment? = + private fun lowestClassWithTag(baseClass: PsiClass, javadocTag: JavadocTag): DocComment? = baseClass.superClass?.let { - findClosestDocComment(it, logger)?.takeIf { tag -> tag.hasTag(javadocTag) } ?: - lowestClassWithTag(it, javadocTag) + findClosestDocComment(it, logger)?.takeIf { tag -> tag.hasTag(javadocTag) } ?: lowestClassWithTag( + it, + javadocTag + ) } private fun lowestMethodWithTag( baseMethod: PsiMethod, javadocTag: JavadocTag, - ): PsiDocComment? = - lowestMethodsWithTag(baseMethod, javadocTag).firstOrNull()?.docComment + ): DocComment? = + lowestMethodsWithTag(baseMethod, javadocTag).firstOrNull() + ?.let { it.docComment?.let { JavaDocComment(it) } ?: it.toKdocComment() } private fun lowestMethodsWithTag(baseMethod: PsiMethod, javadocTag: JavadocTag) = baseMethod.findSuperMethods().filter { findClosestDocComment(it, logger)?.hasTag(javadocTag) == true } - private fun PsiDocComment.hasTagWithExceptionOfType(tag: JavadocTag, exceptionFqName: String): Boolean = - hasTag(tag) && tagsByName(tag).firstIsInstanceOrNull() - ?.resolveToElement() - ?.getKotlinFqName()?.asString() == exceptionFqName - private fun List.withoutReferenceLink(): List = drop(1) } \ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt index dc93568f14..c5f5541ff8 100644 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt @@ -8,13 +8,15 @@ import com.intellij.psi.impl.source.tree.LazyParseablePsiElement import com.intellij.psi.impl.source.tree.LeafPsiElement import com.intellij.psi.javadoc.* import org.intellij.markdown.MarkdownElementTypes +import org.jetbrains.dokka.analysis.DokkaResolutionFacade import org.jetbrains.dokka.analysis.from +import org.jetbrains.dokka.base.parsers.MarkdownParser import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.doc.* import org.jetbrains.dokka.model.doc.Deprecated -import org.jetbrains.dokka.model.doc.Suppress import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.dokka.utilities.enumValueOrNull +import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName import org.jetbrains.kotlin.idea.util.CommentSaver.Companion.tokenType import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace @@ -29,18 +31,49 @@ interface JavaDocumentationParser { } class JavadocParser( - private val logger: DokkaLogger + private val logger: DokkaLogger, + private val resolutionFacade: DokkaResolutionFacade, ) : JavaDocumentationParser { private val inheritDocResolver = InheritDocResolver(logger) + private var inheritDocSection: DocumentationNode? = null override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { - val docComment = findClosestDocComment(element, logger) ?: return DocumentationNode(emptyList()) + return when(val comment = findClosestDocComment(element, logger)){ + is JavaDocComment -> parseDocumentation(comment, element) + is KotlinDocComment -> parseDocumentation(comment) + else -> DocumentationNode(emptyList()) + } + } + + private fun parseDocumentation(element: JavaDocComment, context: PsiNamedElement): DocumentationNode { + val docComment = element.comment val nodes = listOfNotNull(docComment.getDescription()) + docComment.tags.mapNotNull { tag -> - parseDocTag(tag, docComment, element) + parseDocTag(tag, docComment, context) } return DocumentationNode(nodes) } + private fun parseDocumentation(element: KotlinDocComment, parseWithChildren: Boolean = true): DocumentationNode = + MarkdownParser.parseFromKDocTag( + kDocTag = element.comment, + externalDri = { link: String -> + try { + resolveKDocLink( + context = resolutionFacade.resolveSession.bindingContext, + resolutionFacade = resolutionFacade, + fromDescriptor = element.descriptor, + fromSubjectOfTag = null, + qualifiedName = link.split('.') + ).firstOrNull()?.let { DRI.from(it) } + } catch (e1: IllegalArgumentException) { + logger.warn("Couldn't resolve link for $link") + null + } + }, + kdocLocation = null, + parseWithChildren = parseWithChildren + ) + private fun parseDocTag(tag: PsiDocTag, docComment: PsiDocComment, analysedElement: PsiNamedElement): TagWrapper? = enumValueOrNull(tag.name)?.let { javadocTag -> val resolutionContext = CommentResolutionContext(comment = docComment, tag = javadocTag) @@ -184,6 +217,16 @@ class JavadocParser( else -> stringifySimpleElement(state, context) } + private fun DocumentationContent.stringify(state: ParserState, context: CommentResolutionContext): ParsingResult = + when(this){ + is PsiDocumentationContent -> psiElement.stringify(state, context) + is DescriptorDocumentationContent -> { + inheritDocSection = parseDocumentation(KotlinDocComment(element, descriptor), parseWithChildren = false) + ParsingResult(state, "") + } + else -> throw IllegalStateException("Unrecognised documentation content: $this") + } + private fun PsiElement.stringifySimpleElement( state: ParserState, context: CommentResolutionContext @@ -303,43 +346,57 @@ class JavadocParser( } } - private fun createBlock(element: Element, insidePre: Boolean = false): DocTag? { + private fun createBlock(element: Element, insidePre: Boolean = false): List { val children = element.childNodes() - .mapNotNull { convertHtmlNode(it, insidePre = insidePre || element.tagName() == "pre") } + .flatMap { convertHtmlNode(it, insidePre = insidePre || element.tagName() == "pre") } - fun ifChildrenPresent(operation: () -> DocTag): DocTag? { - return if (children.isNotEmpty()) operation() else null + fun ifChildrenPresent(operation: () -> DocTag): List { + return if (children.isNotEmpty()) listOf(operation()) else emptyList() } return when (element.tagName()) { "blockquote" -> ifChildrenPresent { BlockQuote(children) } "p" -> ifChildrenPresent { P(children) } "b" -> ifChildrenPresent { B(children) } "strong" -> ifChildrenPresent { Strong(children) } - "index" -> Index(children) + "index" -> listOf(Index(children)) "i" -> ifChildrenPresent { I(children) } - "em" -> Em(children) - "code" -> ifChildrenPresent { CodeInline(children) } - "pre" -> Pre(children) + "em" -> listOf(Em(children)) + "code" -> ifChildrenPresent { if(insidePre) CodeBlock(children) else CodeInline(children) } + "pre" -> if(children.size == 1 && children.first() is CodeInline) { + listOf(CodeBlock(children.first().children)) + } else { + listOf(Pre(children)) + } "ul" -> ifChildrenPresent { Ul(children) } "ol" -> ifChildrenPresent { Ol(children) } - "li" -> Li(children) - "a" -> createLink(element, children) + "li" -> listOf(Li(children)) + "a" -> listOf(createLink(element, children)) "table" -> ifChildrenPresent { Table(children) } "tr" -> ifChildrenPresent { Tr(children) } - "td" -> Td(children) - "thead" -> THead(children) - "tbody" -> TBody(children) - "tfoot" -> TFoot(children) + "td" -> listOf(Td(children)) + "thead" -> listOf(THead(children)) + "tbody" -> listOf(TBody(children)) + "tfoot" -> listOf(TFoot(children)) "caption" -> ifChildrenPresent { Caption(children) } - else -> Text(body = element.ownText()) + "inheritdoc" -> { + val section = inheritDocSection + inheritDocSection = null + val parsed = section?.children?.flatMap { it.root.children }.orEmpty() + if(parsed.size == 1 && parsed.first() is P){ + parsed.first().children + } else { + parsed + } + } + else -> listOf(Text(body = element.ownText())) } } - private fun convertHtmlNode(node: Node, insidePre: Boolean = false): DocTag? = when (node) { + private fun convertHtmlNode(node: Node, insidePre: Boolean = false): List = when (node) { is TextNode -> (if (insidePre) node.wholeText else node.text() - .takeIf { it.isNotBlank() })?.let { Text(body = it) } + .takeIf { it.isNotBlank() })?.let { listOf(Text(body = it)) }.orEmpty() is Element -> createBlock(node) - else -> null + else -> emptyList() } override fun invoke( @@ -352,7 +409,7 @@ class JavadocParser( }.parsedLine?.let { val trimmed = it.trim() val toParse = if (asParagraph) "

$trimmed

" else trimmed - Jsoup.parseBodyFragment(toParse).body().childNodes().mapNotNull { convertHtmlNode(it) } + Jsoup.parseBodyFragment(toParse).body().childNodes().flatMap { convertHtmlNode(it) } }.orEmpty() } diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt index 798b01db56..1771595a93 100644 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt @@ -7,8 +7,73 @@ import org.jetbrains.dokka.analysis.from import org.jetbrains.dokka.base.translators.psi.findSuperMethodsOrEmptyArray import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.kdoc.findKDoc +import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName +import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +internal interface DocComment { + fun hasTag(tag: JavadocTag): Boolean + fun hasTagWithExceptionOfType(tag: JavadocTag, exceptionFqName: String): Boolean + fun tagsByName(tag: JavadocTag, param: String? = null): List +} + +internal data class JavaDocComment(val comment: PsiDocComment) : DocComment { + override fun hasTag(tag: JavadocTag): Boolean = comment.hasTag(tag) + override fun hasTagWithExceptionOfType(tag: JavadocTag, exceptionFqName: String): Boolean = + comment.hasTag(tag) && comment.tagsByName(tag).firstIsInstanceOrNull() + ?.resolveToElement() + ?.getKotlinFqName()?.asString() == exceptionFqName + + override fun tagsByName(tag: JavadocTag, param: String?): List = + comment.tagsByName(tag).map { PsiDocumentationContent(it, tag) } +} + +internal data class KotlinDocComment(val comment: KDocTag, val descriptor: DeclarationDescriptor) : DocComment { + override fun hasTag(tag: JavadocTag): Boolean = + when (tag) { + JavadocTag.DESCRIPTION -> comment.getContent().isNotEmpty() + else -> tagsWithContent.any { it.text.startsWith("@$tag") } + } + + override fun hasTagWithExceptionOfType(tag: JavadocTag, exceptionFqName: String): Boolean = + tagsWithContent.any { it.hasExceptionWithName(tag, exceptionFqName) } + + override fun tagsByName(tag: JavadocTag, param: String?): List = + when (tag) { + JavadocTag.DESCRIPTION -> listOf(DescriptorDocumentationContent(descriptor, comment, tag)) + else -> comment.children.mapNotNull { (it as? KDocTag) } + .filter { it.name == "$tag" && param?.let { param -> it.hasExceptionWithName(param) } != false } + .map { DescriptorDocumentationContent(descriptor, it, tag) } + } + + private val tagsWithContent: List = comment.children.mapNotNull { (it as? KDocTag) } + + private fun KDocTag.hasExceptionWithName(tag: JavadocTag, exceptionFqName: String) = + text.startsWith("@$tag") && hasExceptionWithName(exceptionFqName) + + private fun KDocTag.hasExceptionWithName(exceptionFqName: String) = + getSubjectName() == exceptionFqName +} + +internal interface DocumentationContent { + val tag: JavadocTag +} + +internal data class PsiDocumentationContent(val psiElement: PsiElement, override val tag: JavadocTag) : + DocumentationContent + +internal data class DescriptorDocumentationContent( + val descriptor: DeclarationDescriptor, + val element: KDocTag, + override val tag: JavadocTag +) : DocumentationContent + internal fun PsiDocComment.hasTag(tag: JavadocTag): Boolean = when (tag) { JavadocTag.DESCRIPTION -> descriptionElements.isNotEmpty() @@ -21,8 +86,10 @@ internal fun PsiDocComment.tagsByName(tag: JavadocTag): List = else -> findTagsByName(tag.toString()).toList() } -internal fun findClosestDocComment(element: PsiNamedElement, logger: DokkaLogger): PsiDocComment? { - (element as? PsiDocCommentOwner)?.docComment?.run { return this } +internal fun findClosestDocComment(element: PsiNamedElement, logger: DokkaLogger): DocComment? { + (element as? PsiDocCommentOwner)?.docComment?.run { return JavaDocComment(this) } + element.toKdocComment()?.run { return this } + if (element is PsiMethod) { val superMethods = element.findSuperMethodsOrEmptyArray(logger) if (superMethods.isEmpty()) return null @@ -51,9 +118,20 @@ internal fun findClosestDocComment(element: PsiNamedElement, logger: DokkaLogger return if (indexOfSuperClass >= 0) superMethodDocumentation[indexOfSuperClass] else superMethodDocumentation.first() } - return element.children.firstIsInstanceOrNull() + return element.children.firstIsInstanceOrNull()?.let { JavaDocComment(it) } } +internal fun PsiNamedElement.toKdocComment(): KotlinDocComment? = + (navigationElement as? KtElement)?.findKDoc { DescriptorToSourceUtils.descriptorToDeclaration(it) } + ?.run { + (this@toKdocComment.navigationElement as? KtDeclaration)?.descriptor?.let { + KotlinDocComment( + this, + it + ) + } + } + internal fun PsiDocTag.contentElementsWithSiblingIfNeeded(): List = if (dataElements.isNotEmpty()) { listOfNotNull( dataElements[0], diff --git a/plugins/base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt b/plugins/base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt new file mode 100644 index 0000000000..a163f7f4dc --- /dev/null +++ b/plugins/base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt @@ -0,0 +1,364 @@ +package model + +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.model.childrenOfType +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.firstMemberOfType +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.ContentText +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import org.junit.jupiter.api.Test +import translators.documentationOf +import utils.docs +import kotlin.test.assertEquals + +class MultiLanguageInheritanceTest : BaseAbstractTest() { + val configuration = dokkaConfiguration { + suppressObviousFunctions = false + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/kotlin") + } + } + } + + @Test + fun `from java to kotlin`() { + testInline( + """ + |/src/main/kotlin/sample/Parent.java + |package sample; + | + |/** + | * Sample description from parent + | */ + |public class Parent { + | /** + | * parent function docs + | * @see java.lang.String for details + | */ + | public void parentFunction(){ + | } + |} + | + |/src/main/kotlin/sample/Child.kt + |package sample + |public class Child : Parent() { + | override fun parentFunction(){ + | + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "Child" }?.functions?.find { it.name == "parentFunction" } + val seeTag = function?.documentation?.values?.first()?.children?.firstIsInstanceOrNull() + + assertEquals("", module.documentationOf("Child")) + assertEquals("parent function docs", module.documentationOf("Child", "parentFunction")) + assertEquals("for details", (seeTag?.root?.dfs { it is Text } as Text).body) + assertEquals("java.lang.String", seeTag.name) + } + } + } + + @Test + fun `from kotlin to java`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent `function docs` + | * + | * ``` + | * code block + | * ``` + | * @see java.lang.String for details + | */ + | public open fun parentFun(){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + |public class ChildInJava extends ParentInKotlin { + | @Override + | public void parentFun() { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" } + val seeTag = function?.documentation?.values?.first()?.children?.firstIsInstanceOrNull() + + val expectedDocs = CustomDocTag( + children = listOf( + P( + listOf( + Text("parent "), + CodeInline( + listOf(Text("function docs")) + ) + ) + ), + CodeBlock( + listOf(Text("code block")) + ) + + ), + params = emptyMap(), + name = "MARKDOWN_FILE" + ) + + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals(expectedDocs, function?.docs()?.firstIsInstanceOrNull()?.root) + assertEquals("for details", (seeTag?.root?.dfs { it is Text } as Text).body) + assertEquals("java.lang.String", seeTag.name) + } + } + } + + @Test + fun `inherit doc on method`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent `function docs` with a link to [defaultString][java.lang.String] + | * + | * ``` + | * code block + | * ``` + | */ + | public open fun parentFun(){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + |public class ChildInJava extends ParentInKotlin { + | /** + | * {@inheritDoc} + | */ + | @Override + | public void parentFun() { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" } + + val expectedDocs = CustomDocTag( + children = listOf( + P( + listOf( + P( + listOf( + Text("parent "), + CodeInline( + listOf(Text("function docs")) + ), + Text(" with a link to "), + DocumentationLink( + DRI("java.lang", "String", null, PointingToDeclaration), + listOf(Text("defaultString")), + params = mapOf("href" to "[java.lang.String]") + ) + ) + ), + CodeBlock( + listOf(Text("code block")) + ) + ) + ) + ), + params = emptyMap(), + name = "MARKDOWN_FILE" + ) + + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals(expectedDocs, function?.docs()?.firstIsInstanceOrNull()?.root) + } + } + } + + @Test + fun `inline inherit doc on method`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent function docs + | * @see java.lang.String string + | */ + | public open fun parentFun(){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + |public class ChildInJava extends ParentInKotlin { + | /** + | * Start {@inheritDoc} end + | */ + | @Override + | public void parentFun() { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" }?.documentation?.values?.first()?.children?.first() + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals("Start parent function docs end", function?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + } + } + } + + @Test + fun `inherit doc on multiple throws`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent function docs + | * @throws java.lang.RuntimeException runtime + | * @throws java.lang.Exception exception + | */ + | public open fun parentFun(){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + |public class ChildInJava extends ParentInKotlin { + | /** + | * Start {@inheritDoc} end + | * @throws java.lang.RuntimeException Testing {@inheritDoc} + | */ + | @Override + | public void parentFun() { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" } + val docs = function?.documentation?.values?.first()?.children?.first() + val throwsTag = function?.documentation?.values?.first()?.children?.firstIsInstanceOrNull() + + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals("Start parent function docs end", docs?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + assertEquals("Testing runtime", throwsTag?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + assertEquals("RuntimeException", throwsTag?.exceptionAddress?.classNames) + } + } + } + + @Test + fun `inherit doc on params`() { + testInline( + """ + |/src/main/kotlin/sample/ParentInKotlin.kt + |package sample + | + |/** + | * Sample description from parent + | */ + |public open class ParentInKotlin { + | /** + | * parent function docs + | * @param fst first docs + | * @param snd second docs + | */ + | public open fun parentFun(fst: String, snd: Int){ + | + | } + |} + | + | + |/src/main/kotlin/sample/ChildInJava.java + |package sample; + | + |import org.jetbrains.annotations.NotNull; + | + |public class ChildInJava extends ParentInKotlin { + | /** + | * @param fst start {@inheritDoc} end + | * @param snd start {@inheritDoc} end + | */ + | @Override + | public void parentFun(@NotNull String fst, int snd) { + | super.parentFun(); + | } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val function = module.packages.flatMap { it.classlikes } + .find { it.name == "ChildInJava" }?.functions?.find { it.name == "parentFun" } + val params = function?.documentation?.values?.first()?.children?.filterIsInstance() + + val fst = params?.first { it.name == "fst" } + val snd = params?.first { it.name == "snd" } + + assertEquals("", module.documentationOf("ChildInJava")) + assertEquals("", module.documentationOf("ChildInJava", "parentFun")) + assertEquals("start first docs end", fst?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + assertEquals("start second docs end", snd?.root?.withDescendants()?.filter { it is Text }?.toList()?.joinToString("") { (it as Text).body }) + } + } + } +} \ No newline at end of file