From 6a1c05c2d340a6812a8b58d3027d8e5712db45a2 Mon Sep 17 00:00:00 2001 From: Marcin Aman Date: Thu, 12 Nov 2020 12:01:00 +0100 Subject: Javadoc @inheritDoc tag support (#1608) --- .../translators/psi/parsers/InheritDocResolver.kt | 106 ++++++ .../translators/psi/parsers/JavadocParser.kt | 381 +++++++++++++++++++++ .../kotlin/translators/psi/parsers/JavadocTag.kt | 20 ++ .../translators/psi/parsers/PsiCommentsUtils.kt | 68 ++++ .../kotlin/translators/psi/parsers/exceptionTag.kt | 15 + 5 files changed, 590 insertions(+) create mode 100644 plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt create mode 100644 plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt create mode 100644 plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt create mode 100644 plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt create mode 100644 plugins/base/src/main/kotlin/translators/psi/parsers/exceptionTag.kt (limited to 'plugins/base/src/main/kotlin/translators/psi/parsers') diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt new file mode 100644 index 00000000..c2fb6fb4 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt @@ -0,0 +1,106 @@ +package org.jetbrains.dokka.base.translators.psi.parsers + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +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.utils.addToStdlib.firstIsInstanceOrNull + +internal data class CommentResolutionContext( + val comment: PsiDocComment, + val tag: JavadocTag, + val name: String? = null, + val parameterIndex: Int? = null, +) + +internal class InheritDocResolver( + private val logger: DokkaLogger +) { + 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.DEPRECATED -> resolveGenericTag(context.comment, JavadocTag.DESCRIPTION) + JavadocTag.SEE -> emptyList() + else -> resolveGenericTag(context.comment, context.tag) + } + + private fun resolveGenericTag(currentElement: PsiDocComment, tag: JavadocTag): List = + 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() + else -> listOf(it) + } + }.orEmpty() + + /** + * Main resolution point for exception like tags + * + * This should be used only with [JavadocTag.EXCEPTION] or [JavadocTag.THROWS] as their resolution path should be the same + */ + private fun resolveThrowsTag( + tag: JavadocTag, + currentElement: PsiDocComment, + exceptionFqName: String + ): List = + (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, tag) } + .orEmpty().firstOrNull { + findClosestDocComment(it, logger)?.hasTagWithExceptionOfType(tag, exceptionFqName) == true + }?.docComment?.tagsByName(tag)?.flatMap { + when (it) { + is PsiDocTag -> it.contentElementsWithSiblingIfNeeded() + else -> listOf(it) + } + }?.withoutReferenceLink().orEmpty() + + private fun resolveParamTag( + currentElement: PsiDocComment, + parameterIndex: Int, + ): 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() + } + } + }.withoutReferenceLink() + + //if we are in psi class javadoc only inherits docs from classes and not from interfaces + private fun lowestClassWithTag(baseClass: PsiClass, javadocTag: JavadocTag): PsiDocComment? = + baseClass.superClass?.let { + findClosestDocComment(it, logger)?.takeIf { tag -> tag.hasTag(javadocTag) } ?: + lowestClassWithTag(it, javadocTag) + } + + private fun lowestMethodWithTag( + baseMethod: PsiMethod, + javadocTag: JavadocTag, + ): PsiDocComment? = + lowestMethodsWithTag(baseMethod, javadocTag).firstOrNull()?.docComment + + 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 new file mode 100644 index 00000000..96c62b36 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt @@ -0,0 +1,381 @@ +package org.jetbrains.dokka.base.translators.psi.parsers + +import com.intellij.lexer.JavaDocTokenTypes +import com.intellij.psi.* +import com.intellij.psi.impl.source.javadoc.PsiDocParamRef +import com.intellij.psi.impl.source.tree.JavaDocElementType +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.from +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.Deprecated +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.utilities.enumValueOrNull +import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName +import org.jetbrains.kotlin.idea.util.CommentSaver.Companion.tokenType +import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode + +interface JavaDocumentationParser { + fun parseDocumentation(element: PsiNamedElement): DocumentationNode +} + +class JavadocParser( + private val logger: DokkaLogger +) : JavaDocumentationParser { + private val inheritDocResolver = InheritDocResolver(logger) + + override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { + val docComment = findClosestDocComment(element, logger) ?: return DocumentationNode(emptyList()) + val nodes = listOfNotNull(docComment.getDescription()) + docComment.tags.mapNotNull { tag -> + parseDocTag(tag, docComment, element) + } + return DocumentationNode(nodes) + } + + private fun parseDocTag(tag: PsiDocTag, docComment: PsiDocComment, analysedElement: PsiNamedElement): TagWrapper? = + enumValueOrNull(tag.name)?.let { javadocTag -> + val resolutionContext = CommentResolutionContext(comment = docComment, tag = javadocTag) + when (resolutionContext.tag) { + JavadocTag.PARAM -> { + val name = tag.dataElements.firstOrNull()?.text.orEmpty() + val index = + (analysedElement as? PsiMethod)?.parameterList?.parameters?.map { it.name }?.indexOf(name) + Param( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded().drop(1), + context = resolutionContext.copy(name = name, parameterIndex = index) + ) + ), + name + ) + } + JavadocTag.THROWS, JavadocTag.EXCEPTION -> { + val resolved = tag.resolveToElement() + val dri = resolved?.let { DRI.from(it) } + val name = resolved?.getKotlinFqName()?.asString() + ?: tag.dataElements.firstOrNull()?.text.orEmpty() + Throws( + root = wrapTagIfNecessary( + convertJavadocElements( + tag.dataElements.drop(1), + context = resolutionContext.copy(name = name) + ) + ), + /* we always would like to have a fully qualified name as name, + * because it will be used as a display name later and we would like to have those unified + * even if documentation states shortened version + * + * Only if dri search fails we should use the provided phrase (since then we are not able to get a fq name) + * */ + name = name, + exceptionAddress = dri + ) + } + JavadocTag.RETURN -> Return( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded(), + context = resolutionContext + ) + ) + ) + JavadocTag.AUTHOR -> Author( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded(), + context = resolutionContext + ) + ) + ) // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`, then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted + JavadocTag.SEE -> { + val name = + tag.resolveToElement()?.getKotlinFqName()?.asString() ?: tag.referenceElement()?.text.orEmpty() + getSeeTagElementContent(tag, resolutionContext.copy(name = name)).let { + See( + wrapTagIfNecessary(it.first), + name, + it.second + ) + } + } + JavadocTag.DEPRECATED -> Deprecated( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded(), + context = resolutionContext + ) + ) + ) + else -> null + //TODO https://github.com/Kotlin/dokka/issues/1618 + } + } + + private fun wrapTagIfNecessary(list: List): CustomDocTag = + if (list.size == 1 && (list.first() as? CustomDocTag)?.name == MarkdownElementTypes.MARKDOWN_FILE.name) + list.first() as CustomDocTag + else + CustomDocTag(list, name = MarkdownElementTypes.MARKDOWN_FILE.name) + + private fun getSeeTagElementContent( + tag: PsiDocTag, + context: CommentResolutionContext + ): Pair, DRI?> { + val linkElement = tag.referenceElement()?.toDocumentationLink(context = context) + val content = convertJavadocElements( + tag.dataElements.dropWhile { it is PsiWhiteSpace || (it as? LazyParseablePsiElement)?.tokenType == JavaDocElementType.DOC_REFERENCE_HOLDER }, + context = context + ) + return Pair(content, linkElement?.dri) + } + + private fun PsiDocComment.getDescription(): Description? { + return convertJavadocElements( + descriptionElements.asIterable(), + context = CommentResolutionContext(this, JavadocTag.DESCRIPTION) + ).takeIf { it.isNotEmpty() }?.let { + Description(wrapTagIfNecessary(it)) + } + } + + private data class ParserState( + val currentJavadocTag: JavadocTag, + val previousElement: PsiElement? = null, + val openPreTags: Int = 0, + val closedPreTags: Int = 0 + ) + + private data class ParsingResult(val newState: ParserState, val parsedLine: String? = null) { + constructor(tag: JavadocTag) : this(ParserState(tag)) + + operator fun plus(other: ParsingResult): ParsingResult = + ParsingResult( + other.newState, + listOfNotNull(parsedLine, other.parsedLine).joinToString(separator = "") + ) + } + + private inner class Parse : (Iterable, Boolean, CommentResolutionContext) -> List { + val driMap = mutableMapOf() + + private fun PsiElement.stringify(state: ParserState, context: CommentResolutionContext): ParsingResult = + when (this) { + is PsiReference -> children.fold(ParsingResult(state)) { acc, e -> + acc + e.stringify(acc.newState, context) + } + else -> stringifySimpleElement(state, context) + } + + private fun PsiElement.stringifySimpleElement( + state: ParserState, + context: CommentResolutionContext + ): ParsingResult { + val openPre = state.openPreTags + "".toRegex().findAll(text).toList().size + val closedPre = state.closedPreTags + "".toRegex().findAll(text).toList().size + val isInsidePre = openPre > closedPre + val parsed = when (this) { + is PsiInlineDocTag -> convertInlineDocTag(this, state.currentJavadocTag, context) + is PsiDocParamRef -> toDocumentationLinkString() + is PsiDocTagValue, + is LeafPsiElement -> { + if (isInsidePre) { + /* + For values in the
 tag we try to keep formatting, so only the leading space is trimmed,
+                        since it is there because it separates this line from the leading asterisk
+                         */
+                        text.let {
+                            if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true && it.firstOrNull() == ' ')
+                                it.drop(1) else it
+                        }.let {
+                            if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it.dropLastWhile { it == ' ' } else it
+                        }
+                    } else {
+                        /*
+                        Outside of the 
 we would like to trim everything from the start and end of a line since
+                        javadoc doesn't care about it.
+                         */
+                        text.let {
+                            if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text != " " && state.previousElement !is PsiInlineDocTag) it?.trimStart() else it
+                        }?.let {
+                            if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text != " ") it.trimEnd() else it
+                        }?.let {
+                            if (shouldHaveSpaceAtTheEnd()) "$it " else it
+                        }
+                    }
+                }
+                else -> null
+            }
+            val previousElement = if (text.trim() == "") state.previousElement else this
+            return ParsingResult(
+                state.copy(
+                    previousElement = previousElement,
+                    closedPreTags = closedPre,
+                    openPreTags = openPre
+                ), parsed
+            )
+        }
+
+        /**
+         * We would like to know if we need to have a space after a this tag
+         *
+         * The space is required when:
+         *  - tag spans multiple lines, between every line we would need a space
+         *
+         *  We wouldn't like to render a space if:
+         *  - tag is followed by an end of comment
+         *  - after a tag there is another tag (eg. multiple @author tags)
+         *  - they end with an html tag like: Something since then the space will be displayed in the following text
+         *  - next line starts with a 

or

 token
+         */
+        private fun PsiElement.shouldHaveSpaceAtTheEnd(): Boolean {
+            val siblings = siblings(withItself = false).toList().filterNot { it.text.trim() == "" }
+            val nextNotEmptySibling = (siblings.firstOrNull() as? PsiDocToken)
+            val furtherNotEmptySibling =
+                (siblings.drop(1).firstOrNull { it is PsiDocToken && !it.isLeadingAsterisk() } as? PsiDocToken)
+            val lastHtmlTag = text.trim().substringAfterLast("<")
+            val endsWithAnUnclosedTag = lastHtmlTag.endsWith(">") && !lastHtmlTag.startsWith("? = null
+        ): String {
+            val label = labelElement?.toList().takeUnless { it.isNullOrEmpty() } ?: listOf(defaultLabel())
+
+            val dri = reference?.resolve()?.takeIf { it !is PsiParameter }?.let {
+                val dri = DRI.from(it)
+                driMap[dri.toString()] = dri
+                dri.toString()
+            } ?: UNRESOLVED_PSI_ELEMENT
+
+            return """${label.joinToString(" ") { it.text }}"""
+        }
+
+        private fun convertInlineDocTag(
+            tag: PsiInlineDocTag,
+            javadocTag: JavadocTag,
+            context: CommentResolutionContext
+        ) =
+            when (tag.name) {
+                "link", "linkplain" -> tag.referenceElement()
+                    ?.toDocumentationLinkString(tag.dataElements.filterIsInstance())
+                "code", "literal" -> "${tag.text}"
+                "index" -> "${tag.children.filterIsInstance().joinToString { it.text }}"
+                "inheritDoc" -> inheritDocResolver.resolveFromContext(context)
+                    ?.fold(ParsingResult(javadocTag)) { result, e ->
+                        result + e.stringify(result.newState, context)
+                    }?.parsedLine.orEmpty()
+                else -> tag.text
+            }
+
+        private fun createLink(element: Element, children: List): DocTag {
+            return when {
+                element.hasAttr("docref") ->
+                    A(children, params = mapOf("docref" to element.attr("docref")))
+                element.hasAttr("href") ->
+                    A(children, params = mapOf("href" to element.attr("href")))
+                element.hasAttr("data-dri") && driMap.containsKey(element.attr("data-dri")) ->
+                    DocumentationLink(driMap[element.attr("data-dri")]!!, children)
+                else -> Text(body = children.filterIsInstance().joinToString { it.body })
+            }
+        }
+
+        private fun createBlock(element: Element, insidePre: Boolean = false): DocTag? {
+            val children = element.childNodes()
+                .mapNotNull { convertHtmlNode(it, insidePre = insidePre || element.tagName() == "pre") }
+
+            fun ifChildrenPresent(operation: () -> DocTag): DocTag? {
+                return if (children.isNotEmpty()) operation() else null
+            }
+            return when (element.tagName()) {
+                "blockquote" -> ifChildrenPresent { BlockQuote(children) }
+                "p" -> ifChildrenPresent { P(children) }
+                "b" -> ifChildrenPresent { B(children) }
+                "strong" -> ifChildrenPresent { Strong(children) }
+                "index" -> Index(children)
+                "i" -> ifChildrenPresent { I(children) }
+                "em" -> Em(children)
+                "code" -> ifChildrenPresent { CodeInline(children) }
+                "pre" -> Pre(children)
+                "ul" -> ifChildrenPresent { Ul(children) }
+                "ol" -> ifChildrenPresent { Ol(children) }
+                "li" -> Li(children)
+                "a" -> createLink(element, children)
+                "table" -> ifChildrenPresent { Table(children) }
+                "tr" -> ifChildrenPresent { Tr(children) }
+                "td" -> Td(children)
+                "thead" -> THead(children)
+                "tbody" -> TBody(children)
+                "tfoot" -> TFoot(children)
+                "caption" -> ifChildrenPresent { Caption(children) }
+                else -> Text(body = element.ownText())
+            }
+        }
+
+        private fun convertHtmlNode(node: Node, insidePre: Boolean = false): DocTag? = when (node) {
+            is TextNode -> (if (insidePre) node.wholeText else node.text()
+                .takeIf { it.isNotBlank() })?.let { Text(body = it) }
+            is Element -> createBlock(node)
+            else -> null
+        }
+
+        override fun invoke(
+            elements: Iterable,
+            asParagraph: Boolean,
+            context: CommentResolutionContext
+        ): List =
+            elements.fold(ParsingResult(context.tag)) { acc, e ->
+                acc + e.stringify(acc.newState, context)
+            }.parsedLine?.let {
+                val trimmed = it.trim()
+                val toParse = if (asParagraph) "

$trimmed

" else trimmed + Jsoup.parseBodyFragment(toParse).body().childNodes().mapNotNull { convertHtmlNode(it) } + }.orEmpty() + } + + private fun convertJavadocElements( + elements: Iterable, + asParagraph: Boolean = true, + context: CommentResolutionContext + ): List = + Parse()(elements, asParagraph, context) + + private fun PsiDocToken.isSharpToken() = tokenType == JavaDocTokenType.DOC_TAG_VALUE_SHARP_TOKEN + + private fun PsiDocToken.isLeadingAsterisk() = tokenType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS + + private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null, context: CommentResolutionContext) = + resolveToGetDri()?.let { + val dri = DRI.from(it) + val label = labelElement ?: defaultLabel() + DocumentationLink(dri, convertJavadocElements(listOfNotNull(label), asParagraph = false, context)) + } + + private fun PsiDocTag.referenceElement(): PsiElement? = + linkElement()?.referenceElementOrSelf() + + private fun PsiElement.defaultLabel() = children.firstOrNull { + it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken() + } ?: this + + private fun PsiDocTag.linkElement(): PsiElement? = + valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } + + companion object { + private const val UNRESOLVED_PSI_ELEMENT = "UNRESOLVED_PSI_ELEMENT" + } +} diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt new file mode 100644 index 00000000..8ea39453 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.base.translators.psi.parsers + +internal enum class JavadocTag { + PARAM, THROWS, RETURN, AUTHOR, SEE, DEPRECATED, EXCEPTION, + + /** + * Artificial tag created to handle tag-less section + */ + DESCRIPTION,; + + override fun toString(): String = super.toString().toLowerCase() + + /* Missing tags: + SERIAL, + SERIAL_DATA, + SERIAL_FIELD, + SINCE, + VERSION + */ +} \ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt new file mode 100644 index 00000000..80b37052 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt @@ -0,0 +1,68 @@ +package org.jetbrains.dokka.base.translators.psi.parsers + +import com.intellij.psi.* +import com.intellij.psi.javadoc.PsiDocComment +import com.intellij.psi.javadoc.PsiDocTag +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.utils.addToStdlib.firstIsInstanceOrNull + +internal fun PsiDocComment.hasTag(tag: JavadocTag): Boolean = + when (tag) { + JavadocTag.DESCRIPTION -> descriptionElements.isNotEmpty() + else -> findTagByName(tag.toString()) != null + } + +internal fun PsiDocComment.tagsByName(tag: JavadocTag): List = + when (tag) { + JavadocTag.DESCRIPTION -> descriptionElements.toList() + else -> findTagsByName(tag.toString()).toList() + } + +internal fun findClosestDocComment(element: PsiNamedElement, logger: DokkaLogger): PsiDocComment? { + (element as? PsiDocCommentOwner)?.docComment?.run { return this } + if (element is PsiMethod) { + val superMethods = element.findSuperMethodsOrEmptyArray(logger) + if (superMethods.isEmpty()) return null + + if (superMethods.size == 1) { + return findClosestDocComment(superMethods.single(), logger) + } + + val superMethodDocumentation = superMethods.map { method -> findClosestDocComment(method, logger) } + if (superMethodDocumentation.size == 1) { + return superMethodDocumentation.single() + } + + logger.warn( + "Conflicting documentation for ${DRI.from(element)}" + + "${superMethods.map { DRI.from(it) }}" + ) + + /* Prioritize super class over interface */ + val indexOfSuperClass = superMethods.indexOfFirst { method -> + val parent = method.parent + if (parent is PsiClass) !parent.isInterface + else false + } + + return if (indexOfSuperClass >= 0) superMethodDocumentation[indexOfSuperClass] + else superMethodDocumentation.first() + } + return element.children.firstIsInstanceOrNull() +} + +internal fun PsiDocTag.contentElementsWithSiblingIfNeeded(): List = if (dataElements.isNotEmpty()) { + listOfNotNull( + dataElements[0], + dataElements[0].nextSibling?.takeIf { it.text != dataElements.drop(1).firstOrNull()?.text }, + *dataElements.drop(1).toTypedArray() + ) +} else { + emptyList() +} + +internal fun PsiDocTag.resolveToElement(): PsiElement? = + dataElements.firstOrNull()?.firstChild?.referenceElementOrSelf()?.resolveToGetDri() \ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/exceptionTag.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/exceptionTag.kt new file mode 100644 index 00000000..6e1850bb --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/exceptionTag.kt @@ -0,0 +1,15 @@ +package org.jetbrains.dokka.base.translators.psi.parsers + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiJavaCodeReferenceElement +import com.intellij.psi.impl.source.tree.JavaDocElementType +import com.intellij.psi.javadoc.PsiDocTag +import com.intellij.psi.util.PsiTreeUtil + +internal fun PsiElement.referenceElementOrSelf(): PsiElement? = + if (node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { + PsiTreeUtil.findChildOfType(this, PsiJavaCodeReferenceElement::class.java) + } else this + +internal fun PsiElement.resolveToGetDri(): PsiElement? = + reference?.resolve() \ No newline at end of file -- cgit