Skip to content

Commit

Permalink
Do not leak implementation details in generated Javadoc links (#2813)
Browse files Browse the repository at this point in the history
Fixes #2803
  • Loading branch information
IgnatBeresnev committed Feb 10, 2023
1 parent f515abc commit 5c30c62
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 54 deletions.
Expand Up @@ -2,14 +2,37 @@ package org.jetbrains.dokka.javadoc.pages

import org.jetbrains.dokka.model.*

internal fun JavadocFunctionNode.getAnchor(): String =
"$name(${parameters.joinToString(",") {
when (val bound = if (it.typeBound is Nullable) it.typeBound.inner else it.typeBound) {
is TypeConstructor -> listOf(bound.dri.packageName, bound.dri.classNames).joinToString(".")
is TypeParameter -> bound.name
is PrimitiveJavaType -> bound.name
is UnresolvedBound -> bound.name
is JavaObject -> "Object"
else -> bound.toString()
}
}})"
/**
* Returns an unencoded, unescaped function anchor.
*
* Should be URL encoded / HTML escaped at call site,
* depending on usage.
*/
// see the discussion in #2813 related to encoding/escaping this value for ids/hrefs
internal fun JavadocFunctionNode.getAnchor(): String {
val parameters = parameters.joinToString(",") { it.typeBound.asString() }
return "$name($parameters)"
}

private fun Bound.asString(): String = when (this) {
is Nullable -> this.inner.asString()
is DefinitelyNonNullable -> this.inner.asString()
is TypeConstructor -> listOf(this.dri.packageName, this.dri.classNames).joinToString(".")
is TypeParameter -> this.name
is PrimitiveJavaType -> this.name
is UnresolvedBound -> this.name
is TypeAliased -> this.typeAlias.asString()
is JavaObject -> "Object"

// Void bound is currently used for return type only,
// which is not used in the anchor generation, but
// the handling for it is added regardless, just in case.
// Note: if you accept `Void` as a param, it'll be a TypeConstructor
Void -> "void"

// Javadoc format currently does not support multiplatform projects,
// so in an ideal world we should not see Dynamic here, but someone
// might disable the checker or the support for it might be added
// by Dokka or another plugin, so the handling is added just in case.
Dynamic -> "dynamic"
}
Expand Up @@ -11,53 +11,22 @@ import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.plugin
import org.jetbrains.dokka.plugability.querySingle
import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest
import org.jetbrains.dokka.javadoc.pages.JavadocFunctionNode
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals

