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 | |
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')
-rw-r--r-- | plugins/base/src/main/kotlin/parsers/MarkdownParser.kt | 20 | ||||
-rw-r--r-- | plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt | 2 | ||||
-rw-r--r-- | plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt | 47 | ||||
-rw-r--r-- | plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt | 106 | ||||
-rw-r--r-- | plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt (renamed from plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt) | 302 | ||||
-rw-r--r-- | plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt | 20 | ||||
-rw-r--r-- | plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt | 68 | ||||
-rw-r--r-- | plugins/base/src/main/kotlin/translators/psi/parsers/exceptionTag.kt | 15 |
8 files changed, 414 insertions, 166 deletions
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/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<PsiClass> { + 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<PsiMethod> { + 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<PsiClass>()?.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<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/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt index 782f792a..96c62b36 100644 --- a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt @@ -1,25 +1,23 @@ -package org.jetbrains.dokka.base.translators.psi +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 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.dokka.utilities.enumValueOrNull import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName -import org.jetbrains.kotlin.name.FqName +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.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 @@ -30,53 +28,97 @@ interface JavaDocumentationParser { } class JavadocParser( - private val logger: DokkaLogger // TODO: Add logging + private val logger: DokkaLogger ) : JavaDocumentationParser { + private val inheritDocResolver = InheritDocResolver(logger) override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { - val docComment = findClosestDocComment(element) ?: return DocumentationNode(emptyList()) - val nodes = mutableListOf<TagWrapper>() - 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 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))), + 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 = resolved?.getKotlinFqName()?.asString() - ?: tag.dataElements.firstOrNull()?.text.orEmpty(), + name = name, 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 + 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 + ) + } } - "deprecated" -> Deprecated(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded()))) + JavadocTag.DEPRECATED -> Deprecated( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded(), + context = resolutionContext + ) + ) + ) else -> null + //TODO https://github.com/Kotlin/dokka/issues/1618 } - }) - return DocumentationNode(nodes) - } - - private fun PsiDocTag.resolveException(): PsiElement? = - dataElements.firstOrNull()?.firstChild?.referenceElementOrSelf()?.resolveToGetDri() + } private fun wrapTagIfNecessary(list: List<DocTag>): CustomDocTag = if (list.size == 1 && (list.first() as? CustomDocTag)?.name == MarkdownElementTypes.MARKDOWN_FILE.name) @@ -84,96 +126,37 @@ class JavadocParser( 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<PsiDocComment>() - } - - /** - * 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<PsiMethod> { - 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<PsiClass>()?.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<PsiClass> { - return sequence { - this.yieldAll(interfaces.toList()) - interfaces.forEach { yieldAll(it.allInterfaces()) } - } - } - - private fun getSeeTagElementContent(tag: PsiDocTag): Pair<List<DocumentationLink>, DRI?> { - val content = tag.referenceElement()?.toDocumentationLink() - return Pair(listOfNotNull(content), content?.dri) + 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()).takeIf { it.isNotEmpty() }?.let { + 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 = ParserState(), val parsedLine: String? = null) { + 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, @@ -181,21 +164,26 @@ class JavadocParser( ) } - private inner class Parse : (Iterable<PsiElement>, Boolean) -> List<DocTag> { + private inner class Parse : (Iterable<PsiElement>, Boolean, CommentResolutionContext) -> List<DocTag> { val driMap = mutableMapOf<String, DRI>() - private fun PsiElement.stringify(state: ParserState): ParsingResult = + private fun PsiElement.stringify(state: ParserState, context: CommentResolutionContext): ParsingResult = when (this) { - is PsiReference -> children.fold(ParsingResult(state)) { acc, e -> acc + e.stringify(acc.newState) } - else -> stringifySimpleElement(state) + is PsiReference -> children.fold(ParsingResult(state)) { acc, e -> + acc + e.stringify(acc.newState, context) + } + else -> stringifySimpleElement(state, context) } - private fun PsiElement.stringifySimpleElement(state: ParserState): ParsingResult { + 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) + is PsiInlineDocTag -> convertInlineDocTag(this, state.currentJavadocTag, context) is PsiDocParamRef -> toDocumentationLinkString() is PsiDocTagValue, is LeafPsiElement -> { @@ -205,7 +193,8 @@ class JavadocParser( 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 + 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 } @@ -256,9 +245,9 @@ class JavadocParser( val endsWithAnUnclosedTag = lastHtmlTag.endsWith(">") && !lastHtmlTag.startsWith("</") return (nextSibling as? PsiWhiteSpace)?.text == "\n " && - (getNextSiblingIgnoringWhitespace() as? PsiDocToken)?.tokenType?.toString() != END_COMMENT_TYPE && + (getNextSiblingIgnoringWhitespace() as? PsiDocToken)?.tokenType != JavaDocTokenTypes.INSTANCE.commentEnd() && nextNotEmptySibling?.isLeadingAsterisk() == true && - furtherNotEmptySibling?.tokenType?.toString() == COMMENT_TYPE && + furtherNotEmptySibling?.tokenType == JavaDocTokenTypes.INSTANCE.commentData() && !endsWithAnUnclosedTag } @@ -276,13 +265,22 @@ class JavadocParser( return """<a data-dri="$dri">${label.joinToString(" ") { it.text }}</a>""" } - private fun convertInlineDocTag(tag: PsiInlineDocTag) = 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>" - else -> tag.text - } + 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 { @@ -335,9 +333,13 @@ class JavadocParser( else -> null } - override fun invoke(elements: Iterable<PsiElement>, asParagraph: Boolean): List<DocTag> = - elements.fold(ParsingResult()) { acc, e -> - acc + e.stringify(acc.newState) + 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 @@ -345,41 +347,27 @@ class JavadocParser( }.orEmpty() } - private 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() - } - - private fun convertJavadocElements(elements: Iterable<PsiElement>, asParagraph: Boolean = true): List<DocTag> = - Parse()(elements, asParagraph) + private fun convertJavadocElements( + elements: Iterable<PsiElement>, + asParagraph: Boolean = true, + context: CommentResolutionContext + ): List<DocTag> = + Parse()(elements, asParagraph, context) - private fun PsiDocToken.isSharpToken() = tokenType.toString() == "DOC_TAG_VALUE_SHARP_TOKEN" + private fun PsiDocToken.isSharpToken() = tokenType == JavaDocTokenType.DOC_TAG_VALUE_SHARP_TOKEN - private fun PsiDocToken.isLeadingAsterisk() = tokenType.toString() == "DOC_COMMENT_LEADING_ASTERISKS" + private fun PsiDocToken.isLeadingAsterisk() = tokenType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS - private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null) = + 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)) + DocumentationLink(dri, convertJavadocElements(listOfNotNull(label), asParagraph = false, context)) } - 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 @@ -389,7 +377,5 @@ class JavadocParser( 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/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 |