Skip to content

Commit 86f9559

Browse files
Add documentation for synthetic Enum values() and valueOf() functions (#2650)
1 parent 9207f8f commit 86f9559

15 files changed

Lines changed: 461 additions & 60 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.jetbrains.dokka.it
2+
3+
import java.net.URL
4+
import kotlin.test.Test
5+
6+
class StdLibDocumentationIntegrationTest {
7+
8+
/**
9+
* Documentation for Enum's synthetic values() and valueOf() functions is only present in source code,
10+
* but not present in the descriptors. However, Dokka needs to generate documentation for these functions,
11+
* so it ships with hardcoded kdoc templates.
12+
*
13+
* This test exists to make sure documentation for these hardcoded synthetic functions does not change,
14+
* and fails if it does, indicating that it needs to be updated.
15+
*/
16+
@Test
17+
fun shouldAssertEnumDocumentationHasNotChanged() {
18+
val sourcesLink = "https://raw.githubusercontent.com/JetBrains/kotlin/master/core/builtins/native/kotlin/Enum.kt"
19+
val sources = URL(sourcesLink).readText()
20+
21+
val expectedValuesDoc =
22+
" /**\n" +
23+
" * Returns an array containing the constants of this enum type, in the order they're declared.\n" +
24+
" * This method may be used to iterate over the constants.\n" +
25+
" * @values\n" +
26+
" */"
27+
check(sources.contains(expectedValuesDoc))
28+
29+
val expectedValueOfDoc =
30+
" /**\n" +
31+
" * Returns the enum constant of this type with the specified name. The string must match exactly " +
32+
"an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)\n" +
33+
" * @throws IllegalArgumentException if this enum type has no constant with the specified name\n" +
34+
" * @valueOf\n" +
35+
" */"
36+
check(sources.contains(expectedValueOfDoc))
37+
}
38+
}

integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/kotlin/StdlibGradleIntegrationTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.jetbrains.dokka.it.gradle.AbstractGradleIntegrationTest
77
import org.jetbrains.dokka.it.gradle.BuildVersions
88
import org.junit.runners.Parameterized
99
import java.io.File
10+
import java.net.URL
1011
import kotlin.test.*
1112

1213
class StdlibGradleIntegrationTest(override val versions: BuildVersions) : AbstractGradleIntegrationTest(),

plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ private class DokkaDescriptorVisitor(
136136
private val logger: DokkaLogger
137137
) {
138138
private val javadocParser = JavadocParser(logger, resolutionFacade)
139+
private val syntheticDocProvider = SyntheticDescriptorDocumentationProvider(resolutionFacade)
139140

140141
private fun Collection<DeclarationDescriptor>.filterDescriptorsInSourceSet() = filter {
141142
it.toSourceElement.containingFile.toString().let { path ->
@@ -577,8 +578,7 @@ private class DokkaDescriptorVisitor(
577578
sources = actual,
578579
visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(),
579580
generics = generics.await(),
580-
documentation = descriptor.takeIf { it.kind != CallableMemberDescriptor.Kind.SYNTHESIZED }
581-
?.resolveDescriptorData() ?: emptyMap(),
581+
documentation = descriptor.getDocumentation(),
582582
modifier = descriptor.modifier().toSourceSetDependent(),
583583
type = descriptor.returnType!!.toBound(),
584584
sourceSets = setOf(sourceSet),
@@ -594,6 +594,15 @@ private class DokkaDescriptorVisitor(
594594
}
595595
}
596596

597+
private fun FunctionDescriptor.getDocumentation(): SourceSetDependent<DocumentationNode> {
598+
val isSynthesized = this.kind == CallableMemberDescriptor.Kind.SYNTHESIZED
599+
return if (isSynthesized) {
600+
syntheticDocProvider.getDocumentation(this)?.toSourceSetDependent() ?: emptyMap()
601+
} else {
602+
this.resolveDescriptorData()
603+
}
604+
}
605+
597606
/**
598607
* `createDRI` returns the DRI of the exact element and potential DRI of an element that is overriding it
599608
* (It can be also FAKE_OVERRIDE which is in fact just inheritance of the symbol)
@@ -609,7 +618,7 @@ private class DokkaDescriptorVisitor(
609618

610619
private fun FunctionDescriptor.isObvious(): Boolean {
611620
return kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE
612-
|| kind == CallableMemberDescriptor.Kind.SYNTHESIZED
621+
|| (kind == CallableMemberDescriptor.Kind.SYNTHESIZED && !syntheticDocProvider.isDocumented(this))
613622
|| containingDeclaration.fqNameOrNull()?.isObvious() == true
614623
}
615624

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.jetbrains.dokka.base.translators.descriptors
2+
3+
import org.jetbrains.dokka.analysis.DokkaResolutionFacade
4+
import org.jetbrains.dokka.analysis.from
5+
import org.jetbrains.dokka.base.parsers.MarkdownParser
6+
import org.jetbrains.dokka.links.DRI
7+
import org.jetbrains.dokka.model.doc.DocumentationNode
8+
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
9+
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
10+
import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink
11+
import org.jetbrains.kotlin.resolve.DescriptorFactory
12+
13+
private const val ENUM_VALUEOF_TEMPLATE_PATH = "/dokka/docs/kdoc/EnumValueOf.kt.template"
14+
private const val ENUM_VALUES_TEMPLATE_PATH = "/dokka/docs/kdoc/EnumValues.kt.template"
15+
16+
internal class SyntheticDescriptorDocumentationProvider(
17+
private val resolutionFacade: DokkaResolutionFacade
18+
) {
19+
fun isDocumented(descriptor: DeclarationDescriptor): Boolean = descriptor is FunctionDescriptor
20+
&& (DescriptorFactory.isEnumValuesMethod(descriptor) || DescriptorFactory.isEnumValueOfMethod(descriptor))
21+
22+
fun getDocumentation(descriptor: DeclarationDescriptor): DocumentationNode? {
23+
val function = descriptor as? FunctionDescriptor ?: return null
24+
return when {
25+
DescriptorFactory.isEnumValuesMethod(function) -> loadTemplate(descriptor, ENUM_VALUES_TEMPLATE_PATH)
26+
DescriptorFactory.isEnumValueOfMethod(function) -> loadTemplate(descriptor, ENUM_VALUEOF_TEMPLATE_PATH)
27+
else -> null
28+
}
29+
}
30+
31+
private fun loadTemplate(descriptor: DeclarationDescriptor, filePath: String): DocumentationNode? {
32+
val kdoc = loadContent(filePath) ?: return null
33+
val parser = MarkdownParser({ link -> resolveLink(descriptor, link)}, filePath)
34+
return parser.parse(kdoc)
35+
}
36+
37+
private fun loadContent(filePath: String): String? = javaClass.getResource(filePath)?.readText()
38+
39+
private fun resolveLink(descriptor: DeclarationDescriptor, link: String): DRI? =
40+
resolveKDocLink(
41+
resolutionFacade.resolveSession.bindingContext,
42+
resolutionFacade,
43+
descriptor,
44+
null,
45+
link.split('.')
46+
).firstOrNull()?.let { DRI.from(it) }
47+
}

plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import com.intellij.lang.jvm.annotation.JvmAnnotationEnumFieldValue
88
import com.intellij.lang.jvm.types.JvmReferenceType
99
import com.intellij.openapi.vfs.VirtualFileManager
1010
import com.intellij.psi.*
11-
import com.intellij.psi.impl.source.PsiClassReferenceType
12-
import com.intellij.psi.impl.source.PsiImmediateClassType
1311
import kotlinx.coroutines.async
1412
import kotlinx.coroutines.coroutineScope
1513
import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet
@@ -99,8 +97,8 @@ class DefaultPsiToDocumentableTranslator(
9997
facade: DokkaResolutionFacade,
10098
private val logger: DokkaLogger
10199
) {
102-
103-
private val javadocParser: JavaDocumentationParser = JavadocParser(logger, facade)
100+
private val javadocParser = JavadocParser(logger, facade)
101+
private val syntheticDocProvider = SyntheticElementDocumentationProvider(javadocParser, facade)
104102

105103
private val cachedBounds = hashMapOf<String, Bound>()
106104

@@ -404,7 +402,7 @@ class DefaultPsiToDocumentableTranslator(
404402
val dri = parentDRI?.let { dri ->
405403
DRI.from(psi).copy(packageName = dri.packageName, classNames = dri.classNames)
406404
} ?: DRI.from(psi)
407-
val docs = javadocParser.parseDocumentation(psi)
405+
val docs = psi.getDocumentation()
408406
return DFunction(
409407
dri = dri,
410408
name = psi.name,
@@ -452,8 +450,13 @@ class DefaultPsiToDocumentableTranslator(
452450
)
453451
}
454452

453+
private fun PsiMethod.getDocumentation(): DocumentationNode =
454+
this.takeIf { it is SyntheticElement }?.let { syntheticDocProvider.getDocumentation(it) }
455+
?: javadocParser.parseDocumentation(this)
456+
455457
private fun PsiMethod.isObvious(inheritedFrom: DRI? = null): Boolean {
456-
return this is SyntheticElement || inheritedFrom?.isObvious() == true
458+
return (this is SyntheticElement && !syntheticDocProvider.isDocumented(this))
459+
|| inheritedFrom?.isObvious() == true
457460
}
458461

459462
private fun DRI.isObvious(): Boolean {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.jetbrains.dokka.base.translators.psi
2+
3+
import com.intellij.psi.*
4+
import com.intellij.psi.javadoc.PsiDocComment
5+
import org.jetbrains.dokka.analysis.DokkaResolutionFacade
6+
import org.jetbrains.dokka.base.translators.psi.parsers.JavadocParser
7+
import org.jetbrains.dokka.model.doc.DocumentationNode
8+
9+
private const val ENUM_VALUEOF_TEMPLATE_PATH = "/dokka/docs/javadoc/EnumValueOf.java.template"
10+
private const val ENUM_VALUES_TEMPLATE_PATH = "/dokka/docs/javadoc/EnumValues.java.template"
11+
12+
internal class SyntheticElementDocumentationProvider(
13+
private val javadocParser: JavadocParser,
14+
private val resolutionFacade: DokkaResolutionFacade
15+
) {
16+
fun isDocumented(psiElement: PsiElement): Boolean = psiElement is PsiMethod
17+
&& (psiElement.isSyntheticEnumValuesMethod() || psiElement.isSyntheticEnumValueOfMethod())
18+
19+
fun getDocumentation(psiElement: PsiElement): DocumentationNode? {
20+
val psiMethod = psiElement as? PsiMethod ?: return null
21+
val templatePath = when {
22+
psiMethod.isSyntheticEnumValuesMethod() -> ENUM_VALUES_TEMPLATE_PATH
23+
psiMethod.isSyntheticEnumValueOfMethod() -> ENUM_VALUEOF_TEMPLATE_PATH
24+
else -> return null
25+
}
26+
val docComment = loadSyntheticDoc(templatePath) ?: return null
27+
return javadocParser.parseDocComment(docComment, psiElement)
28+
}
29+
30+
private fun loadSyntheticDoc(path: String): PsiDocComment? {
31+
val text = javaClass.getResource(path)?.readText() ?: return null
32+
return JavaPsiFacade.getElementFactory(resolutionFacade.project).createDocCommentFromText(text)
33+
}
34+
}
35+
36+
private fun PsiMethod.isSyntheticEnumValuesMethod() = this.isSyntheticEnumFunction() && this.name == "values"
37+
private fun PsiMethod.isSyntheticEnumValueOfMethod() = this.isSyntheticEnumFunction() && this.name == "valueOf"
38+
private fun PsiMethod.isSyntheticEnumFunction() = this is SyntheticElement && this.containingClass?.isEnum == true
39+

plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,13 @@ class JavadocParser(
5050

5151
override fun parseDocumentation(element: PsiNamedElement): DocumentationNode {
5252
return when(val comment = findClosestDocComment(element, logger)){
53-
is JavaDocComment -> parseDocumentation(comment, element)
53+
is JavaDocComment -> parseDocComment(comment.comment, element)
5454
is KotlinDocComment -> parseDocumentation(comment)
5555
else -> DocumentationNode(emptyList())
5656
}
5757
}
5858

59-
private fun parseDocumentation(element: JavaDocComment, context: PsiNamedElement): DocumentationNode {
60-
val docComment = element.comment
59+
internal fun parseDocComment(docComment: PsiDocComment, context: PsiNamedElement): DocumentationNode {
6160
val nodes = listOfNotNull(docComment.getDescription()) + docComment.tags.mapNotNull { tag ->
6261
parseDocTag(tag, docComment, context)
6362
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Returns the enum constant of this type with the specified
3+
* name.
4+
* The string must match exactly an identifier used to declare
5+
* an enum constant in this type. (Extraneous whitespace
6+
* characters are not permitted.)
7+
*
8+
* @return the enum constant with the specified name
9+
* @throws IllegalArgumentException if this enum type has no
10+
* constant with the specified name
11+
*/
12+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Returns an array containing the constants of this enum
3+
* type, in the order they're declared. This method may be
4+
* used to iterate over the constants.
5+
*
6+
* @return an array containing the constants of this enum
7+
* type, in the order they're declared
8+
*/
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Returns the enum constant of this type with the specified name. The string must match exactly an identifier used
2+
to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)
3+
4+
@throws kotlin.IllegalArgumentException if this enum type has no constant with the specified name

0 commit comments

Comments
 (0)