class JavadocLocationTest : BaseAbstractTest() {

private fun locationTestInline(testHandler: (RootPageNode, DokkaContext) -> Unit) {
val config = dokkaConfiguration {
format = "javadoc"
sourceSets {
sourceSet {
sourceRoots = listOf("jvmSrc/")
externalDocumentationLinks = listOf(
DokkaConfiguration.ExternalDocumentationLink.jdk(8),
DokkaConfiguration.ExternalDocumentationLink.kotlinStdlib()
)
analysisPlatform = "jvm"
}
}
}
testInline(
"""
@Test
fun `resolved signature with external links`() {
val query = """
|/jvmSrc/javadoc/test/Test.kt
|package javadoc.test
|import java.io.Serializable
|class Test<A>() : Serializable, Cloneable {
| fun test() {}
| fun test2(s: String) {}
| fun <T> test3(a: A, t: T) {}
|}
|
|/jvmSrc/another/javadoc/example/Referenced.kt
|package javadoc.example.another
|/**
| * Referencing element from another package: [javadoc.test.Test]
| */
|class Referenced {}
""".trimIndent(),
config,
cleanupOutput = false,
pluginOverrides = listOf(JavadocPlugin())
) { renderingStage = testHandler }
}
|class Test : Serializable, Cloneable {}
""".trimIndent()

@Test
fun `resolved signature with external links`() {

locationTestInline { rootPageNode, dokkaContext ->
locationTestInline(query) { rootPageNode, dokkaContext ->
val transformer = htmlTranslator(rootPageNode, dokkaContext)
val testClass = rootPageNode.firstChildOfType<JavadocPackagePageNode> { it.name == "javadoc.test" }
.firstChildOfType<JavadocClasslikePageNode>()
Expand All @@ -70,8 +39,15 @@ class JavadocLocationTest : BaseAbstractTest() {

@Test
fun `resolved signature to no argument function`() {
val query = """
|/jvmSrc/javadoc/test/Test.kt
|package javadoc.test
|class Test {
| fun test() {}
|}
""".trimIndent()

locationTestInline { rootPageNode, dokkaContext ->
locationTestInline(query) { rootPageNode, dokkaContext ->
val transformer = htmlTranslator(rootPageNode, dokkaContext)
val testClassNode = rootPageNode.firstChildOfType<JavadocPackagePageNode> { it.name == "javadoc.test" }
.firstChildOfType<JavadocClasslikePageNode> { it.name == "Test" }
Expand All @@ -88,8 +64,15 @@ class JavadocLocationTest : BaseAbstractTest() {

@Test
fun `resolved signature to one argument function`() {
val query = """
|/jvmSrc/javadoc/test/Test.kt
|package javadoc.test
|class Test {
| fun test2(s: String) {}
|}
""".trimIndent()

locationTestInline { rootPageNode, dokkaContext ->
locationTestInline(query) { rootPageNode, dokkaContext ->
val transformer = htmlTranslator(rootPageNode, dokkaContext)
val testClassNode = rootPageNode.firstChildOfType<JavadocPackagePageNode> { it.name == "javadoc.test" }
.firstChildOfType<JavadocClasslikePageNode> { it.name == "Test" }
Expand All @@ -106,8 +89,15 @@ class JavadocLocationTest : BaseAbstractTest() {

@Test
fun `resolved signature to generic function`() {
val query = """
|/jvmSrc/javadoc/test/Test.kt
|package javadoc.test
|class Test<A>() {
| fun <T> test3(a: A, t: T) {}
|}
""".trimIndent()

locationTestInline { rootPageNode, dokkaContext ->
locationTestInline(query) { rootPageNode, dokkaContext ->
val transformer = htmlTranslator(rootPageNode, dokkaContext)
val testClassNode = rootPageNode.firstChildOfType<JavadocPackagePageNode> { it.name == "javadoc.test" }
.firstChildOfType<JavadocClasslikePageNode> { it.name == "Test" }
Expand All @@ -124,8 +114,13 @@ class JavadocLocationTest : BaseAbstractTest() {

@Test
fun `resolved package path`() {
val query = """
|/jvmSrc/javadoc/test/Test.kt
|package javadoc.test
|class Test {}
""".trimIndent()

locationTestInline { rootPageNode, dokkaContext ->
locationTestInline(query) { rootPageNode, dokkaContext ->
val locationProvider = dokkaContext.plugin<JavadocPlugin>().querySingle { locationProviderFactory }
.getLocationProvider(rootPageNode)
val packageNode = rootPageNode.firstChildOfType<JavadocPackagePageNode> { it.name == "javadoc.test" }
Expand All @@ -137,7 +132,21 @@ class JavadocLocationTest : BaseAbstractTest() {

@Test
fun `resolve link from another package`(){
locationTestInline { rootPageNode, dokkaContext ->
val query = """
|/jvmSrc/javadoc/test/Test.kt
|package javadoc.test
|class Test {}
|
|/jvmSrc/another/javadoc/example/Referenced.kt
|package javadoc.example.another
|
|/**
| * Referencing element from another package: [javadoc.test.Test]
| */
|class Referenced {}
""".trimIndent()

locationTestInline(query) { rootPageNode, dokkaContext ->
val transformer = htmlTranslator(rootPageNode, dokkaContext)
val testClassNode = rootPageNode.firstChildOfType<JavadocPackagePageNode> { it.name == "javadoc.example.another" }
.firstChildOfType<JavadocClasslikePageNode> { it.name == "Referenced" }
Expand All @@ -151,6 +160,98 @@ class JavadocLocationTest : BaseAbstractTest() {
}
}

@Test
fun `should resolve typealias function parameter`() {
val query = """
|/jvmSrc/javadoc/test/FunctionParameters.kt
|package javadoc.test.functionparams
|
|typealias StringTypealias = String
|
|class FunctionParameters {
| fun withTypealias(typeAliasParam: StringTypealias) {}
|}
""".trimIndent()

locationTestInline(query) { rootPageNode, dokkaContext ->
val transformer = htmlTranslator(rootPageNode, dokkaContext)
val methodWithTypealiasParam = rootPageNode.findFunctionNodeWithin(
packageName = "javadoc.test.functionparams",
className = "FunctionParameters",
methodName = "withTypealias"
)
val methodSignatureHtml = transformer.htmlForContentNode(methodWithTypealiasParam.signature, null)

val expectedSignatureHtml = "final <a href=https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html>Unit</a> " +
"<a href=javadoc/test/functionparams/FunctionParameters.html#withTypealias(javadoc.test.functionparams.StringTypealias)>withTypealias</a>" +
"(<a href=https://docs.oracle.com/javase/8/docs/api/java/lang/String.html>String</a> typeAliasParam)"

assertEquals(expectedSignatureHtml, methodSignatureHtml)
}
}

@Test
fun `should resolve definitely non nullable function parameter`() {
val query = """
|/jvmSrc/javadoc/test/FunctionParameters.kt
|package javadoc.test.functionparams
|
|class FunctionParameters {
| fun <T> withDefinitelyNonNullableType(definitelyNonNullable: T & Any) {}
|}
""".trimIndent()

locationTestInline(query) { rootPageNode, dokkaContext ->
val transformer = htmlTranslator(rootPageNode, dokkaContext)
val methodWithVoidParam = rootPageNode.findFunctionNodeWithin(
packageName = "javadoc.test.functionparams",
className = "FunctionParameters",
methodName = "withDefinitelyNonNullableType"
)
val methodSignatureHtml = transformer.htmlForContentNode(methodWithVoidParam.signature, null)

val expectedSignatureHtml = "final &lt;T extends <a href=https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html>Any</a>&gt; " +
"<a href=https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html>Unit</a> " +
"<a href=javadoc/test/functionparams/FunctionParameters.html#withDefinitelyNonNullableType(T)>withDefinitelyNonNullableType</a>" +
"(<a href=javadoc/test/functionparams/FunctionParameters.html#withDefinitelyNonNullableType(T)>T</a> definitelyNonNullable)"

assertEquals(expectedSignatureHtml, methodSignatureHtml)
}
}

private fun RootPageNode.findFunctionNodeWithin(
packageName: String,
className: String,
methodName: String
): JavadocFunctionNode {
return this
.firstChildOfType<JavadocPackagePageNode> { it.name == packageName }
.firstChildOfType<JavadocClasslikePageNode> { it.name == className }
.methods.single { it.name == methodName }
}

private fun locationTestInline(query: String, testHandler: (RootPageNode, DokkaContext) -> Unit) {
val config = dokkaConfiguration {
format = "javadoc"
sourceSets {
sourceSet {
sourceRoots = listOf("jvmSrc/")
externalDocumentationLinks = listOf(
DokkaConfiguration.ExternalDocumentationLink.jdk(8),
DokkaConfiguration.ExternalDocumentationLink.kotlinStdlib()
)
analysisPlatform = "jvm"
}
}
}
testInline(
query = query,
configuration = config,
cleanupOutput = false,
pluginOverrides = listOf(JavadocPlugin())
) { renderingStage = testHandler }
}

private fun htmlTranslator(rootPageNode: RootPageNode, dokkaContext: DokkaContext): JavadocContentToHtmlTranslator {
val locationProvider = dokkaContext.plugin<JavadocPlugin>().querySingle { locationProviderFactory }
.getLocationProvider(rootPageNode) as JavadocLocationProvider
Expand Down

0 comments on commit 5c30c62

Please sign in to comment.