Skip to content

Commit

Permalink
Javadoc @inheritdoc tag support (#1608)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcinAman committed Nov 12, 2020
1 parent 7db15c3 commit 6a1c05c
Show file tree
Hide file tree
Showing 14 changed files with 979 additions and 220 deletions.
1 change: 1 addition & 0 deletions core/src/main/kotlin/model/doc/TagWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ data class Param(override val root: DocTag, override val name: String) : NamedTa
data class Return(override val root: DocTag) : TagWrapper()
data class Receiver(override val root: DocTag) : TagWrapper()
data class Constructor(override val root: DocTag) : TagWrapper()
//TODO this naming is confusing since kotlin has Throws annotation
data class Throws(override val root: DocTag, override val name: String, val exceptionAddress: DRI?) : NamedTagWrapper()
data class Sample(override val root: DocTag, override val name: String) : NamedTagWrapper()
data class Deprecated(override val root: DocTag) : TagWrapper()
Expand Down
50 changes: 0 additions & 50 deletions core/src/main/kotlin/utilities/nodeDebug.kt

This file was deleted.

4 changes: 4 additions & 0 deletions core/src/main/kotlin/utilities/safeEnumValueOf.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.jetbrains.dokka.utilities

inline fun <reified T : Enum<*>> enumValueOrNull(name: String): T? =
T::class.java.enumConstants.firstOrNull { it.name.toUpperCase() == name.toUpperCase() }
20 changes: 12 additions & 8 deletions plugins/base/src/main/kotlin/parsers/MarkdownParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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" -> {
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 6a1c05c

Please sign in to comment.