aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/kotlin/Java
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main/kotlin/Java')
-rw-r--r--core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt87
-rw-r--r--core/src/main/kotlin/Java/JavadocParser.kt353
2 files changed, 364 insertions, 76 deletions
diff --git a/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt
index cf2b0514..f1f170d7 100644
--- a/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt
+++ b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt
@@ -1,9 +1,12 @@
package org.jetbrains.dokka
import com.google.inject.Inject
+import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.*
+import com.intellij.psi.impl.JavaConstantExpressionEvaluator
import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation
import org.jetbrains.kotlin.asJava.elements.KtLightDeclaration
import org.jetbrains.kotlin.asJava.elements.KtLightElement
import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag
@@ -14,14 +17,23 @@ import org.jetbrains.kotlin.psi.KtModifierListOwner
import java.io.File
fun getSignature(element: PsiElement?) = when(element) {
+ is PsiPackage -> element.qualifiedName
is PsiClass -> element.qualifiedName
is PsiField -> element.containingClass!!.qualifiedName + "$" + element.name
is PsiMethod ->
- element.containingClass!!.qualifiedName + "$" + element.name + "(" +
- element.parameterList.parameters.map { it.type.typeSignature() }.joinToString(",") + ")"
+ methodSignature(element)
+ is PsiParameter -> {
+ val method = (element.parent.parent as PsiMethod)
+ methodSignature(method)
+ }
else -> null
}
+private fun methodSignature(method: PsiMethod): String {
+ return method.containingClass!!.qualifiedName + "$" + method.name + "(" +
+ method.parameterList.parameters.map { it.type.typeSignature() }.joinToString(",") + ")"
+}
+
private fun PsiType.typeSignature(): String = when(this) {
is PsiArrayType -> "Array((${componentType.typeSignature()}))"
is PsiPrimitiveType -> "kotlin." + canonicalText.capitalize()
@@ -45,10 +57,16 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder {
private val refGraph: NodeReferenceGraph
private val docParser: JavaDocumentationParser
- @Inject constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph, logger: DokkaLogger) {
+ @Inject constructor(
+ options: DocumentationOptions,
+ refGraph: NodeReferenceGraph,
+ logger: DokkaLogger,
+ signatureProvider: ElementSignatureProvider,
+ externalDocumentationLinkResolver: ExternalDocumentationLinkResolver
+ ) {
this.options = options
this.refGraph = refGraph
- this.docParser = JavadocParser(refGraph, logger)
+ this.docParser = JavadocParser(refGraph, logger, signatureProvider, externalDocumentationLinkResolver)
}
constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph, docParser: JavaDocumentationParser) {
@@ -61,7 +79,7 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder {
if (skipFile(file) || file.classes.all { skipElement(it) }) {
return
}
- val packageNode = module.findOrCreatePackageNode(file.packageName, emptyMap(), refGraph)
+ val packageNode = findOrCreatePackageNode(module, file.packageName, emptyMap(), refGraph)
appendClasses(packageNode, file.classes)
}
@@ -92,9 +110,11 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder {
fun nodeForElement(element: PsiNamedElement,
kind: NodeKind,
- name: String = element.name ?: "<anonymous>"): DocumentationNode {
+ name: String = element.name ?: "<anonymous>",
+ register: Boolean = false): DocumentationNode {
val (docComment, deprecatedContent) = docParser.parseDocumentation(element)
val node = DocumentationNode(name, docComment, kind)
+ if (register) register(element, node)
if (element is PsiModifierListOwner) {
node.appendModifiers(element)
val modifierList = element.modifierList
@@ -139,11 +159,13 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder {
hasSuppressDocTag(element) ||
skipElementBySuppressedFiles(element)
- private fun skipElementByVisibility(element: Any): Boolean = element is PsiModifierListOwner &&
- !(options.effectivePackageOptions((element.containingFile as? PsiJavaFile)?.packageName ?: "").includeNonPublic) &&
- (element.hasModifierProperty(PsiModifier.PRIVATE) ||
- element.hasModifierProperty(PsiModifier.PACKAGE_LOCAL) ||
- element.isInternal())
+ private fun skipElementByVisibility(element: Any): Boolean =
+ element is PsiModifierListOwner &&
+ element !is PsiParameter &&
+ !(options.effectivePackageOptions((element.containingFile as? PsiJavaFile)?.packageName ?: "").includeNonPublic) &&
+ (element.hasModifierProperty(PsiModifier.PRIVATE) ||
+ element.hasModifierProperty(PsiModifier.PACKAGE_LOCAL) ||
+ element.isInternal())
private fun skipElementBySuppressedFiles(element: Any): Boolean =
element is PsiElement && File(element.containingFile.virtualFile.path).absoluteFile in options.suppressedFiles
@@ -161,13 +183,13 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder {
fun PsiClass.build(): DocumentationNode {
val kind = when {
+ isAnnotationType -> NodeKind.AnnotationClass
isInterface -> NodeKind.Interface
isEnum -> NodeKind.Enum
- isAnnotationType -> NodeKind.AnnotationClass
isException() -> NodeKind.Exception
else -> NodeKind.Class
}
- val node = nodeForElement(this, kind)
+ val node = nodeForElement(this, kind, register = isAnnotationType)
superTypes.filter { !ignoreSupertype(it) }.forEach {
node.appendType(it, NodeKind.Supertype)
val superClass = it.resolve()
@@ -200,11 +222,28 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder {
fun PsiField.build(): DocumentationNode {
val node = nodeForElement(this, nodeKind())
node.appendType(type)
- node.appendModifiers(this)
+
+ node.appendConstantValueIfAny(this)
register(this, node)
return node
}
+ private fun DocumentationNode.appendConstantValueIfAny(field: PsiField) {
+ val modifierList = field.modifierList ?: return
+ val initializer = field.initializer ?: return
+ if (modifierList.hasExplicitModifier(PsiModifier.FINAL) &&
+ modifierList.hasExplicitModifier(PsiModifier.STATIC)) {
+ val value = JavaConstantExpressionEvaluator.computeConstantExpression(initializer, false)
+ val text = when(value) {
+ null -> return // No value found
+ is String ->
+ "\"" + StringUtil.escapeStringCharacters(value) + "\""
+ else -> value.toString()
+ }
+ append(DocumentationNode(text, Content.Empty, NodeKind.Value), RefKind.Detail)
+ }
+ }
+
private fun PsiField.nodeKind(): NodeKind = when {
this is PsiEnumConstant -> NodeKind.EnumItem
else -> NodeKind.Field
@@ -274,8 +313,26 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder {
return node
}
+ private fun lookupOrBuildClass(psiClass: PsiClass): DocumentationNode {
+ val existing = refGraph.lookup(getSignature(psiClass)!!)
+ if (existing != null) return existing
+ val new = psiClass.build()
+ val packageNode = findOrCreatePackageNode(null, (psiClass.parent as PsiJavaFile).packageName, emptyMap(), refGraph)
+ packageNode.append(new, RefKind.Member)
+ return new
+ }
+
fun PsiAnnotation.build(): DocumentationNode {
- val node = DocumentationNode(nameReferenceElement?.text ?: "<?>", Content.Empty, NodeKind.Annotation)
+
+ val original = when (this) {
+ is KtLightAbstractAnnotation -> clsDelegate
+ else -> this
+ }
+ val node = DocumentationNode(qualifiedName?.substringAfterLast(".") ?: "<?>", Content.Empty, NodeKind.Annotation)
+ val psiClass = original.nameReferenceElement?.resolve() as? PsiClass
+ if (psiClass != null && psiClass.isAnnotationType) {
+ node.append(lookupOrBuildClass(psiClass), RefKind.Link)
+ }
parameterList.attributes.forEach {
val parameter = DocumentationNode(it.name ?: "value", Content.Empty, NodeKind.Parameter)
val value = it.value
diff --git a/core/src/main/kotlin/Java/JavadocParser.kt b/core/src/main/kotlin/Java/JavadocParser.kt
index c25f5813..70af73f9 100644
--- a/core/src/main/kotlin/Java/JavadocParser.kt
+++ b/core/src/main/kotlin/Java/JavadocParser.kt
@@ -1,14 +1,16 @@
package org.jetbrains.dokka
import com.intellij.psi.*
-import com.intellij.psi.javadoc.PsiDocTag
-import com.intellij.psi.javadoc.PsiDocTagValue
-import com.intellij.psi.javadoc.PsiDocToken
-import com.intellij.psi.javadoc.PsiInlineDocTag
+import com.intellij.psi.impl.source.tree.JavaDocElementType
+import com.intellij.psi.javadoc.*
+import com.intellij.psi.util.PsiTreeUtil
+import com.intellij.util.containers.isNullOrEmpty
+import org.jetbrains.kotlin.utils.keysToMap
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
+import java.net.URI
data class JavadocParseResult(val content: Content, val deprecatedContent: Content?) {
companion object {
@@ -20,75 +22,163 @@ interface JavaDocumentationParser {
fun parseDocumentation(element: PsiNamedElement): JavadocParseResult
}
-class JavadocParser(private val refGraph: NodeReferenceGraph,
- private val logger: DokkaLogger) : JavaDocumentationParser {
+class JavadocParser(
+ private val refGraph: NodeReferenceGraph,
+ private val logger: DokkaLogger,
+ private val signatureProvider: ElementSignatureProvider,
+ private val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver
+) : JavaDocumentationParser {
+
+ private fun ContentSection.appendTypeElement(signature: String, selector: (DocumentationNode) -> DocumentationNode?) {
+ append(LazyContentBlock {
+ val node = refGraph.lookupOrWarn(signature, logger)?.let(selector) ?: return@LazyContentBlock emptyList()
+ listOf(ContentBlock().apply {
+ append(NodeRenderContent(node, LanguageService.RenderMode.SUMMARY))
+ symbol(":")
+ text(" ")
+ })
+ })
+ }
+
override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult {
- val docComment = (element as? PsiDocCommentOwner)?.docComment
- if (docComment == null) return JavadocParseResult.Empty
+ val docComment = (element as? PsiDocCommentOwner)?.docComment ?: return JavadocParseResult.Empty
val result = MutableContent()
var deprecatedContent: Content? = null
- val para = ContentParagraph()
- result.append(para)
- para.convertJavadocElements(docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() })
+
+ val nodes = convertJavadocElements(docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() }, element)
+ val firstParagraphContents = nodes.takeWhile { it !is ContentParagraph }
+ val firstParagraph = ContentParagraph()
+ if (firstParagraphContents.isNotEmpty()) {
+ firstParagraphContents.forEach { firstParagraph.append(it) }
+ result.append(firstParagraph)
+ }
+
+ result.appendAll(nodes.drop(firstParagraphContents.size))
+
+ if (element is PsiMethod) {
+ val tagsByName = element.searchInheritedTags()
+ for ((tagName, tags) in tagsByName) {
+ for ((tag, context) in tags) {
+ val section = result.addSection(javadocSectionDisplayName(tagName), tag.getSubjectName())
+ val signature = signatureProvider.signature(element)
+ when (tagName) {
+ "param" -> {
+ section.appendTypeElement(signature) {
+ it.details
+ .find { node -> node.kind == NodeKind.Parameter && node.name == tag.getSubjectName() }
+ ?.detailOrNull(NodeKind.Type)
+ }
+ }
+ "return" -> {
+ section.appendTypeElement(signature) { it.detailOrNull(NodeKind.Type) }
+ }
+ }
+ section.appendAll(convertJavadocElements(tag.contentElements(), context))
+ }
+ }
+ }
+
docComment.tags.forEach { tag ->
- when(tag.name) {
+ when (tag.name) {
"see" -> result.convertSeeTag(tag)
"deprecated" -> {
- deprecatedContent = Content()
- deprecatedContent!!.convertJavadocElements(tag.contentElements())
+ deprecatedContent = Content().apply {
+ appendAll(convertJavadocElements(tag.contentElements(), element))
+ }
}
+ in tagsToInherit -> {}
else -> {
val subjectName = tag.getSubjectName()
val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName)
- section.convertJavadocElements(tag.contentElements())
+ section.appendAll(convertJavadocElements(tag.contentElements(), element))
}
}
}
return JavadocParseResult(result, deprecatedContent)
}
+ private val tagsToInherit = setOf("param", "return", "throws")
+
+ private data class TagWithContext(val tag: PsiDocTag, val context: PsiNamedElement)
+
+ private fun PsiMethod.searchInheritedTags(): Map<String, Collection<TagWithContext>> {
+
+ val output = tagsToInherit.keysToMap { mutableMapOf<String?, TagWithContext>() }
+
+ fun recursiveSearch(methods: Array<PsiMethod>) {
+ for (method in methods) {
+ recursiveSearch(method.findSuperMethods())
+ }
+ for (method in methods) {
+ for (tag in method.docComment?.tags.orEmpty()) {
+ if (tag.name in tagsToInherit) {
+ output[tag.name]!![tag.getSubjectName()] = TagWithContext(tag, method)
+ }
+ }
+ }
+ }
+
+ recursiveSearch(arrayOf(this))
+ return output.mapValues { it.value.values }
+ }
+
+
private fun PsiDocTag.contentElements(): Iterable<PsiElement> {
val tagValueElements = children
- .dropWhile { it.node?.elementType == JavaDocTokenType.DOC_TAG_NAME }
- .dropWhile { it is PsiWhiteSpace }
- .filterNot { it.node?.elementType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS }
+ .dropWhile { it.node?.elementType == JavaDocTokenType.DOC_TAG_NAME }
+ .dropWhile { it is PsiWhiteSpace }
+ .filterNot { it.node?.elementType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS }
return if (getSubjectName() != null) tagValueElements.dropWhile { it is PsiDocTagValue } else tagValueElements
}
- private fun ContentBlock.convertJavadocElements(elements: Iterable<PsiElement>) {
+ private fun convertJavadocElements(elements: Iterable<PsiElement>, element: PsiNamedElement): List<ContentNode> {
+ val doc = Jsoup.parse(expandAllForElements(elements, element))
+ return doc.body().childNodes().mapNotNull {
+ convertHtmlNode(it)
+ }
+ }
+
+ private fun ContentBlock.appendAll(nodes: List<ContentNode>) {
+ nodes.forEach { append(it) }
+ }
+
+ private fun expandAllForElements(elements: Iterable<PsiElement>, element: PsiNamedElement): String {
val htmlBuilder = StringBuilder()
elements.forEach {
if (it is PsiInlineDocTag) {
- htmlBuilder.append(convertInlineDocTag(it))
+ htmlBuilder.append(convertInlineDocTag(it, element))
} else {
htmlBuilder.append(it.text)
}
}
- val doc = Jsoup.parse(htmlBuilder.toString().trim())
- doc.body().childNodes().forEach {
- convertHtmlNode(it)
- }
+ return htmlBuilder.toString().trim()
}
- private fun ContentBlock.convertHtmlNode(node: Node) {
+ private fun convertHtmlNode(node: Node, insidePre: Boolean = false): ContentNode? {
if (node is TextNode) {
- append(ContentText(node.text()))
+ val text = if (insidePre) node.wholeText else node.text()
+ return ContentText(text)
} else if (node is Element) {
- val childBlock = createBlock(node)
+ val childBlock = createBlock(node, insidePre)
+
node.childNodes().forEach {
- childBlock.convertHtmlNode(it)
+ val child = convertHtmlNode(it, insidePre || childBlock is ContentBlockCode)
+ if (child != null) {
+ childBlock.append(child)
+ }
}
- append(childBlock)
+ return childBlock
}
+ return null
}
- private fun createBlock(element: Element): ContentBlock = when(element.tagName()) {
+ private fun createBlock(element: Element, insidePre: Boolean): ContentBlock = when (element.tagName()) {
"p" -> ContentParagraph()
"b", "strong" -> ContentStrong()
"i", "em" -> ContentEmphasis()
"s", "del" -> ContentStrikethrough()
- "code" -> ContentCode()
+ "code" -> if (insidePre) ContentBlock() else ContentCode()
"pre" -> ContentBlockCode()
"ul" -> ContentUnorderedList()
"ol" -> ContentOrderedList()
@@ -99,45 +189,73 @@ class JavadocParser(private val refGraph: NodeReferenceGraph,
}
private fun createLink(element: Element): ContentBlock {
- val docref = element.attr("docref")
- if (docref != null) {
- return ContentNodeLazyLink(docref, { -> refGraph.lookupOrWarn(docref, logger)})
- }
- val href = element.attr("href")
- if (href != null) {
- return ContentExternalLink(href)
- } else {
- return ContentBlock()
+ return when {
+ element.hasAttr("docref") -> {
+ val docref = element.attr("docref")
+ ContentNodeLazyLink(docref, { -> refGraph.lookupOrWarn(docref, logger) })
+ }
+ element.hasAttr("href") -> {
+ val href = element.attr("href")
+
+ val uri = try {
+ URI(href)
+ } catch (_: Exception) {
+ null
+ }
+
+ if (uri?.isAbsolute == false) {
+ ContentLocalLink(href)
+ } else {
+ ContentExternalLink(href)
+ }
+ }
+ element.hasAttr("name") -> {
+ ContentBookmark(element.attr("name"))
+ }
+ else -> ContentBlock()
}
}
private fun MutableContent.convertSeeTag(tag: PsiDocTag) {
- val linkElement = tag.linkElement()
- if (linkElement == null) {
- return
- }
+ val linkElement = tag.linkElement() ?: return
val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null)
- val linkSignature = resolveLink(linkElement)
+
+ val valueElement = tag.referenceElement()
+ val externalLink = resolveExternalLink(valueElement)
val text = ContentText(linkElement.text)
- if (linkSignature != null) {
- val linkNode = ContentNodeLazyLink(tag.valueElement!!.text, { -> refGraph.lookupOrWarn(linkSignature, logger)})
- linkNode.append(text)
- seeSection.append(linkNode)
- } else {
- seeSection.append(text)
+
+ val linkSignature by lazy { resolveInternalLink(valueElement) }
+ val node = when {
+ externalLink != null -> {
+ val linkNode = ContentExternalLink(externalLink)
+ linkNode.append(text)
+ linkNode
+ }
+ linkSignature != null -> {
+ val linkNode =
+ ContentNodeLazyLink(
+ (tag.valueElement ?: linkElement).text,
+ { -> refGraph.lookupOrWarn(linkSignature, logger) }
+ )
+ linkNode.append(text)
+ linkNode
+ }
+ else -> text
}
+ seeSection.append(node)
}
- private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) {
+ private fun convertInlineDocTag(tag: PsiInlineDocTag, element: PsiNamedElement) = when (tag.name) {
"link", "linkplain" -> {
- val valueElement = tag.linkElement()
- val linkSignature = resolveLink(valueElement)
- if (linkSignature != null) {
+ val valueElement = tag.referenceElement()
+ val externalLink = resolveExternalLink(valueElement)
+ val linkSignature by lazy { resolveInternalLink(valueElement) }
+ if (externalLink != null || linkSignature != null) {
val labelText = tag.dataElements.firstOrNull { it is PsiDocToken }?.text ?: valueElement!!.text
- val link = "<a docref=\"$linkSignature\">${labelText.htmlEscape()}</a>"
+ val linkTarget = if (externalLink != null) "href=\"$externalLink\"" else "docref=\"$linkSignature\""
+ val link = "<a $linkTarget>${labelText.htmlEscape()}</a>"
if (tag.name == "link") "<code>$link</code>" else link
- }
- else if (valueElement != null) {
+ } else if (valueElement != null) {
valueElement.text
} else {
""
@@ -149,16 +267,45 @@ class JavadocParser(private val refGraph: NodeReferenceGraph,
val escaped = text.toString().trimStart().htmlEscape()
if (tag.name == "code") "<code>$escaped</code>" else escaped
}
+ "inheritDoc" -> {
+ val result = (element as? PsiMethod)?.let {
+ // @{inheritDoc} is only allowed on functions
+ val parent = tag.parent
+ when (parent) {
+ is PsiDocComment -> element.findSuperDocCommentOrWarn()
+ is PsiDocTag -> element.findSuperDocTagOrWarn(parent)
+ else -> null
+ }
+ }
+ result ?: tag.text
+ }
else -> tag.text
}
+ private fun PsiDocTag.referenceElement(): PsiElement? =
+ linkElement()?.let {
+ if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) {
+ PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java)
+ } else {
+ it
+ }
+ }
+
private fun PsiDocTag.linkElement(): PsiElement? =
- valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace }
+ valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace }
+
+ private fun resolveExternalLink(valueElement: PsiElement?): String? {
+ val target = valueElement?.reference?.resolve()
+ if (target != null) {
+ return externalDocumentationLinkResolver.buildExternalDocumentationLink(target)
+ }
+ return null
+ }
- private fun resolveLink(valueElement: PsiElement?): String? {
+ private fun resolveInternalLink(valueElement: PsiElement?): String? {
val target = valueElement?.reference?.resolve()
if (target != null) {
- return getSignature(target)
+ return signatureProvider.signature(target)
}
return null
}
@@ -169,4 +316,88 @@ class JavadocParser(private val refGraph: NodeReferenceGraph,
}
return null
}
+
+ private fun PsiMethod.findSuperDocCommentOrWarn(): String {
+ val method = findFirstSuperMethodWithDocumentation(this)
+ if (method != null) {
+ val descriptionElements = method.docComment?.descriptionElements?.dropWhile {
+ it.text.trim().isEmpty()
+ } ?: return ""
+
+ return expandAllForElements(descriptionElements, method)
+ }
+ logger.warn("No docs found on supertype with {@inheritDoc} method ${this.name} in ${this.containingFile.name}:${this.lineNumber()}")
+ return ""
+ }
+
+
+ private fun PsiMethod.findSuperDocTagOrWarn(elementToExpand: PsiDocTag): String {
+ val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, this)
+
+ if (result != null) {
+ val (method, tag) = result
+
+ val contentElements = tag.contentElements().dropWhile { it.text.trim().isEmpty() }
+
+ val expandedString = expandAllForElements(contentElements, method)
+
+ return expandedString
+ }
+ logger.warn("No docs found on supertype for @${elementToExpand.name} ${elementToExpand.getSubjectName()} with {@inheritDoc} method ${this.name} in ${this.containingFile.name}:${this.lineNumber()}")
+ return ""
+ }
+
+ private fun findFirstSuperMethodWithDocumentation(current: PsiMethod): PsiMethod? {
+ val superMethods = current.findSuperMethods()
+ for (method in superMethods) {
+ val docs = method.docComment?.descriptionElements?.dropWhile { it.text.trim().isEmpty() }
+ if (!docs.isNullOrEmpty()) {
+ return method
+ }
+ }
+ for (method in superMethods) {
+ val result = findFirstSuperMethodWithDocumentation(method)
+ if (result != null) {
+ return result
+ }
+ }
+
+ return null
+ }
+
+ private fun findFirstSuperMethodWithDocumentationforTag(elementToExpand: PsiDocTag, current: PsiMethod): Pair<PsiMethod, PsiDocTag>? {
+ val superMethods = current.findSuperMethods()
+ val mappedFilteredTags = superMethods.map {
+ it to it.docComment?.tags?.filter { it.name == elementToExpand.name }
+ }
+
+ for ((method, tags) in mappedFilteredTags) {
+ tags ?: continue
+ for (tag in tags) {
+ val (tagSubject, elementSubject) = when (tag.name) {
+ "throws" -> {
+ // match class names only for throws, ignore possibly fully qualified path
+ // TODO: Always match exactly here
+ tag.getSubjectName()?.split(".")?.last() to elementToExpand.getSubjectName()?.split(".")?.last()
+ }
+ else -> {
+ tag.getSubjectName() to elementToExpand.getSubjectName()
+ }
+ }
+
+ if (tagSubject == elementSubject) {
+ return method to tag
+ }
+ }
+ }
+
+ for (method in superMethods) {
+ val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, method)
+ if (result != null) {
+ return result
+ }
+ }
+ return null
+ }
+
}