package org.jetbrains.dokka.base.translators.psi 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.LeafPsiElement import com.intellij.psi.javadoc.* import com.intellij.psi.util.PsiTreeUtil 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.kotlin.idea.refactoring.fqName.getKotlinFqName import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import org.jetbrains.kotlin.utils.addToStdlib.safeAs 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 // TODO: Add logging ) : JavaDocumentationParser { override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { val docComment = findClosestDocComment(element) ?: return DocumentationNode(emptyList()) val nodes = mutableListOf() docComment.getDescription()?.let { nodes.add(it) } nodes.addAll(docComment.tags.mapNotNull { tag -> when (tag.name) { "param" -> Param( wrapTagIfNecessary(convertJavadocElements(tag.contentElements())), tag.children.firstIsInstanceOrNull()?.text.orEmpty() ) "throws" -> Throws(wrapTagIfNecessary(convertJavadocElements(tag.contentElements())), tag.text) "return" -> Return(wrapTagIfNecessary(convertJavadocElements(tag.contentElements()))) "author" -> Author(wrapTagIfNecessary(convertJavadocElements(tag.authorContentElements()))) // 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 "see" -> getSeeTagElementContent(tag).let { See( wrapTagIfNecessary(it.first), tag.referenceElement()?.text.orEmpty(), it.second ) } "deprecated" -> Deprecated(wrapTagIfNecessary(convertJavadocElements(tag.dataElements.toList()))) else -> null } }) return DocumentationNode(nodes) } 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 findClosestDocComment(element: PsiNamedElement): PsiDocComment? { (element as? PsiDocCommentOwner)?.docComment?.run { return this } if (element is PsiMethod) { val superMethods = element.findSuperMethodsOrEmptyArray() if (superMethods.isEmpty()) return null if (superMethods.size == 1) { return findClosestDocComment(superMethods.single()) } val superMethodDocumentation = superMethods.map(::findClosestDocComment) 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() } /** * Workaround for failing [PsiMethod.findSuperMethods]. * This might be resolved once ultra light classes are enabled for dokka * See [KT-39518](https://youtrack.jetbrains.com/issue/KT-39518) */ private fun PsiMethod.findSuperMethodsOrEmptyArray(): Array { return try { /* We are not even attempting to call "findSuperMethods" on all methods called "getGetter" or "getSetter" on any object implementing "kotlin.reflect.KProperty", since we know that those methods will fail (KT-39518). Just catching the exception is not good enough, since "findSuperMethods" will print the whole exception to stderr internally and then spoil the console. */ val kPropertyFqName = FqName("kotlin.reflect.KProperty") if ( this.parent?.safeAs()?.implementsInterface(kPropertyFqName) == true && (this.name == "getSetter" || this.name == "getGetter") ) { logger.warn("Skipped lookup of super methods for ${getKotlinFqName()} (KT-39518)") return emptyArray() } findSuperMethods() } catch (exception: Throwable) { logger.warn("Failed to lookup of super methods for ${getKotlinFqName()} (KT-39518)") emptyArray() } } private fun PsiClass.implementsInterface(fqName: FqName): Boolean { return allInterfaces().any { it.getKotlinFqName() == fqName } } private fun PsiClass.allInterfaces(): Sequence { return sequence { this.yieldAll(interfaces.toList()) interfaces.forEach { yieldAll(it.allInterfaces()) } } } private fun getSeeTagElementContent(tag: PsiDocTag): Pair, DRI?> { val content = tag.referenceElement()?.toDocumentationLink() return Pair(listOfNotNull(content), content?.dri) } private fun PsiDocComment.getDescription(): Description? { return convertJavadocElements(descriptionElements.asIterable()).takeIf { it.isNotEmpty() }?.let { Description(wrapTagIfNecessary(it)) } } private inner class Parse : (Iterable, Boolean) -> List { val driMap = mutableMapOf() private fun PsiElement.stringify(): String? = when (this) { is PsiReference -> children.joinToString("") { it.stringify().orEmpty() } is PsiInlineDocTag -> convertInlineDocTag(this) is PsiDocParamRef -> toDocumentationLinkString() is PsiDocTagValue, is LeafPsiElement -> text.let { if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it?.drop(1) else it }.let { if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it?.dropLastWhile { it == ' ' } else it } else -> null } private fun PsiElement.toDocumentationLinkString( labelElement: List? = 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) = when (tag.name) { "link", "linkplain" -> tag.referenceElement() ?.toDocumentationLinkString(tag.dataElements.filterIsInstance()) "code", "literal" -> "${tag.text}" "index" -> "${tag.children.filterIsInstance().joinToString { it.text }}" 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) 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): List = Jsoup.parseBodyFragment(elements.mapNotNull { it.stringify() }.dropWhile { it.isBlank() } .dropLastWhile { it.isBlank() }.joinToString( "", prefix = if (asParagraph) "

" else "", postfix = if (asParagraph) "

" else "" ) ).body().childNodes().mapNotNull { convertHtmlNode(it) } } private fun PsiDocTag.contentElements(): List = dataElements.mapNotNull { it.takeIf { it is PsiDocToken && it.text.isNotBlank() } } private fun PsiDocTag.authorContentElements(): List = listOfNotNull( dataElements[0], dataElements[0].nextSibling?.takeIf { it.text != dataElements.drop(1).firstOrNull()?.text }, *dataElements.drop(1).toTypedArray() ) private fun convertJavadocElements(elements: Iterable, asParagraph: Boolean = true): List = Parse()(elements, asParagraph) private fun PsiDocToken.isSharpToken() = tokenType.toString() == "DOC_TAG_VALUE_SHARP_TOKEN" private fun PsiDocToken.isLeadingAsterisk() = tokenType.toString() == "DOC_COMMENT_LEADING_ASTERISKS" private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null) = reference?.resolve()?.let { val dri = DRI.from(it) val label = labelElement ?: defaultLabel() DocumentationLink(dri, convertJavadocElements(listOfNotNull(label), asParagraph = false)) } 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 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" } }