Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javadoc @inheritDoc tag support #1608

Merged
merged 2 commits into from
Nov 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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? =
MarcinAman marked this conversation as resolved.
Show resolved Hide resolved
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't referencedName be used by default and the fqname as a fallback?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to have a fq name every time it is possible because:

/**
* @throws RuntimeException sample runtime exception
* @throws java.lang.IllegalStateException another one
*/
void foo()

If we have such a case then it is nice to unify those names to display them accordingly. I'd like to have a fq name since it is most often unique and can be used to distinguish tags, but i am open for discussion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @@ -14,6 +14,8 @@ import org.jetbrains.dokka.analysis.KotlinAnalysis
import org.jetbrains.dokka.analysis.PsiDocumentableSource
import org.jetbrains.dokka.analysis.from
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)
}