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

Fix resolving DRIs of Enum Entries #2305

Merged
merged 6 commits into from Jan 27, 2022
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/api/core.api
Expand Up @@ -503,6 +503,7 @@ public final class org/jetbrains/dokka/links/DRIKt {
public static final fun getSureClassNames (Lorg/jetbrains/dokka/links/DRI;)Ljava/lang/String;
public static final fun nextTarget (Lorg/jetbrains/dokka/links/DriTarget;)Lorg/jetbrains/dokka/links/DriTarget;
public static final fun withClass (Lorg/jetbrains/dokka/links/DRI;Ljava/lang/String;)Lorg/jetbrains/dokka/links/DRI;
public static final fun withEnumEntryExtra (Lorg/jetbrains/dokka/links/DRI;)Lorg/jetbrains/dokka/links/DRI;
public static final fun withTargetToDeclaration (Lorg/jetbrains/dokka/links/DRI;)Lorg/jetbrains/dokka/links/DRI;
}

Expand Down
12 changes: 11 additions & 1 deletion core/src/main/kotlin/links/DRI.kt
Expand Up @@ -51,9 +51,19 @@ fun DRI.withClass(name: String) = copy(classNames = if (classNames.isNullOrBlank

fun DRI.withTargetToDeclaration() = copy(target = PointingToDeclaration)

fun DRI.withEnumEntryExtra() = copy(
extra = DRIExtraContainer(this.extra).also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode()
)

val DRI.parent: DRI
get() = when {
extra != null -> copy(extra = null)
extra != null -> when {
DRIExtraContainer(extra)[EnumEntryDRIExtra] != null -> copy(
classNames = classNames?.substringBeforeLast(".", "")?.takeIf { it.isNotBlank() },
extra = null
)
else -> copy(extra = null)
}
target != PointingToDeclaration -> copy(target = PointingToDeclaration)
callable != null -> copy(callable = null)
classNames != null -> copy(classNames = classNames.substringBeforeLast(".", "").takeIf { it.isNotBlank() })
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/utilities/Html.kt
Expand Up @@ -7,9 +7,9 @@ import java.net.URLEncoder
* Replaces symbols reserved in HTML with their respective entities.
* Replaces & with &amp;, < with &lt; and > with &gt;
*/
fun String.htmlEscape(): String = replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
fun String.htmlEscape(): String = replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")

fun String.urlEncoded(): String = URLEncoder.encode(this, "UTF-8")

fun String.formatToEndWithHtml() =
if (endsWith(".html") || contains(Regex("\\.html#"))) this else "$this.html"
if (endsWith(".html") || contains(Regex("\\.html#"))) this else "$this.html"
Expand Up @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import org.jetbrains.kotlin.resolve.descriptorUtil.parentsWithSelf
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.addToStdlib.safeAs

fun DRI.Companion.from(descriptor: DeclarationDescriptor) = descriptor.parentsWithSelf.run {
val parameter = firstIsInstanceOrNull<ValueParameterDescriptor>()
Expand All @@ -20,7 +21,7 @@ fun DRI.Companion.from(descriptor: DeclarationDescriptor) = descriptor.parentsWi
?.joinToString(separator = ".") { it.name.asString() },
callable = callable?.let { Callable.from(it) },
target = DriTarget.from(parameter ?: descriptor),
extra = if (descriptor is EnumEntrySyntheticClassDescriptor)
extra = if (descriptor is EnumEntrySyntheticClassDescriptor || descriptor.safeAs<ClassDescriptor>()?.kind == ClassKind.ENUM_ENTRY)
DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode()
else null
)
Expand All @@ -31,11 +32,16 @@ fun DRI.Companion.from(psi: PsiElement) = psi.parentsWithSelf.run {
val psiField = firstIsInstanceOrNull<PsiField>()
val classes = filterIsInstance<PsiClass>().filterNot { it is PsiTypeParameter }
.toList() // We only want exact PsiClass types, not PsiTypeParameter subtype
val additionalClasses = if (psi is PsiEnumConstant) listOfNotNull(psiField?.name) else emptyList()
DRI(
packageName = classes.lastOrNull()?.qualifiedName?.substringBeforeLast('.', "") ?: "",
classNames = classes.toList().takeIf { it.isNotEmpty() }?.asReversed()?.mapNotNull { it.name }
?.joinToString("."),
callable = psiMethod?.let { Callable.from(it) } ?: psiField?.let { Callable.from(it) },
classNames = (additionalClasses + classes.mapNotNull { it.name }).takeIf { it.isNotEmpty() }
?.asReversed()?.joinToString("."),
// The fallback strategy test whether psi is not `PsiEnumConstant`. The reason behind this is that
// we need unified DRI for both Java and Kotlin enums, so we can link them properly and treat them alike.
// To achieve that, we append enum name to classNames list and leave the callable part set to null. For Kotlin enums
// it is by default, while for Java enums we have to explicitly test for that in this `takeUnless` condition.
callable = psiMethod?.let { Callable.from(it) } ?: psiField?.takeUnless { psi is PsiEnumConstant }?.let { Callable.from(it) },
BarkingBad marked this conversation as resolved.
Show resolved Hide resolved
target = DriTarget.from(psi),
extra = if (psi is PsiEnumConstant)
DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode()
Expand Down
Expand Up @@ -30,13 +30,10 @@ open class JavadocExternalLocationProvider(
return "$docWithModule$packageLink/package-summary$extension".htmlEscape()
}

// in Kotlin DRI of enum entry is not callable
if (DRIExtraContainer(extra)[EnumEntryDRIExtra] != null) {
val (classSplit, enumEntityAnchor) = if (callable == null) {
val lastIndex = classNames?.lastIndexOf(".") ?: 0
val lastIndex = classNames?.lastIndexOf(".") ?: 0
val (classSplit, enumEntityAnchor) =
classNames?.substring(0, lastIndex) to classNames?.substring(lastIndex + 1)
} else
classNames to callable?.name

val classLink =
if (packageLink == null) "${classSplit}$extension" else "$packageLink/${classSplit}$extension"
Expand Down
Expand Up @@ -306,7 +306,7 @@ private class DokkaDescriptorVisitor(
val classlikes = async { descriptorsWithKind.classlikes.visitClasslikes(driWithPlatform) }

DEnumEntry(
dri = driWithPlatform.dri,
dri = driWithPlatform.dri.withEnumEntryExtra(),
name = descriptor.name.asString(),
documentation = descriptor.resolveDescriptorData(),
functions = functions.await(),
Expand Down
Expand Up @@ -21,11 +21,11 @@ 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.base.translators.unquotedValue
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.links.nextTarget
import org.jetbrains.dokka.links.withClass
import org.jetbrains.dokka.links.*
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.model.AnnotationTarget
import org.jetbrains.dokka.model.Nullable
import org.jetbrains.dokka.model.TypeConstructor
import org.jetbrains.dokka.model.doc.DocumentationNode
import org.jetbrains.dokka.model.doc.Param
import org.jetbrains.dokka.model.properties.PropertyContainer
Expand Down Expand Up @@ -269,7 +269,7 @@ class DefaultPsiToDocumentableTranslator(
name.orEmpty(),
fields.filterIsInstance<PsiEnumConstant>().map { entry ->
DEnumEntry(
dri.withClass(entry.name),
dri.withClass(entry.name).withEnumEntryExtra(),
entry.name,
javadocParser.parseDocumentation(entry).toSourceSetDependent(),
null,
Expand Down
Expand Up @@ -326,7 +326,7 @@ class JavadocParser(
dri.toString()
} ?: UNRESOLVED_PSI_ELEMENT

return """<a data-dri="$dri">${label.ifBlank{ defaultLabel().text }}</a>"""
return """<a data-dri="${dri.htmlEscape()}">${label.ifBlank{ defaultLabel().text }}</a>"""
}

private fun convertInlineDocTag(
Expand Down
4 changes: 2 additions & 2 deletions plugins/base/src/test/kotlin/enums/EnumsTest.kt
Expand Up @@ -125,7 +125,7 @@ class EnumsTest : BaseAbstractTest() {
pagesGenerationStage = { module ->
val entryPage = module.dfs { it.name == "E1" } as ClasslikePageNode
val signaturePart = (entryPage.content.dfs {
it is ContentGroup && it.dci.toString() == "[enums/Test.E1///PointingToDeclaration/][Symbol]"
it is ContentGroup && it.dci.toString() == "[enums/Test.E1///PointingToDeclaration/{\"org.jetbrains.dokka.links.EnumEntryDRIExtra\":{\"key\":\"org.jetbrains.dokka.links.EnumEntryDRIExtra\"}}][Symbol]"
} as ContentGroup)
assertEquals("(\"e1\", 1, true)", signaturePart.constructorSignature())
}
Expand Down Expand Up @@ -202,7 +202,7 @@ class EnumsTest : BaseAbstractTest() {
) {
pagesTransformationStage = { m ->
val entryNode = m.children.first { it.name == "enums" }.children.first { it.name == "Test" }.children.firstIsInstance<ClasslikePageNode>()
val signature = (entryNode.content as ContentGroup).dfs { it is ContentGroup && it.dci.toString() == "[enums/Test.E1///PointingToDeclaration/][Cover]" } as ContentGroup
val signature = (entryNode.content as ContentGroup).dfs { it is ContentGroup && it.dci.toString() == "[enums/Test.E1///PointingToDeclaration/{\"org.jetbrains.dokka.links.EnumEntryDRIExtra\":{\"key\":\"org.jetbrains.dokka.links.EnumEntryDRIExtra\"}}][Cover]" } as ContentGroup

signature.assertNode {
header(1) { +"E1" }
Expand Down
117 changes: 117 additions & 0 deletions plugins/base/src/test/kotlin/linking/EnumValuesLinkingTest.kt
@@ -0,0 +1,117 @@
package linking

import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest
import org.jetbrains.dokka.links.DRIExtraContainer
import org.jetbrains.dokka.links.EnumEntryDRIExtra
import org.jetbrains.dokka.model.dfs
import org.jetbrains.dokka.model.doc.DocumentationLink
import org.jetbrains.dokka.pages.ContentDRILink
import org.jetbrains.dokka.pages.ContentPage
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import org.jsoup.Jsoup
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.nio.file.Paths
import utils.TestOutputWriterPlugin
import java.lang.AssertionError

class EnumValuesLinkingTest : BaseAbstractTest() {

@Test
fun `check if enum values are correctly linked`() {
val writerPlugin = TestOutputWriterPlugin()
val testDataDir = getTestDataDir("linking").toAbsolutePath()
testFromData(
dokkaConfiguration {
sourceSets {
sourceSet {
sourceRoots = listOf(Paths.get("$testDataDir/jvmMain/kotlin").toString())
analysisPlatform = "jvm"
name = "jvm"
}
}
},
pluginOverrides = listOf(writerPlugin)
) {
documentablesTransformationStage = {
val classlikes = it.packages.single().children
assertEquals(4, classlikes.size)

val javaLinker = classlikes.single { it.name == "JavaLinker" }
javaLinker.documentation.values.single().children.run {
when (val kotlinLink = this[0].children[1].children[1]) {
is DocumentationLink -> kotlinLink.dri.run {
assertEquals("KotlinEnum.ON_CREATE", this.classNames)
assertEquals(null, this.callable)
assertNotNull(DRIExtraContainer(extra)[EnumEntryDRIExtra])
}
else -> throw AssertionError("Link node is not DocumentationLink type")
}

when (val javaLink = this[0].children[2].children[1]) {
is DocumentationLink -> javaLink.dri.run {
assertEquals("JavaEnum.ON_DECEIT", this.classNames)
assertEquals(null, this.callable)
assertNotNull(DRIExtraContainer(extra)[EnumEntryDRIExtra])
}
else -> throw AssertionError("Link node is not DocumentationLink type")
}
}

val kotlinLinker = classlikes.single { it.name == "KotlinLinker" }
kotlinLinker.documentation.values.single().children.run {
when (val kotlinLink = this[0].children[0].children[5]) {
is DocumentationLink -> kotlinLink.dri.run {
assertEquals("KotlinEnum.ON_CREATE", this.classNames)
assertEquals(null, this.callable)
assertNotNull(DRIExtraContainer(extra)[EnumEntryDRIExtra])
}
else -> throw AssertionError("Link node is not DocumentationLink type")
}

when (val javaLink = this[0].children[0].children[9]) {
is DocumentationLink -> javaLink.dri.run {
assertEquals("JavaEnum.ON_DECEIT", this.classNames)
assertEquals(null, this.callable)
assertNotNull(DRIExtraContainer(extra)[EnumEntryDRIExtra])
}
else -> throw AssertionError("Link node is not DocumentationLink type")
}
}

assertEquals(
javaLinker.documentation.values.single().children[0].children[1].children[1].safeAs<DocumentationLink>()?.dri,
kotlinLinker.documentation.values.single().children[0].children[0].children[5].safeAs<DocumentationLink>()?.dri
)

assertEquals(
javaLinker.documentation.values.single().children[0].children[2].children[1].safeAs<DocumentationLink>()?.dri,
kotlinLinker.documentation.values.single().children[0].children[0].children[9].safeAs<DocumentationLink>()?.dri
)
}

renderingStage = { rootPageNode, _ ->
val classlikes = rootPageNode.children.single().children
assertEquals(4, classlikes.size)

val javaLinker = classlikes.single { it.name == "JavaLinker" }
(javaLinker as ContentPage).run {
assertNotNull(content.dfs { it is ContentDRILink && it.address.classNames == "KotlinEnum.ON_CREATE" })
assertNotNull(content.dfs { it is ContentDRILink && it.address.classNames == "JavaEnum.ON_DECEIT" })
}

val kotlinLinker = classlikes.single { it.name == "KotlinLinker" }
(kotlinLinker as ContentPage).run {
assertNotNull(content.dfs { it is ContentDRILink && it.address.classNames == "KotlinEnum.ON_CREATE" })
assertNotNull(content.dfs { it is ContentDRILink && it.address.classNames == "JavaEnum.ON_DECEIT" })
}

// single method will throw an exception if there is no single element (0 or 2+)
Jsoup.parse(writerPlugin.writer.contents["root/linking.source/-java-linker/index.html"]).select("a[href=\"../-kotlin-enum/-o-n_-c-r-e-a-t-e/index.html\"]").single()
Jsoup.parse(writerPlugin.writer.contents["root/linking.source/-java-linker/index.html"]).select("a[href=\"../-java-enum/-o-n_-d-e-c-e-i-t/index.html\"]").single()
Jsoup.parse(writerPlugin.writer.contents["root/linking.source/-kotlin-linker/index.html"]).select("a[href=\"../-kotlin-enum/-o-n_-c-r-e-a-t-e/index.html\"]").single()
Jsoup.parse(writerPlugin.writer.contents["root/linking.source/-kotlin-linker/index.html"]).select("a[href=\"../-java-enum/-o-n_-d-e-c-e-i-t/index.html\"]").single()
}
}
}
}
Expand Up @@ -45,8 +45,8 @@ class JavadocExternalLocationProviderTest : BaseAbstractTest() {
)
val javaDri = DRI(
"java.nio.file",
"StandardOpenOption",
Callable("CREATE", null, emptyList()),
"StandardOpenOption.CREATE",
null,
PointingToDeclaration,
DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode()
)
Expand Down
4 changes: 2 additions & 2 deletions plugins/base/src/test/kotlin/model/JavaTest.kt
Expand Up @@ -391,8 +391,8 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") {
"RUNTIME",
DRI(
"java.lang.annotation",
"RetentionPolicy",
DRICallable("RUNTIME", null, emptyList()),
"RetentionPolicy.RUNTIME",
null,
PointingToDeclaration,
DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode()
)
Expand Down
@@ -0,0 +1,5 @@
package linking.source;

public enum JavaEnum {
ON_DECEIT, ON_DESTROY;
}
@@ -0,0 +1,8 @@
package linking.source;

/**
* Reference link {@link linking.source.KotlinEnum} should resolve <p>
* sjuff sjuff {@link linking.source.KotlinEnum#ON_CREATE} should resolve <p>
* sjujj sjujj {@link linking.source.JavaEnum#ON_DECEIT} should resolve
*/
public class JavaLinker {}
@@ -0,0 +1,5 @@
package linking.source

enum class KotlinEnum {
ON_CREATE, ON_CATASTROPHE
}
@@ -0,0 +1,8 @@
package linking.source

/**
* Reference link [KotlinEnum] should resolve <p>
* stuff stuff [KotlinEnum.ON_CREATE] should resolve <p>
* stuff stuff [JavaEnum.ON_DECEIT] should resolve
*/
class KotlinLinker {}
Expand Up @@ -149,13 +149,13 @@ internal class JavadocClasslikeTemplateMapTest : AbstractJavadocTemplateMapTest(
val map = allPagesOfType<JavadocClasslikePageNode>().first { it.name == "TestClass" }.templateMap
assertEquals("TestClass", map["name"])
val signature = assertIsInstance<Map<String, Any?>>(map["signature"])
assertEquals("@<a href=Author.html>Author</a>(name = \"Benjamin Franklin\")", signature["annotations"])
assertEquals("@<a href=Author.html>Author</a>(name = &quot;Benjamin Franklin&quot;)", signature["annotations"])

val methods = assertIsInstance<Map<Any, Any?>>(map["methods"])
val ownMethods = assertIsInstance<List<*>>(methods["own"])
val method = assertIsInstance<Map<String, Any?>>(ownMethods.single())
val methodSignature = assertIsInstance<Map<String, Any?>>(method["signature"])
assertEquals("@<a href=Author.html>Author</a>(name = \"Franklin D. Roosevelt\")", methodSignature["annotations"])
assertEquals("@<a href=Author.html>Author</a>(name = &quot;Franklin D. Roosevelt&quot;)", methodSignature["annotations"])
}
}

Expand Down