aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src
diff options
context:
space:
mode:
authorMarcin Aman <marcin.aman@gmail.com>2021-06-22 11:18:20 +0200
committerGitHub <noreply@github.com>2021-06-22 11:18:20 +0200
commit8d6536d3a3d0dcd80a2e6b77b047524e15533f0b (patch)
tree202f9db5dc3231899c992671fc89a9a56b63db49 /plugins/base/src
parent2832b9a111ba3af0212cbc180e8b1535b8359a15 (diff)
downloaddokka-8d6536d3a3d0dcd80a2e6b77b047524e15533f0b.tar.gz
dokka-8d6536d3a3d0dcd80a2e6b77b047524e15533f0b.tar.bz2
dokka-8d6536d3a3d0dcd80a2e6b77b047524e15533f0b.zip
Multilanguage docs inheritance (#1951)
* Multilanguage docs inheritance * Add map to store inherit doc entries
Diffstat (limited to 'plugins/base/src')
-rw-r--r--plugins/base/src/main/kotlin/parsers/MarkdownParser.kt3
-rw-r--r--plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt2
-rw-r--r--plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt2
-rw-r--r--plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt2
-rw-r--r--plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt16
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt12
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt82
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt112
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt84
-rw-r--r--plugins/base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt364
10 files changed, 615 insertions, 64 deletions
diff --git a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt
index 80a9e508..34dceeea 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 4eac9d3f..6446a775 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 a8fabc95..f80cb6df 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 a391b534..cadd3de0 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 92ffd9b6..8bb8b527 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<DeclarationDescriptor>.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 f728c8a7..be7b826b 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<JavaSourceRoot>()
@@ -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<String, Bound>()
@@ -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 21c2c72a..67d0a718 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<PsiElement> =
+ 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<PsiElement> =
- (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, tag) }
+ ): List<DocumentationContent> {
+ 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<PsiElement> =
+ ): List<DocumentationContent> =
(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<PsiDocTag>().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<PsiDocTag>().map { it.contentElementsWithSiblingIfNeeded() }.firstOrNull {
+ it.firstOrNull()?.text == method.parameterList.parameters[parameterIndex].name
+ }.orEmpty()
+
+ private fun resolveKdocTag(comment: KotlinDocComment, parameterIndex: Int): List<DocumentationContent> =
+ 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<PsiDocTag>()
- ?.resolveToElement()
- ?.getKotlinFqName()?.asString() == exceptionFqName
-
private fun List<PsiElement>.withoutReferenceLink(): List<PsiElement> = 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 dc93568f..53424ef9 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
@@ -23,24 +25,63 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
+import java.util.*
interface JavaDocumentationParser {
fun parseDocumentation(element: PsiNamedElement): DocumentationNode
}
class JavadocParser(
- private val logger: DokkaLogger
+ private val logger: DokkaLogger,
+ private val resolutionFacade: DokkaResolutionFacade,
) : JavaDocumentationParser {
private val inheritDocResolver = InheritDocResolver(logger)
+ /**
+ * Cache created to make storing entries from kotlin easier.
+ *
+ * It has to be mutable to allow for adding entries when @inheritDoc resolves to kotlin code,
+ * from which we get a DocTags not descriptors.
+ */
+ private var inheritDocSections: MutableMap<UUID, DocumentationNode> = mutableMapOf()
+
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<JavadocTag>(tag.name)?.let { javadocTag ->
val resolutionContext = CommentResolutionContext(comment = docComment, tag = javadocTag)
@@ -184,6 +225,17 @@ 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 -> {
+ val id = UUID.randomUUID()
+ inheritDocSections[id] = parseDocumentation(KotlinDocComment(element, descriptor), parseWithChildren = false)
+ ParsingResult(state, """<inheritdoc id="$id"/>""")
+ }
+ else -> throw IllegalStateException("Unrecognised documentation content: $this")
+ }
+
private fun PsiElement.stringifySimpleElement(
state: ParserState,
context: CommentResolutionContext
@@ -303,43 +355,57 @@ class JavadocParser(
}
}
- private fun createBlock(element: Element, insidePre: Boolean = false): DocTag? {
+ private fun createBlock(element: Element, insidePre: Boolean = false): List<DocTag> {
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<DocTag> {
+ 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 id = UUID.fromString(element.attr("id"))
+ val section = inheritDocSections[id]
+ 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<DocTag> = 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 +418,7 @@ class JavadocParser(
}.parsedLine?.let {
val trimmed = it.trim()
val toParse = if (asParagraph) "<p>$trimmed</p>" 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 798b01db..1771595a 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<DocumentationContent>
+}
+
+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<PsiDocTag>()
+ ?.resolveToElement()
+ ?.getKotlinFqName()?.asString() == exceptionFqName
+
+ override fun tagsByName(tag: JavadocTag, param: String?): List<DocumentationContent> =
+ 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<DocumentationContent> =
+ 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<KDocTag> = 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<PsiElement> =
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<PsiDocComment>()
+ return element.children.firstIsInstanceOrNull<PsiDocComment>()?.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<PsiElement> = 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 00000000..a163f7f4
--- /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<See>()
+
+ 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<See>()
+
+ 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<Description>()?.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<Description>()?.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<Throws>()
+
+ 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<Param>()
+
+ 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