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) --- .../base/src/main/kotlin/parsers/MarkdownParser.kt | 20 +- .../psi/DefaultPsiToDocumentableTranslator.kt | 2 + .../main/kotlin/translators/psi/JavadocParser.kt | 395 --------------------- .../main/kotlin/translators/psi/PsiInheritance.kt | 47 +++ .../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 + 9 files changed, 651 insertions(+), 403 deletions(-) delete mode 100644 plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt create mode 100644 plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt 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') diff --git a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt index f496d704..0b9a5c23 100644 --- a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt +++ b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt @@ -42,10 +42,11 @@ open class MarkdownParser( when (tagName) { "see" -> { val referencedName = content.substringBefore(' ') + val dri = externalDri(referencedName) See( parseStringToDocNode(content.substringAfter(' ')), - referencedName, - externalDri(referencedName) + dri?.fqName() ?: referencedName, + dri ) } "throws", "exception" -> { @@ -481,11 +482,14 @@ open class MarkdownParser( ) KDocKnownTag.RECEIVER -> Receiver(parseStringToDocNode(it.getContent())) KDocKnownTag.RETURN -> Return(parseStringToDocNode(it.getContent())) - KDocKnownTag.SEE -> See( - parseStringToDocNode(it.getContent()), - it.getSubjectName().orEmpty(), - pointedLink(it), - ) + KDocKnownTag.SEE -> { + val dri = pointedLink(it) + See( + parseStringToDocNode(it.getContent()), + dri?.fqName() ?: it.getSubjectName().orEmpty(), + dri, + ) + } KDocKnownTag.SINCE -> Since(parseStringToDocNode(it.getContent())) KDocKnownTag.CONSTRUCTOR -> Constructor(parseStringToDocNode(it.getContent())) KDocKnownTag.PROPERTY -> Property( @@ -504,7 +508,7 @@ open class MarkdownParser( } //Horrible hack but since link resolution is passed as a function i am not able to resolve them otherwise - fun DRI.fqName(): String = "$packageName.$classNames" + fun DRI.fqName(): String? = "$packageName.$classNames".takeIf { packageName != null && classNames != null } private fun findParent(kDoc: PsiElement): PsiElement = if (kDoc is KDocSection) findParent(kDoc.parent) else kDoc diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index 1f52f373..15ef24bb 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -15,6 +15,8 @@ import org.jetbrains.dokka.analysis.PsiDocumentableSource import org.jetbrains.dokka.analysis.from import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.translators.isDirectlyAnException +import org.jetbrains.dokka.base.translators.psi.parsers.JavaDocumentationParser +import org.jetbrains.dokka.base.translators.psi.parsers.JavadocParser import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.links.nextTarget import org.jetbrains.dokka.links.withClass diff --git a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt deleted file mode 100644 index 782f792a..00000000 --- a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt +++ /dev/null @@ -1,395 +0,0 @@ -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.base.parsers.factories.DocTagsFromStringFactory -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.psi.psiUtil.getNextSiblingIgnoringWhitespace -import org.jetbrains.kotlin.psi.psiUtil.siblings -import org.jetbrains.kotlin.tools.projectWizard.core.ParsingState -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.contentElementsWithSiblingIfNeeded().drop(1))), - tag.dataElements.firstOrNull()?.text.orEmpty() - ) - "throws" -> { - val resolved = tag.resolveException() - val dri = resolved?.let { DRI.from(it) } - Throws( - root = wrapTagIfNecessary(convertJavadocElements(tag.dataElements.drop(1))), - /* 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 = resolved?.getKotlinFqName()?.asString() - ?: tag.dataElements.firstOrNull()?.text.orEmpty(), - exceptionAddress = dri - ) - } - "return" -> Return(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded()))) - "author" -> Author(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded()))) // 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.contentElementsWithSiblingIfNeeded()))) - else -> null - } - }) - return DocumentationNode(nodes) - } - - private fun PsiDocTag.resolveException(): PsiElement? = - dataElements.firstOrNull()?.firstChild?.referenceElementOrSelf()?.resolveToGetDri() - - 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 data class ParserState( - val previousElement: PsiElement? = null, - val openPreTags: Int = 0, - val closedPreTags: Int = 0 - ) - - private data class ParsingResult(val newState: ParserState = ParserState(), val parsedLine: String? = null) { - operator fun plus(other: ParsingResult): ParsingResult = - ParsingResult( - other.newState, - listOfNotNull(parsedLine, other.parsedLine).joinToString(separator = "") - ) - } - - private inner class Parse : (Iterable, Boolean) -> List { - val driMap = mutableMapOf() - - private fun PsiElement.stringify(state: ParserState): ParsingResult = - when (this) { - is PsiReference -> children.fold(ParsingResult(state)) { acc, e -> acc + e.stringify(acc.newState) } - else -> stringifySimpleElement(state) - } - - private fun PsiElement.stringifySimpleElement(state: ParserState): 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) - 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) = 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)
-                "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): List =
-            elements.fold(ParsingResult()) { acc, e ->
-                acc + e.stringify(acc.newState)
-            }.parsedLine?.let {
-                val trimmed = it.trim()
-                val toParse = if (asParagraph) "

$trimmed

" else trimmed - Jsoup.parseBodyFragment(toParse).body().childNodes().mapNotNull { convertHtmlNode(it) } - }.orEmpty() - } - - private 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() - } - - 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) = - resolveToGetDri()?.let { - val dri = DRI.from(it) - val label = labelElement ?: defaultLabel() - DocumentationLink(dri, convertJavadocElements(listOfNotNull(label), asParagraph = false)) - } - - private fun PsiElement.resolveToGetDri(): PsiElement? = - reference?.resolve() - - private fun PsiDocTag.referenceElement(): PsiElement? = - linkElement()?.referenceElementOrSelf() - - private fun PsiElement.referenceElementOrSelf(): PsiElement? = - if (node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { - PsiTreeUtil.findChildOfType(this, PsiJavaCodeReferenceElement::class.java) - } else this - - 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" - private const val END_COMMENT_TYPE = "DOC_COMMENT_END" - private const val COMMENT_TYPE = "DOC_COMMENT_DATA" - } -} diff --git a/plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt b/plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt new file mode 100644 index 00000000..dcfbb8e3 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt @@ -0,0 +1,47 @@ +package org.jetbrains.dokka.base.translators.psi + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +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.safeAs + +internal fun PsiClass.implementsInterface(fqName: FqName): Boolean { + return allInterfaces().any { it.getKotlinFqName() == fqName } +} + +internal fun PsiClass.allInterfaces(): Sequence { + return sequence { + this.yieldAll(interfaces.toList()) + interfaces.forEach { yieldAll(it.allInterfaces()) } + } +} + +/** + * 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) + */ +internal fun PsiMethod.findSuperMethodsOrEmptyArray(logger: DokkaLogger): 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() + } +} \ No newline at end of file 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