diff options
author | Marcin Aman <marcin.aman@gmail.com> | 2020-11-12 12:01:00 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-12 12:01:00 +0100 |
commit | 6a1c05c2d340a6812a8b58d3027d8e5712db45a2 (patch) | |
tree | b14c5b0a26fbb61bb5492b1a778e5df57fcd584d /plugins/base/src/main/kotlin/translators/psi/parsers | |
parent | 7db15c357a417ccd9ff8ad1f90f5aff84eec132f (diff) | |
download | dokka-6a1c05c2d340a6812a8b58d3027d8e5712db45a2.tar.gz dokka-6a1c05c2d340a6812a8b58d3027d8e5712db45a2.tar.bz2 dokka-6a1c05c2d340a6812a8b58d3027d8e5712db45a2.zip |
Javadoc @inheritDoc tag support (#1608)
Diffstat (limited to 'plugins/base/src/main/kotlin/translators/psi/parsers')
5 files changed, 590 insertions, 0 deletions
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<PsiElement> = + 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<PsiElement> = + (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<PsiElement> = + (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() + } + } + }.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<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 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<JavadocTag>(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<DocTag>): 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<List<DocTag>, 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<PsiElement>, Boolean, CommentResolutionContext) -> List<DocTag> { + val driMap = mutableMapOf<String, DRI>() + + 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 + "<pre(\\s+.*)?>".toRegex().findAll(text).toList().size + val closedPre = state.closedPreTags + "</pre>".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 <pre> 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 <pre> 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: <a href="...">Something</a> since then the space will be displayed in the following text + * - next line starts with a <p> or <pre> 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("</") + + return (nextSibling as? PsiWhiteSpace)?.text == "\n " && + (getNextSiblingIgnoringWhitespace() as? PsiDocToken)?.tokenType != JavaDocTokenTypes.INSTANCE.commentEnd() && + nextNotEmptySibling?.isLeadingAsterisk() == true && + furtherNotEmptySibling?.tokenType == JavaDocTokenTypes.INSTANCE.commentData() && + !endsWithAnUnclosedTag + } + + private fun PsiElement.toDocumentationLinkString( + labelElement: List<PsiElement>? = 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 """<a data-dri="$dri">${label.joinToString(" ") { it.text }}</a>""" + } + + private fun convertInlineDocTag( + tag: PsiInlineDocTag, + javadocTag: JavadocTag, + context: CommentResolutionContext + ) = + when (tag.name) { + "link", "linkplain" -> tag.referenceElement() + ?.toDocumentationLinkString(tag.dataElements.filterIsInstance<PsiDocToken>()) + "code", "literal" -> "<code data-inline>${tag.text}</code>" + "index" -> "<index>${tag.children.filterIsInstance<PsiDocTagValue>().joinToString { it.text }}</index>" + "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>): 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<Text>().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<PsiElement>, + asParagraph: Boolean, + context: CommentResolutionContext + ): List<DocTag> = + elements.fold(ParsingResult(context.tag)) { acc, e -> + acc + e.stringify(acc.newState, context) + }.parsedLine?.let { + val trimmed = it.trim() + val toParse = if (asParagraph) "<p>$trimmed</p>" else trimmed + Jsoup.parseBodyFragment(toParse).body().childNodes().mapNotNull { convertHtmlNode(it) } + }.orEmpty() + } + + private fun convertJavadocElements( + elements: Iterable<PsiElement>, + asParagraph: Boolean = true, + context: CommentResolutionContext + ): List<DocTag> = + 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<PsiElement> = + 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<PsiDocComment>() +} + +internal fun PsiDocTag.contentElementsWithSiblingIfNeeded(): List<PsiElement> = 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 |